Extend grammar markup style

This commit is contained in:
2025-02-26 15:51:15 +01:00
parent ad256cabef
commit ad13e9849f
2 changed files with 388 additions and 102 deletions

View File

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