Compare commits

..

4 commits
v0.3.0 ... main

Author SHA1 Message Date
338ac8594e
Version bump
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 1m51s
2025-10-20 23:09:34 +02:00
0e392c9dea
Reorganize document menus
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 40s
2025-10-19 21:50:42 +02:00
f27227b3a0
Render more often
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 1m1s
2025-10-19 10:58:10 +02:00
8fcc1c76e9
Do not underline inline LaTeX, inline code, images
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 4m7s
2025-10-19 10:48:45 +02:00
8 changed files with 75 additions and 36 deletions

View file

@ -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",

View file

@ -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",

View file

@ -1,4 +1,5 @@
{
"syspell": "SySpell",
"settings":{
"info": {
"title": "Information",

View file

@ -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
}

View file

@ -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
})
}
}

View file

@ -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;

View file

@ -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,18 +120,9 @@ 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);
}
}
}
// 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 <img> 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();

View file

@ -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