Merge branch 'feature/reword-structure' into develop

This commit is contained in:
Luke Leppan 2021-07-08 22:23:27 +02:00
commit 5331623a5a
19 changed files with 791 additions and 278 deletions

4
.gitignore vendored
View file

@ -10,4 +10,6 @@ package-lock.json
main.js
*.js.map
test-vault/
*.zip
*.zip
dist/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -2,26 +2,26 @@
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/lukeleppan/better-word-count/Build%20Release?logo=github&style=for-the-badge) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/lukeleppan/better-word-count?style=for-the-badge) ![GitHub All Releases](https://img.shields.io/github/downloads/lukeleppan/better-word-count/total?style=for-the-badge)
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 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.
![Better Count Word](https://raw.githubusercontent.com/lukeleppan/better-word-count/master/assets/better-word-count.gif)
### Features
All Optional:
- 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.
- Highly Customizable status bar that can be adapted to your needs.
- Show word count (total when no text selected).
- Show character count.
- Show sentence count.
- Show file count (on non-markdown windows).
#### TODO:
Works with all languages.
#### Coming soon:
- Complete Customization
- Customization presets
- More stats
- [ ] add statistic view
- [ ] add more statistics (make suggestions)
- [ ] add goals
### Contributors
@ -37,3 +37,6 @@ Works with all languages.
- @aproximate
- @Quorafind
- @torantine
- @lammersma
- @aknighty74
- @dhruvik7

View file

@ -4,9 +4,13 @@
"description": "Counts the words of selected text in the editor.",
"main": "main.js",
"scripts": {
"lint": "svelte-check && eslint . --ext .ts",
"dev": "rollup --config rollup.config.js -w",
"build": "rollup --config rollup.config.js"
},
"repository": {
"url": "https://github.com/lukeleppan/better-word-count.git"
},
"keywords": [
"obsidian",
"obsidian-md",
@ -18,11 +22,20 @@
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"@types/node": "^14.14.2",
"@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",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-svelte": "^7.1.0",
"svelte-check": "^2.2.0",
"svelte-preprocess": "^4.7.3",
"ts-node": "^9.1.1",
"tslib": "^2.0.3",
"typescript": "^4.0.3"
},
"dependencies": {
"svelte": "^3.38.3"
}
}

5
presets/default.md Normal file
View file

@ -0,0 +1,5 @@
Query:
{word_count} words {character_count} characters
Alt Query:
{files} files {total_words} words {total_characters} characters

View file

@ -1,7 +1,9 @@
import typescript from '@rollup/plugin-typescript';
import {nodeResolve} from '@rollup/plugin-node-resolve';
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';
export default {
@ -15,8 +17,11 @@ export default {
external: ['obsidian'],
plugins: [
typescript(),
nodeResolve({browser: true}),
nodeResolve({ browser: true }),
commonjs(),
svelte({
preprocess: autoPreprocess(),
}),
copy({
targets: [
{ src: 'dist/main.js', dest: TEST_VAULT },

11
src/constants.ts Normal file
View file

@ -0,0 +1,11 @@
export const VIEW_TYPE_STATS = "vault-stats";
export const STATS_FILE = ".vault-stats";
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_COMMENT = new RegExp(
"<!--[\\s\\S]*?(?:-->)?" +
"<!---+>?" +
"|<!(?![dD][oO][cC][tT][yY][pP][eE]|\\[CDATA\\[)[^>]*>?" +
"|<[?][^>]*>?",
"g"
);

50
src/data/collector.ts Normal file
View file

@ -0,0 +1,50 @@
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 Object.keys(this.metadataCache.resolvedLinks).length;
}
async getTotalWordCount() {
let words = 0;
const files = this.vault.getFiles();
for (const i in files) {
const file = files[i];
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];
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];
sentence += getSentenceCount(await this.vault.cachedRead(file));
}
return sentence;
}
}

173
src/data/manager.ts Normal file
View file

@ -0,0 +1,173 @@
import moment from "moment";
import type { 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 class DataManager {
private vault: Vault;
private metadataCache: MetadataCache;
private history: History;
private today: string;
private collector: DataCollector;
private todayCounts: TodayCounts;
constructor(vault: Vault, metadataCache: MetadataCache) {
this.vault = vault;
this.metadataCache = metadataCache;
this.collector = new DataCollector(vault, metadataCache);
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;
}
async updateFromFile() {
this.history = Object.assign(
JSON.parse(await this.vault.adapter.read(".vault-stats"))
);
}
}

View file

@ -1,8 +1,6 @@
import { TFile } from "obsidian";
import { MATCH_COMMENT } from "src/constants";
// Count Stats
export function getWordCount(text: string): number {
// Thanks to liamcane
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;
@ -29,8 +27,6 @@ export function getCharacterCount(text: string): number {
}
export function getSentenceCount(text: string): number {
// Thanks to Extract Highlights plugin and AngelusDomini
// Also https://stackoverflow.com/questions/5553410
const sentences: number = (
(text || "").match(
/[^.!?\s][^.!?]*(?:[.!?](?!['"]?\s|$)[^.!?]*)*[.!?]?['"]?(?=\s|$)/gm
@ -40,8 +36,6 @@ export function getSentenceCount(text: string): number {
return sentences;
}
// Alt Count Stats
// Thanks to Eleanor Konik for the alternate count idea.
export function getFilesCount(files: TFile[]): number {
return files.length;
export function cleanComments(text: string): string {
return text.replace(MATCH_COMMENT, "");
}

View file

@ -1,166 +1,114 @@
import {
MarkdownView,
Plugin,
TFile,
MetadataCache,
getAllTags,
} from "obsidian";
import { Plugin, TFile, addIcon, WorkspaceLeaf } from "obsidian";
import { BetterWordCountSettingsTab } from "./settings/settings-tab";
import { BetterWordCountSettings } from "./settings/settings";
import {
getWordCount,
getCharacterCount,
getSentenceCount,
getFilesCount,
} from "./stats";
import { StatusBar } from "./status-bar";
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 type CodeMirror from "codemirror";
export default class BetterWordCount extends Plugin {
public recentlyTyped: boolean;
public statusBar: StatusBar;
public currentFile: TFile;
public settings: BetterWordCountSettings;
// public view: StatsView;
public dataManager: DataManager;
public barManager: BarManager;
onunload(): void {
this.app.workspace
.getLeavesOfType(VIEW_TYPE_STATS)
.forEach((leaf) => leaf.detach());
}
async onload() {
let statusBarEl = this.addStatusBarItem();
this.statusBar = new StatusBar(statusBarEl);
this.recentlyTyped = false;
this.settings = (await this.loadData()) || new BetterWordCountSettings();
this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData());
this.addSettingTab(new BetterWordCountSettingsTab(this.app, this));
this.updateAltCount();
let statusBarEl = this.addStatusBarItem();
this.statusBar = new StatusBar(statusBarEl);
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("file-open", this.onFileOpen, this)
this.app.workspace.on("active-leaf-change", this.activeLeafChange, this)
);
this.registerEvent(
this.app.workspace.on("quick-preview", this.onQuickPreview, this)
);
this.registerInterval(
window.setInterval(async () => {
let activeLeaf = this.app.workspace.activeLeaf;
if (!activeLeaf || !(activeLeaf.view instanceof MarkdownView)) {
return;
}
let editor = activeLeaf.view.sourceMode.cmEditor;
if (editor.somethingSelected()) {
let content: string = editor.getSelection();
this.updateWordCount(content);
this.recentlyTyped = false;
} else if (
this.currentFile &&
this.currentFile.extension === "md" &&
!this.recentlyTyped
) {
const contents = await this.app.vault.cachedRead(this.currentFile);
this.updateWordCount(contents);
} else if (!this.recentlyTyped) {
this.updateWordCount("");
}
}, 500)
);
let activeLeaf = this.app.workspace.activeLeaf;
let files: TFile[] = this.app.vault.getMarkdownFiles();
files.forEach((file) => {
if (file.basename === activeLeaf.getDisplayText()) {
this.onFileOpen(file);
}
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.change,
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());
// }
}
async onFileOpen(file: TFile) {
this.currentFile = file;
if (file && file.extension === "md") {
const contents = await this.app.vault.cachedRead(file);
this.recentlyTyped = true;
this.updateWordCount(contents);
} else {
this.updateAltCount();
activeLeafChange(leaf: WorkspaceLeaf) {
if (!(leaf.view.getViewType() === "markdown")) {
this.barManager.updateAltStatusBar();
}
}
onQuickPreview(file: TFile, contents: string) {
this.currentFile = file;
const leaf = this.app.workspace.activeLeaf;
if (leaf && leaf.view.getViewType() === "markdown") {
this.recentlyTyped = true;
this.updateWordCount(contents);
}
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
async updateAltCount() {
const files = getFilesCount(this.app.vault.getFiles());
let displayText: string = `${files} files `;
let allWords = 0;
let allCharacters = 0;
let allSentences = 0;
for (const f of this.app.vault.getMarkdownFiles()) {
let fileContents = await this.app.vault.cachedRead(f);
allWords += getWordCount(fileContents);
allCharacters += getCharacterCount(fileContents);
allSentences += getSentenceCount(fileContents);
}
if (this.settings.showWords) {
displayText =
displayText +
this.settings.wordsPrefix +
allWords +
this.settings.wordsSuffix;
}
if (this.settings.showCharacters) {
displayText =
displayText +
this.settings.charactersPrefix +
allCharacters +
this.settings.charactersSuffix;
}
if (this.settings.showSentences) {
displayText =
displayText +
this.settings.sentencesPrefix +
allSentences +
this.settings.sentencesSuffix;
}
this.statusBar.displayText(displayText);
}
updateWordCount(text: string) {
let displayText: string = "";
if (this.settings.showWords) {
displayText =
displayText +
this.settings.wordsPrefix +
getWordCount(text) +
this.settings.wordsSuffix;
}
if (this.settings.showCharacters) {
displayText =
displayText +
this.settings.charactersPrefix +
getCharacterCount(text) +
this.settings.charactersSuffix;
}
if (this.settings.showSentences) {
displayText =
displayText +
this.settings.sentencesPrefix +
getSentenceCount(text) +
this.settings.sentencesSuffix;
}
this.statusBar.displayText(displayText);
}
// initLeaf(): void {
// if (this.app.workspace.getLeavesOfType(VIEW_TYPE_STATS).length) {
// return;
// }
// this.app.workspace.getRightLeaf(false).setViewState({
// type: VIEW_TYPE_STATS,
// });
// }
}

View file

@ -1,103 +1,100 @@
import { settings } from "cluster";
import { PluginSettingTab, Setting } from "obsidian";
import BetterWordCount from "../main";
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;
const plugin: BetterWordCount = (this as any).plugin;
containerEl.empty();
containerEl.createEl("h2", { text: "Better Word Count Settings" });
// Word Count Settings
containerEl.createEl("h3", { text: "Word Count Settings" });
// General Settings
containerEl.createEl("h3", { text: "General Settings" });
new Setting(containerEl)
.setName("Show Word Count")
.setDesc("Enable this to show the word count.")
.addToggle((boolean) =>
boolean.setValue(plugin.settings.showWords).onChange((value) => {
plugin.settings.showWords = value;
plugin.saveData(plugin.settings);
})
);
.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("Word Count Prefix")
.setDesc("This changes the text in front of the word count number.")
.addText((text) =>
text.setValue(plugin.settings.wordsPrefix).onChange((value) => {
plugin.settings.wordsPrefix = value;
plugin.saveData(plugin.settings);
})
);
new Setting(containerEl)
.setName("Word Count Suffix")
.setDesc("This changes the text after of the word count number.")
.addText((text) =>
text.setValue(plugin.settings.wordsSuffix).onChange((value) => {
plugin.settings.wordsSuffix = value;
plugin.saveData(plugin.settings);
})
);
.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();
});
});
// Character Count Settings
containerEl.createEl("h3", { text: "Character Count Settings" });
// Status Bar Settings
containerEl.createEl("h3", { text: "Status Bar Settings" });
new Setting(containerEl)
.setName("Show Character Count")
.setDesc("Enable this to show the character count.")
.addToggle((boolean) =>
boolean.setValue(plugin.settings.showCharacters).onChange((value) => {
plugin.settings.showCharacters = value;
plugin.saveData(plugin.settings);
})
);
new Setting(containerEl)
.setName("Character Count Prefix")
.setDesc("This changes the text in front of the character count number.")
.addText((text) =>
text.setValue(plugin.settings.charactersPrefix).onChange((value) => {
plugin.settings.charactersPrefix = value;
plugin.saveData(plugin.settings);
})
);
new Setting(containerEl)
.setName("Character Count Suffix")
.setDesc("This changes the text after of the character count number.")
.addText((text) =>
text.setValue(plugin.settings.charactersSuffix).onChange((value) => {
plugin.settings.charactersSuffix = value;
plugin.saveData(plugin.settings);
})
);
.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);
// Sentence Count Settings
containerEl.createEl("h3", { text: "Sentence Count Settings" });
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("Show Sentence Count")
.setDesc("Enable this to show the sentence count.")
.addToggle((boolean) =>
boolean.setValue(plugin.settings.showSentences).onChange((value) => {
plugin.settings.showSentences = value;
plugin.saveData(plugin.settings);
})
);
.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("Sentence Count Prefix")
.setDesc("This changes the text in front of the sentence count number.")
.addText((text) =>
text.setValue(plugin.settings.sentencesPrefix).onChange((value) => {
plugin.settings.sentencesPrefix = value;
plugin.saveData(plugin.settings);
})
);
new Setting(containerEl)
.setName("Sentence Count Suffix")
.setDesc("This changes the text after of the sentence count number.")
.addText((text) =>
text.setValue(plugin.settings.sentencesSuffix).onChange((value) => {
plugin.settings.sentencesSuffix = value;
plugin.saveData(plugin.settings);
})
);
.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();
});
});
}
}

View file

@ -1,11 +1,46 @@
export class BetterWordCountSettings {
showWords: boolean = true;
wordsPrefix: string = "";
wordsSuffix: string = " words ";
showCharacters: boolean = true;
charactersPrefix: string = "";
charactersSuffix: string = " characters ";
showSentences: boolean = false;
sentencesPrefix: string = "";
sentencesSuffix: string = " sentences";
export const DEFAULT_SETTINGS: BetterWordCountSettings = {
preset: {
name: "default",
statusBarQuery: "{word_count} words {character_count} characters",
statusBarAltQuery:
"{files} files {total_words} words {total_characters} characters",
},
statusBarQuery: "{word_count} words {character_count} characters",
statusBarAltQuery:
"{files} files {total_words} words {total_characters} characters",
countComments: false,
collectStats: false,
};
export const PRESETS: PresetOption[] = [
{
name: "default",
statusBarQuery: "{word_count} words {character_count} characters",
statusBarAltQuery:
"{files} files {total_words} words {total_characters} characters",
},
{
name: "minimal",
statusBarQuery: "w: {word_count} c: {character_count}",
statusBarAltQuery: "f: {files} tw: {total_words} tc: {total_characters}",
},
{
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;
}

157
src/status/manager.ts Normal file
View file

@ -0,0 +1,157 @@
import type { MetadataCache, Vault } 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;
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);
}
async updateStatusBar(text: string): Promise<void> {
let newText = "";
const expression: Expression = parse(this.settings.statusBarQuery);
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(text);
break;
case 1:
newText = newText + getCharacterCount(text);
break;
case 2:
newText = newText + getSentenceCount(text);
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.getTotalCharacterCount());
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.getTotalCharacterCount());
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) {
if (cm.somethingSelected()) {
if (this.settings.countComments) {
this.updateStatusBar(cleanComments(cm.getSelection()));
} else {
this.updateStatusBar(cm.getSelection());
}
} else {
if (this.settings.collectStats) {
this.dataManager.updateFromFile();
}
if (this.settings.countComments) {
this.updateStatusBar(cleanComments(cm.getValue()));
} else {
this.updateStatusBar(cm.getValue());
}
}
}
}

