From 0c03890545af5eae05697885f283f493d08be4df Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Mon, 17 Feb 2025 17:00:34 -0500 Subject: [PATCH] feat: repeated words check validation --- src/components/DrawerChecks.vue | 31 +++++++++++++++ src/stores/editor.js | 6 ++- src/tools/repeated-words.js | 69 +++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/tools/repeated-words.js diff --git a/src/components/DrawerChecks.vue b/src/components/DrawerChecks.vue index 0609391..abd7840 100644 --- a/src/components/DrawerChecks.vue +++ b/src/components/DrawerChecks.vue @@ -103,6 +103,7 @@ q-list import { onBeforeUnmount, onMounted } from 'vue' import { useQuasar } from 'quasar' import { checkArticles } from 'src/tools/articles' +import { checkRepeatedWords } from 'src/tools/repeated-words' import { checkHyphenation } from 'src/tools/hyphenation' import { checkInclusiveLanguage } from 'src/tools/inclusive-language' import { checkNonAscii } from 'src/tools/non-ascii' @@ -153,6 +154,13 @@ const valChecks = [ description: 'Check for common placeholders', icon: 'mdi-select-remove', click: () => placeholdersCheck() + }, + { + key: 'repeatedWords', + title: 'Repeated Words Check', + description: 'Check for accidental repeated terms', + icon: 'mdi-repeat', + click: () => repeatedWordsCheck() } ] @@ -251,6 +259,25 @@ function placeholdersCheck (silent = false) { } } +function repeatedWordsCheck (silent) { + const results = checkRepeatedWords(modelStore[docsStore.activeDocument.id].getValue()) + if (results.count < 1) { + editorStore.setValidationCheckState('repeatedWords', 1) + editorStore.setValidationCheckDetails('repeatedWords', []) + if (!silent) { + $q.notify({ + message: 'Looks good!', + caption: 'No repeated terms found.', + color: 'positive', + icon: 'mdi-repeat' + }) + } + } else { + editorStore.setValidationCheckState('repeatedWords', -2) + editorStore.setValidationCheckDetails('repeatedWords', results) + } +} + function runAllChecks () { editorStore.clearErrors() articlesCheck(true) @@ -258,6 +285,7 @@ function runAllChecks () { inclusiveLangCheck(true) nonAsciiCheck(true) placeholdersCheck(true) + repeatedWordsCheck(true) if (editorStore.errors.length < 1) { $q.notify({ @@ -290,6 +318,9 @@ function runSelectedCheck (key) { case 'placeholders': placeholdersCheck(true) break + case 'repeatedWords': + repeatedWordsCheck(true) + break } } diff --git a/src/stores/editor.js b/src/stores/editor.js index cf55e16..90ebbed 100644 --- a/src/stores/editor.js +++ b/src/stores/editor.js @@ -72,14 +72,16 @@ export const useEditorStore = defineStore('editor', { hyphenation: 0, inclusiveLanguage: 0, nonAscii: 0, - placeholders: 0 + placeholders: 0, + repeatedWords: 0 }, validationChecksDetails: { articles: [], hyphenation: [], inclusiveLanguage: [], nonAscii: [], - placeholders: [] + placeholders: [], + repeatedWords: [] }, wordWrap: true, workingDirectory: '', diff --git a/src/tools/repeated-words.js b/src/tools/repeated-words.js new file mode 100644 index 0000000..36b57cd --- /dev/null +++ b/src/tools/repeated-words.js @@ -0,0 +1,69 @@ +import { sortBy } from 'lodash-es' +import { decorationsStore } from 'src/stores/models' + +export function checkRepeatedWords (text) { + const matchRgx = /\b(\w+)\s+\1\b/gi + const textLines = text.split('\n') + + const decorations = [] + const occurences = [] + const details = [] + const termCount = {} + for (const [lineIdx, line] of textLines.entries()) { + for (const match of line.matchAll(matchRgx)) { + const term = match[1].toLowerCase() + let occIdx = occurences.indexOf(term) + if (occIdx < 0) { + occIdx = occurences.push(term) - 1 + } + decorations.push({ + options: { + hoverMessage: { + value: `Repeated term "${match[1]}" detected.` + }, + className: 'dec-warning', + minimap: { + position: 1 + }, + glyphMarginClassName: 'dec-warning-margin' + }, + range: { + startLineNumber: lineIdx + 1, + startColumn: match.index + 1, + endLineNumber: lineIdx + 1, + endColumn: match.index + 1 + match[0].length + } + }) + details.push({ + key: crypto.randomUUID(), + group: occIdx + 1, + message: match[1].toLowerCase(), + range: { + startLineNumber: lineIdx + 1, + startColumn: match.index + 1, + endLineNumber: lineIdx + 1, + endColumn: match.index + 1 + match[0].length + } + }) + if (termCount[term]) { + termCount[term]++ + } else { + termCount[term] = 1 + } + } + } + + decorationsStore.get('repeatedWords').set(decorations) + + return { + count: decorations.length, + details: sortBy(details, d => d.range.startLineNumber), + hasTextOutput: true, + getTextOutput: () => { + return `Repeated Words +------------- +${Object.entries(termCount).map(([k, v]) => `${k} (${v})`).join('\n')} +` + } + } +}