Add frontend parser for text references

This commit is contained in:
IRBorisov 2023-09-25 23:50:41 +03:00
parent 83242dfb69
commit cd4792e96c
11 changed files with 366 additions and 8 deletions

View File

@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"prepare": "lezer-generator src/components/RSInput/rslang/rslangFull.grammar -o src/components/RSInput/rslang/parser.ts", "prepare": "lezer-generator src/components/RSInput/rslang/rslangFull.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts",
"test": "jest", "test": "jest",
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",

View File

@ -45,7 +45,7 @@ const editorSetup: BasicSetupOptions = {
interface RSInputProps interface RSInputProps
extends Pick<ReactCodeMirrorProps, extends Pick<ReactCodeMirrorProps,
'id'| 'editable' | 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' 'id'| 'editable' | 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder'
> { > {
label?: string label?: string
innerref?: RefObject<ReactCodeMirrorRef> | undefined innerref?: RefObject<ReactCodeMirrorRef> | undefined

View File

@ -0,0 +1,172 @@
import { Extension } from '@codemirror/state';
import { tags } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { EditorView } from 'codemirror';
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import useResolveText from '../../hooks/useResolveText';
import Label from '../Common/Label';
import Modal from '../Common/Modal';
import PrettyJson from '../Common/PrettyJSON';
import { NaturalLanguage } from './parse';
import { rshoverTooltip as rsHoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = {
highlightSpecialChars: false,
history: true,
drawSelection: false,
syntaxHighlighting: false,
defaultKeymap: true,
historyKeymap: true,
lineNumbers: false,
highlightActiveLineGutter: false,
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: false,
bracketMatching: false,
closeBrackets: false,
autocompletion: false,
rectangularSelection: false,
crosshairCursor: false,
highlightActiveLine: false,
highlightSelectionMatches: false,
closeBracketsKeymap: false,
searchKeymap: false,
foldKeymap: false,
completionKeymap: false,
lintKeymap: false
};
interface RefsInputInputProps
extends Pick<ReactCodeMirrorProps,
'id'| 'editable' | 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder'
> {
label?: string
innerref?: RefObject<ReactCodeMirrorRef> | undefined
onChange?: (newValue: string) => void
initialValue?: string
value?: string
resolved?: string
}
function RefsInput({
id, label, innerref, onChange, editable,
initialValue, value, resolved,
onFocus, onBlur,
...props
}: RefsInputInputProps) {
const { darkMode, colors } = useConceptTheme();
const { schema } = useRSForm();
const { resolveText, refsData } = useResolveText({schema: schema});
const [showResolve, setShowResolve] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(
() => {
return innerref ?? internalRef;
}, [internalRef, innerref]);
const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]);
const customTheme: Extension = useMemo(
() => createTheme({
theme: darkMode ? 'dark' : 'light',
settings: {
fontFamily: 'inherit',
background: editable ? colors.bgInput : colors.bgDefault,
foreground: colors.fgDefault,
selection: colors.bgHover
},
styles: [
{ tag: tags.name, color: colors.fgPurple }, // GlobalID
{ tag: tags.literal, color: colors.fgBlue }, // literals
]
}), [editable, colors, darkMode]);
const editorExtensions = useMemo(
() => [
EditorView.lineWrapping,
NaturalLanguage,
rsHoverTooltip(schema?.items || []),
], [schema?.items]);
function handleChange(newValue: string) {
if (onChange) onChange(newValue);
}
function handleFocusIn(event: React.FocusEvent<HTMLDivElement>) {
setIsFocused(true);
if (onFocus) onFocus(event);
}
function handleFocusOut(event: React.FocusEvent<HTMLDivElement>) {
setIsFocused(false);
if (onBlur) onBlur(event);
}
const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!thisRef.current) {
event.preventDefault();
return;
}
if (event.altKey) {
if (event.key === 'r' && value) {
event.preventDefault();
resolveText(value, () => {
setShowResolve(true);
});
return;
}
}
}, [thisRef, resolveText, value]);
return (
<>
{ showResolve &&
<Modal
readonly
hideWindow={() => setShowResolve(false)}
>
<div className='max-h-[60vh] max-w-[80vw] overflow-auto'>
<PrettyJson data={refsData} />
</div>
</Modal>}
<div className={`flex flex-col w-full ${cursor}`}>
{label &&
<Label
text={label}
required={false}
htmlFor={id}
className='mb-2'
/>}
<CodeMirror id={id}
ref={thisRef}
basicSetup={editorSetup}
theme={customTheme}
extensions={editorExtensions}
value={isFocused ? value : (value !== initialValue ? value : resolved)}
indentWithTab={false}
onChange={handleChange}
editable={editable}
onKeyDown={handleInput}
onFocus={handleFocusIn}
onBlur={handleFocusOut}
{...props}
/>
</div>
</>);
}
export default RefsInput;

