From 8fcc1c76e9377337c6a9e995c24c9b9b413d4c87 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Sun, 19 Oct 2025 10:48:45 +0200 Subject: [PATCH 1/4] Do not underline inline LaTeX, inline code, images --- src/protyleHelper.ts | 22 ++++++++++++++++++++++ src/spellCheckerUI.ts | 15 +++------------ src/suggestions.ts | 23 ++++++++++++++++++++++- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/protyleHelper.ts b/src/protyleHelper.ts index a5ae0cc..742f7e5 100644 --- a/src/protyleHelper.ts +++ b/src/protyleHelper.ts @@ -41,6 +41,28 @@ export class ProtyleHelper { return document.querySelector(`div.underline-overlay[for-block-id="${blockID}"]`) } + public static getElementAtTextIndex(root: Element, index: number): Node { + let currentOffset = 0; + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); + + while (walker.nextNode()) { + let node = walker.currentNode + const textLength = node.textContent.length; + + if (currentOffset + textLength >= index) { + let parent: Element = node.parentElement; + while (parent && parent != root) { + node = parent + parent = node.parentElement + } + return node; // The element containing this text + } + currentOffset += textLength; + } + + return null; + } + // given an element such as a span inside a block, return its blockID public static getNodeId(el: Element) { let i = 0; diff --git a/src/spellCheckerUI.ts b/src/spellCheckerUI.ts index a662687..d932fd5 100644 --- a/src/spellCheckerUI.ts +++ b/src/spellCheckerUI.ts @@ -49,7 +49,7 @@ export class SpellCheckerUI { // Find the text nodes and character positions const range = this.createRangeFromCharacterIndices(startIndex, endIndex); if (range) { - this.createUnderlineFromRange(range, endIndex - startIndex); + this.createUnderlineFromRange(range); } } @@ -101,7 +101,7 @@ export class SpellCheckerUI { return null; } - private createUnderlineFromRange(range: Range, charsCount: number) { + private createUnderlineFromRange(range: Range) { const rects = range.getClientRects(); const editorRect = this.block.getBoundingClientRect(); @@ -120,19 +120,10 @@ export class SpellCheckerUI { underline.style.top = (top + 2 + offset.v) + 'px'; underline.style.width = width + 'px'; - if(!SpellCheckerUI.checkDontUnderline(width, charsCount)) { - this.overlay.appendChild(underline); - } + this.overlay.appendChild(underline); } } - // if the underline is too wide for the number of characters that are underlined, we don't render it - // this is a consequence of using .innerText: things like tags are only a character - private static checkDontUnderline(width: number, charsCount: number) { - const maxWidthPerChar = 16; - return width > maxWidthPerChar * charsCount - } - private static distance(elA: HTMLElement, elB: HTMLElement): {h: number, v: number} { const rectA = elA.getBoundingClientRect(); const rectB = elB.getBoundingClientRect(); diff --git a/src/suggestions.ts b/src/suggestions.ts index eb95692..d43340a 100644 --- a/src/suggestions.ts +++ b/src/suggestions.ts @@ -20,6 +20,12 @@ export class SuggestionEngine { private blockStorage: BlockStorage = {}; private plugin: SpellCheckPlugin; + private static blacklisted: string[] = [ + "span[data-type='inline-math']", + "span[data-type='img']", + "span[data-type='code']" + ]; + constructor(plugin: SpellCheckPlugin) { this.plugin = plugin } @@ -127,7 +133,8 @@ export class SuggestionEngine { thisBlock.spellChecker.clearUnderlines() thisBlock.suggestions?.forEach(suggestion => { - if(!Settings.isInCustomDictionary(this.suggestionToWrongText(suggestion, blockID), this.plugin.settingsUtil)) { + if(this.shouldSuggest(blockID, thisBlock, suggestion) && + !Settings.isInCustomDictionary(this.suggestionToWrongText(suggestion, blockID), this.plugin.settingsUtil)) { try { thisBlock.spellChecker.highlightCharacterRange(suggestion.offset, suggestion.offset + suggestion.length) }catch (_) { @@ -138,6 +145,20 @@ export class SuggestionEngine { } + private shouldSuggest(blockID: string, block: StoredBlock, suggestion: Suggestion): boolean { + + const element = block.protyle.fastGetBlockElement(blockID) + const eai = ProtyleHelper.getElementAtTextIndex(element, suggestion.offset + suggestion.length) + + for(let blacklisted of SuggestionEngine.blacklisted) { + if(eai instanceof Element && eai.matches(blacklisted)) { + return false + } + } + return true + + } + public suggestionToWrongText(suggestion: Suggestion, blockID: string): string { if(!(blockID in this.blockStorage)) { return From f27227b3a03df92489c53fdc3c0ecb159dc8171d Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Sun, 19 Oct 2025 10:58:10 +0200 Subject: [PATCH 2/4] Render more often --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 0b21f2f..8405f65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,7 @@ export default class SpellCheckPlugin extends Plugin { this.eventBus.on('ws-main', async (event) => { if (event.detail.cmd != 'transactions') { + void this.suggestions.forAllBlocksSuggest(false, true) return } From 0e392c9deaff6ef7cf95c18c7c77f14d32e404b5 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Sun, 19 Oct 2025 21:50:42 +0200 Subject: [PATCH 3/4] Reorganize document menus --- public/i18n/en_US.json | 1 + src/menus.ts | 45 ++++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index 1e4a48f..ab4e6f6 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -1,4 +1,5 @@ { + "syspell": "SySpell", "settings":{ "info": { "title": "Information", diff --git a/src/menus.ts b/src/menus.ts index 6e89181..0651e1b 100644 --- a/src/menus.ts +++ b/src/menus.ts @@ -3,6 +3,7 @@ import SpellCheckPlugin from "@/index"; import {getBlockAttrs, setBlockAttrs} from "@/api"; import {Settings} from "@/settings"; import {ProtyleHelper} from "@/protyleHelper"; +import {Analytics} from "@/analytics"; export class Menus { @@ -67,7 +68,9 @@ export class Menus { public async addSettingsToDocMenu(docID: string, menu: subMenu) { - menu.addItem({ + let submenu = [] + + submenu.push({ icon: 'info', label: this.plugin.i18nx.docMenu.documentStatus, click: async () => { @@ -86,7 +89,7 @@ export class Menus { } }) - menu.addItem({ + submenu.push({ icon: 'toggle', label: this.plugin.i18nx.docMenu.toggleSpellCheck, click: async () => { @@ -104,7 +107,17 @@ export class Menus { } }) - menu.addItem({ + async function setLang(lang: string, analytics: Analytics) { + const attrs = await getBlockAttrs(docID) + attrs[SpellCheckPlugin.LANGUAGE_ATTR] = lang + await setBlockAttrs(docID, attrs) + void analytics.sendEvent('docmenu-click-setlang-2', { + 'language': lang + }); + location.reload() + } + + submenu.push({ icon: 'language', label: this.plugin.i18nx.docMenu.setDocumentLanguage, click: async (_, ev: MouseEvent) => { @@ -114,29 +127,13 @@ export class Menus { langMenu.addItem({ icon: 'autodetect', label: this.plugin.i18nx.docMenu.autodetectLanguage, - click: async () => { - const attrs = await getBlockAttrs(docID) - attrs[SpellCheckPlugin.LANGUAGE_ATTR] = 'auto' - await setBlockAttrs(docID, attrs) - void this.plugin.analytics.sendEvent('docmenu-click-setlang-2', { - 'language': 'auto' - }); - location.reload() - } + click: async () => setLang('auto', this.plugin.analytics) }); languages.forEach(language => { langMenu.addItem({ icon: 'language', label: language.name + ' [' + language.longCode + ']', - click: async () => { - const attrs = await getBlockAttrs(docID) - attrs[SpellCheckPlugin.LANGUAGE_ATTR] = language.longCode - await setBlockAttrs(docID, attrs) - void this.plugin.analytics.sendEvent('docmenu-click-setlang-2', { - 'language': language.longCode - }); - location.reload() - } + click: async () => setLang(language.longCode, this.plugin.analytics) }); }); langMenu.open({ x: ev.clientX, y: ev.clientY }); @@ -144,6 +141,12 @@ export class Menus { } }) + menu.addItem({ + icon: 'spellcheck', + label: this.plugin.i18nx.syspell, + submenu: submenu + }) + } } \ No newline at end of file From 338ac8594e1e20ca57b6dea849905fa217716f42 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Mon, 20 Oct 2025 23:09:34 +0200 Subject: [PATCH 4/4] Version bump --- package.json | 2 +- plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 13bd2ab..7905fa3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "syspell", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "description": "This SiYuan plugin adds a fully featured grammar and spell checker, powered by LanguageTool.", "repository": "https://git.massive.box/massivebox/syspell", diff --git a/plugin.json b/plugin.json index 9671f15..4f756c6 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "syspell", "author": "massivebox", "url": "https://git.massive.box/massivebox/syspell", - "version": "0.3.0", + "version": "0.4.0", "minAppVersion": "3.0.12", "backends": [ "windows",