Compare commits
58 commits
Author | SHA1 | Date | |
---|---|---|---|
|
3f7ec6561e | ||
|
8e7860d510 | ||
|
141c39dbb7 | ||
|
0a208ea8ef | ||
|
d2f99e548a | ||
|
66c00b24c2 | ||
|
437547f02e | ||
|
0712d9f61a | ||
|
d3b6938e48 | ||
|
b2a43d9ca3 | ||
|
9bbeeff386 | ||
|
4a7ed4730c | ||
|
3412745204 | ||
|
fee3421508 | ||
|
b2ef959464 | ||
|
86ca1199cc | ||
|
d865d61e23 | ||
|
ec21413d55 | ||
|
fbf7977188 | ||
|
2b847ddcc9 | ||
|
aabd175d5d | ||
|
f43895645f | ||
|
5e970cad74 | ||
|
de3172ee95 | ||
|
759b4d949c | ||
|
9885035ac8 | ||
|
2cc592c13e | ||
|
88a3bd1a2a | ||
|
547fb32f95 | ||
|
9cdab1d1d7 | ||
|
44dfcb9fe3 | ||
|
566f8531b5 | ||
|
496d708117 | ||
|
3871f71aae | ||
|
909e954c3e | ||
|
76ac1fbc86 | ||
|
da44785003 | ||
|
6676fc69f4 | ||
|
113c9ed172 | ||
|
0ae40119e7 | ||
|
07cdeffa31 | ||
|
67c8162296 | ||
|
29766d2fe6 | ||
|
9e6550f407 | ||
|
4e1b952c1e | ||
|
f807c531e9 | ||
|
8b253b71eb | ||
|
8a522c9cdd | ||
|
5c7ca2abc5 | ||
|
67221411f8 | ||
|
88eea5ad67 | ||
|
8b7630a357 | ||
|
c33160dff5 | ||
|
1ae2418394 | ||
|
3192b681b8 | ||
|
fa94b19660 | ||
|
56fe1653be | ||
|
3e3656c7d9 |
29 changed files with 1696 additions and 953 deletions
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
@ -70,3 +70,13 @@ jobs:
|
|||
asset_path: ./manifest.json
|
||||
asset_name: manifest.json
|
||||
asset_content_type: application/json
|
||||
- name: Upload CSS
|
||||
id: upload-css
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./src/styles.css
|
||||
asset_name: styles.css
|
||||
asset_content_type: text/css
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Luke Leppan
|
||||
Copyright (c) 2022 Luke Leppan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
38
README.md
38
README.md
|
@ -1,36 +1,36 @@
|
|||
# Better Word Count
|
||||
|
||||
  