View File

@ -0,0 +1,10 @@
import {styleTags, tags} from '@lezer/highlight';
export const highlighting = styleTags({
RefEntity: tags.name,
Global: tags.name,
Gram: tags.name,
RefSyntactic: tags.literal,
Offset: tags.literal,
});

View File

@ -0,0 +1,8 @@
import {LRLanguage} from '@codemirror/language'
import { parser } from './parser';
export const NaturalLanguage = LRLanguage.define({
parser: parser,
languageData: {}
});

View File

@ -0,0 +1,10 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Text = 1,
RefEntity = 2,
Global = 3,
Gram = 4,
RefSyntactic = 5,
Offset = 6,
Nominal = 7,
Word = 8

View File

@ -0,0 +1,26 @@
import { printTree } from '../../../utils/print-lezer-tree';
import { parser } from './parser';
const testData = [
['', '[Text]'],
['тест русский', '[Text[Word][Word]]'],
['test english', '[Text[Word][Word]]'],
['test greek σσσ', '[Text[Word][Word][Word]]'],
['X1 раз два X2', '[Text[Word][Word][Word][Word]]'],
['@{1| черный }', '[Text[RefSyntactic[Offset][Nominal[Word]]]]'],
['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal[Word]]]]'],
['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal[Word][Word]]]]'],
['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Gram][Gram][Gram]]]'],
['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Gram][Gram][Gram]]]'],
];
describe('Testing NaturalParser', () => {
it.each(testData)('Parse %p',
(input: string, expectedTree: string) => {
// NOTE: use strict parser to determine exact error position
// const tree = parser.configure({strict: true}).parse(input);
const tree = parser.parse(input);
expect(printTree(tree)).toBe(expectedTree);
});
});

View File

