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.
+
+
+
+
+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 {