Extend grammar markup style continued

This commit is contained in:
Simon Rozman 2025-02-27 13:52:14 +01:00
parent 53fc05a2f8
commit bb822f0bbc
5 changed files with 399 additions and 141 deletions

View File

@ -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.
<img src="samples/markup_underline.png" alt="underline" width="448"/>
<img src="samples/markup_lector.png" alt="lector" width="448"/>
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`.

BIN
samples/markup_lector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -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()
}
/**

View File

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