@ -0,0 +1,18 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {highlighting} from "./highlight.ts"
export const parser = LRParser.deserialize({
version: 14,
states: "#rQVQPOOO_QQO'#C^OOQO'#Ck'#CkOOQO'#Cj'#CjOOQO'#Ce'#CeQVQPOOOgQPO,58xOlQPO,58{OOQO-E6c-E6cOqQSO1G.dOvQPO1G.gO{QQO'#CoO!TQPO7+$OOOQO'#Cf'#CfO!YQPO'#CcO!bQPO7+$ROqQSO,59ZOOQO<<Gj<<GjOOQO-E6d-E6dOOQO<<Gm<<GmOOQO1G.u1G.u",
stateData: "!j~O]OS~OWROaPO~ORUOUVO~ObXO~ObYO~OSZO~OW]O~Od`O`cX~O`aO~OW]O`VX~O`cO~OW]~",
goto: "!WdPPePPePiPlrPPPx|PPP!QTQOTR_YQTORWTQ^YRb^TSOTTROTQ[XRd`",
nodeNames: "⚠ Text RefEntity Global Gram RefSyntactic Offset Nominal Word",
maxTerm: 20,
propSources: [highlighting],
skippedNodes: [0],
repeatNodeCount: 2,
tokenData: "-}~R!TOX$bXZ%SZ^%w^p$bpq%Sq|$b|}'p}!O(^!O!Q$b!Q!R)a!R![*a![!b$b!b!c+c!c!d+n!d!e)a!e!f+n!f!g+n!g!h)a!h!i+n!i!r)a!r!s+n!s!t)a!t!u+n!u!v+n!v!w+n!w!z)a!z!{+n!{!})a!}#T$b#T#o)a#p#q-s#q#r-x#r#y$b#y#z%w#z$f$b$f$g%w$g#BY$b#BY#BZ%w#BZ$IS$b$IS$I_%w$I_$I|$b$I|$JO%w$JO$JT$b$JT$JU%w$JU$KV$b$KV$KW%w$KW&FU$b&FU&FV%w&FV;'S$b;'S;=`$|<%lO$bP$gVWPOX$bZp$bq!b$b!c#o$b#r;'S$b;'S;=`$|<%lO$bP%PP;=`<%l$b~%XY]~X^%Spq%S#y#z%S$f$g%S#BY#BZ%S$IS$I_%S$I|$JO%S$JT$JU%S$KV$KW%S&FU&FV%S~&OjWP]~OX$bXZ%SZ^%w^p$bpq%Sq!b$b!c#o$b#r#y$b#y#z%w#z$f$b$f$g%w$g#BY$b#BY#BZ%w#BZ$IS$b$IS$I_%w$I_$I|$b$I|$JO%w$JO$JT$b$JT$JU%w$JU$KV$b$KV$KW%w$KW&FU$b&FU&FV%w&FV;'S$b;'S;=`$|<%lO$bR'wVdQWPOX$bZp$bq!b$b!c#o$b#r;'S$b;'S;=`$|<%lO$bV(e^SSWPOX$bZp$bq}$b}!O)a!O!Q$b!Q!R)a!R![*a![!b$b!c!})a!}#T$b#T#o)a#r;'S$b;'S;=`$|<%lO$bT)h]SSWPOX$bZp$bq}$b}!O)a!O!Q$b!Q![)a![!b$b!c!})a!}#T$b#T#o)a#r;'S$b;'S;=`$|<%lO$bV*j]SSUQWPOX$bZp$bq}$b}!O)a!O!Q$b!Q![*a![!b$b!c!})a!}#T$b#T#o)a#r;'S$b;'S;=`$|<%lO$b~+fP#o#p+i~+nOa~V+u^SSWPOX$bZp$bq}$b}!O)a!O!Q$b!Q!R)a!R![,q![!b$b!c!})a!}#T$b#T#o)a#r;'S$b;'S;=`$|<%lO$bV,z]RQSSWPOX$bZp$bq}$b}!O)a!O!Q$b!Q![,q![!b$b!c!})a!}#T$b#T#o)a#r;'S$b;'S;=`$|<%lO$b~-xOb~~-}O`~",
tokenizers: [0, 1, 2],
topRules: {"Text":[0,1]},
tokenPrec: 69
})

View File

@ -0,0 +1,50 @@
@precedence {
p1
p2
}
@top Text { textItem* }
@skip { space }
@tokens {
space { @whitespace+ }
Offset { $[-]?$[1-9]$[0-9]* }
Global { $[XCSDATFPR]$[1-9]$[0-9]* }
Word { ![@{|} \t\n]+ }
Gram { $[-a-zA-Z0-9]+ }
@precedence { Word, space }
}
textItem {
!p1 ref |
!p2 Word
}
ref {
RefEntity |
RefSyntactic
}
RefEntity {
"@{" Global "|" grams "}"
}
RefSyntactic {
"@{" Offset "|" Nominal "}"
}
Nominal { Word+ }
grams {
Gram |
Gram "," grams
}
@detectDelim
@external propSource highlighting from "./highlight.ts"

View File

