generated from mirrors/plugin-sample-vite-svelte
Smarter document loading
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 1m5s
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 1m5s
This commit is contained in:
parent
095e2a2bb9
commit
631cfd14bf
6 changed files with 155 additions and 98 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,3 +9,4 @@ dev
|
|||
dist
|
||||
build
|
||||
tmp
|
||||
.lingma
|
||||
|
|
|
|||
66
src/index.ts
66
src/index.ts
|
|
@ -1,5 +1,5 @@
|
|||
import {Plugin, showMessage} from 'siyuan';
|
||||
import {ProtyleHelpers} from "@/protyleHelpers";
|
||||
import {IProtyle, Plugin, showMessage} from 'siyuan';
|
||||
import {ProtyleHelper} from "@/protyleHelper";
|
||||
import {Icons} from "@/icons";
|
||||
import {Settings} from "@/settings";
|
||||
import {SettingUtils} from "@/libs/setting-utils";
|
||||
|
|
@ -15,6 +15,7 @@ import {Language} from "@/spellChecker";
|
|||
export default class SpellCheckPlugin extends Plugin {
|
||||
|
||||
private menus: Menus
|
||||
private currentlyEditing: { protyle: ProtyleHelper, enabled: boolean, language: string };
|
||||
|
||||
public settingsUtil: SettingUtils;
|
||||
public suggestions: SuggestionEngine
|
||||
|
|
@ -68,24 +69,28 @@ export default class SpellCheckPlugin extends Plugin {
|
|||
|
||||
this.eventBus.on('ws-main', async (event) => {
|
||||
|
||||
if (event.detail.cmd != 'transactions') { return }
|
||||
if (event.detail.cmd != 'transactions') {
|
||||
return
|
||||
}
|
||||
|
||||
const operation = event.detail.data[0].doOperations[0]
|
||||
const action = operation.action
|
||||
const blockID = operation.id
|
||||
|
||||
if(action != 'update') { return }
|
||||
if (action != 'update' || !this.currentlyEditing.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.suggestions.storeBlocks(this.currentlyEditing.protyle, this.currentlyEditing.language)
|
||||
await this.suggestions.suggestAndRender(blockID)
|
||||
void this.suggestions.forAllBlocksSuggest(this.suggestions.documentID, false, true, false)
|
||||
void this.suggestions.forAllBlocksSuggest(false, true)
|
||||
|
||||
})
|
||||
|
||||
this.eventBus.on('open-menu-content', async (event) => {
|
||||
|
||||
if(!this.suggestions.documentEnabled) { return }
|
||||
void this.analytics.sendEvent('menu-open-any');
|
||||
const blockID = ProtyleHelpers.getNodeId(event.detail.range.startContainer.parentElement)
|
||||
const blockID = ProtyleHelper.getNodeId(event.detail.range.startContainer.parentElement)
|
||||
|
||||
const suggNo = this.suggestions.getSuggestionNumber(blockID, event.detail.range)
|
||||
this.menus.addCorrectionsToParagraphMenu(blockID, suggNo, event.detail.menu)
|
||||
|
|
@ -93,31 +98,46 @@ export default class SpellCheckPlugin extends Plugin {
|
|||
})
|
||||
|
||||
this.eventBus.on('open-menu-doctree', async (event) => {
|
||||
const docID = ProtyleHelpers.getNodeId(event.detail.elements[0]) // @TODO this is ugly, why does the event not carry the docID?
|
||||
const docID = ProtyleHelper.getNodeId(event.detail.elements[0]) // @TODO this is ugly, why does the event not carry the docID?
|
||||
void this.menus.addSettingsToDocMenu(docID, event.detail.menu)
|
||||
})
|
||||
|
||||
this.eventBus.on('switch-protyle', async (event) => {
|
||||
void this.suggestions.forAllBlocksSuggest(false, true)
|
||||
const settings = await ProtyleHelper.getDocumentSettings(event.detail.protyle.block.id,
|
||||
this.settingsUtil.get('enabledByDefault'), this.settingsUtil.get('defaultLanguage'))
|
||||
this.currentlyEditing = {
|
||||
protyle: new ProtyleHelper(event.detail.protyle.contentElement),
|
||||
enabled: settings.enabled,
|
||||
language: settings.language
|
||||
}
|
||||
new ResizeObserver(
|
||||
this.suggestions.forAllBlocksSuggest.bind(this.suggestions)
|
||||
).observe(event.detail.protyle.contentElement)
|
||||
})
|
||||
|
||||
const docID = event.detail.protyle.block.id
|
||||
const settings = await ProtyleHelpers.getDocumentSettings(docID, this.settingsUtil.get('enabledByDefault'), this.settingsUtil.get('defaultLanguage'))
|
||||
|
||||
this.suggestions.documentID = docID
|
||||
this.suggestions.documentEnabled = settings.enabled
|
||||
this.suggestions.documentLanguage = settings.language
|
||||
|
||||
this.suggestions.clearStorage()
|
||||
void this.suggestions.forAllBlocksSuggest(docID, true, true, false)
|
||||
|
||||
const activeEditor = document.querySelector('.fn__flex-1.protyle:not([class*="fn__none"])')
|
||||
new ResizeObserver(this.reRenderSuggestions.bind(this)).observe(activeEditor)
|
||||
|
||||
this.eventBus.on('loaded-protyle-static', async (event) => {
|
||||
await this.protyleLoad(event)
|
||||
})
|
||||
this.eventBus.on('loaded-protyle-dynamic', async (event) => {
|
||||
await this.protyleLoad(event)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private reRenderSuggestions() {
|
||||
void this.suggestions.forAllBlocksSuggest(this.suggestions.documentID, false, true, false)
|
||||
private async protyleLoad(event: CustomEvent<{ protyle: IProtyle; }>) {
|
||||
|
||||
const protyle = new ProtyleHelper(event.detail.protyle.contentElement)
|
||||
const docID = event.detail.protyle.block.id
|
||||
|
||||
const settings = await ProtyleHelper.getDocumentSettings(docID,
|
||||
this.settingsUtil.get('enabledByDefault'), this.settingsUtil.get('defaultLanguage'))
|
||||
|
||||
if(settings.enabled) {
|
||||
await this.suggestions.storeBlocks(protyle, settings.language)
|
||||
void this.suggestions.forAllBlocksSuggest(true, true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import {Menu, showMessage, subMenu} from 'siyuan';
|
|||
import SpellCheckPlugin from "@/index";
|
||||
import {getBlockAttrs, setBlockAttrs} from "@/api";
|
||||
import {Settings} from "@/settings";
|
||||
import {ProtyleHelpers} from "@/protyleHelpers";
|
||||
import {SuggestionEngine} from "@/suggestions";
|
||||
import {ProtyleHelper} from "@/protyleHelper";
|
||||
|
||||
export class Menus {
|
||||
|
||||
|
|
@ -37,7 +36,7 @@ export class Menus {
|
|||
label: this.plugin.i18nx.textMenu.addToDictionary,
|
||||
click: async () => {
|
||||
void this.plugin.analytics.sendEvent('menu-click-add-to-dictionary');
|
||||
const word = SuggestionEngine.suggestionToWrongText(suggestion, blockID)
|
||||
const word = this.plugin.suggestions.suggestionToWrongText(suggestion, blockID)
|
||||
await Settings.addToDictionary(word, this.plugin.settingsUtil)
|
||||
showMessage(this.plugin.i18nx.textMenu.addedToDictionary + word, 5000, 'info')
|
||||
await this.plugin.suggestions.renderSuggestions(blockID)
|
||||
|
|
@ -72,7 +71,7 @@ export class Menus {
|
|||
icon: 'info',
|
||||
label: this.plugin.i18nx.docMenu.documentStatus,
|
||||
click: async () => {
|
||||
const settings = await ProtyleHelpers.getDocumentSettings(docID, this.plugin.settingsUtil.get('enabledByDefault'), this.plugin.settingsUtil.get('defaultLanguage'))
|
||||
const settings = await ProtyleHelper.getDocumentSettings(docID, this.plugin.settingsUtil.get('enabledByDefault'), this.plugin.settingsUtil.get('defaultLanguage'))
|
||||
if(settings == null) {
|
||||
void this.plugin.analytics.sendEvent('docmenu-click-info-notebook');
|
||||
showMessage(this.plugin.i18nx.errors.notImplementedNotebookSettings, 5000, 'info')
|
||||
|
|
@ -93,7 +92,7 @@ export class Menus {
|
|||
click: async () => {
|
||||
void this.plugin.analytics.sendEvent('docmenu-click-toggle');
|
||||
const attrs = await getBlockAttrs(docID)
|
||||
const settings = await ProtyleHelpers.getDocumentSettings(docID, this.plugin.settingsUtil.get('enabledByDefault'), this.plugin.settingsUtil.get('defaultLanguage'))
|
||||
const settings = await ProtyleHelper.getDocumentSettings(docID, this.plugin.settingsUtil.get('enabledByDefault'), this.plugin.settingsUtil.get('defaultLanguage'))
|
||||
if(settings == null) {
|
||||
void this.plugin.analytics.sendEvent('docmenu-click-info-notebook');
|
||||
showMessage(this.plugin.i18nx.errors.notImplementedNotebookSettings, 5000, 'info')
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
import {getBlockAttrs} from "@/api";
|
||||
import SpellCheckPlugin from "@/index";
|
||||
|
||||
export class ProtyleHelpers {
|
||||
export class ProtyleHelper {
|
||||
|
||||
// We shouldn't use JavaScript elements to get and set data in blocks, but the kernel API is noticeably too slow for this.
|
||||
private contentElement: Element
|
||||
|
||||
constructor(contentElement?: Element) {
|
||||
this.contentElement = contentElement || document.documentElement;
|
||||
}
|
||||
|
||||
// We shouldn't use JavaScript elements to get data in blocks, but the kernel API is noticeably too slow for this.
|
||||
// We must try to keep the dependency to the HTML to a minimum.
|
||||
|
||||
// doesn't use kernel API, so it's faster
|
||||
public static fastGetBlockElement(blockID: string): Element {
|
||||
public fastGetBlockElement(blockID: string): Element {
|
||||
const wrapper = Array.from(
|
||||
document.querySelectorAll(`div[data-node-id="${blockID}"]`)
|
||||
this.contentElement.querySelectorAll(`div[data-node-id="${blockID}"]`)
|
||||
).find(el =>
|
||||
!el.closest('.protyle-wysiwyg__embed') // true = not inside that class
|
||||
);
|
||||
|
|
@ -17,16 +23,16 @@ export class ProtyleHelpers {
|
|||
return wrapper?.querySelector(':scope > [contenteditable="true"]') ?? null;
|
||||
}
|
||||
|
||||
public static fastGetBlockHTML(blockID: string): string {
|
||||
public fastGetBlockHTML(blockID: string): string {
|
||||
return this.fastGetBlockElement(blockID).innerHTML
|
||||
}
|
||||
|
||||
public static fastGetBlockText(blockID: string): string {
|
||||
public fastGetBlockText(blockID: string): string {
|
||||
return this.fastGetBlockElement(blockID)?.textContent
|
||||
}
|
||||
|
||||
public static fastGetTitleElement(docID: string) {
|
||||
const container = document.querySelector(`div.protyle-title.protyle-wysiwyg--attr[data-node-id="${docID}"]`);
|
||||
public fastGetTitleElement() {
|
||||
const container = this.contentElement.querySelector(`div.protyle-title.protyle-wysiwyg--attr`); // [data-node-id="${docID}"]
|
||||
if (!container) return null;
|
||||
return container.querySelector('div.protyle-title__input[contenteditable="true"]');
|
||||
}
|
||||
|
|
@ -57,9 +63,16 @@ export class ProtyleHelpers {
|
|||
}
|
||||
}
|
||||
|
||||
public static isProtyleReady(docID: string): boolean {
|
||||
const protyleTitleContainer = document.querySelector(`div[class="protyle-title protyle-wysiwyg--attr"]`)
|
||||
return protyleTitleContainer.getAttribute('data-node-id') == docID
|
||||
public getBlockElements(): Element[] {
|
||||
const allElements: Set<Element> = new Set();
|
||||
this.contentElement.querySelectorAll('[data-node-id]').forEach(el => {
|
||||
allElements.add(el);
|
||||
});
|
||||
return Array.from(allElements);
|
||||
}
|
||||
|
||||
public toNode(): Node {
|
||||
return this.contentElement
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,28 +1,31 @@
|
|||
import {ProtyleHelpers} from "@/protyleHelpers";
|
||||
import {ProtyleHelper} from "@/protyleHelper";
|
||||
|
||||
export class SpellCheckerUI {
|
||||
|
||||
private readonly blockID: string;
|
||||
private readonly docID: string;
|
||||
private readonly protyle: ProtyleHelper;
|
||||
private block: HTMLElement;
|
||||
private overlay: HTMLElement;
|
||||
|
||||
constructor(blockID: string, docID: string) {
|
||||
constructor(blockID: string, protyle: ProtyleHelper) {
|
||||
this.blockID = blockID;
|
||||
this.docID = docID;
|
||||
this.protyle = protyle;
|
||||
this.setBlock()
|
||||
}
|
||||
|
||||
private setBlock() {
|
||||
|
||||
this.block = <HTMLElement>ProtyleHelpers.fastGetBlockElement(this.blockID)
|
||||
let overlay = <HTMLElement>ProtyleHelpers.fastGetOverlayElement(this.blockID)
|
||||
this.block = <HTMLElement>this.protyle.fastGetBlockElement(this.blockID)
|
||||
if(this.block == null) {
|
||||
throw new Error(`Block ${this.blockID} not found`);
|
||||
}
|
||||
let overlay = <HTMLElement>ProtyleHelper.fastGetOverlayElement(this.blockID)
|
||||
|
||||
if(overlay == null) {
|
||||
this.overlay = document.createElement('div')
|
||||
this.overlay.className = 'underline-overlay';
|
||||
this.overlay.setAttribute('for-block-id', this.blockID)
|
||||
const protyleTitle = ProtyleHelpers.fastGetTitleElement(this.docID)
|
||||
const protyleTitle = this.protyle.fastGetTitleElement()
|
||||
protyleTitle?.append(this.overlay)
|
||||
}else{
|
||||
if(this.overlay == null) {
|
||||
|
|
@ -144,7 +147,7 @@ export class SpellCheckerUI {
|
|||
}
|
||||
|
||||
public destroy() {
|
||||
let overlay = <HTMLElement>ProtyleHelpers.fastGetOverlayElement(this.blockID)
|
||||
let overlay = <HTMLElement>ProtyleHelper.fastGetOverlayElement(this.blockID)
|
||||
overlay?.remove();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {ProtyleHelpers} from "@/protyleHelpers";
|
||||
import {ProtyleHelper} from "@/protyleHelper";
|
||||
import {Settings} from "@/settings";
|
||||
import {getChildBlocks, updateBlock} from "@/api";
|
||||
import {updateBlock} from "@/api";
|
||||
import {SpellCheckerUI} from "@/spellCheckerUI";
|
||||
import {showMessage} from "siyuan";
|
||||
import SpellCheckPlugin from "@/index";
|
||||
|
|
@ -8,7 +8,9 @@ import {Suggestion} from "@/spellChecker";
|
|||
|
||||
interface StoredBlock {
|
||||
spellChecker: SpellCheckerUI;
|
||||
suggestions: Suggestion[];
|
||||
language: string;
|
||||
suggestions: Suggestion[] | null;
|
||||
protyle: ProtyleHelper;
|
||||
}
|
||||
|
||||
type BlockStorage = Record<string, StoredBlock>;
|
||||
|
|
@ -18,10 +20,6 @@ export class SuggestionEngine {
|
|||
private blockStorage: BlockStorage = {};
|
||||
private plugin: SpellCheckPlugin;
|
||||
|
||||
public documentID: string;
|
||||
public documentEnabled: boolean = false;
|
||||
public documentLanguage: string = 'auto';
|
||||
|
||||
constructor(plugin: SpellCheckPlugin) {
|
||||
this.plugin = plugin
|
||||
}
|
||||
|
|
@ -36,72 +34,79 @@ export class SuggestionEngine {
|
|||
}
|
||||
}
|
||||
|
||||
private async discoverBlocks(blockID: string) {
|
||||
const children = await getChildBlocks(blockID)
|
||||
if(children.length == 0) {
|
||||
if(!(blockID in this.blockStorage)) {
|
||||
const spellChecker = new SpellCheckerUI(blockID, this.documentID)
|
||||
this.blockStorage[blockID] = {
|
||||
spellChecker: spellChecker,
|
||||
suggestions: []
|
||||
}
|
||||
}
|
||||
}else{
|
||||
for (const child of children) {
|
||||
await this.discoverBlocks(child.id)
|
||||
}
|
||||
}
|
||||
public getProtyle(blockID: string) {
|
||||
if(!(blockID in this.blockStorage)) { return null }
|
||||
return this.blockStorage[blockID].protyle
|
||||
}
|
||||
|
||||
public async forAllBlocksSuggest(docID: string, suggest: boolean, render: boolean, remove: boolean) {
|
||||
if(!this.documentEnabled) { return }
|
||||
if(suggest) {
|
||||
await this.discoverBlocks(docID) // updates this.blockStorage
|
||||
public async storeBlocks(protyle: ProtyleHelper, documentLanguage: string) {
|
||||
const blocks = protyle.getBlockElements()
|
||||
blocks.forEach(block => {
|
||||
const blockID = ProtyleHelper.getNodeId(block)
|
||||
if(!blockID) {
|
||||
return
|
||||
}
|
||||
if(!(blockID in this.blockStorage)) {
|
||||
try {
|
||||
const spellChecker = new SpellCheckerUI(blockID, protyle)
|
||||
this.blockStorage[blockID] = {
|
||||
spellChecker: spellChecker,
|
||||
language: documentLanguage,
|
||||
suggestions: null,
|
||||
protyle: protyle
|
||||
}
|
||||
}catch (_) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public async forAllBlocksSuggest(suggest: boolean = false, render: boolean = true) {
|
||||
const blockPromises = Object.keys(this.blockStorage).map(async (blockID) => {
|
||||
if(suggest) {
|
||||
if(!(blockID in this.blockStorage)) {
|
||||
return
|
||||
}
|
||||
if(suggest && this.blockStorage[blockID].suggestions == null) {
|
||||
await this.suggestForBlock(blockID)
|
||||
}
|
||||
if(render) {
|
||||
await this.renderSuggestions(blockID)
|
||||
}
|
||||
if(remove) {
|
||||
await this.removeSuggestionsAndRender(blockID)
|
||||
}
|
||||
});
|
||||
await Promise.all(blockPromises);
|
||||
}
|
||||
|
||||
public async suggestAndRender(blockID: string) {
|
||||
if(!this.documentEnabled) { return }
|
||||
await this.suggestForBlock(blockID)
|
||||
await this.renderSuggestions(blockID)
|
||||
}
|
||||
|
||||
public async suggestForBlock(blockID: string) {
|
||||
|
||||
let suggestions: Suggestion[]
|
||||
const text = ProtyleHelpers.fastGetBlockText(blockID)
|
||||
if(text == null || !this.documentEnabled) {
|
||||
if(!(blockID in this.blockStorage)) {
|
||||
return
|
||||
}
|
||||
if(!(blockID in this.blockStorage)) {
|
||||
await this.discoverBlocks(blockID)
|
||||
return this.suggestForBlock(blockID)
|
||||
const thisBlock = this.blockStorage[blockID]
|
||||
thisBlock.suggestions = [] // we change from null so that it doesn't run again in forAllBlocksSuggest if we're waiting for the spell checker
|
||||
|
||||
let suggestions: Suggestion[]
|
||||
const text = thisBlock.protyle.fastGetBlockText(blockID)
|
||||
if(text == null || text == '') {
|
||||
return
|
||||
}
|
||||
|
||||
if(this.plugin.settingsUtil.get('offline')) {
|
||||
suggestions = await this.plugin.offlineSpellChecker.check(text, [this.documentLanguage])
|
||||
suggestions = await this.plugin.offlineSpellChecker.check(text, [thisBlock.language])
|
||||
thisBlock.suggestions = suggestions
|
||||
}else{
|
||||
try {
|
||||
suggestions = await this.plugin.onlineSpellChecker.check(text, [this.documentLanguage])
|
||||
suggestions = await this.plugin.onlineSpellChecker.check(text, [thisBlock.language])
|
||||
thisBlock.suggestions = suggestions
|
||||
}catch (_) {
|
||||
showMessage(this.plugin.i18nx.errors.checkServer, 5000, 'error')
|
||||
thisBlock.suggestions = null
|
||||
}
|
||||
}
|
||||
|
||||
this.blockStorage[blockID].suggestions = suggestions
|
||||
|
||||
}
|
||||
|
||||
public async removeSuggestionsAndRender(blockID: string) {
|
||||
|
|
@ -109,19 +114,35 @@ export class SuggestionEngine {
|
|||
}
|
||||
|
||||
public async renderSuggestions(blockID: string) {
|
||||
if(!(blockID in this.blockStorage) || !this.documentEnabled) {
|
||||
|
||||
if(!(blockID in this.blockStorage)) {
|
||||
return
|
||||
}
|
||||
this.blockStorage[blockID].spellChecker.clearUnderlines()
|
||||
this.blockStorage[blockID].suggestions.forEach(suggestion => {
|
||||
if(!Settings.isInCustomDictionary(SuggestionEngine.suggestionToWrongText(suggestion, blockID), this.plugin.settingsUtil)) {
|
||||
this.blockStorage[blockID].spellChecker.highlightCharacterRange(suggestion.offset, suggestion.offset + suggestion.length)
|
||||
}
|
||||
})
|
||||
const thisBlock = this.blockStorage[blockID]
|
||||
if(!document.contains(thisBlock.protyle.toNode())) {
|
||||
delete this.blockStorage[blockID]
|
||||
return
|
||||
}
|
||||
|
||||
static suggestionToWrongText(suggestion: Suggestion, blockID: string): string {
|
||||
const blockTxt = ProtyleHelpers.fastGetBlockText(blockID)
|
||||
thisBlock.spellChecker.clearUnderlines()
|
||||
|
||||
thisBlock.suggestions?.forEach(suggestion => {
|
||||
if(!Settings.isInCustomDictionary(this.suggestionToWrongText(suggestion, blockID), this.plugin.settingsUtil)) {
|
||||
try {
|
||||
thisBlock.spellChecker.highlightCharacterRange(suggestion.offset, suggestion.offset + suggestion.length)
|
||||
}catch (_) {
|
||||
delete this.blockStorage[blockID]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public suggestionToWrongText(suggestion: Suggestion, blockID: string): string {
|
||||
if(!(blockID in this.blockStorage)) {
|
||||
return
|
||||
}
|
||||
const blockTxt = this.blockStorage[blockID].protyle.fastGetBlockText(blockID)
|
||||
return blockTxt.slice(suggestion.offset, suggestion.offset + suggestion.length)
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +187,7 @@ export class SuggestionEngine {
|
|||
console.log("dbg " + blockID + ' ' + suggestionNumber + ' ' + correctionNumber)
|
||||
console.log(this.blockStorage)
|
||||
const suggestion = this.blockStorage[blockID].suggestions[suggestionNumber]
|
||||
const rich = ProtyleHelpers.fastGetBlockHTML(blockID)
|
||||
const rich = new ProtyleHelper().fastGetBlockHTML(blockID)
|
||||
const fixedOffset = this.adjustIndexForTags(rich, suggestion.offset)
|
||||
const newStr = rich.slice(0, fixedOffset) + suggestion.replacements[correctionNumber] + rich.slice(fixedOffset + suggestion.length)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue