diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json index b4915909..9d5ff7ad 100644 --- a/rsconcept/frontend/package.json +++ b/rsconcept/frontend/package.json @@ -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", diff --git a/rsconcept/frontend/src/components/RSInput/index.tsx b/rsconcept/frontend/src/components/RSInput/index.tsx index 6a9f4fd8..c0098fb3 100644 --- a/rsconcept/frontend/src/components/RSInput/index.tsx +++ b/rsconcept/frontend/src/components/RSInput/index.tsx @@ -45,7 +45,7 @@ const editorSetup: BasicSetupOptions = { interface RSInputProps extends Pick { label?: string innerref?: RefObject | undefined diff --git a/rsconcept/frontend/src/components/RefsInput/index.tsx b/rsconcept/frontend/src/components/RefsInput/index.tsx new file mode 100644 index 00000000..b7b16a76 --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/index.tsx @@ -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 { + label?: string + innerref?: RefObject | 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(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) { + setIsFocused(true); + if (onFocus) onFocus(event); + } + + function handleFocusOut(event: React.FocusEvent) { + setIsFocused(false); + if (onBlur) onBlur(event); + } + + const handleInput = useCallback( + (event: React.KeyboardEvent) => { + 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 && + setShowResolve(false)} + > +
+ +
+
} +
+ {label && +
+ ); +} + +export default RefsInput; diff --git a/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts b/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts new file mode 100644 index 00000000..b7ec1084 --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts @@ -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, +}); \ No newline at end of file diff --git a/rsconcept/frontend/src/components/RefsInput/parse/index.ts b/rsconcept/frontend/src/components/RefsInput/parse/index.ts new file mode 100644 index 00000000..14ead267 --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/index.ts @@ -0,0 +1,8 @@ +import {LRLanguage} from '@codemirror/language' + +import { parser } from './parser'; + +export const NaturalLanguage = LRLanguage.define({ + parser: parser, + languageData: {} +}); \ No newline at end of file diff --git a/rsconcept/frontend/src/components/RefsInput/parse/parser.terms.ts b/rsconcept/frontend/src/components/RefsInput/parse/parser.terms.ts new file mode 100644 index 00000000..69d3403e --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.terms.ts @@ -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 diff --git a/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts b/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts new file mode 100644 index 00000000..476ce5df --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts @@ -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); + }); +}); diff --git a/rsconcept/frontend/src/components/RefsInput/parse/parser.ts b/rsconcept/frontend/src/components/RefsInput/parse/parser.ts new file mode 100644 index 00000000..e8e291c7 --- /dev/null +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.ts @@ -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<${cst.alias}: ${labelCstTypification(cst)}`; + dom.appendChild(alias); + if (cst.term_resolved) { + const term = document.createElement('p'); + term.innerHTML = `Термин: ${cst.term_resolved}`; + dom.appendChild(term); + } + if (cst.definition_formal) { + const expression = document.createElement('p'); + expression.innerHTML = `Выражение: ${cst.definition_formal}`; + dom.appendChild(expression); + } + if (cst.definition_resolved) { + const definition = document.createElement('p'); + definition.innerHTML = `Определение: ${cst.definition_resolved}`; + dom.appendChild(definition); + } + if (cst.convention) { + const convention = document.createElement('p'); + convention.innerHTML = `Конвенция: ${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)]; +} diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx index e6c3972a..23db4fe4 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx @@ -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({ } @@ -219,15 +220,15 @@ function EditorConstituenta({ onChange={newValue => setExpression(newValue)} setTypification={setTypification} /> - setTextDefinition(event.target.value)} + editable={isEnabled} + // spellCheck + onChange={newValue => setTextDefinition(newValue)} onFocus={() => setEditMode(EditMode.TEXT)} />