parent
dccbf7cde8
commit
26b23d879b
@ -0,0 +1,8 @@ |
|||||||
|
.validated-text { |
||||||
|
white-space: pre-line; |
||||||
|
} |
||||||
|
|
||||||
|
.fragment-incorrect { |
||||||
|
font-weight: bold; |
||||||
|
color: red; |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
import './errorsDisplay.css'; |
||||||
|
|
||||||
|
export type ErrorsDisplayParams = { |
||||||
|
originalText: string; |
||||||
|
spellcheckErrors: { |
||||||
|
start: number; |
||||||
|
end: number; |
||||||
|
}[]; |
||||||
|
}; |
||||||
|
|
||||||
|
type TextChunk = { |
||||||
|
text: string; |
||||||
|
index: number; |
||||||
|
isCorrect: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
const toChunks = ({ originalText, spellcheckErrors }: ErrorsDisplayParams) => { |
||||||
|
const chunks: TextChunk[] = []; |
||||||
|
let lastIndex = 0; |
||||||
|
for (const error of spellcheckErrors) { |
||||||
|
chunks.push({ |
||||||
|
text: originalText.slice(lastIndex, error.start), |
||||||
|
index: lastIndex, |
||||||
|
isCorrect: true, |
||||||
|
}); |
||||||
|
chunks.push({ |
||||||
|
text: originalText.slice(error.start, error.end), |
||||||
|
index: error.start, |
||||||
|
isCorrect: false, |
||||||
|
}); |
||||||
|
|
||||||
|
lastIndex = error.end; |
||||||
|
} |
||||||
|
|
||||||
|
chunks.push({ |
||||||
|
text: originalText.slice(lastIndex, originalText.length), |
||||||
|
index: lastIndex, |
||||||
|
isCorrect: true, |
||||||
|
}); |
||||||
|
|
||||||
|
return chunks.filter(({ text }) => !!text.length); |
||||||
|
}; |
||||||
|
|
||||||
|
export const ErrorsDisplay = ({ |
||||||
|
originalText, |
||||||
|
spellcheckErrors, |
||||||
|
}: ErrorsDisplayParams) => { |
||||||
|
if (!spellcheckErrors.length) { |
||||||
|
return <p>No errors!</p>; |
||||||
|
} |
||||||
|
|
||||||
|
const chunks = toChunks({ originalText, spellcheckErrors }); |
||||||
|
|
||||||
|
return ( |
||||||
|
<p class="validated-text"> |
||||||
|
{chunks.map((chunk) => ( |
||||||
|
<span |
||||||
|
key={chunk.index} |
||||||
|
class={ |
||||||
|
chunk.isCorrect |
||||||
|
? 'fragment-correct' |
||||||
|
: 'fragment-incorrect' |
||||||
|
} |
||||||
|
> |
||||||
|
{chunk.text} |
||||||
|
</span> |
||||||
|
))} |
||||||
|
</p> |
||||||
|
); |
||||||
|
}; |
@ -1,17 +1,79 @@ |
|||||||
import { useEffect, useState } from 'preact/hooks'; |
import { useCallback, useEffect, useState } from 'preact/hooks'; |
||||||
|
import type { JSX } from 'preact/jsx-runtime'; |
||||||
|
import { ErrorsDisplay, ErrorsDisplayParams } from './errorsDisplay'; |
||||||
|
|
||||||
export const SpellChecker = () => { |
export const SpellChecker = () => { |
||||||
const [regex, setRegex] = useState<string>(); |
const [regex, setRegex] = useState<RegExp>(); |
||||||
|
const [errorsDisplayParams, setErrorsDisplayParams] = |
||||||
|
useState<ErrorsDisplayParams>({ |
||||||
|
originalText: '', |
||||||
|
spellcheckErrors: [], |
||||||
|
}); |
||||||
|
|
||||||
useEffect(() => { |
useEffect(() => { |
||||||
if (!regex) { |
if (!regex) { |
||||||
setRegex(new URL(`./assets/img/abc.def`, import.meta.url).href); |
window |
||||||
|
.fetch( |
||||||
|
new URL( |
||||||
|
`./assets/th-en-x-basic.ooo-thesaurus`, |
||||||
|
import.meta.url, |
||||||
|
).href, |
||||||
|
) |
||||||
|
.then((response) => response.text()) |
||||||
|
.then((regexString) => { |
||||||
|
console.time('regex-parse'); |
||||||
|
setRegex(new RegExp(regexString, 'gi')); |
||||||
|
console.timeEnd('regex-parse'); |
||||||
|
}) |
||||||
|
.catch((err) => console.error(err)); |
||||||
} |
} |
||||||
}, [regex]); |
}, [regex]); |
||||||
|
|
||||||
|
const handleSubmit: JSX.GenericEventHandler<HTMLFormElement> = useCallback( |
||||||
|
(e) => { |
||||||
|
e.preventDefault(); |
||||||
|
|
||||||
|
if (!regex) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const formData = new FormData(e.currentTarget); |
||||||
|
const originalText = formData.get('text')?.toString() ?? ''; |
||||||
|
|
||||||
|
console.time('regex-execute'); |
||||||
|
const results = [...originalText.matchAll(regex)]; |
||||||
|
console.timeEnd('regex-execute'); |
||||||
|
|
||||||
|
const spellcheckErrors = results.map((result) => { |
||||||
|
const start = result.index; |
||||||
|
if (start === undefined) { |
||||||
|
throw new Error('Should not happen for non-empty results'); |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
start, |
||||||
|
end: start + result[0].length, |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
setErrorsDisplayParams({ |
||||||
|
originalText, |
||||||
|
spellcheckErrors, |
||||||
|
}); |
||||||
|
}, |
||||||
|
[regex], |
||||||
|
); |
||||||
|
|
||||||
return ( |
return ( |
||||||
<section> |
<section> |
||||||
<textarea rows={10} cols={100}> |
<form onSubmit={handleSubmit}> |
||||||
{regex} |
<textarea rows={10} cols={100} name="text" /> |
||||||
</textarea> |
<br /> |
||||||
|
<button type="submit" disabled={!regex}> |
||||||
|
Validate |
||||||
|
</button> |
||||||
|
</form> |
||||||
|
<ErrorsDisplay {...errorsDisplayParams} /> |
||||||
</section> |
</section> |
||||||
); |
); |
||||||
}; |
}; |
||||||
|
@ -0,0 +1,4 @@ |
|||||||
|
export type SpellcheckError = { |
||||||
|
start: number; |
||||||
|
end: number; |
||||||
|
}; |
Loading…
Reference in new issue