|
||||
|
||||
**IMPORTANT NOTICE:** Due to the introduction of the new Live Preview feature, this plugin needed to be almost entirely rewritten. Currently the plugin only displays the only the word and character count with selecting text features. ALL other features have been scrapped and will be ported in future updates. **DO NOT UPDATE** If you want to use the legacy editor since their is no backwards compatibility yet (I know I'm lazy but I wanted to release the fix, its been too long).
|
||||
|
||||
This plugin is the same as the built-in **Word Count** plugin, except when you select text, it will count the selected word instead of the whole document. I recommend turning off the built-in **Word Count** because this plugin is designed to replace that. This plugin also has the ability to store statistics about your vault.
|
||||
   
|
||||
|
||||

|
||||
|
||||
This plugin is the same as the built-in **Word Count** plugin, except when you select text, it will count the selected word instead of the whole document. I recommend turning off the built-in **Word Count** because this plugin is designed to replace that. This plugin also has the ability to store statistics about your vault.
|
||||
|
||||
## Features
|
||||
|
||||
- Allows you to store statistics about your vault.
|
||||
- Works with all languages.
|
||||
- Can display a variety of different stats. Including:
|
||||
- Words, Characters and Sentences in current file.
|
||||
- Total Words, Characters and Sentences in vault.
|
||||
- Words, Characters and Sentences typed today.
|
||||
- Total Files in vault.
|
||||
- Words, Characters, Sentences, Footnotes, and Pandoc Citations in current file.
|
||||
- Total Words, Characters, Sentences, Footnotes, Pandoc Citations, and Files in vault.
|
||||
- Words, Characters, Sentences, Footnotes, and Pandoc Citations typed today.
|
||||
- Highly Customizable status bar that can be adapted to your needs.
|
||||
|
||||
### TODO
|
||||
|
||||
- [ ] add statistic view
|
||||
- [ ] add more statistics (make suggestions)
|
||||
- [ ] add goals
|
||||
|
||||
## Contributors
|
||||
|
||||
- @leoccyao
|
||||
- Added all word, char, sentence count when not viewing a markdown file.
|
||||
- @chrisgrieser
|
||||
- Added Footnotes and Citation Counting.
|
||||
- @bakuzan
|
||||
- Added page counts.
|
||||
- Fixed issue that caused errors at start up.
|
||||
- @THeK3nger
|
||||
- Fixed issue that occurred when renaming files.
|
||||
- @lishid
|
||||
- Helped solve the performace issue.
|
||||
- Helped solve the performance issue.
|
||||
- @Noxellar
|
||||
- Fixed alt bar spacing issue.
|
||||
|
||||
### Special Thanks
|
||||
|
||||
|
@ -44,3 +44,9 @@ This plugin is the same as the built-in **Word Count** plugin, except when you s
|
|||
- @lammersma
|
||||
- @aknighty74
|
||||
- @dhruvik7
|
||||
|
||||
### Support Me
|
||||
|
||||
If you enjoyed the plugin you can support me by clicking the button below. I am currently a student so anything helps.
|
||||
|
||||
[<img src="https://cdn.buymeacoffee.com/buttons/v2/default-violet.png" alt="BuyMeACoffee" width="100">](https://www.buymeacoffee.com/lukeleppan)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "better-word-count",
|
||||
"name": "Better Word Count",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.6",
|
||||
"description": "Counts the words of selected text in the editor.",
|
||||
"author": "Luke Leppan",
|
||||
"authorUrl": "https://lukeleppan.com",
|
||||
|
|
22
package.json
22
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "better-word-count",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.6",
|
||||
"description": "Counts the words of selected text in the editor.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
@ -19,23 +19,16 @@
|
|||
"author": "Luke Leppan",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.1.2",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/search": "^6.2.2",
|
||||
"@codemirror/state": "^6.1.2",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@codemirror/commands": "^0.19.0",
|
||||
"@codemirror/fold": "0.19.0",
|
||||
"@codemirror/history": "^0.19.0",
|
||||
"@codemirror/language": "^0.19.0",
|
||||
"@codemirror/matchbrackets": "^0.19.0",
|
||||
"@codemirror/panel": "^0.19.0",
|
||||
"@codemirror/rangeset": "^0.19.0",
|
||||
"@codemirror/search": "^0.19.0",
|
||||
"@codemirror/state": "^0.19.0",
|
||||
"@codemirror/stream-parser": "^0.19.0",
|
||||
"@codemirror/view": "^0.19.0",
|
||||
"@codemirror/view": "^6.4.0",
|
||||
"@rollup/plugin-commonjs": "^15.1.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@tsconfig/svelte": "^1.0.13",
|
||||
"@types/moment": "^2.13.0",
|
||||
"@types/node": "^14.17.3",
|
||||
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
|
||||
"rollup": "^2.32.1",
|
||||
|
@ -48,6 +41,7 @@
|
|||
"typescript": "^4.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"svelte": "^3.38.3"
|
||||
"svelte": "^3.38.3",
|
||||
"svelte-icons": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import typescript from '@rollup/plugin-typescript';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import copy from "rollup-plugin-copy";
|
||||
import svelte from "rollup-plugin-svelte";
|
||||
import autoPreprocess from "svelte-preprocess";
|
||||
const TEST_VAULT = 'test-vault/.obsidian/plugins/better-word-count';
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
const TEST_VAULT = "test-vault/.obsidian/plugins/better-word-count";
|
||||
|
||||
export default {
|
||||
input: 'src/main.ts',
|
||||
input: "src/main.ts",
|
||||
output: {
|
||||
dir: 'dist/',
|
||||
sourcemap: 'inline',
|
||||
format: 'cjs',
|
||||
exports: 'default'
|
||||
dir: "dist/",
|
||||
sourcemap: "inline",
|
||||
format: "cjs",
|
||||
exports: "default",
|
||||
},
|
||||
external: [
|
||||
"obsidian",
|
||||
|
@ -47,13 +47,17 @@ export default {
|
|||
nodeResolve({ browser: true }),
|
||||
commonjs(),
|
||||
svelte({
|
||||
preprocess: autoPreprocess(),
|
||||
include: "src/**/*.svelte",
|
||||
compilerOptions: { css: true },
|
||||
preprocess: sveltePreprocess(),
|
||||
}),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: 'dist/main.js', dest: TEST_VAULT },
|
||||
{ src: ['manifest.json'], dest: TEST_VAULT }
|
||||
], flatten: true
|
||||
})
|
||||
]
|
||||
};
|
||||
{ src: "src/styles.css", dest: TEST_VAULT },
|
||||
{ src: "dist/main.js", dest: TEST_VAULT },
|
||||
{ src: ["manifest.json"], dest: TEST_VAULT },
|
||||
],
|
||||
flatten: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export const VIEW_TYPE_STATS = "vault-stats";
|
||||
export const STATS_FILE = ".vault-stats";
|
||||
export const STATS_FILE = ".obsidian/vault-stats.json";
|
||||
export const STATS_ICON = `<g transform="matrix(0.95,0,0,0.95,2.5,2.5)"><path fill="currentColor" stroke="currentColor" d="M3.77,100L22.421,100C24.503,100 26.19,98.013 26.19,95.561L26.19,34.813C26.19,32.361 24.503,30.374 22.421,30.374L3.77,30.374C1.688,30.374 -0,32.361 -0,34.813L-0,95.561C-0,98.013 1.688,100 3.77,100ZM40.675,100L59.325,100C61.408,100 63.095,98.013 63.095,95.561L63.095,4.439C63.095,1.987 61.408,-0 59.325,-0L40.675,-0C38.592,-0 36.905,1.987 36.905,4.439L36.905,95.561C36.905,98.013 38.592,100 40.675,100ZM77.579,100L96.23,100C98.312,100 100,98.013 100,95.561L100,46.495C100,44.043 98.312,42.056 96.23,42.056L77.579,42.056C75.497,42.056 73.81,44.043 73.81,46.495L73.81,95.561C73.81,98.013 75.497,100 77.579,100Z" style="fill:none;fill-rule:nonzero;stroke-width:8px;"/></g>`;
|
||||
export const STATS_ICON_NAME = "stats-graph";
|
||||
export const MATCH_HTML_COMMENT = new RegExp(
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
import moment from "moment";
|
||||
import type { MetadataCache, TFile, Vault } from "obsidian";
|
||||
import { getCharacterCount, getSentenceCount, getWordCount } from "./stats";
|
||||
|
||||
export class DataCollector {
|
||||
private vault: Vault;
|
||||
private metadataCache: MetadataCache;
|
||||
|
||||
constructor(vault: Vault, metadataCache: MetadataCache) {
|
||||
this.vault = vault;
|
||||
this.metadataCache = metadataCache;
|
||||
}
|
||||
|
||||
getTotalFileCount() {
|
||||
return this.vault.getMarkdownFiles().length;
|
||||
}
|
||||
|
||||
async getTotalWordCount() {
|
||||
let words = 0;
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
words += getWordCount(await this.vault.cachedRead(file));
|
||||
}
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
async getTotalCharacterCount() {
|
||||
let characters = 0;
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
characters += getCharacterCount(await this.vault.cachedRead(file));
|
||||
}
|
||||
}
|
||||
|
||||
return characters;
|
||||
}
|
||||
|
||||
async getTotalSentenceCount() {
|
||||
let sentence = 0;
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
sentence += getSentenceCount(await this.vault.cachedRead(file));
|
||||
}
|
||||
}
|
||||
|
||||
return sentence;
|
||||
}
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
import moment from "moment";
|
||||
import { debounce, Debouncer, MetadataCache, TFile, Vault } from "obsidian";
|
||||
import { STATS_FILE } from "src/constants";
|
||||
import { DataCollector } from "./collector";
|
||||
import { getCharacterCount, getSentenceCount, getWordCount } from "./stats";
|
||||
|
||||
type History = Record<string, Day>;
|
||||
|
||||
interface Day {
|
||||
files: number;
|
||||
modifiedFiles: ModFiles;
|
||||
words: number;
|
||||
characters: number;
|
||||
sentences: number;
|
||||
totalWords: number;
|
||||
totalCharacters: number;
|
||||
totalSentences: number;
|
||||
}
|
||||
|
||||
type ModFiles = Record<string, FileStats>;
|
||||
|
||||
type FileStats = Record<number, Count>;
|
||||
|
||||
interface Count {
|
||||
initial: number;
|
||||
current: number;
|
||||
}
|
||||
|
||||
export interface TodayCounts {
|
||||
words: number;
|
||||
characters: number;
|
||||
sentences: number;
|
||||
}
|
||||
|
||||
export interface TotalCounts {
|
||||
words: number;
|
||||
characters: number;
|
||||
sentences: number;
|
||||
}
|
||||
|
||||
export class DataManager {
|
||||
private vault: Vault;
|
||||
private metadataCache: MetadataCache;
|
||||
private history: History;
|
||||
private today: string;
|
||||
private collector: DataCollector;
|
||||
private todayCounts: TodayCounts;
|
||||
public debounceChange: Debouncer<[file: TFile, data: string]>;
|
||||
|
||||
constructor(vault: Vault, metadataCache: MetadataCache) {
|
||||
this.vault = vault;
|
||||
this.metadataCache = metadataCache;
|
||||
this.collector = new DataCollector(vault, metadataCache);
|
||||
this.debounceChange = debounce(
|
||||
(file: TFile, data: string) => this.change(file, data),
|
||||
1000,
|
||||
false
|
||||
);
|
||||
|
||||
this.vault.adapter.exists(".vault-stats").then(async (exists) => {
|
||||
if (!exists) {
|
||||
this.vault.adapter.write(".vault-stats", "{}");
|
||||
}
|
||||
|
||||
this.history = Object.assign(
|
||||
JSON.parse(await this.vault.adapter.read(".vault-stats"))
|
||||
);
|
||||
|
||||
this.updateToday();
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
|
||||
async update(): Promise<void> {
|
||||
this.vault.adapter.write(STATS_FILE, JSON.stringify(this.history));
|
||||
}
|
||||
|
||||
async updateToday(): Promise<void> {
|
||||
const newDay: Day = {
|
||||
files: this.collector.getTotalFileCount(),
|
||||
modifiedFiles: {},
|
||||
words: 0,
|
||||
characters: 0,
|
||||
sentences: 0,
|
||||
totalWords: await this.collector.getTotalWordCount(),
|
||||
totalCharacters: await this.collector.getTotalCharacterCount(),
|
||||
totalSentences: await this.collector.getTotalSentenceCount(),
|
||||
};
|
||||
|
||||
if (!this.history.hasOwnProperty(moment().format("YYYY-MM-DD"))) {
|
||||
this.history[moment().format("YYYY-MM-DD")] = newDay;
|
||||
}
|
||||
|
||||
this.today = moment().format("YYYY-MM-DD");
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
async setTotalStats() {
|
||||
this.history[this.today].files = this.collector.getTotalFileCount();
|
||||
this.history[this.today].totalWords =
|
||||
await this.collector.getTotalWordCount();
|
||||
this.history[this.today].totalCharacters =
|
||||
await this.collector.getTotalCharacterCount();
|
||||
this.history[this.today].totalSentences =
|
||||
await this.collector.getTotalSentenceCount();
|
||||
this.update();
|
||||
}
|
||||
|
||||
change(file: TFile, data: string) {
|
||||
const currentWords = getWordCount(data);
|
||||
const currentCharacters = getCharacterCount(data);
|
||||
const currentSentences = getSentenceCount(data);
|
||||
|
||||
if (
|
||||
this.history.hasOwnProperty(this.today) &&
|
||||
this.today === moment().format("YYYY-MM-DD")
|
||||
) {
|
||||
if (!this.history[this.today].modifiedFiles.hasOwnProperty(file.path)) {
|
||||
const newWordCount: Count = {
|
||||
initial: currentWords,
|
||||
current: currentWords,
|
||||
};
|
||||
const newCharacterCount: Count = {
|
||||
initial: currentCharacters,
|
||||
current: currentCharacters,
|
||||
};
|
||||
const newSentenceCount: Count = {
|
||||
initial: currentSentences,
|
||||
current: currentSentences,
|
||||
};
|
||||
const fileStats: FileStats = {
|
||||
0: newWordCount,
|
||||
1: newCharacterCount,
|
||||
2: newSentenceCount,
|
||||
};
|
||||
|
||||
this.history[this.today].modifiedFiles[file.path] = fileStats;
|
||||
} else {
|
||||
this.history[this.today].modifiedFiles[file.path][0].current =
|
||||
currentWords;
|
||||
this.history[this.today].modifiedFiles[file.path][1].current =
|
||||
currentCharacters;
|
||||
this.history[this.today].modifiedFiles[file.path][2].current =
|
||||
currentSentences;
|
||||
}
|
||||
this.updateTodayCounts();
|
||||
this.update();
|
||||
} else {
|
||||
this.updateToday();
|
||||
}
|
||||
}
|
||||
|
||||
updateTodayCounts() {
|
||||
const words = Object.values(this.history[this.today].modifiedFiles)
|
||||
.map((counts) => Math.max(0, counts[0].current - counts[0].initial))
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const characters = Object.values(this.history[this.today].modifiedFiles)
|
||||
.map((counts) => Math.max(0, counts[1].current - counts[1].initial))
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const sentences = Object.values(this.history[this.today].modifiedFiles)
|
||||
.map((counts) => Math.max(0, counts[2].current - counts[2].initial))
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
this.history[this.today].words = words;
|
||||
this.history[this.today].characters = characters;
|
||||
this.history[this.today].sentences = sentences;
|
||||
|
||||
this.todayCounts = {
|
||||
words: words,
|
||||
characters: characters,
|
||||
sentences: sentences,
|
||||
};
|
||||
}
|
||||
|
||||
getTodayCounts(): TodayCounts {
|
||||
return this.todayCounts;
|
||||
}
|
||||
|
||||
getTotalCounts(): TotalCounts {
|
||||
return {
|
||||
words: this.history[this.today].totalWords,
|
||||
characters: this.history[this.today].totalCharacters,
|
||||
sentences: this.history[this.today].totalSentences,
|
||||
};
|
||||
}
|
||||
|
||||
async updateFromFile() {
|
||||
this.history = Object.assign(
|
||||
JSON.parse(await this.vault.adapter.read(".vault-stats"))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import { MATCH_HTML_COMMENT, MATCH_COMMENT } from "src/constants";
|
||||
|
||||
export function getWordCount(text: string): number {
|
||||
const spaceDelimitedChars =
|
||||
/A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC/
|
||||
.source;
|
||||
const nonSpaceDelimitedWords =
|
||||
/\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5/.source;
|
||||
|
||||
const nonSpaceDelimitedWordsOther =
|
||||
/[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5]{1}/
|
||||
.source;
|
||||
|
||||
const pattern = new RegExp(
|
||||
[
|
||||
`(?:[0-9]+(?:(?:,|\\.)[0-9]+)*|[\\-${spaceDelimitedChars}])+`,
|
||||
nonSpaceDelimitedWords,
|
||||
nonSpaceDelimitedWordsOther,
|
||||
].join("|"),
|
||||
"g"
|
||||
);
|
||||
return (text.match(pattern) || []).length;
|
||||
}
|
||||
|
||||
export function getCharacterCount(text: string): number {
|
||||
return text.length;
|
||||
}
|
||||
|
||||
export function getSentenceCount(text: string): number {
|
||||
const sentences: number = (
|
||||
(text || "").match(
|
||||
/[^.!?\s][^.!?]*(?:[.!?](?!['"]?\s|$)[^.!?]*)*[.!?]?['"]?(?=\s|$)/gm
|
||||
) || []
|
||||
).length;
|
||||
|
||||
return sentences;
|
||||
}
|
||||
|
||||
export function cleanComments(text: string): string {
|
||||
return text.replace(MATCH_COMMENT, "").replace(MATCH_HTML_COMMENT, "");
|
||||
}
|
76
src/editor/EditorPlugin.ts
Normal file
76
src/editor/EditorPlugin.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { Transaction } from "@codemirror/state";
|
||||
import {
|
||||
ViewUpdate,
|
||||
PluginValue,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
} from "@codemirror/view";
|
||||
import type BetterWordCount from "src/main";
|
||||
|
||||
class EditorPlugin implements PluginValue {
|
||||
hasPlugin: boolean;
|
||||
view: EditorView;
|
||||
private plugin: BetterWordCount;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.view = view;
|
||||
this.hasPlugin = false;
|
||||
}
|
||||
|
||||
update(update: ViewUpdate): void {
|
||||
if (!this.hasPlugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tr = update.transactions[0];
|
||||
|
||||
if (!tr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When selecting text with Shift+Home the userEventType is undefined.
|
||||
// This is probably a bug in codemirror, for the time being doing an explict check
|
||||
// for the type allows us to update the stats for the selection.
|
||||
const userEventTypeUndefined =
|
||||
tr.annotation(Transaction.userEvent) === undefined;
|
||||
|
||||
if (
|
||||
(tr.isUserEvent("select") || userEventTypeUndefined) &&
|
||||
tr.newSelection.ranges[0].from !== tr.newSelection.ranges[0].to
|
||||
) {
|
||||
let text = "";
|
||||
const selection = tr.newSelection.main;
|
||||
const textIter = tr.newDoc.iterRange(selection.from, selection.to);
|
||||
while (!textIter.done) {
|
||||
text = text + textIter.next().value;
|
||||
}
|
||||
this.plugin.statusBar.debounceStatusBarUpdate(text);
|
||||
} else if (
|
||||
tr.isUserEvent("input") ||
|
||||
tr.isUserEvent("delete") ||
|
||||
tr.isUserEvent("move") ||
|
||||
tr.isUserEvent("undo") ||
|
||||
tr.isUserEvent("redo") ||
|
||||
tr.isUserEvent("select")
|
||||
) {
|
||||
const textIter = tr.newDoc.iter();
|
||||
let text = "";
|
||||
while (!textIter.done) {
|
||||
text = text + textIter.next().value;
|
||||
}
|
||||
if (tr.docChanged && this.plugin.statsManager) {
|
||||
this.plugin.statsManager.debounceChange(text);
|
||||
}
|
||||
this.plugin.statusBar.debounceStatusBarUpdate(text);
|
||||
}
|
||||
}
|
||||
|
||||
addPlugin(plugin: BetterWordCount) {
|
||||
this.plugin = plugin;
|
||||
this.hasPlugin = true;
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
}
|
||||
|
||||
export const editorPlugin = ViewPlugin.fromClass(EditorPlugin);
|
261
src/main.ts
261
src/main.ts
|
@ -1,200 +1,89 @@
|
|||
import { Plugin, TFile, addIcon, WorkspaceLeaf, MarkdownEditView, MarkdownView, MarkdownSourceView, editorViewField, editorEditorField, Notice } from "obsidian";
|
||||
// import { BetterWordCountSettingsTab } from "./settings/settings-tab";
|
||||
// import { BetterWordCountSettings, DEFAULT_SETTINGS } from "./settings/settings";
|
||||
// import { StatusBar } from "./status/bar";
|
||||
// import { STATS_ICON, STATS_ICON_NAME, VIEW_TYPE_STATS } from "./constants";
|
||||
// import StatsView from "./view/view";
|
||||
// import { DataManager } from "./data/manager";
|
||||
// import { BarManager } from "./status/manager";
|
||||
|
||||
// import {} from "@codemirror/text";
|
||||
import {EditorState, StateEffect, StateField, Transaction} from "@codemirror/state";
|
||||
|
||||
// import type CodeMirror from "codemirror";
|
||||
import { Decoration, DecorationSet, EditorView } from "@codemirror/view";
|
||||
import { StatusBar } from "./status/bar";
|
||||
import { Plugin, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import BetterWordCountSettingsTab from "./settings/SettingsTab";
|
||||
import StatsManager from "./stats/StatsManager";
|
||||
import StatusBar from "./status/StatusBar";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { editorPlugin } from "./editor/EditorPlugin";
|
||||
import {
|
||||
BetterWordCountSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
} from "src/settings/Settings";
|
||||
import { settingsStore } from "./utils/SvelteStores";
|
||||
|
||||
export default class BetterWordCount extends Plugin {
|
||||
public static statusBar: StatusBar;
|
||||
// public currentFile: TFile;
|
||||
// public settings: BetterWordCountSettings;
|
||||
// public view: StatsView;
|
||||
// public dataManager: DataManager;
|
||||
// public barManager: BarManager;
|
||||
public settings: BetterWordCountSettings;
|
||||
public statusBar: StatusBar;
|
||||
public statsManager: StatsManager;
|
||||
|
||||
// onunload(): void {
|
||||
// this.app.workspace
|
||||
// .getLeavesOfType(VIEW_TYPE_STATS)
|
||||
// .forEach((leaf) => leaf.detach());
|
||||
// }
|
||||
async onunload(): Promise<void> {
|
||||
this.statsManager = null;
|
||||
this.statusBar = null;
|
||||
}
|
||||
|
||||
onload() {
|
||||
async onload() {
|
||||
// Settings Store
|
||||
// this.register(
|
||||
// settingsStore.subscribe((value) => {
|
||||
// this.settings = value;
|
||||
// })
|
||||
// );
|
||||
// Handle Settings
|
||||
this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData());
|
||||
this.addSettingTab(new BetterWordCountSettingsTab(this.app, this));
|
||||
|
||||
// Handle Statistics
|
||||
if (this.settings.collectStats) {
|
||||
this.statsManager = new StatsManager(this.app.vault, this.app.workspace, this);
|
||||
}
|
||||
|
||||
// Handle Status Bar
|
||||
let statusBarEl = this.addStatusBarItem();
|
||||
BetterWordCount.statusBar = new StatusBar(statusBarEl);
|
||||
this.statusBar = new StatusBar(statusBarEl, this);
|
||||
|
||||
this.createCMExtension();
|
||||
// Handle the Editor Plugin
|
||||
this.registerEditorExtension(editorPlugin);
|
||||
|
||||
// this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData());
|
||||
// this.addSettingTab(new BetterWordCountSettingsTab(this.app, this));
|
||||
|
||||
// let statusBarElTest = this.addStatusBarItem();
|
||||
// statusBarElTest.setText("§l§aTest§r");
|
||||
// let statusBarEl = this.addStatusBarItem();
|
||||
// this.statusBar = new StatusBar(statusBarEl);
|
||||
// this.statusBar.displayText("Awesome");
|
||||
// this.barManager = new BarManager(
|
||||
// this.statusBar,
|
||||
// this.settings,
|
||||
// this.app.vault,
|
||||
// this.app.metadataCache
|
||||
// );
|
||||
|
||||
// if (this.settings.collectStats) {
|
||||
// this.dataManager = new DataManager(
|
||||
// this.app.vault,
|
||||
// this.app.metadataCache
|
||||
// );
|
||||
// }
|
||||
|
||||
// this.registerEvent(
|
||||
// this.app.workspace.on("active-leaf-change", this.activeLeafChange, this)
|
||||
// );
|
||||
|
||||
// this.registerCodeMirror((cm: CodeMirror.Editor) => {
|
||||
// cm.on("cursorActivity", (cm: CodeMirror.Editor) =>
|
||||
// this.barManager.cursorActivity(cm)
|
||||
// );
|
||||
// });
|
||||
|
||||
|
||||
|
||||
|
||||
// if (this.settings.collectStats) {
|
||||
// this.registerEvent(
|
||||
// this.app.workspace.on(
|
||||
// "quick-preview",
|
||||
// this.dataManager.debounceChange,
|
||||
// this.dataManager
|
||||
// )
|
||||
// );
|
||||
|
||||
// this.registerInterval(
|
||||
// window.setInterval(() => {
|
||||
// this.dataManager.setTotalStats();
|
||||
// }, 1000 * 60)
|
||||
// );
|
||||
// }
|
||||
|
||||
// addIcon(STATS_ICON_NAME, STATS_ICON);
|
||||
|
||||
// this.addCommand({
|
||||
// id: "show-vault-stats-view",
|
||||
// name: "Open Statistics",
|
||||
// checkCallback: (checking: boolean) => {
|
||||
// if (checking) {
|
||||
// return this.app.workspace.getLeavesOfType("vault-stats").length === 0;
|
||||
// }
|
||||
// this.initLeaf();
|
||||
// },
|
||||
// });
|
||||
|
||||
// this.registerView(
|
||||
// VIEW_TYPE_STATS,
|
||||
// (leaf: WorkspaceLeaf) => (this.view = new StatsView(leaf))
|
||||
// );
|
||||
|
||||
// if (this.app.workspace.layoutReady) {
|
||||
// this.initLeaf();
|
||||
// } else {
|
||||
// this.app.workspace.onLayoutReady(() => this.initLeaf());
|
||||
// }
|
||||
}
|
||||
|
||||
// activeLeafChange(leaf: WorkspaceLeaf) {
|
||||
// if (!(leaf.view.getViewType() === "markdown")) {
|
||||
// this.barManager.updateAltStatusBar();
|
||||
// }
|
||||
// }
|
||||
|
||||
// async saveSettings(): Promise<void> {
|
||||
// await this.saveData(this.settings);
|
||||
// }
|
||||
|
||||
// initLeaf(): void {
|
||||
// if (this.app.workspace.getLeavesOfType(VIEW_TYPE_STATS).length) {
|
||||
// return;
|
||||
// }
|
||||
// this.app.workspace.getRightLeaf(false).setViewState({
|
||||
// type: VIEW_TYPE_STATS,
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
createCMExtension() {
|
||||
|
||||
const cmStateField = StateField.define<DecorationSet>({
|
||||
create(state: EditorState) {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(effects: DecorationSet, tr: Transaction) {
|
||||
let text = "";
|
||||
const selection = tr.newSelection.main;
|
||||
if (selection.empty) {
|
||||
const textIter = tr.newDoc.iter();
|
||||
while (!textIter.done) {
|
||||
text = text + textIter.next().value;
|
||||
}
|
||||
} else {
|
||||
const textIter = tr.newDoc.iterRange(selection.from, selection.to);
|
||||
while (!textIter.done) {
|
||||
text = text + textIter.next().value;
|
||||
}
|
||||
}
|
||||
|
||||
BetterWordCount.updateStatusBar(text);
|
||||
|
||||
return effects;
|
||||
},
|
||||
|
||||
provide: (f: any) => EditorView.decorations.from(f),
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.giveEditorPlugin(this.app.workspace.getMostRecentLeaf());
|
||||
});
|
||||
|
||||
this.registerEditorExtension(cmStateField);
|
||||
}
|
||||
|
||||
static updateStatusBar(text: string) {
|
||||
const words = this.getWordCount(text);
|
||||
const chars = this.getCharacterCount(text);
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"active-leaf-change",
|
||||
async (leaf: WorkspaceLeaf) => {
|
||||
this.giveEditorPlugin(leaf);
|
||||
if (leaf.view.getViewType() !== "markdown") {
|
||||
this.statusBar.updateAltBar();
|
||||
}
|
||||
|
||||
this.statusBar.displayText(`${words} words ${chars} characters`);
|
||||
}
|
||||
|
||||
static getWordCount(text: string): number {
|
||||
const spaceDelimitedChars =
|
||||
/A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC/
|
||||
.source;
|
||||
const nonSpaceDelimitedWords =
|
||||
/\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5/.source;
|
||||
|
||||
const nonSpaceDelimitedWordsOther =
|
||||
/[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5]{1}/
|
||||
.source;
|
||||
|
||||
const pattern = new RegExp(
|
||||
[
|
||||
`(?:[0-9]+(?:(?:,|\\.)[0-9]+)*|[\\-${spaceDelimitedChars}])+`,
|
||||
nonSpaceDelimitedWords,
|
||||
nonSpaceDelimitedWordsOther,
|
||||
].join("|"),
|
||||
"g"
|
||||
if (!this.settings.collectStats) return;
|
||||
await this.statsManager.recalcTotals();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.vault.on("delete", async () => {
|
||||
if (!this.settings.collectStats) return;
|
||||
await this.statsManager.recalcTotals();
|
||||
})
|
||||
);
|
||||
return (text.match(pattern) || []).length;
|
||||
}
|
||||
|
||||
static getCharacterCount(text: string): number {
|
||||
return text.length;
|
||||
}
|
||||
|
||||
|
||||
giveEditorPlugin(leaf: WorkspaceLeaf): void {
|
||||
//@ts-expect-error, not typed
|
||||
const editor = leaf?.view?.editor;
|
||||
if (editor) {
|
||||
const editorView = editor.cm as EditorView;
|
||||
const editorPlug = editorView.plugin(editorPlugin);
|
||||
editorPlug.addPlugin(this);
|
||||
//@ts-expect-error, not typed
|
||||
const data: string = leaf.view.data;
|
||||
this.statusBar.updateStatusBar(data);
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
79
src/settings/Settings.ts
Normal file
79
src/settings/Settings.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
export enum MetricCounter {
|
||||
words,
|
||||
characters,
|
||||
sentences,
|
||||
footnotes,
|
||||
citations,
|
||||
pages,
|
||||
files,
|
||||
}
|
||||
|
||||
export enum MetricType {
|
||||
file,
|
||||
daily,
|
||||
total,
|
||||
folder,
|
||||
}
|
||||
|
||||
export interface Metric {
|
||||
type: MetricType;
|
||||
counter: MetricCounter;
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
export interface StatusBarItem {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
metric: Metric;
|
||||
}
|
||||
|
||||
export const BLANK_SB_ITEM: StatusBarItem = {
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
metric: {
|
||||
type: null,
|
||||
counter: null,
|
||||
},
|
||||
};
|
||||
|
||||
export interface BetterWordCountSettings {
|
||||
statusBar: StatusBarItem[];
|
||||
altBar: StatusBarItem[];
|
||||
countComments: boolean;
|
||||
collectStats: boolean;
|
||||
pageWords: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: BetterWordCountSettings = {
|
||||
statusBar: [
|
||||
{
|
||||
prefix: "",
|
||||
suffix: " words",
|
||||
metric: {
|
||||
type: MetricType.file,
|
||||
counter: MetricCounter.words,
|
||||
},
|
||||
},
|
||||
{
|
||||
prefix: " ",
|
||||
suffix: " characters",
|
||||
metric: {
|
||||
type: MetricType.file,
|
||||
counter: MetricCounter.characters,
|
||||
},
|
||||
},
|
||||
],
|
||||
altBar: [
|
||||
{
|
||||
prefix: "",
|
||||
suffix: " files",
|
||||
metric: {
|
||||
type: MetricType.total,
|
||||
counter: MetricCounter.files,
|
||||
},
|
||||
},
|
||||
],
|
||||
countComments: false,
|
||||
collectStats: false,
|
||||
pageWords: 300,
|
||||
};
|
56
src/settings/SettingsTab.ts
Normal file
56
src/settings/SettingsTab.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { App, PluginSettingTab, Setting, ToggleComponent, TextComponent } from "obsidian";
|
||||
import type BetterWordCount from "src/main";
|
||||
import { addStatusBarSettings } from "./StatusBarSettings";
|
||||
|
||||
export default class BetterWordCountSettingsTab extends PluginSettingTab {
|
||||
constructor(app: App, private plugin: BetterWordCount) {
|
||||
super(app, plugin);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
let { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h3", { text: "Better Word Count Settings" });
|
||||
|
||||
// General Settings
|
||||
containerEl.createEl("h4", { text: "General Settings" });
|
||||
new Setting(containerEl)
|
||||
.setName("Collect Statistics")
|
||||
.setDesc(
|
||||
"Reload Required for change to take effect. Turn on to start collecting daily statistics of your writing. Stored in the vault-stats.json file in the .obsidian of your vault. This is required for counts of the day as well as total counts."
|
||||
)
|
||||
.addToggle((cb: ToggleComponent) => {
|
||||
cb.setValue(this.plugin.settings.collectStats);
|
||||
cb.onChange(async (value: boolean) => {
|
||||
this.plugin.settings.collectStats = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Don't Count Comments")
|
||||
.setDesc("Turn on if you don't want markdown comments to be counted.")
|
||||
.addToggle((cb: ToggleComponent) => {
|
||||
cb.setValue(this.plugin.settings.countComments);
|
||||
cb.onChange(async (value: boolean) => {
|
||||
this.plugin.settings.countComments = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Page Word Count")
|
||||
.setDesc("Set how many words count as one \"page\"")
|
||||
.addText((text: TextComponent) => {
|
||||
text.inputEl.type = "number";
|
||||
text.setPlaceholder("300");
|
||||
text.setValue(this.plugin.settings.pageWords.toString());
|
||||
text.onChange(async (value: string) => {
|
||||
this.plugin.settings.pageWords = parseInt(value);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Status Bar Settings
|
||||
addStatusBarSettings(this.plugin, containerEl);
|
||||
}
|
||||
}
|
448
src/settings/StatusBarSettings.svelte
Normal file
448
src/settings/StatusBarSettings.svelte
Normal file
|
@ -0,0 +1,448 @@
|
|||
<script lang="ts">
|
||||
import type { StatusBarItem } from "./Settings";
|
||||
import { MetricType, MetricCounter, BLANK_SB_ITEM, DEFAULT_SETTINGS } from "./Settings";
|
||||
import type BetterWordCount from "src/main";
|
||||
|
||||
export let plugin: BetterWordCount;
|
||||
const { settings } = plugin;
|
||||
|
||||
let statusItems: StatusBarItem[] = [...plugin.settings.statusBar];
|
||||
let altSItems: StatusBarItem[] = [...plugin.settings.altBar];
|
||||
|
||||
function metricToString(metric: Metric): string {
|
||||
if (metric.type === MetricType.file) {
|
||||
switch (metric.counter) {
|
||||
case MetricCounter.words:
|
||||
return "Words in Note"
|
||||
case MetricCounter.characters:
|
||||
return "Chars in Note"
|
||||
case MetricCounter.sentences:
|
||||
return "Sentences in Note"
|
||||
case MetricCounter.footnotes:
|
||||
return "Footnotes in Note"
|
||||
case MetricCounter.citations:
|
||||
return "Citations in Note"
|
||||
case MetricCounter.pages:
|
||||
return "Pages in Note"
|
||||
case MetricCounter.files:
|
||||
return "Total Notes"
|
||||
}
|
||||
} else if (metric.type === MetricType.daily) {
|
||||
switch (metric.counter) {
|
||||
case MetricCounter.words:
|
||||
return "Daily Words"
|
||||
case MetricCounter.characters:
|
||||
return "Daily Chars"
|
||||
case MetricCounter.sentences:
|
||||
return "Daily Sentences"
|
||||
case MetricCounter.footnotes:
|
||||
return "Daily Footnotes"
|
||||
case MetricCounter.citations:
|
||||
return "Daily Citations"
|
||||
case MetricCounter.pages:
|
||||
return "Daily Pages"
|
||||
case MetricCounter.files:
|
||||
return "Total Notes"
|
||||
}
|
||||
} else if (metric.type === MetricType.total) {
|
||||
switch (metric.counter) {
|
||||
case MetricCounter.words:
|
||||
return "Total Words"
|
||||
case MetricCounter.characters:
|
||||
return "Total Chars"
|
||||
case MetricCounter.sentences:
|
||||
return "Total Sentences"
|
||||
case MetricCounter.footnotes:
|
||||
return "Total Footnotes"
|
||||
case MetricCounter.citations:
|
||||
return "Total Citations"
|
||||
case MetricCounter.pages:
|
||||
return "Total Pages"
|
||||
case MetricCounter.files:
|
||||
return "Total Notes"
|
||||
}
|
||||
} else {
|
||||
return "Select Options"
|
||||
}
|
||||
}
|
||||
|
||||
function swapStatusBarItems(i: number, j: number, arr: StatusBarItem[]) {
|
||||
const max = arr.length - 1;
|
||||
if (i < 0 || i > max || j < 0 || j > max) return arr;
|
||||
const tmp = arr[i];
|
||||
arr[i] = arr[j];
|
||||
arr[j] = tmp;
|
||||
return arr;
|
||||
}
|
||||
|
||||
async function update(statusItems: StatusBarItem[]) {
|
||||
plugin.settings.statusBar = statusItems.filter((item) => {
|
||||
if (metricToString(item.metric) !== "Select Options") {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
|
||||
await plugin.saveSettings();
|
||||
}
|
||||
|
||||
async function updateAlt(altSItems: StatusBarItem[]) {
|
||||
plugin.settings.altBar = altSItems.filter((item) => {
|
||||
if (metricToString(item.metric) !== "Select Options") {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
|
||||
await plugin.saveSettings();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h4>Markdown Status Bar</h4>
|
||||
<p>Here you can customize what statistics are displayed on the status bar when editing a markdown note.</p>
|
||||
<div class="bwc-sb-buttons">
|
||||
<button
|
||||
aria-label="Add New Status Bar Item"
|
||||
on:click={async () => (statusItems = [...statusItems, JSON.parse(JSON.stringify(BLANK_SB_ITEM))])}
|
||||
>
|
||||
<div class="icon">
|
||||
Add Item
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reset Status Bar to Default"
|
||||
on:click={async () => {
|
||||
statusItems = [
|
||||
{
|
||||
prefix: "",
|
||||
suffix: " words",
|
||||
metric: {
|
||||
type: MetricType.file,
|
||||
counter: MetricCounter.words,
|
||||
},
|
||||
},
|
||||
{
|
||||
prefix: " ",
|
||||
suffix: " characters",
|
||||
metric: {
|
||||
type: MetricType.file,
|
||||
counter: MetricCounter.characters,
|
||||
},
|
||||
},
|
||||
];
|
||||
await update(statusItems);
|
||||
}}
|
||||
>
|
||||
<div class="icon">
|
||||
Reset
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{#each statusItems as item, i}
|
||||
<details class="bwc-sb-item-setting">
|
||||
<summary>
|
||||
<span class="bwc-sb-item-text">
|
||||
{metricToString(item.metric)}
|
||||
</span>
|
||||
<span class="bwc-sb-buttons">
|
||||
{#if i !== 0}
|
||||
<button
|
||||
aria-label="Move Status Bar Item Up"
|
||||
on:click={async () => {
|
||||
statusItems = swapStatusBarItems(i, i-1, statusItems);
|
||||
await update(statusItems);
|
||||
}}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
{/if}
|
||||
{#if i !== statusItems.length - 1}
|
||||
<button
|
||||
aria-label="Move Status Bar Item Down"
|
||||
on:click={async () => {
|
||||
statusItems = swapStatusBarItems(i, i+1, statusItems);
|
||||
await update(statusItems);
|
||||
}}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
aria-label="Remove Status Bar Item"
|
||||
on:click={async () => {
|
||||
statusItems = statusItems.filter((item, j) => i !== j);
|
||||
await update(statusItems);
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">Metric Counter</div>
|
||||
<div class="setting-item-description">
|
||||
Select the counter to display, e.g. words, characters.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
<select
|
||||
class="dropdown"
|
||||
value={item.metric.counter}
|
||||
on:change={async (e) => {
|
||||
const {value} = e.target;
|
||||
item.metric.counter = MetricCounter[MetricCounter[value]];
|
||||
await update(statusItems);
|
||||
await plugin.saveSettings();
|
||||
}}
|
||||
>
|
||||
<option value>Select Option</option>
|
||||
<option value={MetricCounter.words}>Words</option>
|
||||
<option value={MetricCounter.characters}>Characters</option>
|
||||
<option value={MetricCounter.sentences}>Sentences</option>
|
||||
<option value={MetricCounter.footnotes}>Footnotes</option>
|
||||
<option value={MetricCounter.citations}>Citations</option>
|
||||
<option value={MetricCounter.pages}>Pages</option>
|
||||
<option value={MetricCounter.files}>Files</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">Metric Type</div>
|
||||
<div class="setting-item-description">
|
||||
Select the type of metric that you want displayed.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
<select
|
||||
class="dropdown"
|
||||
value={item.metric.type}
|
||||
on:change={async (e) => {
|
||||
const {value} = e.target;
|
||||
item.metric.type = MetricType[MetricType[value]];
|
||||
await update(statusItems);
|
||||
await plugin.saveSettings();
|
||||
}}
|
||||
>
|
||||
<option value>Select Option</option>
|
||||
<option value={MetricType.file}>Current Note</option>
|
||||
<option value={MetricType.daily}>Daily Metric</option>
|
||||
<option value={MetricType.total}>Total in Vault</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">Prefix Text</div>
|
||||
<div class="setting-item-description">
|
||||
This is the text that is placed before the count.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
<input
|
||||
type="text"
|
||||
name="prefix"
|
||||
value={item.prefix}
|
||||
on:change={async (e) => {
|
||||
const { value } = e.target;
|
||||
item.prefix = value;
|
||||
await update(statusItems);
|
||||
await plugin.saveSettings();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">Suffix Text</div>
|
||||
<div class="setting-item-description">
|
||||
This is the text that is placed after the count.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
<input
|
||||
type="text"
|
||||
name="suffix"
|
||||
value={item.suffix}
|
||||
on:change={async (e) => {
|
||||
const { value } = e.target;
|
||||
item.suffix = value;
|
||||
await update(statusItems);
|
||||
await plugin.saveSettings();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
<h4>Alternative Status Bar</h4>
|
||||
<p>Here you can customize what statistics are displayed on the status bar when not editing a markdown file.</p>
|
||||
<div class="bwc-sb-buttons">
|
||||
<button
|
||||
aria-label="Add New Status Bar Item"
|
||||
on:click={async () => (altSItems = [...altSItems, JSON.parse(JSON.stringify(BLANK_SB_ITEM))])}
|
||||
>
|
||||
<div class="icon">
|
||||
Add Item
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reset Status Bar to Default"
|
||||
on:click={async () => {
|
||||
altSItems = [
|
||||
{
|
||||
prefix: "",
|
||||
suffix: " files",
|
||||
metric: {
|
||||
type: MetricType.total,
|
||||
counter: MetricCounter.files,
|
||||
},
|
||||
},
|
||||
];
|
||||
await update(statusItems);
|
||||
}}
|
||||
>
|
||||
<div class="icon">
|
||||
Reset
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{#each altSItems as item, i}
|
||||
<details class="bwc-sb-item-setting">
|
||||
<summary>
|
||||
<span class="bwc-sb-item-text">
|
||||
{metricToString(item.metric)}
|
||||
</span>
|
||||
<span class="bwc-sb-buttons">
|
||||
{#if i !== 0}
|
||||
<button
|
||||
aria-label="Move Status Bar Item Up"
|
||||
on:click={async () => {
|
||||
altSItems = swapStatusBarItems(i, i-1, altSItems);
|
||||
await updateAlt(altSItems);
|
||||
}}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
{/if}
|
||||
{#if i !== altSItems.length - 1}
|
||||
<button
|
||||
aria-label="Move Status Bar Item Down"
|
||||
on:click={async () => {
|
||||
altSItems = swapStatusBarItems(i, i+1, altSItems);
|
||||
await updateAlt(altSItems);
|
||||
}}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
aria-label="Remove Status Bar Item"
|
||||
on:click={async () => {
|
||||
altSItems = altSItems.filter((item, j) => i !== j);
|
||||
await updateAlt(altSItems);
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">Metric Counter</div>
|
||||
<div class="setting-item-description">
|
||||
Select the counter to display, e.g. words, characters.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
<select
|
||||
class="dropdown"
|
||||
value={item.metric.counter}
|
||||
on:change={async (e) => {
|
||||
const {value} = e.target;
|
||||
item.metric.counter = MetricCounter[MetricCounter[value]];
|
||||
await updateAlt(altSItems);
|
||||
await plugin.saveSettings();
|
||||
}}
|
||||
>
|
||||
<option value>Select Option</option>
|
||||
<option value={MetricCounter.words}>Words</option>
|
||||
<option value={MetricCounter.characters}>Characters</option>
|
||||
<option value={MetricCounter.sentences}>Sentences</option>
|
||||
<option value={MetricCounter.footnotes}>Footnotes</option>
|
||||
<option value={MetricCounter.citations}>Citations</option>
|
||||
<option value={MetricCounter.pages}>Pages</option>
|
||||
<option value={MetricCounter.files}>Files</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">Metric Type</div>
|
||||
<div class="setting-item-description">
|
||||
Select the type of metric that you want displayed.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
<select
|
||||
class="dropdown"
|
||||
value={item.metric.type}
|
||||
on:change={async (e) => {
|
||||
const {value} = e.target;
|
||||
item.metric.type = MetricType[MetricType[value]];
|
||||
await updateAlt(altSItems);
|
||||
await plugin.saveSettings();
|
||||
}}
|
||||
>
|
||||
<option value>Select Option</option>
|
||||
<option value={MetricType.file}>Current Note</option>
|
||||
<option value={MetricType.daily}>Daily Metric</option>
|
||||
<option value={MetricType.total}>Total in Vault</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">Prefix Text</div>
|
||||
<div class="setting-item-description">
|
||||
This is the text that is placed before the count.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
<input
|
||||
type="text"
|
||||
name="prefix"
|
||||
value={item.prefix}
|
||||
on:change={async (e) => {
|
||||
const { value } = e.target;
|
||||
item.prefix = value;
|
||||
await updateAlt(altSItems);
|
||||
await plugin.saveSettings();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">Suffix Text</div>
|
||||
<div class="setting-item-description">
|
||||
This is the text that is placed after the count.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
<input
|
||||
type="text"
|
||||
name="suffix"
|
||||
value={item.suffix}
|
||||
on:change={async (e) => {
|
||||
const { value } = e.target;
|
||||
item.suffix = value;
|
||||
await updateAlt(altSItems);
|
||||
await plugin.saveSettings();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
14
src/settings/StatusBarSettings.ts
Normal file
14
src/settings/StatusBarSettings.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import StatusBarSettings from "./StatusBarSettings.svelte";
|
||||
import type BetterWordCount from "../main";
|
||||
|
||||
export function addStatusBarSettings(
|
||||
plugin: BetterWordCount,
|
||||
containerEl: HTMLElement
|
||||
) {
|
||||
const statusItemsEl = containerEl.createEl("div");
|
||||
|
||||
new StatusBarSettings({
|
||||
target: statusItemsEl,
|
||||
props: { plugin },
|
||||
});
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
import {
|
||||
App,
|
||||
DropdownComponent,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
TextAreaComponent,
|
||||
ToggleComponent,
|
||||
} from "obsidian";
|
||||
import type BetterWordCount from "src/main";
|
||||
import { PRESETS, PresetOption } from "../settings/settings";
|
||||
|
||||
export class BetterWordCountSettingsTab extends PluginSettingTab {
|
||||
private disableTextAreas: boolean;
|
||||
|
||||
constructor(app: App, private plugin: BetterWordCount) {
|
||||
super(app, plugin);
|
||||
this.disableTextAreas =
|
||||
this.plugin.settings.preset.name === "custom" ? false : true;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
let { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "Better Word Count Settings" });
|
||||
|
||||
// General Settings
|
||||
containerEl.createEl("h3", { text: "General Settings" });
|
||||
new Setting(containerEl)
|
||||
.setName("Collect Statistics")
|
||||
.setDesc(
|
||||
"Reload Required for change to take effect. Turn on to start collecting daily statistics of your writing. Stored in the .vault-stats file in the root of your vault. This is required for counts of the day."
|
||||
)
|
||||
.addToggle((cb: ToggleComponent) => {
|
||||
cb.setValue(this.plugin.settings.collectStats);
|
||||
cb.onChange(async (value: boolean) => {
|
||||
this.plugin.settings.collectStats = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Don't Count Comments")
|
||||
.setDesc("Turn on if you don't want markdown comments to be counted.")
|
||||
.addToggle((cb: ToggleComponent) => {
|
||||
cb.setValue(this.plugin.settings.countComments);
|
||||
cb.onChange(async (value: boolean) => {
|
||||
this.plugin.settings.countComments = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Status Bar Settings
|
||||
containerEl.createEl("h3", { text: "Status Bar Settings" });
|
||||
new Setting(containerEl)
|
||||
.setName("Select a Preset")
|
||||
.setDesc(
|
||||
"Presets are premade status bar expressions. Overides status bar settings."
|
||||
)
|
||||
.addDropdown((cb: DropdownComponent) => {
|
||||
PRESETS.forEach((preset: PresetOption) => {
|
||||
cb.addOption(preset.name, preset.name);
|
||||
});
|
||||
cb.setValue(this.plugin.settings.preset.name);
|
||||
|
||||
cb.onChange(async (value: string) => {
|
||||
let newPreset = PRESETS.find((preset) => preset.name === value);
|
||||
this.plugin.settings.preset = newPreset;
|
||||
this.plugin.settings.statusBarQuery = newPreset.statusBarQuery;
|
||||
this.plugin.settings.statusBarAltQuery = newPreset.statusBarAltQuery;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Status Bar Text")
|
||||
.setDesc("Customize the Status Bar text with this.")
|
||||
.addTextArea((cb: TextAreaComponent) => {
|
||||
cb.setPlaceholder("Enter an expression...");
|
||||
cb.setValue(this.plugin.settings.statusBarQuery);
|
||||
cb.onChange((value: string) => {
|
||||
let newPreset = PRESETS.find((preset) => preset.name === "custom");
|
||||
this.plugin.settings.preset = newPreset;
|
||||
this.plugin.settings.statusBarQuery = value;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Alternative Status Bar Text")
|
||||
.setDesc("Customize the Alternative Status Bar text with this.")
|
||||
.addTextArea((cb: TextAreaComponent) => {
|
||||
cb.setPlaceholder("Enter an expression...");
|
||||
cb.setValue(this.plugin.settings.statusBarAltQuery);
|
||||
cb.onChange((value: string) => {
|
||||
let newPreset = PRESETS.find((preset) => preset.name === "custom");
|
||||
this.plugin.settings.preset = newPreset;
|
||||
this.plugin.settings.statusBarAltQuery = value;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
this.containerEl.createEl("h3", {
|
||||
text: "Syntax for the status bars works like this: ",
|
||||
});
|
||||
|
||||
this.containerEl.createEl("li", {
|
||||
text: "To get a stat input the name of the stat in between `{}` eg. `{word_count}`.",
|
||||
});
|
||||
|
||||
this.containerEl.createEl("li", {
|
||||
text: "All other words remain.",
|
||||
});
|
||||
|
||||
this.containerEl.createEl("br");
|
||||
|
||||
this.containerEl.createEl("h4", {
|
||||
text: "Available Stats:",
|
||||
});
|
||||
|
||||
this.containerEl.createEl("p", {
|
||||
text:
|
||||
"word_count, " +
|
||||
"character_count, " +
|
||||
"sentence_count, " +
|
||||
"total_word_count, " +
|
||||
"total_character_count, " +
|
||||
"total_sentence_count, " +
|
||||
"file_count, " +
|
||||
"words_today, " +
|
||||
"characters_today, " +
|
||||
"sentences_today, ",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
export const DEFAULT_SETTINGS: BetterWordCountSettings = {
|
||||
preset: {
|
||||
name: "default",
|
||||
statusBarQuery: "{word_count} words {character_count} characters",
|
||||
statusBarAltQuery:
|
||||
"{file_count} files {total_word_count} words {total_character_count} characters",
|
||||
},
|
||||
statusBarQuery: "{word_count} words {character_count} characters",
|
||||
statusBarAltQuery:
|
||||
"{file_count} files {total_word_count} words {total_character_count} characters",
|
||||
countComments: false,
|
||||
collectStats: false,
|
||||
};
|
||||
|
||||
export const PRESETS: PresetOption[] = [
|
||||
{
|
||||
name: "default",
|
||||
statusBarQuery: "{word_count} words {character_count} characters",
|
||||
statusBarAltQuery:
|
||||
"{file_count} files {total_word_count} words {total_character_count} characters",
|
||||
},
|
||||
{
|
||||
name: "minimal",
|
||||
statusBarQuery: "w: {word_count} c: {character_count}",
|
||||
statusBarAltQuery:
|
||||
"f: {file_count} tw: {total_word_count} tc: {total_character_count}",
|
||||
},
|
||||
{
|
||||
name: "custom",
|
||||
statusBarQuery: "",
|
||||
statusBarAltQuery: "",
|
||||
},
|
||||
];
|
||||
|
||||
export interface BetterWordCountSettings {
|
||||
preset: PresetOption;
|
||||
statusBarQuery: string;
|
||||
statusBarAltQuery: string;
|
||||
countComments: boolean;
|
||||
collectStats: boolean;
|
||||
}
|
||||
|
||||
export interface PresetOption {
|
||||
name: string;
|
||||
statusBarQuery: string;
|
||||
statusBarAltQuery: string;
|
||||
}
|
38
src/stats/Stats.ts
Normal file
38
src/stats/Stats.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
export interface VaultStatistics {
|
||||
history: History;
|
||||
modifiedFiles: ModifiedFiles;
|
||||
}
|
||||
|
||||
export type History = Record<string, Day>;
|
||||
|
||||
export interface Day {
|
||||
words: number;
|
||||
characters: number;
|
||||
sentences: number;
|
||||
pages: number;
|
||||
files: number;
|
||||
footnotes: number;
|
||||
citations: number;
|
||||
totalWords: number;
|
||||
totalCharacters: number;
|
||||
totalSentences: number;
|
||||
totalFootnotes: number;
|
||||
totalCitations: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export type ModifiedFiles = Record<string, FileStat>;
|
||||
|
||||
export interface FileStat {
|
||||
footnotes: CountDiff;
|
||||
citations: CountDiff;
|
||||
words: CountDiff;
|
||||
characters: CountDiff;
|
||||
sentences: CountDiff;
|
||||
pages: CountDiff;
|
||||
}
|
||||
|
||||
export interface CountDiff {
|
||||
initial: number;
|
||||
current: number;
|
||||
}
|
375
src/stats/StatsManager.ts
Normal file
375
src/stats/StatsManager.ts
Normal file
|
@ -0,0 +1,375 @@
|
|||
import { debounce, Debouncer, TFile, Vault, Workspace } from "obsidian";
|
||||
import type BetterWordCount from "../main";
|
||||
import { STATS_FILE } from "../constants";
|
||||
import type { Day, VaultStatistics } from "./Stats";
|
||||
import moment from "moment";
|
||||
import {
|
||||
getCharacterCount,
|
||||
getSentenceCount,
|
||||
getPageCount,
|
||||
getWordCount,
|
||||
getCitationCount,
|
||||
getFootnoteCount,
|
||||
cleanComments,
|
||||
} from "../utils/StatUtils";
|
||||
|
||||
export default class StatsManager {
|
||||
private vault: Vault;
|
||||
private workspace: Workspace;
|
||||
private plugin: BetterWordCount;
|
||||
private vaultStats: VaultStatistics;
|
||||
private today: string;
|
||||
public debounceChange;
|
||||
|
||||
constructor(vault: Vault, workspace: Workspace, plugin: BetterWordCount) {
|
||||
this.vault = vault;
|
||||
this.workspace = workspace;
|
||||
this.plugin = plugin;
|
||||
this.debounceChange = debounce(
|
||||
(text: string) => this.change(text),
|
||||
50,
|
||||
false
|
||||
);
|
||||
|
||||
this.vault.on("rename", (new_name, old_path) => {
|
||||
if (this.vaultStats.modifiedFiles.hasOwnProperty(old_path)) {
|
||||
const content = this.vaultStats.modifiedFiles[old_path];
|
||||
delete this.vaultStats.modifiedFiles[old_path];
|
||||
this.vaultStats.modifiedFiles[new_name.path] = content;
|
||||
}
|
||||
});
|
||||
|
||||
this.vault.on("delete", (deleted_file) => {
|
||||
if (this.vaultStats.modifiedFiles.hasOwnProperty(deleted_file.path)) {
|
||||
delete this.vaultStats.modifiedFiles[deleted_file.path];
|
||||
}
|
||||
});
|
||||
|
||||
this.vault.adapter.exists(STATS_FILE).then(async (exists) => {
|
||||
if (!exists) {
|
||||
const vaultSt: VaultStatistics = {
|
||||
history: {},
|
||||
modifiedFiles: {},
|
||||
};
|
||||
await this.vault.adapter.write(STATS_FILE, JSON.stringify(vaultSt));
|
||||
this.vaultStats = JSON.parse(await this.vault.adapter.read(STATS_FILE));
|
||||
} else {
|
||||
this.vaultStats = JSON.parse(await this.vault.adapter.read(STATS_FILE));
|
||||
if (!this.vaultStats.hasOwnProperty("history")) {
|
||||
const vaultSt: VaultStatistics = {
|
||||
history: {},
|
||||
modifiedFiles: {},
|
||||
};
|
||||
await this.vault.adapter.write(STATS_FILE, JSON.stringify(vaultSt));
|
||||
}
|
||||
this.vaultStats = JSON.parse(await this.vault.adapter.read(STATS_FILE));
|
||||
}
|
||||
|
||||
await this.updateToday();
|
||||
});
|
||||
}
|
||||
|
||||
async update(): Promise<void> {
|
||||
this.vault.adapter.write(STATS_FILE, JSON.stringify(this.vaultStats));
|
||||
}
|
||||
|
||||
async updateToday(): Promise<void> {
|
||||
if (this.vaultStats.history.hasOwnProperty(moment().format("YYYY-MM-DD"))) {
|
||||
this.today = moment().format("YYYY-MM-DD");
|
||||
return;
|
||||
}
|
||||
|
||||
this.today = moment().format("YYYY-MM-DD");
|
||||
const totalWords = await this.calcTotalWords();
|
||||
const totalCharacters = await this.calcTotalCharacters();
|
||||
const totalSentences = await this.calcTotalSentences();
|
||||
const totalFootnotes = await this.calcTotalFootnotes();
|
||||
const totalCitations = await this.calcTotalCitations();
|
||||
const totalPages = await this.calcTotalPages();
|
||||
|
||||
const newDay: Day = {
|
||||
words: 0,
|
||||
characters: 0,
|
||||
sentences: 0,
|
||||
pages: 0,
|
||||
files: 0,
|
||||
footnotes: 0,
|
||||
citations: 0,
|
||||
totalWords: totalWords,
|
||||
totalCharacters: totalCharacters,
|
||||
totalSentences: totalSentences,
|
||||
totalFootnotes: totalFootnotes,
|
||||
totalCitations: totalCitations,
|
||||
totalPages: totalPages,
|
||||
};
|
||||
|
||||
this.vaultStats.modifiedFiles = {};
|
||||
this.vaultStats.history[this.today] = newDay;
|
||||
await this.update();
|
||||
}
|
||||
|
||||
public async change(text: string) {
|
||||
if (this.plugin.settings.countComments) {
|
||||
text = cleanComments(text);
|
||||
}
|
||||
const fileName = this.workspace.getActiveFile().path;
|
||||
const currentWords = getWordCount(text);
|
||||
const currentCharacters = getCharacterCount(text);
|
||||
const currentSentences = getSentenceCount(text);
|
||||
const currentCitations = getCitationCount(text);
|
||||
const currentFootnotes = getFootnoteCount(text);
|
||||
const currentPages = getPageCount(text, this.plugin.settings.pageWords);
|
||||
|
||||
if (
|
||||
this.vaultStats.history.hasOwnProperty(this.today) &&
|
||||
this.today === moment().format("YYYY-MM-DD")
|
||||
) {
|
||||
let modFiles = this.vaultStats.modifiedFiles;
|
||||
|
||||
if (modFiles.hasOwnProperty(fileName)) {
|
||||
this.vaultStats.history[this.today].totalWords +=
|
||||
currentWords - modFiles[fileName].words.current;
|
||||
this.vaultStats.history[this.today].totalCharacters +=
|
||||
currentCharacters - modFiles[fileName].characters.current;
|
||||
this.vaultStats.history[this.today].totalSentences +=
|
||||
currentSentences - modFiles[fileName].sentences.current;
|
||||
this.vaultStats.history[this.today].totalFootnotes +=
|
||||
currentSentences - modFiles[fileName].footnotes.current;
|
||||
this.vaultStats.history[this.today].totalCitations +=
|
||||
currentSentences - modFiles[fileName].citations.current;
|
||||
this.vaultStats.history[this.today].totalPages +=
|
||||
currentPages - modFiles[fileName].pages.current;
|
||||
|
||||
modFiles[fileName].words.current = currentWords;
|
||||
modFiles[fileName].characters.current = currentCharacters;
|
||||
modFiles[fileName].sentences.current = currentSentences;
|
||||
modFiles[fileName].footnotes.current = currentFootnotes;
|
||||
modFiles[fileName].citations.current = currentCitations;
|
||||
modFiles[fileName].pages.current = currentPages;
|
||||
} else {
|
||||
modFiles[fileName] = {
|
||||
words: {
|
||||
initial: currentWords,
|
||||
current: currentWords,
|
||||
},
|
||||
characters: {
|
||||
initial: currentCharacters,
|
||||
current: currentCharacters,
|
||||
},
|
||||
sentences: {
|
||||
initial: currentSentences,
|
||||
current: currentSentences,
|
||||
},
|
||||
footnotes: {
|
||||
initial: currentFootnotes,
|
||||
current: currentFootnotes,
|
||||
},
|
||||
citations: {
|
||||
initial: currentCitations,
|
||||
current: currentCitations,
|
||||
},
|
||||
pages: {
|
||||
initial: currentPages,
|
||||
current: currentPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const words = Object.values(modFiles)
|
||||
.map((counts) =>
|
||||
Math.max(0, counts.words.current - counts.words.initial)
|
||||
)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const characters = Object.values(modFiles)
|
||||
.map((counts) =>
|
||||
Math.max(0, counts.characters.current - counts.characters.initial)
|
||||
)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const sentences = Object.values(modFiles)
|
||||
.map((counts) =>
|
||||
Math.max(0, counts.sentences.current - counts.sentences.initial)
|
||||
)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
const footnotes = Object.values(modFiles)
|
||||
.map((counts) =>
|
||||
Math.max(0, counts.footnotes.current - counts.footnotes.initial)
|
||||
)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const citations = Object.values(modFiles)
|
||||
.map((counts) =>
|
||||
Math.max(0, counts.citations.current - counts.citations.initial)
|
||||
).reduce((a, b) => a + b, 0);
|
||||
const pages = Object.values(modFiles)
|
||||
.map((counts) =>
|
||||
Math.max(0, counts.pages.current - counts.pages.initial)
|
||||
)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
this.vaultStats.history[this.today].words = words;
|
||||
this.vaultStats.history[this.today].characters = characters;
|
||||
this.vaultStats.history[this.today].sentences = sentences;
|
||||
this.vaultStats.history[this.today].footnotes = footnotes;
|
||||
this.vaultStats.history[this.today].citations = citations;
|
||||
this.vaultStats.history[this.today].pages = pages;
|
||||
this.vaultStats.history[this.today].files = this.getTotalFiles();
|
||||
|
||||
await this.update();
|
||||
} else {
|
||||
this.updateToday();
|
||||
}
|
||||
}
|
||||
|
||||
public async recalcTotals() {
|
||||
if (!this.vaultStats) return;
|
||||
if (
|
||||
this.vaultStats.history.hasOwnProperty(this.today) &&
|
||||
this.today === moment().format("YYYY-MM-DD")
|
||||
) {
|
||||
const todayHist: Day = this.vaultStats.history[this.today];
|
||||
todayHist.totalWords = await this.calcTotalWords();
|
||||
todayHist.totalCharacters = await this.calcTotalCharacters();
|
||||
todayHist.totalSentences = await this.calcTotalSentences();
|
||||
todayHist.totalFootnotes = await this.calcTotalFootnotes();
|
||||
todayHist.totalCitations = await this.calcTotalCitations();
|
||||
todayHist.totalPages = await this.calcTotalPages();
|
||||
this.update();
|
||||
} else {
|
||||
this.updateToday();
|
||||
}
|
||||
}
|
||||
|
||||
private async calcTotalWords(): Promise<number> {
|
||||
let words = 0;
|
||||
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
words += getWordCount(await this.vault.cachedRead(file));
|
||||
}
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
private async calcTotalCharacters(): Promise<number> {
|
||||
let characters = 0;
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
characters += getCharacterCount(await this.vault.cachedRead(file));
|
||||
}
|
||||
}
|
||||
return characters;
|
||||
}
|
||||
|
||||
private async calcTotalSentences(): Promise<number> {
|
||||
let sentence = 0;
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
sentence += getSentenceCount(await this.vault.cachedRead(file));
|
||||
}
|
||||
}
|
||||
return sentence;
|
||||
}
|
||||
|
||||
private async calcTotalPages(): Promise<number> {
|
||||
let pages = 0;
|
||||
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
pages += getPageCount(await this.vault.cachedRead(file), this.plugin.settings.pageWords);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
private async calcTotalFootnotes(): Promise<number> {
|
||||
let footnotes = 0;
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
footnotes += getFootnoteCount(await this.vault.cachedRead(file));
|
||||
}
|
||||
}
|
||||
return footnotes;
|
||||
}
|
||||
|
||||
private async calcTotalCitations(): Promise<number> {
|
||||
let citations = 0;
|
||||
const files = this.vault.getFiles();
|
||||
for (const i in files) {
|
||||
const file = files[i];
|
||||
if (file.extension === "md") {
|
||||
citations += getCitationCount(await this.vault.cachedRead(file));
|
||||
}
|
||||
}
|
||||
return citations;
|
||||
}
|
||||
|
||||
public getDailyWords(): number {
|
||||
return this.vaultStats.history[this.today].words;
|
||||
}
|
||||
|
||||
public getDailyCharacters(): number {
|
||||
return this.vaultStats.history[this.today].characters;
|
||||
}
|
||||
|
||||
public getDailySentences(): number {
|
||||
return this.vaultStats.history[this.today].sentences;
|
||||
}
|
||||
|
||||
|
||||
public getDailyFootnotes(): number {
|
||||
return this.vaultStats.history[this.today].footnotes;
|
||||
}
|
||||
|
||||
public getDailyCitations(): number {
|
||||
return this.vaultStats.history[this.today].citations;
|
||||
}
|
||||
public getDailyPages(): number {
|
||||
return this.vaultStats.history[this.today].pages;
|
||||
}
|
||||
|
||||
public getTotalFiles(): number {
|
||||
return this.vault.getMarkdownFiles().length;
|
||||
}
|
||||
|
||||
public async getTotalWords(): Promise<number> {
|
||||
if (!this.vaultStats) return await this.calcTotalWords();
|
||||
return this.vaultStats.history[this.today].totalWords;
|
||||
}
|
||||
|
||||
public async getTotalCharacters(): Promise<number> {
|
||||
if (!this.vaultStats) return await this.calcTotalCharacters();
|
||||
return this.vaultStats.history[this.today].totalCharacters;
|
||||
}
|
||||
|
||||
public async getTotalSentences(): Promise<number> {
|
||||
if (!this.vaultStats) return await this.calcTotalSentences();
|
||||
return this.vaultStats.history[this.today].totalSentences;
|
||||
}
|
||||
|
||||
public async getTotalFootnotes(): Promise<number> {
|
||||
if (!this.vaultStats) return await this.calcTotalFootnotes();
|
||||
return this.vaultStats.history[this.today].totalFootnotes;
|
||||
}
|
||||
|
||||
public async getTotalCitations(): Promise<number> {
|
||||
if (!this.vaultStats) return await this.calcTotalCitations();
|
||||
return this.vaultStats.history[this.today].totalCitations;
|
||||
}
|
||||
|
||||
public async getTotalPages(): Promise<number> {
|
||||
if (!this.vaultStats) return await this.calcTotalPages();
|
||||
return this.vaultStats.history[this.today].totalPages;
|
||||
}
|
||||
}
|
371
src/status/StatusBar.ts
Normal file
371
src/status/StatusBar.ts
Normal file
|
@ -0,0 +1,371 @@
|
|||
import { MetricCounter, MetricType } from "src/settings/Settings";
|
||||
import type BetterWordCount from "../main";
|
||||
import {
|
||||
getWordCount,
|
||||
getCharacterCount,
|
||||
getSentenceCount,
|
||||
getCitationCount,
|
||||
getFootnoteCount,
|
||||
getPageCount,
|
||||
cleanComments,
|
||||
} from "src/utils/StatUtils";
|
||||
import { debounce } from "obsidian";
|
||||
|
||||
export default class StatusBar {
|
||||
private statusBarEl: HTMLElement;
|
||||
private plugin: BetterWordCount;
|
||||
public debounceStatusBarUpdate;
|
||||
|
||||
constructor(statusBarEl: HTMLElement, plugin: BetterWordCount) {
|
||||
this.statusBarEl = statusBarEl;
|
||||
this.plugin = plugin;
|
||||
this.debounceStatusBarUpdate = debounce(
|
||||
(text: string) => this.updateStatusBar(text),
|
||||
20,
|
||||
false
|
||||
);
|
||||
|
||||
this.statusBarEl.classList.add("mod-clickable");
|
||||
this.statusBarEl.setAttribute("aria-label", "!!!");
|
||||
this.statusBarEl.setAttribute("aria-label-position", "top");
|
||||
this.statusBarEl.addEventListener("click", (ev: MouseEvent) =>
|
||||
this.onClick(ev)
|
||||
);
|
||||
}
|
||||
|
||||
onClick(ev: MouseEvent) {
|
||||
ev;
|
||||
}
|
||||
|
||||
displayText(text: string) {
|
||||
this.statusBarEl.setText(text);
|
||||
}
|
||||
|
||||
async updateStatusBar(text: string) {
|
||||
const sb = this.plugin.settings.statusBar;
|
||||
let display = "";
|
||||
|
||||
if (!this.plugin.settings.countComments) {
|
||||
text = cleanComments(text);
|
||||
}
|
||||
|
||||
for (let i = 0; i < sb.length; i++) {
|
||||
const sbItem = sb[i];
|
||||
|
||||
display = display + sbItem.prefix;
|
||||
const metric = sbItem.metric;
|
||||
|
||||
if (metric.counter === MetricCounter.words) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + getWordCount(text);
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyWords()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalWords()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.characters) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + getCharacterCount(text);
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyCharacters()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalCharacters()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.sentences) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + getSentenceCount(text);
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailySentences()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalSentences()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.footnotes) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + getFootnoteCount(text);
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyFootnotes()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalFootnotes()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.citations) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + getCitationCount(text);
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyCitations()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalCitations()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.pages) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + getPageCount(text, this.plugin.settings.pageWords);
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyPages()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalPages()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.files) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalFiles()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalFiles()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalFiles()
|
||||
: 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
display = display + sbItem.suffix;
|
||||
}
|
||||
|
||||
this.displayText(display);
|
||||
}
|
||||
|
||||
async updateAltBar() {
|
||||
const ab = this.plugin.settings.altBar;
|
||||
let display = "";
|
||||
|
||||
for (let i = 0; i < ab.length; i++) {
|
||||
const sbItem = ab[i];
|
||||
|
||||
display = display + sbItem.prefix;
|
||||
const metric = sbItem.metric;
|
||||
|
||||
if (metric.counter === MetricCounter.words) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + 0;
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyWords()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalWords()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.characters) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + 0;
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyCharacters()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalCharacters()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.sentences) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + 0;
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailySentences()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalSentences()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.footnotes) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + 0;
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyFootnotes()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalFootnotes()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.citations) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + 0;
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyCitations()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalCitations()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.pages) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display = display + 0;
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getDailyPages()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(await (this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalPages()
|
||||
: 0));
|
||||
break;
|
||||
}
|
||||
} else if (metric.counter === MetricCounter.files) {
|
||||
switch (metric.type) {
|
||||
case MetricType.file:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalFiles()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.daily:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalFiles()
|
||||
: 0);
|
||||
break;
|
||||
case MetricType.total:
|
||||
display =
|
||||
display +
|
||||
(this.plugin.settings.collectStats
|
||||
? this.plugin.statsManager.getTotalFiles()
|
||||
: 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
display = display + sbItem.suffix;
|
||||
}
|
||||
|
||||
this.displayText(display);
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
export class StatusBar {
|
||||
private statusBarEl: HTMLElement;
|
||||
|
||||
constructor(statusBarEl: HTMLElement) {
|
||||
this.statusBarEl = statusBarEl;
|
||||
}
|
||||
|
||||
displayText(text: string) {
|
||||
this.statusBarEl.setText(text);
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
import { MetadataCache, Vault, Debouncer, debounce } from "obsidian";
|
||||
import { DataCollector } from "src/data/collector";
|
||||
import { DataManager } from "src/data/manager";
|
||||
import type { TodayCounts } from "src/data/manager";
|
||||
import type { BetterWordCountSettings } from "src/settings/settings";
|
||||
import {
|
||||
getWordCount,
|
||||
getCharacterCount,
|
||||
getSentenceCount,
|
||||
cleanComments,
|
||||
} from "../data/stats";
|
||||
import type { StatusBar } from "./bar";
|
||||
import { Expression, parse } from "./parse";
|
||||
|
||||
export class BarManager {
|
||||
private statusBar: StatusBar;
|
||||
private settings: BetterWordCountSettings;
|
||||
private vault: Vault;
|
||||
private dataCollector: DataCollector;
|
||||
private dataManager: DataManager;
|
||||
private deboucer: Debouncer<[text: string]>;
|
||||
private expression: Expression;
|
||||
|
||||
constructor(
|
||||
statusBar: StatusBar,
|
||||
settings: BetterWordCountSettings,
|
||||
vault: Vault,
|
||||
metadataCache: MetadataCache
|
||||
) {
|
||||
this.statusBar = statusBar;
|
||||
this.settings = settings;
|
||||
this.vault = vault;
|
||||
this.dataCollector = new DataCollector(vault, metadataCache);
|
||||
this.dataManager = new DataManager(vault, metadataCache);
|
||||
this.deboucer = debounce(
|
||||
(text: string) => this.updateStatusBar(text),
|
||||
20,
|
||||
false
|
||||
);
|
||||
this.expression = parse(this.settings.statusBarQuery);
|
||||
}
|
||||
|
||||
updateStatusBar(text: string): void {
|
||||
let newText = "";
|
||||
|
||||
if (this.settings.collectStats) {
|
||||
this.dataManager.updateTodayCounts();
|
||||
}
|
||||
const todayCounts: TodayCounts = this.settings.collectStats
|
||||
? this.dataManager.getTodayCounts()
|
||||
: { words: 0, characters: 0, sentences: 0 };
|
||||
|
||||
let varsIndex = 0;
|
||||
for (const i in this.expression.parsed) {
|
||||
const e = this.expression.parsed[i];
|
||||
newText = newText + e;
|
||||
switch (this.expression.vars[varsIndex]) {
|
||||
case 0:
|
||||
newText = newText + getWordCount(text);
|
||||
break;
|
||||
case 1:
|
||||
newText = newText + getCharacterCount(text);
|
||||
break;
|
||||
case 2:
|
||||
newText = newText + getSentenceCount(text);
|
||||
break;
|
||||
case 3:
|
||||
newText = newText + this.dataManager.getTotalCounts().words;
|
||||
break;
|
||||
case 4:
|
||||
newText = newText + this.dataManager.getTotalCounts().characters;
|
||||
break;
|
||||
case 5:
|
||||
newText = newText + this.dataManager.getTotalCounts().sentences;
|
||||
break;
|
||||
case 6:
|
||||
newText = newText + this.dataCollector.getTotalFileCount();
|
||||
break;
|
||||
case 7:
|
||||
newText = newText + todayCounts.words;
|
||||
break;
|
||||
case 8:
|
||||
newText = newText + todayCounts.characters;
|
||||
break;
|
||||
case 9:
|
||||
newText = newText + todayCounts.sentences;
|
||||
break;
|
||||
}
|
||||
varsIndex++;
|
||||
}
|
||||
|
||||
this.statusBar.displayText(newText);
|
||||
}
|
||||
|
||||
async updateAltStatusBar(): Promise<void> {
|
||||
let newText = "";
|
||||
const expression: Expression = parse(this.settings.statusBarAltQuery);
|
||||
if (this.settings.collectStats) {
|
||||
this.dataManager.updateTodayCounts();
|
||||
}
|
||||
|
||||
const todayCounts: TodayCounts = this.settings.collectStats
|
||||
? this.dataManager.getTodayCounts()
|
||||
: { words: 0, characters: 0, sentences: 0 };
|
||||
|
||||
let varsIndex = 0;
|
||||
for (const i in expression.parsed) {
|
||||
const e = expression.parsed[i];
|
||||
newText = newText + e;
|
||||
switch (expression.vars[varsIndex]) {
|
||||
case 0:
|
||||
newText = newText + getWordCount("");
|
||||
break;
|
||||
case 1:
|
||||
newText = newText + getCharacterCount("");
|
||||
break;
|
||||
case 2:
|
||||
newText = newText + getSentenceCount("");
|
||||
break;
|
||||
case 3:
|
||||
newText = newText + (await this.dataCollector.getTotalWordCount());
|
||||
break;
|
||||
case 4:
|
||||
newText =
|
||||
newText + (await this.dataCollector.getTotalCharacterCount());
|
||||
break;
|
||||
case 5:
|
||||
newText =
|
||||
newText + (await this.dataCollector.getTotalSentenceCount());
|
||||
break;
|
||||
case 6:
|
||||
newText = newText + this.dataCollector.getTotalFileCount();
|
||||
break;
|
||||
case 7:
|
||||
newText = newText + todayCounts.words;
|
||||
break;
|
||||
case 8:
|
||||
newText = newText + todayCounts.characters;
|
||||
break;
|
||||
case 9:
|
||||
newText = newText + todayCounts.sentences;
|
||||
break;
|
||||
}
|
||||
varsIndex++;
|
||||
}
|
||||
|
||||
this.statusBar.displayText(newText);
|
||||
}
|
||||
|
||||
cursorActivity(cm: CodeMirror.Editor) {
|
||||
console.log("detected cursor activity");
|
||||
if (cm.somethingSelected()) {
|
||||
if (this.settings.countComments) {
|
||||
this.deboucer(cleanComments(cm.getSelection()));
|
||||
} else {
|
||||
this.deboucer(cm.getSelection());
|
||||
}
|
||||
} else {
|
||||
if (this.settings.collectStats) {
|
||||
this.dataManager.updateFromFile();
|
||||
}
|
||||
if (this.settings.countComments) {
|
||||
this.deboucer(cleanComments(cm.getValue()));
|
||||
} else {
|
||||
this.deboucer(cm.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
const REGEX: RegExp = /{(.*?)}/g;
|
||||
export interface Expression {
|
||||
parsed: string[];
|
||||
vars: number[];
|
||||
}
|
||||
|
||||
// Could be done better
|
||||
export function parse(query: string): Expression {
|
||||
let parsed: string[] = [];
|
||||
let vars: number[] = [];
|
||||
|
||||
query.split(REGEX).forEach((s) => {
|
||||
switch (s) {
|
||||
case "word_count":
|
||||
vars.push(0);
|
||||
break;
|
||||
case "character_count":
|
||||
vars.push(1);
|
||||
break;
|
||||
case "sentence_count":
|
||||
vars.push(2);
|
||||
break;
|
||||
case "total_word_count":
|
||||
vars.push(3);
|
||||
break;
|
||||
case "total_character_count":
|
||||
vars.push(4);
|
||||
break;
|
||||
case "total_sentence_count":
|
||||
vars.push(5);
|
||||
break;
|
||||
case "file_count":
|
||||
vars.push(6);
|
||||
break;
|
||||
case "words_today":
|
||||
vars.push(7);
|
||||
break;
|
||||
case "characters_today":
|
||||
vars.push(8);
|
||||
break;
|
||||
case "sentences_today":
|
||||
vars.push(9);
|
||||
break;
|
||||
|
||||
default:
|
||||
parsed.push(s);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
parsed: parsed,
|
||||
vars: vars,
|
||||
};
|
||||
}
|
||||
|
||||
const varNames = {
|
||||
word_count: 0,
|
||||
charater_count: 1,
|
||||
sentence_count: 2,
|
||||
total_word_count: 3,
|
||||
total_charater_count: 4,
|
||||
total_sentence_count: 5,
|
||||
file_count: 6,
|
||||
};
|
21
src/styles.css
Normal file
21
src/styles.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
details.bwc-sb-item-setting {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 5px 20px 10px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.bwc-sb-item-setting summary::marker {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* .bwc-sb-item-setting summary { */
|
||||
/* margin-bottom: 5px; */
|
||||
/* } */
|
||||
.bwc-sb-item-setting summary span.bwc-sb-buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.bwc-status-bar-settings-title {
|
||||
margin-bottom: 0px;
|
||||
}
|
67
src/utils/StatUtils.ts
Normal file
67
src/utils/StatUtils.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import type { Vault } from "obsidian";
|
||||
import { MATCH_HTML_COMMENT, MATCH_COMMENT } from "src/constants";
|
||||
|
||||
export function getWordCount(text: string): number {
|
||||
const spaceDelimitedChars =
|
||||
/'’A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC/
|
||||
.source;
|
||||
const nonSpaceDelimitedWords =
|
||||
/\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5/.source;
|
||||
|
||||
const nonSpaceDelimitedWordsOther =
|
||||
/[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5]{1}/
|
||||
.source;
|
||||
|
||||
const pattern = new RegExp(
|
||||
[
|
||||
`(?:[0-9]+(?:(?:,|\\.)[0-9]+)*|[\\-${spaceDelimitedChars}])+`,
|
||||
nonSpaceDelimitedWords,
|
||||
nonSpaceDelimitedWordsOther,
|
||||
].join("|"),
|
||||
"g"
|
||||
);
|
||||
return (text.match(pattern) || []).length;
|
||||
}
|
||||
|
||||
export function getCharacterCount(text: string): number {
|
||||
return text.length;
|
||||
}
|
||||
|
||||
export function getFootnoteCount(text: string): number {
|
||||
const regularFn = text.match(/\[\^\S+](?!:)/g);
|
||||
const inlineFn = text.match(/\^\[[^^].+?]/g);
|
||||
|
||||
let overallFn = 0;
|
||||
if (regularFn) overallFn += regularFn.length;
|
||||
if (inlineFn) overallFn += inlineFn.length;
|
||||
return overallFn;
|
||||
}
|
||||
|
||||
export function getCitationCount(text: string): number {
|
||||
const pandocCitations = text.match(/@[A-Za-z0-9-]+[,;\]](?!\()/gi);
|
||||
if (!pandocCitations) return 0;
|
||||
const uniqueCitations = [...new Set(pandocCitations)].length;
|
||||
return uniqueCitations;
|
||||
}
|
||||
|
||||
export function getSentenceCount(text: string): number {
|
||||
const sentences: number = (
|
||||
(text || "").match(
|
||||
/[^.!?\s][^.!?]*(?:[.!?](?!['"]?\s|$)[^.!?]*)*[.!?]?['"]?(?=\s|$)/gm
|
||||
) || []
|
||||
).length;
|
||||
|
||||
return sentences;
|
||||
}
|
||||
|
||||
export function getPageCount(text: string, pageWords: number): number {
|
||||
return parseFloat((getWordCount(text) / pageWords).toFixed(1));
|
||||
}
|
||||
|
||||
export function getTotalFileCount(vault: Vault): number {
|
||||
return vault.getMarkdownFiles().length;
|
||||
}
|
||||
|
||||
export function cleanComments(text: string): string {
|
||||
return text.replace(MATCH_COMMENT, "").replace(MATCH_HTML_COMMENT, "");
|
||||
}
|
8
src/utils/SvelteStores.ts
Normal file
8
src/utils/SvelteStores.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import {
|
||||
BetterWordCountSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
} from "src/settings/Settings";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export const settingsStore =
|
||||
writable<BetterWordCountSettings>(DEFAULT_SETTINGS);
|
|
@ -1,9 +1,11 @@
|
|||
import { ItemView, WorkspaceLeaf } from "obsidian";
|
||||
import { STATS_ICON_NAME, VIEW_TYPE_STATS } from "src/constants";
|
||||
import type BetterWordCount from "src/main";
|
||||
//@ts-ignore
|
||||
import Statistics from "./Statistics.svelte";
|
||||
|
||||
export default class StatsView extends ItemView {
|
||||
private plugin: BetterWordCount;
|
||||
private statistics: Statistics;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"importHelpers": true
|
||||
// "lib": ["dom", "es5", "scripthost", "es2015"]
|
||||
},
|
||||
// "include": ["**/*.ts"],
|
||||
"include": ["**/main.ts", "**/bar.ts"],
|
||||
"include": ["**/*.ts", "**/*.svelte"],
|
||||
"exclude": ["node_modules/*"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue