From 9447faa024472f995547d9af132a0f694d9256cd Mon Sep 17 00:00:00 2001 From: Simon Rozman Date: Wed, 31 Jan 2024 12:34:38 +0100 Subject: [PATCH] Attempt to extend grammar checking to HTML --- index.html | 2 +- online-editor.js | 238 +++++++++++++++++++++++------------------------ 2 files changed, 120 insertions(+), 120 deletions(-) diff --git a/index.html b/index.html index 4faf85c..20544bd 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@ -
Popravite kar želite.
Na mizo nisem položil knjigo.
+
Popravite kar želite.
Na mizo nisem položil knjigo.
diff --git a/online-editor.js b/online-editor.js index e3cdd31..532d15c 100644 --- a/online-editor.js +++ b/online-editor.js @@ -1,23 +1,137 @@ +const besUrl = 'http://localhost:225/api/v2/check' + let besEditors = {} // Collection of all editors on page window.onload = () => { // Search and prepare all our editors found in the document. - document.querySelectorAll('.online-editor').forEach(ed => { + document.querySelectorAll('.online-editor').forEach(edit => { let editor = { ignoreInput: false, timer: null } - besEditors[ed.id] = editor - besCheckText(ed.id) + besEditors[edit.id] = editor + besCheckText(edit) - ed.addEventListener('beforeinput', e => besBeforeInput(ed.id, e), false) + //edit.addEventListener('beforeinput', e => besBeforeInput(edit.id, e), false) - ed.addEventListener('click', e => { + edit.addEventListener('click', e => { besHandleClick(e) }) }) } +async function besCheckText(el) +{ + switch (el.nodeType) { + case Node.TEXT_NODE: + return [{ text: el.textContent, el: el }] + + case Node.ELEMENT_NODE: + let data = [] + for (const el2 of el.childNodes) { + data = data.concat(await besCheckText(el2)) + } + let defaultView = document.defaultView; + switch (defaultView.getComputedStyle(el, null).getPropertyValue('display')) + { + case 'inline': + case 'inline-block': + data.splice(0, 0, { markup: '<'+el.tagName+'>', el: el }) + data.splice(data.length, 0, { markup: '' }) + return data; + default: + let regAllWhitespace = /^\s*$/ + if (data.some(x => 'text' in x && !regAllWhitespace.test(x.text))) { + const requestData = { + format: 'plain', + data: JSON.stringify({annotation: data}), + language: 'sl', + level: 'picky' + } + const request = new Request(besUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(requestData) + }) + fetch(request) + .then(response => { + if (!response.ok) { + // TODO: Make connectivity and BesStr issues non-fatal. + throw new Error('Backend server response was not OK') + } + return response.json() + }) + .then(responseData => { + responseData.matches.sort((a, b) => a.offset < b.offset ? -1 : a.offset > b.offset ? +1 : 0) + responseData.matches.forEach(match => { + let range = document.createRange() + + // Locate start of the grammar mistake. + for (let idx = 0, startingOffset = 0; ; ) { + if ('text' in data[idx]) { + if (startingOffset <= match.offset && match.offset < startingOffset + data[idx].text.length) { + range.setStart(data[idx].el, match.offset - startingOffset) + break + } + startingOffset += data[idx++].text.length + } + else startingOffset += data[idx++].markup.length + } + + // Locate end of the grammar mistake. + let endOffset = match.offset + match.length + for (let idx = 0, startingOffset = 0; ; ) { + if ('text' in data[idx]) { + if (startingOffset <= endOffset && endOffset <= startingOffset + data[idx].text.length) { + range.setEnd(data[idx].el, endOffset - startingOffset) + break + } + startingOffset += data[idx++].text.length + } + else startingOffset += data[idx++].markup.length + } + + const span = document.createElement('span') + span.classList.add('typo-mistake') + //range.insertNode(span) + }) + }) + .catch(error => { + // TODO: Make parsing issues non-fatal. + throw new Error('Request to backend server failed: ' + error) + }) + } + return [{ markup: '<'+el.tagName+'/>', el: el }] + } + + default: + return [{ markup: '', el: el }] + } +} + +function besBeforeInput(editorId, event) +{ + let editor = besEditors[editorId] + if (editor.ignoreInput) return + + if (editor.timer) clearTimeout(editor.timer) + editor.timer = setTimeout(function(){ besCheckText(editorId) }, 1000) + + let edit = document.getElementById(editorId) + event.getTargetRanges().forEach(range => { + if (range.startContainer === edit) + return + for (var el = range.startContainer; el ; el = el.nextSibling) { + for (var el2 = el; el2 && el2 !== edit; el2 = el2.parentElement) { + if (!(el2 instanceof HTMLElement) || el2.tagName !== 'DIV') continue + el2.removeAttribute('data-info') + } + if (el === range.endContainer) break + } + }) +} + + function besHandleClick(e) { switch (e.target) { case e.target.closest('span'): @@ -29,117 +143,3 @@ function besHandleClick(e) { break } } - -async function besCheckText(editorId) { - let text = '' - let editor = besEditors[editorId] - let ed = document.getElementById(editorId) - let paragraphs = [] - let divElements = ed.getElementsByTagName('div') - if (divElements.length === 0) { - // Editor is empty or has plain text only - text = ed.textContent - } else { - // Editor contains
paragraphs - for (const para of divElements) { - if (para.getAttribute('data-info') === 'checked') continue - let p = { start: text.length } - text += para.textContent.replace(/\r?\n/g, ' ') - text += '\n\n' - p.end = text.length - paragraphs.push(p) - } - } - let result = await ajaxCheck(text) - result.matches.sort((a, b) => a.offset < b.offset ? -1 : a.offset > b.offset ? +1 : 0) - if (divElements.length === 0) { - // Editor is empty or has plain text only - let textOut = '' - let offset = 0 - for (var mistakeIdx = 0; mistakeIdx < result.matches.length; ++mistakeIdx) { - let mistakeStart = result.matches[mistakeIdx].offset - let mistakeEnd = mistakeStart + result.matches[mistakeIdx].length - textOut += text.substring(offset, mistakeStart) - textOut += '' + text.substring(mistakeStart, mistakeEnd) + '' - offset = mistakeEnd - } - textOut += text.substring(offset) - editor.ignoreInput = true - ed.innerHTML = textOut - editor.ignoreInput = false - } else { - // Editor contains
paragraphs - let idx = 0 - let mistakeIdx = 0 - for (const para of divElements) { - if (para.getAttribute('data-info') === 'checked') continue - let p = paragraphs[idx++] - let textOut = '' - let offset = p.start - for (; mistakeIdx < result.matches.length && result.matches[mistakeIdx].offset < p.end; ++mistakeIdx) { - let mistakeStart = result.matches[mistakeIdx].offset - let mistakeEnd = mistakeStart + result.matches[mistakeIdx].length - textOut += text.substring(offset, mistakeStart) - textOut += '' + text.substring(mistakeStart, mistakeEnd) + '' - offset = mistakeEnd - } - textOut += text.substring(offset, p.end - 2 /* Compensate for '\n\n' we appended to each paragraph. */) - editor.ignoreInput = true - para.innerHTML = textOut - editor.ignoreInput = false - para.setAttribute('data-info', 'checked') - } - } -} - -function besBeforeInput(editorId, event) -{ - let editor = besEditors[editorId] - if (editor.ignoreInput) return - - if (editor.timer) clearTimeout(editor.timer) - editor.timer = setTimeout(function(){ besCheckText(editorId) }, 1000) - - let ed = document.getElementById(editorId) - event.getTargetRanges().forEach(range => { - if (range.startContainer === ed) - return - for (var el = range.startContainer; el ; el = el.nextSibling) { - for (var el2 = el; el2 && el2 !== ed; el2 = el2.parentElement) { - if (!(el2 instanceof HTMLElement) || el2.tagName !== 'DIV') continue - el2.removeAttribute('data-info') - } - if (el === range.endContainer) break - } - }) -} - -async function ajaxCheck(paragraph) { - const url = 'http://localhost:225/api/v2/check' - const data = { - format: 'plain', - text: paragraph, - language: 'sl', - level: 'picky' - } - - const formData = new URLSearchParams(data) - const request = new Request(url, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: formData - }) - return fetch(request) - .then(response => { - if (!response.ok) { - throw new Error('Backend server response was not OK') - } - return response.json() - }) - .then(data => { - return data - }) - .catch(error => { - throw new Error('Request to backend server failed: ' + error) - }) -}