365 lines
13 KiB
JavaScript
365 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
var obsidian = require('obsidian');
|
|
|
|
const DEFAULT_SETTINGS = {
|
|
blacklist: 'the,and,to,of,a,in,for,on,is,it,that,with,as,this,by,your,you',
|
|
threshold: 3,
|
|
};
|
|
const ELEMENT_CLASSES = {
|
|
containerButton: 'word-frequency-button-container',
|
|
containerContent: 'word-frequency-sidebar-content',
|
|
containerCount: 'word-frequency-count-container',
|
|
containerFilter: 'word-frequency-filter-container',
|
|
containerRow: 'word-frequency-row',
|
|
containerThreshold: 'word-frequency-threshold-display',
|
|
containerWordList: 'word-frequency-word-list',
|
|
filter: 'word-frequency-filter',
|
|
settingBlacklist: 'word-frequency-setting-blacklist',
|
|
settingInfoItem: 'word-frequency-setting-item-info',
|
|
settingItem: 'word-frequency-setting-item',
|
|
};
|
|
const COMMAND_ID_SHOW_SIDEBAR = 'word-frequency-show-sidebar';
|
|
const EVENT_UPDATE = 'word-frequency-update';
|
|
const FREQUENCY_ICON = 'file-chart-column-increasing';
|
|
const PLUGIN_NAME = 'Word frequency';
|
|
const SETTINGS_DESCRIPTIONS = {
|
|
blacklist: 'Comma-separated list of words to exclude.',
|
|
threshold: 'Only show words that appear at least this many times.',
|
|
};
|
|
const SETTINGS_NAMES = {
|
|
blacklist: 'Blacklist',
|
|
threshold: 'Word frequency threshold',
|
|
};
|
|
const VIEW_TYPE = 'word-frequency-view';
|
|
|
|
class ViewManager {
|
|
plugin;
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
}
|
|
getOrCreateLeaf(workspace, viewType) {
|
|
const leaves = workspace.getLeavesOfType(viewType);
|
|
if (leaves.length > 0) {
|
|
return leaves[0];
|
|
}
|
|
return workspace.getRightLeaf(false);
|
|
}
|
|
async setViewState(leaf, viewType) {
|
|
await leaf.setViewState({
|
|
type: viewType,
|
|
active: true,
|
|
});
|
|
}
|
|
updateContent() {
|
|
const editor = this.plugin.app.workspace.getActiveViewOfType(obsidian.MarkdownView)?.editor;
|
|
this.plugin.frequencyCounter.triggerUpdateContent(editor);
|
|
}
|
|
}
|
|
|
|
function segmentText(content) {
|
|
const normalized = content.toLowerCase().normalize('NFKC');
|
|
const stripped = normalized.replace(/[^\p{L}\p{N}\s]+/gu, '');
|
|
return stripped
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter((word) => word.length > 0);
|
|
}
|
|
|
|
class WordFrequencyCounter {
|
|
debouncedEditorChange;
|
|
lastActiveEditor;
|
|
plugin;
|
|
constructor(plugin,
|
|
/* eslint-disable no-unused-vars */
|
|
debouncedEditorChange = obsidian.debounce((editor) => this.triggerUpdateContent(editor), 3000)
|
|
/* eslint-enable no-unused-vars */
|
|
) {
|
|
this.debouncedEditorChange = debouncedEditorChange;
|
|
this.plugin = plugin;
|
|
}
|
|
calculateWordFrequencies(content) {
|
|
if (content.length === 0) {
|
|
return [];
|
|
}
|
|
const words = segmentText(content);
|
|
const wordCounts = new Map();
|
|
for (const word of words) {
|
|
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
|
|
}
|
|
return Array.from(wordCounts.entries()).sort(([, countA], [, countB]) => countB - countA);
|
|
}
|
|
handleActiveLeafChange(leaf, workspace) {
|
|
if (leaf === null) {
|
|
return;
|
|
}
|
|
if (!(leaf.view instanceof obsidian.MarkdownView)) {
|
|
return;
|
|
}
|
|
this.plugin.registerEvent(workspace.on('editor-change', (editor) => this.debouncedEditorChange(editor)));
|
|
const activeView = workspace.getActiveViewOfType(obsidian.MarkdownView);
|
|
if (activeView) {
|
|
this.lastActiveEditor = activeView.editor;
|
|
}
|
|
if (workspace.getLeavesOfType(VIEW_TYPE).length > 0) {
|
|
this.triggerUpdateContent(this.lastActiveEditor);
|
|
}
|
|
}
|
|
triggerUpdateContent(editor) {
|
|
if (editor === undefined) {
|
|
if (this.lastActiveEditor === undefined) {
|
|
return;
|
|
}
|
|
editor = this.lastActiveEditor;
|
|
}
|
|
try {
|
|
const wordCounts = this.calculateWordFrequencies(editor.getValue());
|
|
window.document.dispatchEvent(new CustomEvent(EVENT_UPDATE, { detail: { wordCounts } }));
|
|
}
|
|
catch (error) {
|
|
console.error('error in triggerUpdateContent', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
class WordFrequencyDisplay {
|
|
getFilter;
|
|
debouncedFilterInput;
|
|
filter = '';
|
|
plugin;
|
|
view;
|
|
constructor(plugin, view,
|
|
/* eslint-disable no-unused-vars */
|
|
getFilter = () => this.filter, debouncedFilterInput = obsidian.debounce((event) => {
|
|
const target = event.target;
|
|
this.filter = target.value;
|
|
this.view.updateContent();
|
|
}, 500)
|
|
/* eslint-enable no-unused-vars */
|
|
) {
|
|
this.getFilter = getFilter;
|
|
this.debouncedFilterInput = debouncedFilterInput;
|
|
this.plugin = plugin;
|
|
this.view = view;
|
|
}
|
|
addWordToSidebar(blacklist, word, count, contentContainer) {
|
|
if (blacklist.has(word) ||
|
|
count < this.plugin.settings.threshold ||
|
|
!word.toLowerCase().includes(this.getFilter().toLowerCase())) {
|
|
return;
|
|
}
|
|
const row = contentContainer.createEl('div', {
|
|
cls: ELEMENT_CLASSES.containerRow,
|
|
});
|
|
const wordCountContainer = row.createEl('div', {
|
|
cls: ELEMENT_CLASSES.containerCount,
|
|
});
|
|
wordCountContainer.createEl('span', { text: word });
|
|
wordCountContainer.createEl('span', { text: count.toString() });
|
|
const buttonContainer = row.createEl('div', {
|
|
cls: ELEMENT_CLASSES.containerButton,
|
|
});
|
|
const button = buttonContainer.createEl('button');
|
|
obsidian.setIcon(button, 'trash-2');
|
|
this.plugin.registerDomEvent(button, 'click', () => {
|
|
this.saveWordToBlacklist(word);
|
|
});
|
|
}
|
|
createFilter(contentEl) {
|
|
const filterContainer = contentEl.createEl('div', {
|
|
cls: ELEMENT_CLASSES.containerFilter,
|
|
});
|
|
const filterInput = filterContainer.createEl('input', {
|
|
cls: ELEMENT_CLASSES.filter,
|
|
attr: {
|
|
type: 'text',
|
|
placeholder: 'Type to filter results',
|
|
},
|
|
});
|
|
this.plugin.registerDomEvent(filterInput, 'input', (event) => this.debouncedFilterInput(event));
|
|
}
|
|
createHeader(contentEl) {
|
|
const headerContainer = contentEl.createEl('div');
|
|
const headerElement = headerContainer.createEl('h4');
|
|
headerElement.setText(PLUGIN_NAME);
|
|
}
|
|
createThresholdDisplay(contentEl) {
|
|
const thresholdDisplay = contentEl.createEl('div', {
|
|
cls: ELEMENT_CLASSES.containerThreshold,
|
|
});
|
|
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) {
|
|
const settings = this.plugin.settings;
|
|
settings.blacklist += `,${word}`;
|
|
this.plugin.saveData(settings);
|
|
this.view.updateContent();
|
|
}
|
|
}
|
|
|
|
class WordFrequencyView extends obsidian.ItemView {
|
|
display;
|
|
eventListener = () => { };
|
|
plugin;
|
|
wordCountList = [];
|
|
wordListContainer;
|
|
constructor(leaf, plugin, display, divElement) {
|
|
super(leaf);
|
|
this.plugin = plugin;
|
|
this.display = display ?? new WordFrequencyDisplay(plugin, this);
|
|
// TODO: find a better way to set a default value
|
|
this.wordListContainer = divElement ?? this.containerEl.createDiv();
|
|
}
|
|
getDisplayText() {
|
|
return PLUGIN_NAME;
|
|
}
|
|
getIcon() {
|
|
return FREQUENCY_ICON;
|
|
}
|
|
getPlugin() {
|
|
return this.plugin;
|
|
}
|
|
getViewType() {
|
|
return VIEW_TYPE;
|
|
}
|
|
async onOpen() {
|
|
this.eventListener = (event) => {
|
|
if (event.type === EVENT_UPDATE) {
|
|
this.wordCountList = event.detail.wordCounts;
|
|
this.updateContent();
|
|
}
|
|
};
|
|
window.document.addEventListener(EVENT_UPDATE, this.eventListener);
|
|
this.contentEl.empty();
|
|
const contentContainer = this.contentEl.createDiv({
|
|
cls: ELEMENT_CLASSES.containerContent,
|
|
});
|
|
this.display.createHeader(contentContainer);
|
|
this.display.createFilter(contentContainer);
|
|
this.wordListContainer = contentContainer.createDiv({
|
|
cls: ELEMENT_CLASSES.containerWordList,
|
|
});
|
|
this.display.createThresholdDisplay(contentContainer);
|
|
this.updateContent();
|
|
}
|
|
async onClose() {
|
|
window.document.removeEventListener(EVENT_UPDATE, this.eventListener);
|
|
}
|
|
updateContent() {
|
|
this.wordListContainer.empty();
|
|
const blacklist = new Set(this.plugin.settings.blacklist.split(',').map((word) => word.trim()));
|
|
this.wordCountList.forEach(([word, count]) => {
|
|
this.display.addWordToSidebar(blacklist, word, count, this.wordListContainer);
|
|
});
|
|
}
|
|
}
|
|
|
|
class WordFrequencySettingTab extends obsidian.PluginSettingTab {
|
|
plugin;
|
|
settingFactory;
|
|
constructor(plugin, settingFactory = (element) => new obsidian.Setting(element)) {
|
|
super(plugin.app, plugin);
|
|
this.plugin = plugin;
|
|
this.settingFactory = settingFactory;
|
|
}
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
const blacklist = this.settingFactory(containerEl)
|
|
.setName(SETTINGS_NAMES.blacklist)
|
|
.setDesc(SETTINGS_DESCRIPTIONS.blacklist)
|
|
.setClass(ELEMENT_CLASSES.settingItem)
|
|
.addTextArea((text) => {
|
|
text.setValue(this.plugin.settings.blacklist)
|
|
.onChange(async (value) => {
|
|
await this.saveBlacklistValue(value);
|
|
})
|
|
.inputEl.classList.add(ELEMENT_CLASSES.settingBlacklist);
|
|
});
|
|
blacklist.infoEl.addClass(ELEMENT_CLASSES.settingInfoItem);
|
|
this.settingFactory(containerEl)
|
|
.setName(SETTINGS_NAMES.threshold)
|
|
.setDesc(SETTINGS_DESCRIPTIONS.threshold)
|
|
.addText((text) => text
|
|
.setPlaceholder('3')
|
|
.setValue(this.plugin.settings.threshold.toString())
|
|
.onChange(async (value) => {
|
|
await this.updateThreshold(value);
|
|
}));
|
|
}
|
|
async saveBlacklistValue(value) {
|
|
this.plugin.settings.blacklist = value;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
async updateThreshold(value) {
|
|
const num = parseInt(value, 10);
|
|
if (isNaN(num)) {
|
|
return;
|
|
}
|
|
this.plugin.settings.threshold = num;
|
|
await this.plugin.saveSettings();
|
|
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE).forEach((leaf) => {
|
|
if (leaf.view instanceof WordFrequencyView) {
|
|
leaf.view.updateContent();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class WordFrequencyPlugin extends obsidian.Plugin {
|
|
createView;
|
|
frequencyCounter;
|
|
settings = DEFAULT_SETTINGS;
|
|
settingTab;
|
|
viewManager;
|
|
constructor(app, manifest, viewManager, settingTab, frequencyCounter,
|
|
/* eslint-disable no-unused-vars */
|
|
createView = (leaf) => new WordFrequencyView(leaf, this)
|
|
/* eslint-enable no-unused-vars */
|
|
) {
|
|
super(app, manifest);
|
|
this.createView = createView;
|
|
this.settingTab = settingTab ?? new WordFrequencySettingTab(this);
|
|
this.viewManager = viewManager ?? new ViewManager(this);
|
|
this.frequencyCounter =
|
|
frequencyCounter ?? new WordFrequencyCounter(this);
|
|
}
|
|
async onload() {
|
|
const title = `Show ${PLUGIN_NAME.toLowerCase()} sidebar`;
|
|
await this.loadSettings();
|
|
this.registerView(VIEW_TYPE, this.createView);
|
|
this.addRibbonIcon(FREQUENCY_ICON, title, () => this.activateView());
|
|
this.addCommand({
|
|
id: COMMAND_ID_SHOW_SIDEBAR,
|
|
name: title,
|
|
callback: () => this.activateView(),
|
|
});
|
|
this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => {
|
|
this.frequencyCounter.handleActiveLeafChange(leaf, this.app.workspace);
|
|
}));
|
|
this.addSettingTab(this.settingTab);
|
|
}
|
|
onunload() { }
|
|
async activateView() {
|
|
const { workspace } = this.app;
|
|
const leaf = this.viewManager.getOrCreateLeaf(workspace, VIEW_TYPE);
|
|
if (leaf === null) {
|
|
return;
|
|
}
|
|
await this.viewManager.setViewState(leaf, VIEW_TYPE);
|
|
await workspace.revealLeaf(leaf);
|
|
this.viewManager.updateContent();
|
|
}
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
async loadSettings() {
|
|
const settings = await this.loadData();
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, settings);
|
|
}
|
|
}
|
|
|
|
module.exports = WordFrequencyPlugin;
|
|
|
|
/* nosourcemap */ |