diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 26b042d..da28040 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -3,10 +3,10 @@ name: Word Frequency Pipeline on: push: branches: - - '*' + - "*" pull_request: branches: - - '*' + - "*" jobs: build_test: @@ -19,11 +19,14 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '23' + node-version: "23" - name: Install dependencies run: npm install + - name: Run Prettier + run: npx prettier --check + - name: Lint code run: npm run lint @@ -54,7 +57,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '23' + node-version: "23" - name: Install dependencies run: npm install diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1b8ac88 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..97a8477 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "bracketSameLine": true, + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/README.md b/README.md index 47e46dd..2880350 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # obsidian-word-frequency + Count word frequency in current note within Obsidian diff --git a/__mocks__/obsidian.ts b/__mocks__/obsidian.ts index be7054f..68b148f 100644 --- a/__mocks__/obsidian.ts +++ b/__mocks__/obsidian.ts @@ -1,55 +1,55 @@ class MockMarkdownView { - editor = { - getValue: jest.fn(), - }; - containerEl = { - addEventListener: jest.fn(), - }; - getViewType = jest.fn(); - getViewData = jest.fn(); - onunload = jest.fn(); - getDisplayText = jest.fn(); - constructor() { } + editor = { + getValue: jest.fn(), + }; + containerEl = { + addEventListener: jest.fn(), + }; + getViewType = jest.fn(); + getViewData = jest.fn(); + onunload = jest.fn(); + getDisplayText = jest.fn(); + constructor() {} } module.exports = { - setIcon: jest.fn(), - App: jest.fn(), - Plugin: jest.fn(), - MarkdownView: MockMarkdownView, - WorkspaceLeaf: jest.fn(), - Editor: jest.fn(), - PluginSettingTab: jest.fn(), - ItemView: jest.fn(), - CustomEvent: jest.fn(), - Setting: jest.fn().mockImplementation((containerEl: HTMLElement) => { - const settingEl = document.createElement('div'); - const textArea = document.createElement('textarea'); - textArea.classList.add('word-frequency-setting-blacklist'); - settingEl.appendChild(textArea); - containerEl.appendChild(settingEl); + setIcon: jest.fn(), + App: jest.fn(), + Plugin: jest.fn(), + MarkdownView: MockMarkdownView, + WorkspaceLeaf: jest.fn(), + Editor: jest.fn(), + PluginSettingTab: jest.fn(), + ItemView: jest.fn(), + CustomEvent: jest.fn(), + Setting: jest.fn().mockImplementation((containerEl: HTMLElement) => { + const settingEl = document.createElement("div"); + const textArea = document.createElement("textarea"); + textArea.classList.add("word-frequency-setting-blacklist"); + settingEl.appendChild(textArea); + containerEl.appendChild(settingEl); + return { + setName: jest.fn().mockReturnThis(), + setDesc: jest.fn().mockReturnThis(), + addTextArea: jest.fn().mockImplementation((callback) => { + callback({ + setValue: function (value: string) { + textArea.value = value; + return this; + }, + onChange: function (handler: (value: string) => void) { + textArea.addEventListener("change", (event) => { + handler((event.target as HTMLTextAreaElement).value); + }); + return this; + }, + inputEl: textArea, + }); return { - setName: jest.fn().mockReturnThis(), - setDesc: jest.fn().mockReturnThis(), - addTextArea: jest.fn().mockImplementation((callback) => { - callback({ - setValue: function (value: string) { - textArea.value = value; - return this; - }, - onChange: function (handler: (value: string) => void) { - textArea.addEventListener('change', (event) => { - handler((event.target as HTMLTextAreaElement).value); - }); - return this; - }, - inputEl: textArea, - }); - return { - inputEl: textArea, - }; - }), + inputEl: textArea, }; - }), -}; \ No newline at end of file + }), + }; + }), +}; diff --git a/__mocks__/utils.ts b/__mocks__/utils.ts index 91787fc..f128829 100644 --- a/__mocks__/utils.ts +++ b/__mocks__/utils.ts @@ -1,3 +1,3 @@ export const debounce = jest.fn((func) => { - return jest.fn(func); -}); \ No newline at end of file + return jest.fn(func); +}); diff --git a/eslint.config.js b/eslint.config.js index d57dc19..07b012c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,31 +1,28 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; +import pluginJs from '@eslint/js'; +import prettierConfig from 'eslint-config-prettier'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; /** @type {import('eslint').Linter.Config[]} */ export default [ { - files: ['**/*.{js,mjs,cjs,ts}'] + files: ['**/*.{js,mjs,cjs,ts}'], }, { languageOptions: { globals: { ...globals.browser, - ...globals.node + ...globals.node, }, parserOptions: { - sourceType: 'module' - } - } + sourceType: 'module', + }, + }, }, - { - ignores: [ - '__mocks__/**', - 'coverage/**', - 'dist/**', - 'node_modules/**', - ], - }, - pluginJs.configs.recommended, + { + ignores: ['__mocks__/**', 'coverage/**', 'dist/**', 'node_modules/**'], + }, + pluginJs.configs.recommended, ...tseslint.configs.recommended, + prettierConfig, ]; diff --git a/jest.config.cjs b/jest.config.cjs index 5a80ee7..3b5dc01 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,20 +1,17 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'jsdom', - modulePathIgnorePatterns: ['/dist/'], - testMatch: ['/src/__tests__/**/*.test.ts'], - coverageDirectory: 'coverage', - coverageReporters: ['cobertura', 'html', 'text', 'text-summary'], - collectCoverageFrom: [ - 'src/**/*.{js,jsx,ts,tsx}', - '!src/**/*.d.ts', - ], - coverageThreshold: { - global: { - branches: 90, - functions: 70, - lines: 90, - statements: 90, - }, + preset: "ts-jest", + testEnvironment: "jsdom", + modulePathIgnorePatterns: ["/dist/"], + testMatch: ["/src/__tests__/**/*.test.ts"], + coverageDirectory: "coverage", + coverageReporters: ["cobertura", "html", "text", "text-summary"], + collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"], + coverageThreshold: { + global: { + branches: 90, + functions: 70, + lines: 90, + statements: 90, }, + }, }; diff --git a/package-lock.json b/package-lock.json index e1012f6..7f3b11b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "eslint": "^9.22.0", + "eslint-config-prettier": "^10.1.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", "globals": "^16.0.0", @@ -27,6 +28,7 @@ "jest-environment-jsdom": "^29.7.0", "jsdom": "^26.0.0", "obsidian": "^1.8.7", + "prettier": "^3.5.3", "rollup": "^4.34.9", "rollup-plugin-typescript2": "^0.36.0", "ts-jest": "^29.2.6", @@ -3471,6 +3473,18 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", + "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -6616,6 +6630,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -10534,6 +10563,13 @@ } } }, + "eslint-config-prettier": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", + "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", + "dev": true, + "requires": {} + }, "eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -12791,6 +12827,12 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true + }, "pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", diff --git a/package.json b/package.json index 36478e6..181d0bb 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "build": "rollup -c", "dev": "npm run build && obsidian --dev", "watch": "rollup -c --watch", + "pretty-fix": "npx prettier src --write", "lint": "eslint . --ext .js,.ts", + "lint-fix": "eslint . --ext .js,.ts --fix", "test": "jest", "coverage": "npx jest --coverage" }, @@ -38,6 +40,7 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "eslint": "^9.22.0", + "eslint-config-prettier": "^10.1.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", "globals": "^16.0.0", @@ -45,6 +48,7 @@ "jest-environment-jsdom": "^29.7.0", "jsdom": "^26.0.0", "obsidian": "^1.8.7", + "prettier": "^3.5.3", "rollup": "^4.34.9", "rollup-plugin-typescript2": "^0.36.0", "ts-jest": "^29.2.6", diff --git a/rollup.config.js b/rollup.config.js index 480c626..8adab1c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,19 +1,14 @@ -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import typescript from '@rollup/plugin-typescript'; -import json from '@rollup/plugin-json'; +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import typescript from "@rollup/plugin-typescript"; +import json from "@rollup/plugin-json"; export default { - input: 'src/main.ts', - output: { - file: 'dist/main.js', - format: 'cjs', - }, - plugins: [ - commonjs(), - json(), - resolve(), - typescript() - ], - external: ['obsidian'], + input: "src/main.ts", + output: { + file: "dist/main.js", + format: "cjs", + }, + plugins: [commonjs(), json(), resolve(), typescript()], + external: ["obsidian"], }; diff --git a/src/ViewManager.ts b/src/ViewManager.ts index 48f73a8..5dd6d43 100644 --- a/src/ViewManager.ts +++ b/src/ViewManager.ts @@ -8,7 +8,10 @@ export class ViewManager { this.plugin = plugin; } - getOrCreateLeaf(workspace: Workspace, viewType: string): WorkspaceLeaf | null { + getOrCreateLeaf( + workspace: Workspace, + viewType: string + ): WorkspaceLeaf | null { const leaves = workspace.getLeavesOfType(viewType); if (leaves.length > 0) { return leaves[0]; @@ -20,12 +23,13 @@ export class ViewManager { async setViewState(leaf: WorkspaceLeaf, viewType: string): Promise { await leaf.setViewState({ type: viewType, - active: true + active: true, }); } updateContent(): void { - const editor = this.plugin.app.workspace.getActiveViewOfType(MarkdownView)?.editor; + const editor = + this.plugin.app.workspace.getActiveViewOfType(MarkdownView)?.editor; this.plugin.frequencyCounter.triggerUpdateContent(editor); } } diff --git a/src/WordFrequencyCounter.ts b/src/WordFrequencyCounter.ts index db30fbb..ab206e3 100644 --- a/src/WordFrequencyCounter.ts +++ b/src/WordFrequencyCounter.ts @@ -25,7 +25,10 @@ export class WordFrequencyCounter { return Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]); } - handleActiveLeafChange(leaf: WorkspaceLeaf | null, workspace: Workspace): void { + handleActiveLeafChange( + leaf: WorkspaceLeaf | null, + workspace: Workspace + ): void { if (leaf === null) { return; } @@ -64,7 +67,9 @@ export class WordFrequencyCounter { } try { const wordCounts = this.calculateWordFrequencies(editor.getValue()); - window.document.dispatchEvent(new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } })); + window.document.dispatchEvent( + new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } }) + ); } catch (error) { console.error('error in triggerUpdateContent', error); } diff --git a/src/WordFrequencyDisplay.ts b/src/WordFrequencyDisplay.ts index b9c3ce8..75e8f17 100644 --- a/src/WordFrequencyDisplay.ts +++ b/src/WordFrequencyDisplay.ts @@ -12,18 +12,27 @@ export class WordFrequencyDisplay { this.view = view; } - addWordToSidebar(blacklist: Set, word: string, count: number, contentContainer: HTMLDivElement) { + addWordToSidebar( + blacklist: Set, + word: string, + count: number, + contentContainer: HTMLDivElement + ) { if (blacklist.has(word) || count < this.plugin.settings.threshold) { return; } const row = contentContainer.createEl('div', { cls: 'word-row' }); - const wordCountContainer = row.createEl('div', { cls: 'word-count-container' }); + const wordCountContainer = row.createEl('div', { + cls: 'word-count-container', + }); wordCountContainer.createEl('span', { text: word }); wordCountContainer.createEl('span', { text: count.toString() }); - const buttonContainer = row.createEl('div', { cls: 'button-container' }); + const buttonContainer = row.createEl('div', { + cls: 'button-container', + }); const button = buttonContainer.createEl('button'); setIcon(button, 'trash-2'); button.addEventListener('click', () => { @@ -38,9 +47,16 @@ export class WordFrequencyDisplay { } createThresholdDisplay(contentEl: HTMLElement) { - const thresholdDisplay = contentEl.createEl('div', { cls: 'threshold-display' }); - thresholdDisplay.setText(`Current Frequency Threshold is ${this.plugin.settings.threshold}.`); - thresholdDisplay.setAttr('title', 'Configure settings for this plugin to update the frequency threshold.'); + const thresholdDisplay = contentEl.createEl('div', { + cls: 'threshold-display', + }); + thresholdDisplay.setText( + `Current Frequency Threshold is ${this.plugin.settings.threshold}.` + ); + thresholdDisplay.setAttr( + 'title', + 'Configure settings for this plugin to update the frequency threshold.' + ); } saveWordToBlacklist(word: string) { diff --git a/src/WordFrequencySettingTab.ts b/src/WordFrequencySettingTab.ts index 879039c..01d698d 100644 --- a/src/WordFrequencySettingTab.ts +++ b/src/WordFrequencySettingTab.ts @@ -20,26 +20,24 @@ export class WordFrequencySettingTab extends PluginSettingTab { .setName('Blacklist') .setDesc('Comma-separated list of words to exclude.') .addTextArea((text) => { - text - .setValue(this.plugin.settings.blacklist) + text.setValue(this.plugin.settings.blacklist) .onChange(async (value) => { await this.saveBlacklistValue(value); }) - .inputEl.classList.add('word-frequency-setting-blacklist') - } - ); + .inputEl.classList.add('word-frequency-setting-blacklist'); + }); new Setting(containerEl) .setName('Word Frequency Threshold') .setDesc('Only show words that appear at least this many times.') - .addText(text => text - .setPlaceholder('3') - .setValue(this.plugin.settings.threshold.toString()) - .onChange(async (value) => { - await this.updateThreshold(value); - } - ) - ); + .addText((text) => + text + .setPlaceholder('3') + .setValue(this.plugin.settings.threshold.toString()) + .onChange(async (value) => { + await this.updateThreshold(value); + }) + ); } async saveBlacklistValue(value: string) { @@ -55,7 +53,7 @@ export class WordFrequencySettingTab extends PluginSettingTab { this.plugin.settings.threshold = num; await this.plugin.saveSettings(); - this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE).forEach(leaf => { + this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE).forEach((leaf) => { (leaf.view as WordFrequencyView).updateContent(); }); } diff --git a/src/WordFrequencyView.ts b/src/WordFrequencyView.ts index 5fc77dc..e09fc62 100644 --- a/src/WordFrequencyView.ts +++ b/src/WordFrequencyView.ts @@ -1,16 +1,24 @@ import { ItemView, WorkspaceLeaf } from 'obsidian'; -import { EVENT_UPDATE, FREQUENCY_ICON, PLUGIN_NAME, VIEW_TYPE } from './constants'; +import { + EVENT_UPDATE, + FREQUENCY_ICON, + PLUGIN_NAME, + VIEW_TYPE, +} from './constants'; import WordFrequencyPlugin from './main'; import { WordFrequencyDisplay } from './WordFrequencyDisplay'; export class WordFrequencyView extends ItemView { private display: WordFrequencyDisplay; - private eventListener: (event: CustomEvent) => void = () => { - }; + private eventListener: (event: CustomEvent) => void = () => {}; private readonly plugin: WordFrequencyPlugin; private wordCountList: [string, number][] = []; - constructor(leaf: WorkspaceLeaf, plugin: WordFrequencyPlugin, display?: WordFrequencyDisplay) { + constructor( + leaf: WorkspaceLeaf, + plugin: WordFrequencyPlugin, + display?: WordFrequencyDisplay + ) { super(leaf); this.plugin = plugin; this.display = display ?? new WordFrequencyDisplay(plugin, this); @@ -39,23 +47,36 @@ export class WordFrequencyView extends ItemView { this.updateContent(); } }; - window.document.addEventListener(EVENT_UPDATE, this.eventListener as EventListener); + window.document.addEventListener( + EVENT_UPDATE, + this.eventListener as EventListener + ); this.updateContent(); } async onClose() { - window.document.removeEventListener(EVENT_UPDATE, this.eventListener as EventListener); + window.document.removeEventListener( + EVENT_UPDATE, + this.eventListener as EventListener + ); } updateContent() { this.contentEl.empty(); this.display.createHeader(this.contentEl); const contentContainer = this.contentEl.createEl('div'); - const blacklist = new Set(this.plugin.settings.blacklist.split(',').map(word => word.trim())); + const blacklist = new Set( + this.plugin.settings.blacklist.split(',').map((word) => word.trim()) + ); this.wordCountList.forEach(([word, count]) => { - this.display.addWordToSidebar(blacklist, word, count, contentContainer); + this.display.addWordToSidebar( + blacklist, + word, + count, + contentContainer + ); }); this.display.createThresholdDisplay(this.contentEl); } diff --git a/src/__tests__/ViewManager.test.ts b/src/__tests__/ViewManager.test.ts index ae3332e..5e75753 100644 --- a/src/__tests__/ViewManager.test.ts +++ b/src/__tests__/ViewManager.test.ts @@ -29,7 +29,9 @@ describe('ViewManager', () => { it('should get or create a leaf', () => { const mockLeaf = {} as WorkspaceLeaf; - (workspace.getLeavesOfType as jest.Mock).mockReturnValueOnce([mockLeaf]); + (workspace.getLeavesOfType as jest.Mock).mockReturnValueOnce([ + mockLeaf, + ]); const leaf = viewManager.getOrCreateLeaf(workspace, VIEW_TYPE); expect(leaf).toBe(mockLeaf); @@ -62,6 +64,8 @@ describe('ViewManager', () => { viewManager.updateContent(); - expect(plugin.frequencyCounter.triggerUpdateContent).toHaveBeenCalledWith(mockEditor); + expect( + plugin.frequencyCounter.triggerUpdateContent + ).toHaveBeenCalledWith(mockEditor); }); }); diff --git a/src/__tests__/WordFrequencyCounter.test.ts b/src/__tests__/WordFrequencyCounter.test.ts index 2144b9c..4f8260d 100644 --- a/src/__tests__/WordFrequencyCounter.test.ts +++ b/src/__tests__/WordFrequencyCounter.test.ts @@ -15,7 +15,10 @@ describe('WordFrequencyCounter tests', () => { const result = counter.calculateWordFrequencies(content); - expect(result).toEqual([['hello', 2], ['world', 1]]); + expect(result).toEqual([ + ['hello', 2], + ['world', 1], + ]); }); it('should calculate word frequencies with punctuation and mixed case', () => { @@ -23,7 +26,10 @@ describe('WordFrequencyCounter tests', () => { const result = counter.calculateWordFrequencies(content); - expect(result).toEqual([['hello', 2], ['world', 2]]); + expect(result).toEqual([ + ['hello', 2], + ['world', 2], + ]); }); it('should calculate word frequencies with numbers and special characters', () => { @@ -31,7 +37,12 @@ describe('WordFrequencyCounter tests', () => { const result = counter.calculateWordFrequencies(content); - expect(result).toEqual([['word1', 2], ['hello', 2], ['word2', 1], ['123', 1]]); + expect(result).toEqual([ + ['word1', 2], + ['hello', 2], + ['word2', 1], + ['123', 1], + ]); }); it('should calculate word frequencies with periods, colons, and slashes', () => { @@ -69,7 +80,7 @@ describe('WordFrequencyCounter tests', () => { }; const workspace: Workspace = {} as unknown as Workspace; const leafMock = { - view: null + view: null, } as unknown as WorkspaceLeaf; counterMock.handleActiveLeafChange(leafMock, workspace); @@ -96,7 +107,10 @@ describe('WordFrequencyCounter tests', () => { counter.handleActiveLeafChange(leafMock, workspace); - expect(containerElMock.addEventListener).toHaveBeenCalledWith('keyup', expect.any(Function)); + expect(containerElMock.addEventListener).toHaveBeenCalledWith( + 'keyup', + expect.any(Function) + ); }); it('should set lastActiveEditor with a valid MarkdownView', () => { @@ -108,7 +122,9 @@ describe('WordFrequencyCounter tests', () => { containerEl: containerElMock, } as unknown as MarkdownView; const workspace: Workspace = { - getActiveViewOfType: jest.fn().mockReturnValue(markdownViewMock), + getActiveViewOfType: jest + .fn() + .mockReturnValue(markdownViewMock), getLeavesOfType: jest.fn().mockReturnValue([]), } as unknown as Workspace; const leafMock = { @@ -212,7 +228,10 @@ describe('WordFrequencyCounter tests', () => { it('should not dispatch an event or calculate word frequencies', () => { const editor = undefined; const counterMock = jest.spyOn(counter, 'calculateWordFrequencies'); - const dispatchEventMock = jest.spyOn(window.document, 'dispatchEvent'); + const dispatchEventMock = jest.spyOn( + window.document, + 'dispatchEvent' + ); counter.triggerUpdateContent(editor); @@ -225,12 +244,17 @@ describe('WordFrequencyCounter tests', () => { it('should dispatch an event after calculating word frequencies', () => { const counterMock = jest.spyOn(counter, 'calculateWordFrequencies'); - const dispatchEventMock = jest.spyOn(window.document, 'dispatchEvent'); - const expectedValue = 'hello world hello'; - const expectedEvent = new CustomEvent( - EVENT_UPDATE, - { detail: [['hello', 2], ['world', 1]] } + const dispatchEventMock = jest.spyOn( + window.document, + 'dispatchEvent' ); + const expectedValue = 'hello world hello'; + const expectedEvent = new CustomEvent(EVENT_UPDATE, { + detail: [ + ['hello', 2], + ['world', 1], + ], + }); const editor: Editor = { getValue: jest.fn().mockReturnValue(expectedValue), } as unknown as Editor; @@ -256,23 +280,33 @@ describe('WordFrequencyCounter tests', () => { const editor: Editor = { getValue: jest.fn().mockReturnValue(expectedValue), } as unknown as Editor; - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); counterMock.triggerUpdateContent(editor); - expect(consoleErrorSpy).toHaveBeenCalledWith('error in triggerUpdateContent', expectedError); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'error in triggerUpdateContent', + expectedError + ); consoleErrorSpy.mockRestore(); }); it('should dispatch an event with last active editor when editor is undefined', () => { const counterMock = jest.spyOn(counter, 'calculateWordFrequencies'); - const dispatchEventMock = jest.spyOn(window.document, 'dispatchEvent'); - const expectedValue = 'hello world hello'; - const expectedEvent = new CustomEvent( - EVENT_UPDATE, - { detail: [['hello', 2], ['world', 1]] } + const dispatchEventMock = jest.spyOn( + window.document, + 'dispatchEvent' ); + const expectedValue = 'hello world hello'; + const expectedEvent = new CustomEvent(EVENT_UPDATE, { + detail: [ + ['hello', 2], + ['world', 1], + ], + }); const editor: Editor = { getValue: jest.fn().mockReturnValue(expectedValue), } as unknown as Editor; diff --git a/src/__tests__/WordFrequencyDisplay.test.ts b/src/__tests__/WordFrequencyDisplay.test.ts index ea03b08..0466f5f 100644 --- a/src/__tests__/WordFrequencyDisplay.test.ts +++ b/src/__tests__/WordFrequencyDisplay.test.ts @@ -39,7 +39,9 @@ describe('WordFrequencyDisplay', () => { setText: jest.fn(), }), } as unknown as HTMLElement; - blacklist = new Set(mockPlugin.settings.blacklist.split(',').map(word => word.trim())); + blacklist = new Set( + mockPlugin.settings.blacklist.split(',').map((word) => word.trim()) + ); display = new WordFrequencyDisplay(mockPlugin, mockView); }); @@ -77,7 +79,8 @@ describe('WordFrequencyDisplay', () => { addEventListener: jest.fn(), } as unknown as HTMLButtonElement; const innerElement = { - createEl: jest.fn() + createEl: jest + .fn() .mockReturnValueOnce(spanElement) .mockReturnValueOnce(spanElement) .mockReturnValueOnce(buttonElement), @@ -93,8 +96,12 @@ describe('WordFrequencyDisplay', () => { display.addWordToSidebar(blacklist, word, count, contentContainer); - expect(innerElement.createEl).toHaveBeenNthCalledWith(1, 'span', { text: word }); - expect(innerElement.createEl).toHaveBeenNthCalledWith(2, 'span', { text: count.toString() }); + expect(innerElement.createEl).toHaveBeenNthCalledWith(1, 'span', { + text: word, + }); + expect(innerElement.createEl).toHaveBeenNthCalledWith(2, 'span', { + text: count.toString(), + }); expect(innerElement.createEl).toHaveBeenNthCalledWith(3, 'button'); }); @@ -104,8 +111,12 @@ describe('WordFrequencyDisplay', () => { display.saveWordToBlacklist(word); - expect(mockPlugin.settings.blacklist).toBe(`${originalBlacklist},${word}`); - expect(mockPlugin.saveData).toHaveBeenCalledWith(mockPlugin.settings); + expect(mockPlugin.settings.blacklist).toBe( + `${originalBlacklist},${word}` + ); + expect(mockPlugin.saveData).toHaveBeenCalledWith( + mockPlugin.settings + ); expect(mockView.updateContent).toHaveBeenCalled(); }); @@ -129,11 +140,20 @@ describe('WordFrequencyDisplay', () => { it('should set text and attribute in the content', () => { display.createThresholdDisplay(contentEl); - const thresholdDisplay = contentEl.createEl('div', { cls: 'threshold-display' }); + const thresholdDisplay = contentEl.createEl('div', { + cls: 'threshold-display', + }); - expect(contentEl.createEl).toHaveBeenCalledWith('div', { cls: 'threshold-display' }); - expect(thresholdDisplay.setText).toHaveBeenCalledWith(`Current Frequency Threshold is ${mockPlugin.settings.threshold}.`); - expect(thresholdDisplay.setAttr).toHaveBeenCalledWith('title', 'Configure settings for this plugin to update the frequency threshold.'); + expect(contentEl.createEl).toHaveBeenCalledWith('div', { + cls: 'threshold-display', + }); + expect(thresholdDisplay.setText).toHaveBeenCalledWith( + `Current Frequency Threshold is ${mockPlugin.settings.threshold}.` + ); + expect(thresholdDisplay.setAttr).toHaveBeenCalledWith( + 'title', + 'Configure settings for this plugin to update the frequency threshold.' + ); }); }); }); diff --git a/src/__tests__/WordFrequencySettingTab.test.ts b/src/__tests__/WordFrequencySettingTab.test.ts index 5b23d5b..fbba9d2 100644 --- a/src/__tests__/WordFrequencySettingTab.test.ts +++ b/src/__tests__/WordFrequencySettingTab.test.ts @@ -23,14 +23,18 @@ describe('WordFrequencySettingTab', () => { plugin = { settings: { blacklist: '', - threshold: 0 + threshold: 0, }, saveSettings: jest.fn().mockResolvedValue(undefined), app: { workspace: { - getLeavesOfType: jest.fn().mockReturnValue([{ view: { updateContent: jest.fn() } }]) - } - } + getLeavesOfType: jest + .fn() + .mockReturnValue([ + { view: { updateContent: jest.fn() } }, + ]), + }, + }, } as unknown as WordFrequencyPlugin; containerEl = createMockContainerEl(); @@ -58,13 +62,17 @@ describe('WordFrequencySettingTab', () => { await settingTab.updateThreshold(value); - const view = plugin.app.workspace.getLeavesOfType(VIEW_TYPE)[0] as unknown as { - view: WordFrequencyView + const view = plugin.app.workspace.getLeavesOfType( + VIEW_TYPE + )[0] as unknown as { + view: WordFrequencyView; }; expect(plugin.settings.threshold).toBe(5); expect(plugin.saveSettings).toHaveBeenCalled(); - expect(plugin.app.workspace.getLeavesOfType).toHaveBeenCalledWith(VIEW_TYPE); + expect(plugin.app.workspace.getLeavesOfType).toHaveBeenCalledWith( + VIEW_TYPE + ); expect(view.view.updateContent).toHaveBeenCalled(); }); diff --git a/src/__tests__/WordFrequencyView.test.ts b/src/__tests__/WordFrequencyView.test.ts index 57dc905..3a42c52 100644 --- a/src/__tests__/WordFrequencyView.test.ts +++ b/src/__tests__/WordFrequencyView.test.ts @@ -1,5 +1,10 @@ import { WorkspaceLeaf } from 'obsidian'; -import { EVENT_UPDATE, FREQUENCY_ICON, PLUGIN_NAME, VIEW_TYPE } from '../constants'; +import { + EVENT_UPDATE, + FREQUENCY_ICON, + PLUGIN_NAME, + VIEW_TYPE, +} from '../constants'; import WordFrequencyPlugin from '../main'; import { WordFrequencyDisplay } from '../WordFrequencyDisplay'; import { WordFrequencyView } from '../WordFrequencyView'; @@ -37,7 +42,11 @@ describe('WordFrequencyView', () => { }); it('should use the provided WordFrequencyDisplay instance if passed', async () => { - const view = new WordFrequencyView(mockLeaf, mockPlugin, mockDisplay); + const view = new WordFrequencyView( + mockLeaf, + mockPlugin, + mockDisplay + ); const viewMock = { onOpen: view.onOpen, @@ -81,11 +90,17 @@ describe('WordFrequencyView', () => { describe('onOpen', () => { it('should add an event listener for EVENT_UPDATE', async () => { - const addEventListenerSpy = jest.spyOn(window.document, 'addEventListener'); + const addEventListenerSpy = jest.spyOn( + window.document, + 'addEventListener' + ); await view.onOpen(); - expect(addEventListenerSpy).toHaveBeenCalledWith(EVENT_UPDATE, expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith( + EVENT_UPDATE, + expect.any(Function) + ); }); it('should call updateContent on open', async () => { @@ -104,8 +119,13 @@ describe('WordFrequencyView', () => { await view.onOpen(); - const wordCounts = [['apple', 5], ['banana', 3]]; - const event = new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } }); + const wordCounts = [ + ['apple', 5], + ['banana', 3], + ]; + const event = new CustomEvent(EVENT_UPDATE, { + detail: { wordCounts }, + }); window.document.dispatchEvent(event); expect(updateContentSpy).toHaveBeenCalled(); @@ -117,20 +137,34 @@ describe('WordFrequencyView', () => { describe('onClose', () => { it('should remove the event listener for EVENT_UPDATE', async () => { await view.onOpen(); - const removeEventListenerSpy = jest.spyOn(window.document, 'removeEventListener'); + const removeEventListenerSpy = jest.spyOn( + window.document, + 'removeEventListener' + ); await view.onClose(); - expect(removeEventListenerSpy).toHaveBeenCalledWith(EVENT_UPDATE, expect.any(Function)); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + EVENT_UPDATE, + expect.any(Function) + ); }); }); describe('updateContent', () => { it('should add words to sidebar for each word', async () => { await view.onOpen(); - const wordCounts = [['apple', 5], ['banana', 3]]; - const event = new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } }); - const addWordToSidebarSpy = jest.spyOn(mockDisplay, 'addWordToSidebar'); + const wordCounts = [ + ['apple', 5], + ['banana', 3], + ]; + const event = new CustomEvent(EVENT_UPDATE, { + detail: { wordCounts }, + }); + const addWordToSidebarSpy = jest.spyOn( + mockDisplay, + 'addWordToSidebar' + ); window.document.dispatchEvent(event); diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index d729783..3cf9e9f 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -1,5 +1,10 @@ import { App, WorkspaceLeaf, PluginManifest, Workspace } from 'obsidian'; -import { DEFAULT_SETTINGS, FREQUENCY_ICON, PLUGIN_NAME, VIEW_TYPE } from '../constants'; +import { + DEFAULT_SETTINGS, + FREQUENCY_ICON, + PLUGIN_NAME, + VIEW_TYPE, +} from '../constants'; import WordFrequencyPlugin from '../main'; import { ViewManager } from '../ViewManager'; import { WordFrequencySettingTab } from '../WordFrequencySettingTab'; @@ -48,7 +53,11 @@ describe('WordFrequencyPlugin', () => { updateContent: jest.fn(), } as unknown as ViewManager; - plugin = new WordFrequencyPlugin(mockApp, mockManifest, mockViewManager); + plugin = new WordFrequencyPlugin( + mockApp, + mockManifest, + mockViewManager + ); plugin['app'] = mockApp; plugin['loadData'] = jest.fn().mockResolvedValue(DEFAULT_SETTINGS); plugin['saveData'] = jest.fn().mockResolvedValue(undefined); @@ -68,40 +77,61 @@ describe('WordFrequencyPlugin', () => { await plugin.onload(); - expect(plugin.registerView).toHaveBeenCalledWith(VIEW_TYPE, expect.any(Function)); + expect(plugin.registerView).toHaveBeenCalledWith( + VIEW_TYPE, + expect.any(Function) + ); expect(plugin.addRibbonIcon).toHaveBeenCalledWith( FREQUENCY_ICON, `Show ${PLUGIN_NAME} Sidebar`, expect.any(Function) ); - expect(plugin.app.workspace.on).toHaveBeenCalledWith('active-leaf-change', expect.any(Function)); + expect(plugin.app.workspace.on).toHaveBeenCalledWith( + 'active-leaf-change', + expect.any(Function) + ); expect(plugin.addSettingTab).toHaveBeenCalledWith(settingTab); }); }); describe('activateView', () => { - it('should set the view state and show the leaf with content', async() => { + it('should set the view state and show the leaf with content', async () => { const mockLeaf = jest.fn() as unknown as WorkspaceLeaf; mockViewManager = { getOrCreateLeaf: jest.fn().mockReturnValue(mockLeaf), setViewState: jest.fn(), updateContent: jest.fn(), } as unknown as ViewManager; - plugin = new WordFrequencyPlugin(mockApp, mockManifest, mockViewManager); + plugin = new WordFrequencyPlugin( + mockApp, + mockManifest, + mockViewManager + ); plugin['app'] = mockApp; await plugin.activateView(); - expect(mockViewManager.getOrCreateLeaf).toHaveBeenCalledWith(plugin.app.workspace, VIEW_TYPE); - expect(mockViewManager.setViewState).toHaveBeenCalledWith(mockLeaf, VIEW_TYPE); - expect(plugin.app.workspace.revealLeaf).toHaveBeenCalledWith(mockLeaf); + expect(mockViewManager.getOrCreateLeaf).toHaveBeenCalledWith( + plugin.app.workspace, + VIEW_TYPE + ); + expect(mockViewManager.setViewState).toHaveBeenCalledWith( + mockLeaf, + VIEW_TYPE + ); + expect(plugin.app.workspace.revealLeaf).toHaveBeenCalledWith( + mockLeaf + ); expect(mockViewManager.updateContent).toHaveBeenCalled(); }); - it('should not set view state or reveal leaf when there is no leaf', async() => { + it('should not set view state or reveal leaf when there is no leaf', async () => { await plugin.activateView(); - expect(mockViewManager.getOrCreateLeaf).toHaveBeenCalledWith(plugin.app.workspace, VIEW_TYPE); + expect(mockViewManager.getOrCreateLeaf).toHaveBeenCalledWith( + plugin.app.workspace, + VIEW_TYPE + ); expect(mockViewManager.setViewState).not.toHaveBeenCalled(); expect(plugin.app.workspace.revealLeaf).not.toHaveBeenCalled(); expect(mockViewManager.updateContent).not.toHaveBeenCalled(); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 3c8de3e..d0b64c6 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -108,4 +108,4 @@ describe('debounce', () => { expect(mockFunc).toHaveBeenCalledTimes(1); expect(mockFunc).toHaveReturnedWith('testValue'); }); -}); \ No newline at end of file +}); diff --git a/src/main.ts b/src/main.ts index 4edbc97..ff0bb5b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,11 @@ import { App, Plugin, PluginManifest, WorkspaceLeaf } from 'obsidian'; -import { WordFrequencySettings, DEFAULT_SETTINGS, PLUGIN_NAME, VIEW_TYPE, FREQUENCY_ICON } from './constants'; +import { + WordFrequencySettings, + DEFAULT_SETTINGS, + PLUGIN_NAME, + VIEW_TYPE, + FREQUENCY_ICON, +} from './constants'; import { ViewManager } from './ViewManager'; import { WordFrequencyCounter } from './WordFrequencyCounter'; import { WordFrequencySettingTab } from './WordFrequencySettingTab'; @@ -11,7 +17,12 @@ export default class WordFrequencyPlugin extends Plugin { settingTab: WordFrequencySettingTab; viewManager: ViewManager; - constructor(app: App, manifest: PluginManifest, viewManager?: ViewManager, settingTab?: WordFrequencySettingTab) { + constructor( + app: App, + manifest: PluginManifest, + viewManager?: ViewManager, + settingTab?: WordFrequencySettingTab + ) { super(app, manifest); this.settingTab = settingTab ?? new WordFrequencySettingTab(this); this.viewManager = viewManager ?? new ViewManager(this); @@ -26,24 +37,27 @@ export default class WordFrequencyPlugin extends Plugin { (leaf: WorkspaceLeaf) => new WordFrequencyView(leaf, this) ); - this.addRibbonIcon(FREQUENCY_ICON, `Show ${PLUGIN_NAME} Sidebar`, () => { - this.activateView(); - }); + this.addRibbonIcon( + FREQUENCY_ICON, + `Show ${PLUGIN_NAME} Sidebar`, + () => { + this.activateView(); + } + ); this.registerEvent( - this.app.workspace.on( - 'active-leaf-change', - (leaf) => { - this.frequencyCounter.handleActiveLeafChange(leaf, this.app.workspace); - } - ) + this.app.workspace.on('active-leaf-change', (leaf) => { + this.frequencyCounter.handleActiveLeafChange( + leaf, + this.app.workspace + ); + }) ); this.addSettingTab(this.settingTab); } - onunload() { - } + onunload() {} async activateView() { const { workspace } = this.app; diff --git a/src/utils.ts b/src/utils.ts index 2a1fff1..1f1bd05 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,7 @@ -export function debounce) => void>(func: T, delay: number): (...args: Parameters) => void { +export function debounce) => void>( + func: T, + delay: number +): (...args: Parameters) => void { let timeoutId: ReturnType; return (...args: Parameters): void => { clearTimeout(timeoutId); diff --git a/styles.css b/styles.css index 3e67d85..daad160 100644 --- a/styles.css +++ b/styles.css @@ -1,41 +1,41 @@ .setting-item { - display: flex; - align-items: flex-start; + display: flex; + align-items: flex-start; } .setting-item-info { - flex-shrink: 0; + flex-shrink: 0; } .word-frequency-setting-blacklist { - min-height: 150px; - width: 100%; + min-height: 150px; + width: 100%; } .word-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; } .word-count-container { - display: flex; - justify-content: space-between; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; + display: flex; + justify-content: space-between; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; } .word-count-container span { - flex-shrink: 1; - max-width: 80%; - overflow: hidden; - padding-right: 10px; + flex-shrink: 1; + max-width: 80%; + overflow: hidden; + padding-right: 10px; } .button-container { - margin-left: 10px; - margin-top: 0; + margin-left: 10px; + margin-top: 0; } diff --git a/tsconfig.json b/tsconfig.json index bf06066..ad4e093 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "baseUrl": ".", "esModuleInterop": true, "jsx": "react", - "lib": ["ESNext", "dom"], + "lib": ["ESNext", "dom"], "module": "ESNext", "moduleResolution": "Node", "outDir": "./dist", @@ -13,10 +13,7 @@ "skipLibCheck": true, "strict": true, "target": "ESNext", - "types": ["obsidian", "node", "jest"], + "types": ["obsidian", "node", "jest"] }, - "include": [ - "src/**/*", - "node_modules/@types/jest/index.d.ts", - ] + "include": ["src/**/*", "node_modules/@types/jest/index.d.ts"] }