65
src/status/parse.ts Normal file
View file

@ -0,0 +1,65 @@
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,
};

View file

@ -0,0 +1,9 @@
<svelte:options immutable />
<script lang="ts">
</script>
<div id="statistics-container" class="container">
<h1>Coming Soon!</h1>
</div>

30
src/view/view.ts Normal file
View file

@ -0,0 +1,30 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import { STATS_ICON_NAME, VIEW_TYPE_STATS } from "src/constants";
//@ts-ignore
import Statistics from "./Statistics.svelte";
export default class StatsView extends ItemView {
private statistics: Statistics;
constructor(leaf: WorkspaceLeaf) {
super(leaf);
}
getViewType(): string {
return VIEW_TYPE_STATS;
}
getDisplayText(): string {
return "Statistics";
}
getIcon(): string {
return STATS_ICON_NAME;
}
async onOpen(): Promise<void> {
this.statistics = new Statistics({
target: (this as any).contentEl,
});
}
}

View file

@ -1,22 +1,17 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"types": ["node", "svelte"],
"inlineSources": true,
"module": "ESNext",
"target": "es5",
// "module": "ESNext",
// "target": "es5",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"lib": [
"dom",
"es5",
"scripthost",
"es2015"
]
"importHelpers": true
// "lib": ["dom", "es5", "scripthost", "es2015"]
},
"include": [
"**/*.ts"
]
"include": ["**/*.ts"],
"exclude": ["node_modules/*"]
}