diff --git a/Readme.md b/Readme.md index f6c8717..e7eb001 100644 --- a/Readme.md +++ b/Readme.md @@ -87,6 +87,24 @@ Kategorije pravopisnih pravil so: | BESANA_CAT_5 | slovnične napake | | BESANA_CAT_6 | nomotehnične napake (potrebuje posebno licenco) | +### 4. Nastavljanje videza korekturnih znamenj + +Privzeto servis uporablja podčrtovanje pravopisnih napak (nastavitev `'underline'`). Videz lahko spreminjamo. + +underline +lector + +Levo `'underline'`, desno `'lector'`. + +Primer: + +```JavaScript +// Registriramo servis za naš urejevalnik. +BesService + .registerByElement(el) + .setMarkupStyle('lector') +``` + ## Navodila za razvijalce Programsko kodo v tem repozitoriju razvijamo s programom Visual Studio Code. Potrebna je namestitev vtičnika `esbenp.prettier-vscode`. diff --git a/samples/markup_lector.png b/samples/markup_lector.png new file mode 100644 index 0000000..eb75f92 Binary files /dev/null and b/samples/markup_lector.png differ diff --git a/samples/markup_underline.png b/samples/markup_underline.png new file mode 100644 index 0000000..5e96429 Binary files /dev/null and b/samples/markup_underline.png differ diff --git a/service.js b/service.js index 6a327f7..7ff1c03 100644 --- a/service.js +++ b/service.js @@ -362,50 +362,36 @@ class BesService { */ addMistakeMarkup(match) { const range = match.range - const ruleId = match.match.rule.id const scrollPanelRect = this.scrollPanel.getBoundingClientRect() - const dpr = window.devicePixelRatio - const markerX = this.canvasPanel.width * dpr + const scrollX = scrollPanelRect.left + const scrollY = scrollPanelRect.top match.highlights = Array.from(range.getClientRects()) + const dpr = window.devicePixelRatio this.ctx.lineWidth = 2 * dpr // Use 2 for clearer visibility + const ruleId = match.match.rule.id this.ctx.strokeStyle = ruleId.startsWith('MORFOLOGIK_RULE') - ? 'rgba(255, 115, 0, 0.5)' - : 'rgba(0, 123, 255, 0.5)' - const drawSideMarker = (y1, y2) => { - this.ctx.beginPath() - this.ctx.moveTo(markerX, y1 * dpr) - this.ctx.lineTo(markerX, y2 * dpr) - this.ctx.stroke() - } + ? 'rgba(0, 123, 255, 0.8)' + : 'rgba(255, 115, 0, 0.8)' + this.ctx.fillStyle = ruleId.startsWith('MORFOLOGIK_RULE') + ? 'rgba(0, 123, 255, 0.8)' + : 'rgba(255, 115, 0, 0.8)' + let markerY1, markerY2 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() - } + if (ruleId === 'BESANA_6' /*PR_VNAP_V_STAVKU_MANJKA_VEJICA*/) { + // Thou we should draw ┘ after the word before match.match.offset, if there is a line break inbetween, + // the correction markup would be drawn in one line and the highlight rectangle for onClick would reside + // in another line, making a confusing UX. + markerY1 = match.highlights[0].top + markerY2 = match.highlights[0].bottom + const scale = (markerY2 - markerY1) / 18 + const x = match.highlights[0].left + const y = match.highlights[0].bottom + this.drawMissingComma(x - scrollX, y - scrollY, scale, '?') + break + } + if (match.match.replacements && match.match.replacements.length === 1) { const context = match.match.context.text.substr( match.match.context.offset, match.match.context.length @@ -418,52 +404,81 @@ class BesService { ) { // Something to insert before const toInsert = replacement.substr(0, lengthDiff) + markerY1 = match.highlights[0].top + markerY2 = match.highlights[0].bottom + const scale = (markerY2 - markerY1) / 18 if (toInsert === ',') { - const x = match.highlights[0].left - scrollPanelRect.left - const y = match.highlights[0].bottom - scrollPanelRect.top - drawMissingComma(x, y) + // Thou we should draw ┘ after the word before match.match.offset, if there is a line break inbetween, + // the correction markup would be drawn in one line and the highlight rectangle for onClick would reside + // in another line, making a confusing UX. + const x = match.highlights[0].left + const y = match.highlights[0].bottom + this.drawMissingComma(x - scrollX, y - scrollY, scale) } 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) + const x = match.highlights[0].left + const y1 = match.highlights[0].bottom - 2 * scale + const y2 = match.highlights[0].top + 2 * scale + this.drawWrongSpacing( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale + ) } else { - // TODO + const x = match.highlights[0].left - 1 * scale + const y1 = match.highlights[0].bottom + const y2 = match.highlights[0].top + this.drawMissingText( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale, + replacement.substr(lengthDiff).trim() + ) } - - 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) + markerY1 = match.highlights.at(-1).top + markerY2 = match.highlights.at(-1).bottom + const scale = (markerY2 - markerY1) / 18 if (toInsert === ',') { - const x = match.highlights.at(-1).right - scrollPanelRect.left - const y = match.highlights.at(-1).bottom - scrollPanelRect.top - drawMissingComma(x, y) + const x = match.highlights.at(-1).right + const y = match.highlights.at(-1).bottom + this.drawMissingComma(x - scrollX, y - scrollY, scale) } 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) + const x = match.highlights.at(-1).right + const y1 = match.highlights.at(-1).bottom - 2 * scale + const y2 = match.highlights.at(-1).top + 2 * scale + this.drawWrongSpacing( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale + ) } else { - // TODO + const x = match.highlights.at(-1).right + 1 * scale + const y1 = match.highlights.at(-1).bottom + const y2 = match.highlights.at(-1).top + this.drawMissingText( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale, + replacement.substr(-lengthDiff).trim() + ) } - - 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) + markerY1 = match.highlights[0].top + markerY2 = match.highlights[0].bottom + const scale = (markerY2 - markerY1) / 18 if (/^\s+$/.test(toRemove)) { const rect = this.makeRange( @@ -471,31 +486,34 @@ class BesService { 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) + const x = (rect.left + rect.right) / 2 + const y1 = rect.top + const y2 = rect.bottom + this.drawWrongSpacing( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale + ) } 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 + this.drawExcessiveText( + rect.left - scrollX, + rect.bottom - scrollY, + rect.right - scrollX, + rect.top - scrollY ) } - - 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) + markerY1 = match.highlights.at(-1).top + markerY2 = match.highlights.at(-1).bottom + const scale = (markerY2 - markerY1) / 18 if (/^\s+$/.test(toRemove)) { const rect = this.makeRange( @@ -503,86 +521,308 @@ class BesService { 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) + const x = (rect.left + rect.right) / 2 + const y1 = rect.top + const y2 = rect.bottom + this.drawWrongSpacing( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale + ) } 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 + this.drawExcessiveText( + rect.left - scrollX, + rect.bottom - scrollY, + rect.right - scrollX, + rect.top - scrollY ) } - - drawSideMarker( - match.highlights.at(-1).top - scrollPanelRect.top, - match.highlights.at(-1).bottom - scrollPanelRect.top - ) } else { // Sugesstion and context are different. - // TODO + const lengthL = BesService.commonPrefixLength(context, replacement) + const lengthR = BesService.commonSuffixLength(context, replacement) + if ( + lengthL + lengthR < + Math.min(context.length, replacement.length) * 0.6 + ) { + // Replace everything. + markerY1 = Math.min(...match.highlights.map(rect => rect.top)) + markerY2 = Math.max(...match.highlights.map(rect => rect.bottom)) + const scale = + (match.highlights[0].bottom - match.highlights[0].top) / 18 + let first = true + for (let rect of match.highlights) { + if (first) { + this.drawWrongText( + rect.left - scrollX, + rect.bottom - scrollY, + rect.right - scrollX, + rect.top - scrollY, + scale, + replacement + ) + first = false + } else { + this.drawExcessiveText( + rect.left - scrollX, + rect.bottom - scrollY, + rect.right - scrollX, + rect.top - scrollY + ) + } + } + } else { + // Patch differences. + const rects = Array.from( + this.makeRange( + match.data, + match.match.offset + lengthL, + match.match.offset + match.match.length - lengthR + )?.getClientRects() + ) + markerY1 = Math.min(...rects.map(rect => rect.top)) + markerY2 = Math.max(...rects.map(rect => rect.bottom)) - drawSideMarker( - Math.min(...match.highlights.map(rect => rect.top)) - - scrollPanelRect.top, - Math.max(...match.highlights.map(rect => rect.bottom)) - - scrollPanelRect.top - ) + if (lengthL + lengthR === context.length) { + // Something to insert + const toInsert = replacement.substring( + lengthL, + replacement.length - lengthR + ) + const scale = (rects[0].bottom - rects[0].top) / 18 + const x = rects[0].left + + if (/^\s+$/.test(toInsert)) { + const y1 = rects[0].bottom - 2 * scale + const y2 = rects[0].top + 2 * scale + this.drawWrongSpacing( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale + ) + } else { + const y1 = rects[0].bottom + const y2 = rects[0].top + this.drawMissingText( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale, + toInsert.trim() + ) + } + } else if (lengthL + lengthR === replacement.length) { + // Something to remove + const toRemove = context.substring( + lengthL, + replacement.length - lengthR + ) + const scale = (rects[0].bottom - rects[0].top) / 18 + + if (/^\s+$/.test(toRemove)) { + const x = (rects[0].left + rects[0].right) / 2 + const y1 = rects[0].top + const y2 = rects[0].bottom + this.drawWrongSpacing( + x - scrollX, + y1 - scrollY, + y2 - scrollY, + scale + ) + } else { + for (let rect of rects) + this.drawExcessiveText( + rect.left - scrollX, + rect.bottom - scrollY, + rect.right - scrollX, + rect.top - scrollY + ) + } + } else { + // Something to replace + const toReplace = replacement.substring( + lengthL, + replacement.length - lengthR + ) + const scale = (rects[0].bottom - rects[0].top) / 18 + let first = true + for (let rect of rects) { + if (first) { + this.drawWrongText( + rect.left - scrollX, + rect.bottom - scrollY, + rect.right - scrollX, + rect.top - scrollY, + scale, + toReplace + ) + first = false + } else { + this.drawExcessiveText( + rect.left - scrollX, + rect.bottom - scrollY, + rect.right - scrollX, + rect.top - scrollY + ) + } + } + } + } } - } else { - // TODO - - drawSideMarker( - Math.min(...match.highlights.map(rect => rect.top)) - - scrollPanelRect.top, - Math.max(...match.highlights.map(rect => rect.bottom)) - - scrollPanelRect.top - ) + break } - // 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() + const x1 = rect.left + const x2 = rect.right + const y = rect.bottom + const scale = (rect.bottom - rect.top) / 18 + this.drawAttentionRequired( + x1 - scrollX, + x2 - scrollX, + y - scrollY, + scale + ) } - drawSideMarker( - Math.min(...match.highlights.map(rect => rect.top)) - - scrollPanelRect.top, - Math.max(...match.highlights.map(rect => rect.bottom)) - - scrollPanelRect.top - ) + markerY1 = Math.min(...match.highlights.map(rect => rect.top)) + markerY2 = Math.max(...match.highlights.map(rect => rect.bottom)) } + + this.drawSideMarker(markerY1 - scrollY, markerY2 - scrollY) + } + + static commonPrefixLength(s1, s2) { + let i = 0 + let len = Math.min(s1.length, s2.length) + while (i < len && s1[i] === s2[i]) i++ + return i + } + + static commonSuffixLength(s1, s2) { + let i = 0 + let i1 = s1.length + let i2 = s2.length + while (0 < i1-- && 0 < i2-- && s1[i1] === s2[i2]) i++ + return i + } + + drawSideMarker(y1, y2) { + const dpr = window.devicePixelRatio + const markerX = this.canvasPanel.width - 5 * dpr + this.ctx.beginPath() + this.ctx.moveTo(markerX, y1 * dpr) + this.ctx.lineTo(markerX, y2 * dpr) + this.ctx.stroke() + } + + drawMissingComma(x, y, scale, comment) { + const dpr = window.devicePixelRatio + this.ctx.beginPath() + this.ctx.moveTo((x - 2 * scale) * dpr, y * dpr) + this.ctx.lineTo((x + 2 * scale) * dpr, y * dpr) + this.ctx.lineTo((x + 2 * scale) * dpr, (y - 4 * scale) * dpr) + this.ctx.stroke() + + if (comment) { + this.ctx.font = `${12 * scale * dpr}px ${this.textFont}` + this.ctx.textAlign = 'center' + this.ctx.textBaseline = 'bottom' + this.ctx.fillText('?', (x + 2 * scale) * dpr, (y - 6 * scale) * dpr) + } + } + + drawWrongSpacing(x, y1, y2, scale) { + const dpr = window.devicePixelRatio + this.ctx.beginPath() + this.ctx.moveTo((x - 4 * scale) * dpr, (y1 + 4 * scale) * dpr) + this.ctx.lineTo(x * dpr, y1 * dpr) + this.ctx.lineTo((x + 4 * scale) * dpr, (y1 + 4 * scale) * dpr) + this.ctx.moveTo(x * dpr, y1 * dpr) + this.ctx.lineTo(x * dpr, y2 * dpr) + this.ctx.moveTo((x - 4 * scale) * dpr, (y2 - 4 * scale) * dpr) + this.ctx.lineTo(x * dpr, y2 * dpr) + this.ctx.lineTo((x + 4 * scale) * dpr, (y2 - 4 * scale) * dpr) + this.ctx.stroke() + } + + drawExcessiveText(x1, y1, x2, y2) { + const dpr = window.devicePixelRatio + this.ctx.beginPath() + this.ctx.moveTo(x1 * dpr, y1 * dpr) + this.ctx.lineTo(x2 * dpr, y2 * dpr) + this.ctx.stroke() + } + + drawWrongText(x1, y1, x2, y2, scale, text) { + const dpr = window.devicePixelRatio + this.ctx.beginPath() + this.ctx.moveTo(x1 * dpr, (y1 - 6 * scale) * dpr) + this.ctx.lineTo(x2 * dpr, (y2 + 6 * scale) * dpr) + this.ctx.stroke() + + this.ctx.font = `${12 * scale * dpr}px ${this.textFont}` + this.ctx.textAlign = 'left' // Thou we want the text to be centered, we align it manually to prevent it getting off canvas. + this.ctx.textBaseline = 'bottom' + const textMetrics = this.ctx.measureText(text) + this.ctx.fillText( + text, + Math.max( + Math.min( + ((x1 + x2) / 2) * dpr - textMetrics.width / 2, + this.canvasPanel.width - textMetrics.width + ), + 0 + ), + (y2 + 6 * scale) * dpr + ) + } + + drawMissingText(x, y1, y2, scale, text) { + const dpr = window.devicePixelRatio + this.ctx.beginPath() + this.ctx.moveTo((x - 6 * scale) * dpr, y1 * dpr) + this.ctx.lineTo((x + 6 * scale) * dpr, y1 * dpr) + this.ctx.moveTo(x * dpr, y1 * dpr) + this.ctx.lineTo(x * dpr, (y2 + 6 * scale) * dpr) + this.ctx.stroke() + + this.ctx.font = `${12 * scale * dpr}px ${this.textFont}` + this.ctx.textAlign = 'left' // Thou we want the text to be centered, we align it manually to prevent it getting off canvas. + this.ctx.textBaseline = 'bottom' + const textMetrics = this.ctx.measureText(text) + this.ctx.fillText( + text, + Math.max( + Math.min( + x * dpr - textMetrics.width / 2, + this.canvasPanel.width - textMetrics.width + ), + 0 + ), + (y2 + 6 * scale) * dpr + ) + } + + drawAttentionRequired(x1, x2, y, scale) { + const dpr = window.devicePixelRatio + this.ctx.beginPath() + this.ctx.moveTo(x1 * dpr, (y - scale) * dpr) + for (let x = x1; ; ) { + if (x >= x2) break + this.ctx.lineTo((x += 2 * scale) * dpr, (y + scale) * dpr) + if (x >= x2) break + this.ctx.lineTo((x += 2 * scale) * dpr, (y - scale) * dpr) + } + this.ctx.stroke() } /** diff --git a/styles.css b/styles.css index 9cd8425..603eef4 100644 --- a/styles.css +++ b/styles.css @@ -15,11 +15,11 @@ } .bes-highlight-spelling-rect { - background: rgb(255, 115, 0); + background: rgb(0, 123, 255); } .bes-highlight-grammar-rect { - background: rgb(0, 123, 255); + background: rgb(255, 115, 0); } .bes-canvas {