From ad13e9849f40119a0e6febf6e41f460e749a4f16 Mon Sep 17 00:00:00 2001 From: Simon Rozman Date: Wed, 26 Feb 2025 15:51:15 +0100 Subject: [PATCH] Extend grammar markup style --- samples/markup-style.html | 86 ++++++++ service.js | 404 ++++++++++++++++++++++++++++---------- 2 files changed, 388 insertions(+), 102 deletions(-) create mode 100644 samples/markup-style.html diff --git a/samples/markup-style.html b/samples/markup-style.html new file mode 100644 index 0000000..2e757e9 --- /dev/null +++ b/samples/markup-style.html @@ -0,0 +1,86 @@ + + + + + + BesService Markup Styles Example + + + + + + + + +

This is an example how to switch markup style.

+

+
+ +

+

+ + +

<textarea> Control

+
+ +
+ +

<div contenteditable="true"> Control

+
+

Tukaj vpišite besedilo ki ga želite popraviti.

+

Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.

+

To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.

+

Na mizo nisem položil knjigo.

+

Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.

+
+ +

Static Content

+
+

Tukaj vpišite besedilo ki ga želite popraviti.

+

Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.

+

To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.

+

Na mizo nisem položil knjigo.

+

Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.

+
+ +

CKEditor Control

+
+
+

Tukaj vpišite besedilo ki ga želite popraviti.

+

Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.

+

To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.

+

Na mizo nisem položil knjigo.

+

Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.

