mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add frontend parser for text references
This commit is contained in:
parent
83242dfb69
commit
cd4792e96c
|
@ -4,7 +4,7 @@
|
|||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"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",
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
|
|
@ -45,7 +45,7 @@ const editorSetup: BasicSetupOptions = {
|
|||
|
||||
interface RSInputProps
|
||||
extends Pick<ReactCodeMirrorProps,
|
||||
'id'| 'editable' | 'height' | 'value' | 'className' | 'onFocus' | 'onBlur'
|
||||
'id'| 'editable' | 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder'
|
||||
> {
|
||||
label?: string
|
||||
innerref?: RefObject<ReactCodeMirrorRef> | undefined
|
||||
|
|
172
rsconcept/frontend/src/components/RefsInput/index.tsx
Normal file
172
rsconcept/frontend/src/components/RefsInput/index.tsx
Normal 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;
|
|
@ -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,
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import {LRLanguage} from '@codemirror/language'
|
||||
|
||||
import { parser } from './parser';
|
||||
|
||||
export const NaturalLanguage = LRLanguage.define({
|
||||
parser: parser,
|
||||
languageData: {}
|
||||
});
|
|
@ -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
|
|
@ -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);
|
||||
});
|
||||
});
|
18
rsconcept/frontend/src/components/RefsInput/parse/parser.ts
Normal file
18
rsconcept/frontend/src/components/RefsInput/parse/parser.ts
Normal 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
|
||||
})
|
|
@ -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"
|
63
rsconcept/frontend/src/components/RefsInput/tooltip.ts
Normal file
63
rsconcept/frontend/src/components/RefsInput/tooltip.ts
Normal 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)];
|
||||
}
|
|
@ -8,6 +8,7 @@ import SubmitButton from '../../components/Common/SubmitButton';
|
|||
import TextArea from '../../components/Common/TextArea';
|
||||
import HelpConstituenta from '../../components/Help/HelpConstituenta';
|
||||
import { DumpBinIcon, HelpIcon, PenIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
||||
import RefsInput from '../../components/RefsInput';
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import useWindowSize from '../../hooks/useWindowSize';
|
||||
import { EditMode } from '../../models/miscelanious';
|
||||
|
@ -144,7 +145,7 @@ function EditorConstituenta({
|
|||
<MiniButton
|
||||
tooltip={`Редактировать словоформы термина: ${activeCst.term_forms.length}`}
|
||||
disabled={!isEnabled}
|
||||
dimensions='w-fit ml-[3.2rem] pt-[0.4rem]'
|
||||
dimensions='w-fit ml-[3.2rem] pt-[0.3rem]'
|
||||
noHover
|
||||
onClick={onEditTerm}
|
||||
icon={<PenIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
|
||||
|
@ -219,15 +220,15 @@ function EditorConstituenta({
|
|||
onChange={newValue => setExpression(newValue)}
|
||||
setTypification={setTypification}
|
||||
/>
|
||||
<ReferenceInput id='definition' label='Текстовое определение'
|
||||
<RefsInput id='definition' label='Текстовое определение'
|
||||
placeholder='Лингвистическая интерпретация формального выражения'
|
||||
rows={4}
|
||||
height='6.3rem'
|
||||
value={textDefinition}
|
||||
initialValue={activeCst?.definition_raw ?? ''}
|
||||
resolved={activeCst?.definition_resolved ?? ''}
|
||||
disabled={!isEnabled}
|
||||
spellCheck
|
||||
onChange={event => setTextDefinition(event.target.value)}
|
||||
editable={isEnabled}
|
||||
// spellCheck
|
||||
onChange={newValue => setTextDefinition(newValue)}
|
||||
onFocus={() => setEditMode(EditMode.TEXT)}
|
||||
/>
|
||||
<TextArea id='convention' label='Конвенция / Комментарий'
|
||||
|
|
Loading…
Reference in New Issue
Block a user