@ -0,0 +1,63 @@
import { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view';
import { IConstituenta } from '../../models/rsform';
import { labelCstTypification } from '../../utils/labels';
function createTooltipFor(cst: IConstituenta) {
const dom = document.createElement('div');
dom.className = 'overflow-y-auto border shadow-md max-h-[25rem] max-w-[25rem] min-w-[10rem] w-fit z-tooltip text-sm px-2 py-2';
const alias = document.createElement('p');
alias.innerHTML = `<b>${cst.alias}:</b> ${labelCstTypification(cst)}`;
dom.appendChild(alias);
if (cst.term_resolved) {
const term = document.createElement('p');
term.innerHTML = `<b>Термин:</b> ${cst.term_resolved}`;
dom.appendChild(term);
}
if (cst.definition_formal) {
const expression = document.createElement('p');
expression.innerHTML = `<b>Выражение:</b> ${cst.definition_formal}`;
dom.appendChild(expression);
}
if (cst.definition_resolved) {
const definition = document.createElement('p');
definition.innerHTML = `<b>Определение:</b> ${cst.definition_resolved}`;
dom.appendChild(definition);
}
if (cst.convention) {
const convention = document.createElement('p');
convention.innerHTML = `<b>Конвенция:</b> ${cst.convention}`;
dom.appendChild(convention);
}
return { dom: dom }
}
export const getHoverTooltip = (items: IConstituenta[]) => {
return hoverTooltip((view, pos, side) => {
const {from, to, text} = view.state.doc.lineAt(pos);
let start = pos, end = pos;
while (start > from && /\w/.test(text[start - from - 1]))
start--;
while (end < to && /\w/.test(text[end - from]))
end++;
if (start === pos && side < 0 || end === pos && side > 0) {
return null;
}
const alias = text.slice(start - from, end - from);
const cst = items.find(cst => cst.alias === alias);
if (!cst) {
return null;
}
return {
pos: start,
end: end,
above: false,
create: () => createTooltipFor(cst)
}
});
}
export function rshoverTooltip(items: IConstituenta[]): Extension {
return [getHoverTooltip(items)];
}

View File

@ -8,6 +8,7 @@ import SubmitButton from '../../components/Common/SubmitButton';
import TextArea from '../../components/Common/TextArea'; import TextArea from '../../components/Common/TextArea';
import HelpConstituenta from '../../components/Help/HelpConstituenta'; import HelpConstituenta from '../../components/Help/HelpConstituenta';
import { DumpBinIcon, HelpIcon, PenIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons'; import { DumpBinIcon, HelpIcon, PenIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import RefsInput from '../../components/RefsInput';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import useWindowSize from '../../hooks/useWindowSize'; import useWindowSize from '../../hooks/useWindowSize';
import { EditMode } from '../../models/miscelanious'; import { EditMode } from '../../models/miscelanious';
@ -144,7 +145,7 @@ function EditorConstituenta({
<MiniButton <MiniButton
tooltip={`Редактировать словоформы термина: ${activeCst.term_forms.length}`} tooltip={`Редактировать словоформы термина: ${activeCst.term_forms.length}`}
disabled={!isEnabled} disabled={!isEnabled}
dimensions='w-fit ml-[3.2rem] pt-[0.4rem]' dimensions='w-fit ml-[3.2rem] pt-[0.3rem]'
noHover noHover
onClick={onEditTerm} onClick={onEditTerm}
icon={<PenIcon size={4} color={isEnabled ? 'text-primary' : ''} />} icon={<PenIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
@ -219,15 +220,15 @@ function EditorConstituenta({
onChange={newValue => setExpression(newValue)} onChange={newValue => setExpression(newValue)}
setTypification={setTypification} setTypification={setTypification}
/> />
<ReferenceInput id='definition' label='Текстовое определение' <RefsInput id='definition' label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения' placeholder='Лингвистическая интерпретация формального выражения'
rows={4} height='6.3rem'
value={textDefinition} value={textDefinition}
initialValue={activeCst?.definition_raw ?? ''} initialValue={activeCst?.definition_raw ?? ''}
resolved={activeCst?.definition_resolved ?? ''} resolved={activeCst?.definition_resolved ?? ''}
disabled={!isEnabled} editable={isEnabled}
spellCheck // spellCheck
onChange={event => setTextDefinition(event.target.value)} onChange={newValue => setTextDefinition(newValue)}
onFocus={() => setEditMode(EditMode.TEXT)} onFocus={() => setEditMode(EditMode.TEXT)}
/> />
<TextArea id='convention' label='Конвенция / Комментарий' <TextArea id='convention' label='Конвенция / Комментарий'