+
+
+ + + + + diff --git a/service.js b/service.js index 788e05c..7c174ba 100644 --- a/service.js +++ b/service.js @@ -43,6 +43,7 @@ class BesService { this.results = [] // Results of grammar-checking, one per each block/paragraph of text this.highlightElements = [] this.createCorrectionPanel() + this.markupStyle = 'underline' // Disable browser built-in spell-checker to prevent collision with our grammar markup. this.originalSpellcheck = this.hostElement.spellcheck @@ -188,6 +189,18 @@ class BesService { return this } + /** + * Sets markup style. + * + * @param {String} style Can be one of the following values: + * 'underline' Underline parts of sentences where grammar mistake is detected (default) + * 'lector' Use lector signs to markup grammar mistakes + */ + setMarkupStyle(style) { + this.markupStyle = style + this.repositionAllMarkup() + } + /** * Schedules proofing after given number of milliseconds. * @@ -343,41 +356,233 @@ class BesService { } /** - * Creates grammar mistake markup in DOM. + * Creates grammar mistake markup in DOM and populates collection of highlight rectangles. * - * @param {Range} range Grammar mistake range - * @param {String} ruleId Grammar mistake rule ID as reported by BesStr - * @returns {Array} Grammar mistake highlight elements + * @param {*} match Grammar checking rule match */ - addMistakeMarkup(range, ruleId) { + addMistakeMarkup(match) { + const range = match.range + const ruleId = match.match.rule.id const scrollPanelRect = this.scrollPanel.getBoundingClientRect() - let highlights = [] const dpr = window.devicePixelRatio + const markerX = this.canvasPanel.width - 30 * dpr + match.highlights = Array.from(range.getClientRects()) this.ctx.lineWidth = 2 * dpr // Use 2 for clearer visibility this.ctx.strokeStyle = ruleId.startsWith('MORFOLOGIK_RULE') ? 'rgba(255, 115, 0, 0.5)' : 'rgba(0, 123, 255, 0.5)' - for (let rect of range.getClientRects()) { - const x = (rect.left - scrollPanelRect.left) * dpr - const y = (rect.top - scrollPanelRect.top) * dpr - const width = rect.width * dpr - const height = rect.height * dpr - - // MOCKUP text drawing - this.ctx.font = `5px ${this.textFont}` // Font se lahko doda na sledeč način: `25px 'Times New Roman', serif` - const text = 'To je izmišljeno besedilo' - const textLength = this.ctx.measureText(text) - console.log(`Dolžina texta: ${textLength.width}px`) // Dolžina texta - this.ctx.fillText(text, x, y) - - // Draw the underline + const drawSideMarker = (y1, y2) => { this.ctx.beginPath() - this.ctx.moveTo(x, y + height) - this.ctx.lineTo(x + width, y + height) + this.ctx.moveTo(markerX, y1 * dpr) + this.ctx.lineTo(markerX, y2 * dpr) this.ctx.stroke() - highlights.push(rect) } - return highlights + switch (this.markupStyle) { + case 'lector': + if (match.match.replacements && match.match.replacements.length) { + const drawMissingComma = (x, y) => { + this.ctx.beginPath() + this.ctx.moveTo((x - 2) * dpr, y * dpr) + this.ctx.lineTo((x + 2) * dpr, y * dpr) + this.ctx.lineTo((x + 2) * dpr, (y - 4) * dpr) + this.ctx.stroke() + } + const drawWrongSpacing = (x, y1, y2) => { + this.ctx.beginPath() + this.ctx.moveTo((x - 4) * dpr, (y1 + 4) * dpr) + this.ctx.lineTo(x * dpr, y1 * dpr) + this.ctx.lineTo((x + 4) * dpr, (y1 + 4) * dpr) + this.ctx.moveTo(x * dpr, y1 * dpr) + this.ctx.lineTo(x * dpr, y2 * dpr) + this.ctx.moveTo((x - 4) * dpr, (y2 - 4) * dpr) + this.ctx.lineTo(x * dpr, y2 * dpr) + this.ctx.lineTo((x + 4) * dpr, (y2 - 4) * dpr) + this.ctx.stroke() + } + const drawExcessiveText = (x1, y1, x2, y2) => { + this.ctx.beginPath() + this.ctx.moveTo(x1 * dpr, y1 * dpr) + this.ctx.lineTo(x2 * dpr, y2 * dpr) + this.ctx.stroke() + } + + const context = match.match.context.text.substr( + match.match.context.offset, + match.match.context.length + ) + const replacement = match.match.replacements[0].value + const lengthDiff = replacement.length - context.length + if ( + lengthDiff > 0 && + replacement.substr(-context.length) === context + ) { + // Something to insert before + const toInsert = replacement.substr(0, lengthDiff) + + if (toInsert === ',') { + const x = match.highlights[0].left - scrollPanelRect.left + const y = match.highlights[0].bottom - scrollPanelRect.top + drawMissingComma(x, y) + } else if (/^\s+$/.test(toInsert)) { + const x = match.highlights[0].left - scrollPanelRect.left + const y1 = match.highlights[0].bottom - scrollPanelRect.top - 2 + const y2 = match.highlights[0].top - scrollPanelRect.top + 2 + drawWrongSpacing(x, y1, y2) + } else { + // TODO + } + + drawSideMarker( + match.highlights[0].top - scrollPanelRect.top, + match.highlights[0].bottom - scrollPanelRect.top + ) + } else if (replacement.substr(0, context.length) === context) { + // Something to insert after + const toInsert = replacement.substr(-lengthDiff) + + if (toInsert === ',') { + const x = match.highlights.at(-1).right - scrollPanelRect.left + const y = match.highlights.at(-1).bottom - scrollPanelRect.top + drawMissingComma(x, y) + } else if (/^\s+$/.test(toInsert)) { + const x = match.highlights.at(-1).right - scrollPanelRect.left + const y1 = + match.highlights.at(-1).bottom - scrollPanelRect.top - 2 + const y2 = match.highlights.at(-1).top - scrollPanelRect.top + 2 + drawWrongSpacing(x, y1, y2) + } else { + // TODO + } + + drawSideMarker( + match.highlights.at(-1).top - scrollPanelRect.top, + match.highlights.at(-1).bottom - scrollPanelRect.top + ) + } else if ( + lengthDiff < 0 && + context.substr(-replacement.length) === replacement + ) { + // Something to remove before + const toRemove = context.substr(0, -lengthDiff) + + if (/^\s+$/.test(toRemove)) { + const rect = this.makeRange( + match.data, + match.match.offset, + match.match.offset - lengthDiff + )?.getClientRects()[0] + const x = (rect.left + rect.right) / 2 - scrollPanelRect.left + const y1 = rect.top - scrollPanelRect.top + const y2 = rect.bottom - scrollPanelRect.top + drawWrongSpacing(x, y1, y2) + } else { + for (let rect of this.makeRange( + match.data, + match.match.offset, + match.match.offset - lengthDiff + )?.getClientRects()) + drawExcessiveText( + rect.left - scrollPanelRect.left, + rect.bottom - scrollPanelRect.top, + rect.right - scrollPanelRect.left, + rect.top - scrollPanelRect.top + ) + } + + drawSideMarker( + match.highlights[0].top - scrollPanelRect.top, + match.highlights[0].bottom - scrollPanelRect.top + ) + } else if (context.substr(0, replacement.length) === replacement) { + // Something to remove after + const toRemove = context.substr(lengthDiff) + + if (/^\s+$/.test(toRemove)) { + const rect = this.makeRange( + match.data, + match.match.offset + match.match.length + lengthDiff, + match.match.offset + match.match.length + )?.getClientRects()[0] + const x = (rect.left + rect.right) / 2 - scrollPanelRect.left + const y1 = rect.top - scrollPanelRect.top + const y2 = rect.bottom - scrollPanelRect.top + drawWrongSpacing(x, y1, y2) + } else { + for (let rect of this.makeRange( + match.data, + match.match.offset + match.match.length + lengthDiff, + match.match.offset + match.match.length + )?.getClientRects()) + drawExcessiveText( + rect.left - scrollPanelRect.left, + rect.bottom - scrollPanelRect.top, + rect.right - scrollPanelRect.left, + rect.top - scrollPanelRect.top + ) + } + + drawSideMarker( + match.highlights.at(-1).top - scrollPanelRect.top, + match.highlights.at(-1).bottom - scrollPanelRect.top + ) + } else { + // Sugesstion and context are different. + // TODO + + drawSideMarker( + Math.min(...match.highlights.map(rect => rect.top)) - + scrollPanelRect.top, + Math.max(...match.highlights.map(rect => rect.bottom)) - + scrollPanelRect.top + ) + } + } else { + // TODO + + drawSideMarker( + Math.min(...match.highlights.map(rect => rect.top)) - + scrollPanelRect.top, + Math.max(...match.highlights.map(rect => rect.bottom)) - + scrollPanelRect.top + ) + } + + // for (let rect of match.highlights) { + // const x = (rect.left - scrollPanelRect.left) * dpr + // const y = (rect.top - scrollPanelRect.top) * dpr + // const width = rect.width * dpr + // const height = rect.height * dpr + + // // MOCKUP text drawing + // this.ctx.font = `5px ${this.textFont}` // Font se lahko doda na sledeč način: `25px 'Times New Roman', serif` + // const text = 'To je izmišljeno besedilo' + // const textLength = this.ctx.measureText(text) + // console.log(`Dolžina texta: ${textLength.width}px`) // Dolžina texta + // this.ctx.fillText(text, x, y) + // } + break + + default: + for (let rect of match.highlights) { + const x = (rect.left - scrollPanelRect.left) * dpr + const y = (rect.top - scrollPanelRect.top) * dpr + const width = rect.width * dpr + const height = rect.height * dpr + + // Draw the underline. + this.ctx.beginPath() + this.ctx.moveTo(x, y + height) + this.ctx.lineTo(x + width, y + height) + this.ctx.stroke() + } + + drawSideMarker( + Math.min(...match.highlights.map(rect => rect.top)) - + scrollPanelRect.top, + Math.max(...match.highlights.map(rect => rect.bottom)) - + scrollPanelRect.top + ) + } } /** @@ -450,8 +655,8 @@ class BesService { this.canvasPanel.width !== canvasPanelRect.width * dpr || this.canvasPanel.height !== canvasPanelRect.height * dpr ) { - this.canvasPanel.width = canvasPanelRect.width * dpr - this.canvasPanel.height = canvasPanelRect.height * dpr + this.canvasPanel.width = Math.round(canvasPanelRect.width * dpr) + this.canvasPanel.height = Math.round(canvasPanelRect.height * dpr) this.repositionAllMarkup() } if (this.isHostElementInline()) { @@ -539,21 +744,9 @@ class BesService { * Updates all grammar mistake markup positions. */ repositionAllMarkup() { - const dpr = window.devicePixelRatio - this.ctx.clearRect( - 0, - 0, - this.canvasPanel.width * dpr, - this.canvasPanel.height * dpr - ) + this.ctx.clearRect(0, 0, this.canvasPanel.width, this.canvasPanel.height) this.results.forEach(result => { - result.matches.forEach(match => { - if (match.highlights) delete match.highlights - match.highlights = this.addMistakeMarkup( - match.range, - match.match.rule.id - ) - }) + result.matches.forEach(match => this.addMistakeMarkup(match)) }) } @@ -700,49 +893,17 @@ class BesTreeService extends BesService { .then(responseData => { let matches = [] responseData.matches.forEach(match => { - let range = document.createRange() - - // Locate start of the grammar mistake. - for ( - let idx = 0, startingOffset = 0; - ; - startingOffset += data[idx++].text.length - ) { - if ( - !data[idx].markup && - /*startingOffset <= match.offset &&*/ match.offset < - startingOffset + data[idx].text.length - ) { - range.setStart( - data[idx].node, - match.offset - startingOffset - ) - break - } - } - - // Locate end of the grammar mistake. - let endOffset = match.offset + match.length - for ( - let idx = 0, startingOffset = 0; - ; - startingOffset += data[idx++].text.length - ) { - if ( - !data[idx].markup && - /*startingOffset <= endOffset &&*/ endOffset <= - startingOffset + data[idx].text.length - ) { - range.setEnd(data[idx].node, endOffset - startingOffset) - break - } - } - - matches.push({ - highlights: this.addMistakeMarkup(range, match.rule.id), - range: range, + let m = { + data: data, + range: this.makeRange( + data, + match.offset, + match.offset + match.length + ), match: match - }) + } + this.addMistakeMarkup(m) + matches.push(m) }) this.markProofed(node, matches) this.onProofingProgress(matches.length) @@ -763,6 +924,34 @@ class BesTreeService extends BesService { } } + makeRange(data, start, end) { + let range = document.createRange() + + // Locate start of the grammar mistake. + for (let idx = 0, offset = 0; ; offset += data[idx++].text.length) { + if ( + !data[idx].markup && + /*offset <= start &&*/ start < offset + data[idx].text.length + ) { + range.setStart(data[idx].node, start - offset) + break + } + } + + // Locate end of the grammar mistake. + for (let idx = 0, offset = 0; ; offset += data[idx++].text.length) { + if ( + !data[idx].markup && + /*offset <= end &&*/ end <= offset + data[idx].text.length + ) { + range.setEnd(data[idx].node, end - offset) + break + } + } + + return range + } + /** * Tests if given block element has already been grammar-checked. * @@ -1454,27 +1643,18 @@ class BesPlainTextService extends BesService { .then(responseData => { let matches = [] responseData.matches.forEach(match => { - let matchRange = document.createRange() - let nodeIdx = 0, - matchStart = start + match.offset - while (nodeIdx < nodes.length && nodes[nodeIdx].end < matchStart) - nodeIdx++ - matchRange.setStart( - nodes[nodeIdx].node, - matchStart - nodes[nodeIdx].start - ) - let matchEnd = matchStart + match.length - while (nodeIdx < nodes.length && nodes[nodeIdx].end < matchEnd) - nodeIdx++ - matchRange.setEnd( - nodes[nodeIdx].node, - matchEnd - nodes[nodeIdx].start - ) - matches.push({ - highlights: this.addMistakeMarkup(matchRange, match.rule.id), - range: matchRange, + const d = { nodes, start, end } + let m = { + data: d, + range: this.makeRange( + d, + match.offset, + match.offset + match.length + ), match: match - }) + } + this.addMistakeMarkup(m) + matches.push(m) }) this.markProofed(paragraphRange, matches) this.onProofingProgress(matches.length) @@ -1486,6 +1666,26 @@ class BesPlainTextService extends BesService { this.onProofingProgress(0) } + makeRange(data, start, end) { + let matchRange = document.createRange() + let nodeIdx = 0, + matchStart = data.start + start + while (nodeIdx < data.nodes.length && data.nodes[nodeIdx].end < matchStart) + nodeIdx++ + matchRange.setStart( + data.nodes[nodeIdx].node, + matchStart - data.nodes[nodeIdx].start + ) + let matchEnd = data.start + end + while (nodeIdx < data.nodes.length && data.nodes[nodeIdx].end < matchEnd) + nodeIdx++ + matchRange.setEnd( + data.nodes[nodeIdx].node, + matchEnd - data.nodes[nodeIdx].start + ) + return matchRange + } + /** * Concatenates child text nodes *