From 78c6a2306ec9e7bf0a228ae064d9ba3e806cfd98 Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Wed, 27 Sep 2023 23:36:51 +0300 Subject: [PATCH] Display text reference tooltips --- .../frontend/src/components/RSInput/index.tsx | 2 +- .../src/components/RSInput/rslang/index.ts | 5 + .../components/RSInput/rslang/parser.test.ts | 2 +- .../src/components/RSInput/tooltip.ts | 69 +++--- .../src/components/RefsInput/index.tsx | 8 +- .../components/RefsInput/parse/highlight.ts | 3 +- .../RefsInput/parse/parser.terms.ts | 4 +- .../components/RefsInput/parse/parser.test.ts | 20 +- .../src/components/RefsInput/parse/parser.ts | 14 +- .../RefsInput/parse/refsText.grammar | 30 ++- .../src/components/RefsInput/tooltip.ts | 82 +++---- .../frontend/src/models/language.test.ts | 30 ++- rsconcept/frontend/src/models/language.ts | 98 ++++---- .../src/pages/RSFormPage/DlgEditTerm.tsx | 54 +++-- rsconcept/frontend/src/utils/codemirror.ts | 219 ++++++++++++++++++ rsconcept/frontend/src/utils/color.ts | 23 +- rsconcept/frontend/src/utils/labels.ts | 21 +- .../frontend/src/utils/print-lezer-tree.ts | 60 ----- rsconcept/frontend/src/utils/selectors.ts | 38 ++- 19 files changed, 490 insertions(+), 292 deletions(-) create mode 100644 rsconcept/frontend/src/utils/codemirror.ts delete mode 100644 rsconcept/frontend/src/utils/print-lezer-tree.ts diff --git a/rsconcept/frontend/src/components/RSInput/index.tsx b/rsconcept/frontend/src/components/RSInput/index.tsx index c0098fb3..7b427eff 100644 --- a/rsconcept/frontend/src/components/RSInput/index.tsx +++ b/rsconcept/frontend/src/components/RSInput/index.tsx @@ -13,7 +13,7 @@ import Label from '../Common/Label'; import { ccBracketMatching } from './bracketMatching'; import { RSLanguage } from './rslang'; import { getSymbolSubstitute,TextWrapper } from './textEditing'; -import { rshoverTooltip as rsHoverTooltip } from './tooltip'; +import { rsHoverTooltip } from './tooltip'; const editorSetup: BasicSetupOptions = { highlightSpecialChars: false, diff --git a/rsconcept/frontend/src/components/RSInput/rslang/index.ts b/rsconcept/frontend/src/components/RSInput/rslang/index.ts index 5849a4db..8278433b 100644 --- a/rsconcept/frontend/src/components/RSInput/rslang/index.ts +++ b/rsconcept/frontend/src/components/RSInput/rslang/index.ts @@ -1,6 +1,11 @@ import {LRLanguage} from '@codemirror/language' import { parser } from './parser'; +import { Function, Global, Predicate } from './parser.terms'; + +export const GlobalTokens: number[] = [ + Global, Function, Predicate +] export const RSLanguage = LRLanguage.define({ parser: parser, diff --git a/rsconcept/frontend/src/components/RSInput/rslang/parser.test.ts b/rsconcept/frontend/src/components/RSInput/rslang/parser.test.ts index 65873971..57d1b166 100644 --- a/rsconcept/frontend/src/components/RSInput/rslang/parser.test.ts +++ b/rsconcept/frontend/src/components/RSInput/rslang/parser.test.ts @@ -1,4 +1,4 @@ -import { printTree } from '../../../utils/print-lezer-tree'; +import { printTree } from '../../../utils/codemirror'; import { parser } from './parser'; const testData = [ diff --git a/rsconcept/frontend/src/components/RSInput/tooltip.ts b/rsconcept/frontend/src/components/RSInput/tooltip.ts index 42e546cc..a3b22251 100644 --- a/rsconcept/frontend/src/components/RSInput/tooltip.ts +++ b/rsconcept/frontend/src/components/RSInput/tooltip.ts @@ -1,50 +1,33 @@ +import { syntaxTree } from "@codemirror/language" import { Extension } from '@codemirror/state'; import { hoverTooltip } from '@codemirror/view'; +import { EditorState } from '@uiw/react-codemirror'; import { IConstituenta } from '../../models/rsform'; -import { labelCstTypification } from '../../utils/labels'; +import { findEnvelopingNodes } from '../../utils/codemirror'; +import { domTooltipConstituenta } from '../../utils/codemirror'; +import { GlobalTokens } from './rslang'; -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 = `${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 } +function findAliasAt(pos: number, state: EditorState) { + const { from: lineStart, to: lineEnd, text } = state.doc.lineAt(pos); + const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), GlobalTokens); + let alias = ''; + let start = 0; + let end = 0; + nodes.forEach( + node => { + if (node.to <= lineEnd && node.from >= lineStart) { + alias = text.slice(node.from - lineStart, node.to - lineStart); + start = node.from; + end = node.to; + } + }); + return {alias, start, end}; } -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 globalsHoverTooltip = (items: IConstituenta[]) => { + return hoverTooltip((view, pos) => { + const { alias, start, end } = findAliasAt(pos, view.state); const cst = items.find(cst => cst.alias === alias); if (!cst) { return null; @@ -53,11 +36,11 @@ export const getHoverTooltip = (items: IConstituenta[]) => { pos: start, end: end, above: false, - create: () => createTooltipFor(cst) + create: () => domTooltipConstituenta(cst) } }); } -export function rshoverTooltip(items: IConstituenta[]): Extension { - return [getHoverTooltip(items)]; +export function rsHoverTooltip(items: IConstituenta[]): Extension { + return [globalsHoverTooltip(items)]; } diff --git a/rsconcept/frontend/src/components/RefsInput/index.tsx b/rsconcept/frontend/src/components/RefsInput/index.tsx index f6eaf1a9..00c88c1d 100644 --- a/rsconcept/frontend/src/components/RefsInput/index.tsx +++ b/rsconcept/frontend/src/components/RefsInput/index.tsx @@ -13,7 +13,7 @@ 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'; +import { refsHoverTooltip } from './tooltip'; const editorSetup: BasicSetupOptions = { highlightSpecialChars: false, @@ -87,7 +87,7 @@ function RefsInput({ selection: colors.bgHover }, styles: [ - { tag: tags.name, color: colors.fgPurple }, // GlobalID + { tag: tags.name, color: colors.fgPurple, cursor: 'pointer' }, // GlobalID { tag: tags.literal, color: colors.fgTeal }, // literals ] }), [editable, colors, darkMode]); @@ -96,8 +96,8 @@ function RefsInput({ () => [ EditorView.lineWrapping, NaturalLanguage, - rsHoverTooltip(schema?.items || []), - ], [schema?.items]); + refsHoverTooltip(schema?.items || [], colors), + ], [schema?.items, colors]); function handleChange(newValue: string) { if (onChange) onChange(newValue); diff --git a/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts b/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts index b7ec1084..f8706409 100644 --- a/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts +++ b/rsconcept/frontend/src/components/RefsInput/parse/highlight.ts @@ -3,8 +3,9 @@ import {styleTags, tags} from '@lezer/highlight'; export const highlighting = styleTags({ RefEntity: tags.name, Global: tags.name, - Gram: tags.name, + Grams: tags.name, RefSyntactic: tags.literal, Offset: tags.literal, + Nominal: tags.literal, }); \ 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 index 69d3403e..6d5beffc 100644 --- a/rsconcept/frontend/src/components/RefsInput/parse/parser.terms.ts +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.terms.ts @@ -3,8 +3,8 @@ export const Text = 1, RefEntity = 2, Global = 3, - Gram = 4, + Grams = 4, RefSyntactic = 5, Offset = 6, Nominal = 7, - Word = 8 + Filler = 8 diff --git a/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts b/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts index 476ce5df..26d19865 100644 --- a/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.test.ts @@ -1,18 +1,18 @@ -import { printTree } from '../../../utils/print-lezer-tree'; +import { printTree } from '../../../utils/codemirror'; 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]]'], + ['тест русский', '[Text[Filler]]'], + ['test english', '[Text[Filler]]'], + ['test greek σσσ', '[Text[Filler]]'], + ['X1 раз два X2', '[Text[Filler]]'], - ['@{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]]]'], + ['@{1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'], + ['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'], + ['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'], + ['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'], + ['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'], ]; describe('Testing NaturalParser', () => { diff --git a/rsconcept/frontend/src/components/RefsInput/parse/parser.ts b/rsconcept/frontend/src/components/RefsInput/parse/parser.ts index e8e291c7..16518a8a 100644 --- a/rsconcept/frontend/src/components/RefsInput/parse/parser.ts +++ b/rsconcept/frontend/src/components/RefsInput/parse/parser.ts @@ -3,16 +3,16 @@ 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) { +export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => { + return hoverTooltip((view, pos) => { + const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), [RefEntity, RefSyntactic]); + if (nodes.length !== 1) { 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) + const start = nodes[0].from; + const end = nodes[0].to; + const text = view.state.doc.sliceString(start, end); + if (nodes[0].type.id === RefEntity) { + const ref = parseEntityReference(text); + const cst = items.find(cst => cst.alias === ref.entity); + return { + pos: start, + end: end, + above: false, + create: () => domTooltipEntityReference(ref, cst, colors) + } + } else { + const ref = parseSyntacticReference(text); + return { + pos: start, + end: end, + above: false, + create: () => domTooltipSyntacticReference(ref) + } } }); } -export function rshoverTooltip(items: IConstituenta[]): Extension { - return [getHoverTooltip(items)]; +export function refsHoverTooltip(items: IConstituenta[], colors: IColorTheme): Extension { + return [globalsHoverTooltip(items, colors)]; } diff --git a/rsconcept/frontend/src/models/language.test.ts b/rsconcept/frontend/src/models/language.test.ts index 38db0f25..f049e814 100644 --- a/rsconcept/frontend/src/models/language.test.ts +++ b/rsconcept/frontend/src/models/language.test.ts @@ -1,4 +1,4 @@ -import { Grammeme, parseGrammemes } from './language'; +import { Grammeme, parseEntityReference, parseGrammemes, parseSyntacticReference } from './language'; describe('Testing grammeme parsing', () => { @@ -11,10 +11,28 @@ describe('Testing grammeme parsing', () => { test('regular grammemes', () => { - expect(parseGrammemes('NOUN')).toStrictEqual([{type: Grammeme.NOUN, data: 'NOUN'}]); - expect(parseGrammemes('sing,nomn')).toStrictEqual([ - {type: Grammeme.sing, data: 'sing'}, - {type: Grammeme.nomn, data: 'nomn'} - ]); + expect(parseGrammemes('NOUN')).toStrictEqual([Grammeme.NOUN]); + expect(parseGrammemes('sing,nomn')).toStrictEqual([Grammeme.sing, Grammeme.nomn]); + expect(parseGrammemes('nomn,sing')).toStrictEqual([Grammeme.sing, Grammeme.nomn]); + expect(parseGrammemes('nomn,invalid,sing')).toStrictEqual([Grammeme.sing, Grammeme.nomn, 'invalid']); + expect(parseGrammemes('invalid,test')).toStrictEqual(['invalid', 'test']); + }); +}); + + +describe('Testing reference parsing', () => { + test('entity reference', + () => { + expect(parseEntityReference('@{ X1 | NOUN,sing }')).toStrictEqual({entity: 'X1', form: 'NOUN,sing'}); + expect(parseEntityReference('@{X1|NOUN,sing}')).toStrictEqual({entity: 'X1', form: 'NOUN,sing'}); + expect(parseEntityReference('@{X111|NOUN,sing}')).toStrictEqual({entity: 'X111', form: 'NOUN,sing'}); + }); + + test('syntactic reference', + () => { + expect(parseSyntacticReference('@{1|test test}')).toStrictEqual({offset: 1, nominal: 'test test'}); + expect(parseSyntacticReference('@{101|test test}')).toStrictEqual({offset: 101, nominal: 'test test'}); + expect(parseSyntacticReference('@{-1|test test}')).toStrictEqual({offset: -1, nominal: 'test test'}); + expect(parseSyntacticReference('@{-99|test test}')).toStrictEqual({offset: -99, nominal: 'test test'}); }); }); diff --git a/rsconcept/frontend/src/models/language.ts b/rsconcept/frontend/src/models/language.ts index 5a0b25f9..f6990dee 100644 --- a/rsconcept/frontend/src/models/language.ts +++ b/rsconcept/frontend/src/models/language.ts @@ -1,4 +1,6 @@ -// Module: Natural language model declarations. +/** + * Module: Natural language model declarations. + */ /** * Represents API result for text output. @@ -11,9 +13,6 @@ export interface ITextResult { * Represents single unit of language Morphology. */ export enum Grammeme { - // Неизвестная граммема - UNKN = 'UNKN', - // Части речи NOUN = 'NOUN', ADJF = 'ADJF', ADJS = 'ADJS', COMP = 'COMP', VERB = 'VERB', INFN = 'INFN', PRTF = 'PRTF', PRTS = 'PRTS', @@ -204,17 +203,14 @@ export const VerbGrams = [ /** * Represents {@link Grammeme} parse data. */ -export interface IGramData { - type: Grammeme - data: string -} +export type GramData = Grammeme | string; /** * Represents specific wordform attached to {@link Grammeme}s. */ export interface IWordForm { text: string - grams: IGramData[] + grams: GramData[] } /** @@ -232,16 +228,6 @@ export interface ILexemeData { items: IWordFormPlain[] } -/** - * Equality comparator for {@link IGramData}. Compares text data for unknown grammemes - */ -export function matchGrammeme(left: IGramData, right: IGramData): boolean { - if (left.type !== right.type) { - return false; - } - return left.type !== Grammeme.UNKN || left.data === right.data; -} - /** * Equality comparator for {@link IWordForm}. Compares a set of Grammemes attached to wordforms */ @@ -250,41 +236,43 @@ export function matchWordForm(left: IWordForm, right: IWordForm): boolean { return false; } for (let index = 0; index < left.grams.length; ++index) { - if (!matchGrammeme(left.grams[index], right.grams[index])) { + if (left.grams[index] !== right.grams[index]) { return false; } } return true; } -function parseSingleGrammeme(text: string): IGramData { +function parseSingleGrammeme(text: string): GramData { if (Object.values(Grammeme).includes(text as Grammeme)) { - return { - data: text, - type: text as Grammeme - } + return text as Grammeme; } else { - return { - data: text, - type: Grammeme.UNKN - } + return text; } } -export function sortGrammemes(input: TData[]): TData[] { - const result: TData[] = []; - Object.values(Grammeme).forEach( - gram => { - const item = input.find(data => data.type === gram); - if (item) { - result.push(item); - } - }); - return result; +/** + * Compares {@link GramData} based on Grammeme enum and alpha order for strings. + */ +export function compareGrammemes(left: GramData, right: GramData): number { + const indexLeft = Object.values(Grammeme).findIndex(gram => gram === left as Grammeme); + const indexRight = Object.values(Grammeme).findIndex(gram => gram === right as Grammeme); + if (indexLeft === -1 && indexRight === -1) { + return left.localeCompare(right); + } else if (indexLeft === -1 && indexRight !== -1) { + return 1; + } else if (indexLeft !== -1 && indexRight === -1) { + return -1; + } else { + return indexLeft - indexRight; + } } -export function parseGrammemes(termForm: string): IGramData[] { - const result: IGramData[] = []; +/** + * Transforms {@link Grammeme} enumeration to {@link GramData}. + */ +export function parseGrammemes(termForm: string): GramData[] { + const result: GramData[] = []; const chunks = termForm.split(','); chunks.forEach(chunk => { chunk = chunk.trim(); @@ -292,7 +280,7 @@ export function parseGrammemes(termForm: string): IGramData[] { result.push(parseSingleGrammeme(chunk)); } }); - return sortGrammemes(result); + return result.sort(compareGrammemes); } // ====== Reference resolution ===== @@ -353,3 +341,29 @@ export interface IResolutionData { output: string refs: IResolvedReference[] } + +/** + * Extracts {@link IEntityReference} from string representation. + * + * @param text - Reference text in a valid pattern. Must fit format '\@\{GLOBAL_ID|GRAMMEMES\}' + */ +export function parseEntityReference(text: string): IEntityReference { + const blocks = text.slice(2, text.length - 1).split('|'); + return { + entity: blocks[0].trim(), + form: blocks[1].trim() + } +} + +/** + * Extracts {@link ISyntacticReference} from string representation. + * + * @param text - Reference text in a valid pattern. Must fit format '\@\{OFFSET|NOMINAL_FORM\}' + */ +export function parseSyntacticReference(text: string): ISyntacticReference { + const blocks = text.slice(2, text.length - 1).split('|'); + return { + offset: Number(blocks[0].trim()), + nominal: blocks[1].trim() + } +} \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx b/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx index 2363c046..0ca36ace 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx @@ -10,15 +10,13 @@ import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDoubleDownIcon, CrossI import { useConceptTheme } from '../../context/ThemeContext'; import useConceptText from '../../hooks/useConceptText'; import { - Grammeme, GrammemeGroups, ITextRequest, IWordForm, - IWordFormPlain, - matchWordForm, NounGrams, parseGrammemes, - sortGrammemes, VerbGrams + GramData, Grammeme, GrammemeGroups, ITextRequest, IWordForm, + IWordFormPlain, matchWordForm, NounGrams, parseGrammemes, VerbGrams } from '../../models/language'; import { IConstituenta, TermForm } from '../../models/rsform'; import { colorfgGrammeme } from '../../utils/color'; import { labelGrammeme } from '../../utils/labels'; -import { IGrammemeOption, SelectorGrammemesList, SelectorGrammems } from '../../utils/selectors'; +import { compareGrammemeOptions,IGrammemeOption, SelectorGrammemesList, SelectorGrammems } from '../../utils/selectors'; interface DlgEditTermProps { hideWindow: () => void @@ -44,7 +42,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { forms.forEach( ({text, grams}) => result.push({ text: text, - tags: grams.map(gram => gram.data).join(',') + tags: grams.join(',') })); return result; } @@ -67,32 +65,32 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { // Filter grammemes when input changes useEffect( () => { - let newFilter: Grammeme[] = []; - inputGrams.forEach(({type: gram}) => { + let newFilter: GramData[] = []; + inputGrams.forEach(({value: gram}) => { if (!newFilter.includes(gram)) { - if (NounGrams.includes(gram)) { + if (NounGrams.includes(gram as Grammeme)) { newFilter.push(...NounGrams); } - if (VerbGrams.includes(gram)) { + if (VerbGrams.includes(gram as Grammeme)) { newFilter.push(...VerbGrams); } } }); - inputGrams.forEach(({type: gram}) => + inputGrams.forEach(({value: gram}) => GrammemeGroups.forEach(group => { - if (group.includes(gram)) { - newFilter = newFilter.filter(item => !group.includes(item) || item === gram); + if (group.includes(gram as Grammeme)) { + newFilter = newFilter.filter(item => !group.includes(item as Grammeme) || item === gram); } })); - newFilter.push(...inputGrams.map(({type: gram}) => gram)); + newFilter.push(...inputGrams.map(({value}) => value)); if (newFilter.length === 0) { newFilter = [...VerbGrams, ...NounGrams]; } newFilter = [... new Set(newFilter)]; - setOptions(SelectorGrammems.filter(({type: gram}) => newFilter.includes(gram))); + setOptions(SelectorGrammems.filter(({value}) => newFilter.includes(value))); }, [inputGrams]); const handleSubmit = () => onSave(getData()); @@ -100,10 +98,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { function handleAddForm() { const newForm: IWordForm = { text: inputText, - grams: inputGrams.map(item => ({ - type: item.type, - data: item.data - })) + grams: inputGrams.map(item => item.value) }; setForms(forms => [ newForm, @@ -127,7 +122,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { function handleRowClicked(form: IWordForm) { setInputText(form.text); - setInputGrams(SelectorGrammems.filter(gram => form.grams.find(test => test.type === gram.type))); + setInputGrams(SelectorGrammems.filter(gram => form.grams.find(test => test === gram.value))); } function handleResetForm() { @@ -138,7 +133,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { function handleInflect() { const data: IWordFormPlain = { text: term, - grams: inputGrams.map(gram => gram.data).join(',') + grams: inputGrams.map(gram => gram.value).join(',') } textProcessor.inflect(data, response => setInputText(response.result)); } @@ -149,7 +144,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { } textProcessor.parse(data, response => { const grams = parseGrammemes(response.result); - setInputGrams(SelectorGrammems.filter(gram => grams.find(test => test.type === gram.type))); + setInputGrams(SelectorGrammems.filter(gram => grams.find(test => test === gram.value))); }); } @@ -166,7 +161,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { const newForms: IWordForm[] = response.items.map( form => ({ text: form.text, - grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram.type)) + grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram as Grammeme)) })); setForms(forms => [ ...newForms, @@ -196,13 +191,13 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { { props.getValue().map( gram =>
setInputText(event.target.value)} /> -
+
+
+ Словоформ: {forms.length} +
setInputGrams(sortGrammemes([...newValue]))} + onChange={newValue => setInputGrams([...newValue].sort(compareGrammemeOptions))} />
diff --git a/rsconcept/frontend/src/utils/codemirror.ts b/rsconcept/frontend/src/utils/codemirror.ts new file mode 100644 index 00000000..a2636237 --- /dev/null +++ b/rsconcept/frontend/src/utils/codemirror.ts @@ -0,0 +1,219 @@ +import { NodeType, Tree, TreeCursor } from '@lezer/common' + +import { IEntityReference, ISyntacticReference, parseGrammemes } from '../models/language' +import { IConstituenta } from '../models/rsform' +import { colorfgGrammeme,IColorTheme } from './color' +import { describeConstituentaTerm, labelCstTypification, labelGrammeme } from './labels' + +/** + * Represents syntax tree node data. +*/ +export interface SyntaxNode { + type: NodeType + from: number + to: number +} + +/** + * Represents syntax tree cursor data. +*/ +export interface CursorNode +extends SyntaxNode { + isLeaf: boolean +} + +function cursorNode({ type, from, to }: TreeCursor, isLeaf = false): CursorNode { + return { type, from, to, isLeaf } +} + +type TreeTraversalOptions = { + beforeEnter?: (cursor: TreeCursor) => void + onEnter: (node: CursorNode) => false | void + onLeave?: (node: CursorNode) => false | void +} + +/** + * Implements depth-first traversal. +*/ +export function traverseTree(tree: Tree, { beforeEnter, onEnter, onLeave, }: TreeTraversalOptions) { + const cursor = tree.cursor(); + for (;;) { + let node = cursorNode(cursor) + let leave = false + const enter = !node.type.isAnonymous + if (enter && beforeEnter) beforeEnter(cursor) + node.isLeaf = !cursor.firstChild() + if (enter) { + leave = true + if (onEnter(node) === false) return + } + if (!node.isLeaf) continue + for (;;) { + node = cursorNode(cursor, node.isLeaf) + if (leave && onLeave) if (onLeave(node) === false) return; + leave = cursor.type.isAnonymous + node.isLeaf = false + if (cursor.nextSibling()) break; + if (!cursor.parent()) return; + leave = true + } + } +} + +/** + * Prints tree to compact string. +*/ +export function printTree(tree: Tree): string { + const state = { + output: '', + prefixes: [] as string[] + } + traverseTree(tree, { + onEnter: node => { + state.output += '['; + state.output += node.type.name; + }, + onLeave: () => { + state.output += ']'; + }, + }) + return state.output; +} + +/** + * Reteives a list of all nodes, containing given range and corresponding to a filter. +*/ +export function findEnvelopingNodes(start: number, finish: number, tree: Tree, filter?: number[]): SyntaxNode[] { + const result: SyntaxNode[] = []; + tree.cursor().iterate( + node => { + if ( + (!filter || filter.includes(node.type.id)) && + node.to >= start && node.from <= finish + ) { + result.push({ + type: node.type, + to: node.to, + from: node.from + }); + } + }); + return result; +} + +/** + * Reteives a list of all nodes, contained in given range and corresponding to a filter. +*/ +export function findContainedNodes(start: number, finish: number, tree: Tree, filter?: number[]): SyntaxNode[] { + const result: SyntaxNode[] = []; + tree.cursor().iterate( + node => { + if ( + (!filter || filter.includes(node.type.id)) && + node.to <= start && node.from >= finish + ) { + result.push({ + type: node.type, + to: node.to, + from: node.from + }); + } + }); + return result; +} + +/** + * Create DOM tooltip for {@link Constituenta}. +*/ +export function domTooltipConstituenta(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 = `${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 }; +} + +/** + * Create DOM tooltip for {@link IEntityReference}. +*/ +export function domTooltipEntityReference(ref: IEntityReference, cst: IConstituenta | undefined, colors: IColorTheme) { + const DIMENSIONS = 'max-h-[25rem] max-w-[25rem] min-w-[10rem] w-fit z-tooltip px-2 py-2'; + const LAYOUT = 'flex flex-col gap-1 overflow-y-auto' + + const dom = document.createElement('div'); + dom.className = `${DIMENSIONS} ${LAYOUT} border shadow-md text-sm select-none cursor-auto`; + + const term = document.createElement('p'); + term.innerHTML = `${ref.entity}: ${describeConstituentaTerm(cst)}`; + dom.appendChild(term); + + const grams = document.createElement('div'); + grams.className = 'flex flex-wrap gap-1'; + parseGrammemes(ref.form).forEach( + gramStr => { + const gram = document.createElement('div'); + gram.id =`tooltip-${gramStr}`; + gram.className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap'; + gram.style.borderWidth = '1px'; + gram.style.borderColor = colorfgGrammeme(gramStr, colors); + gram.style.color = colorfgGrammeme(gramStr, colors); + gram.style.fontWeight = '600'; + gram.style.backgroundColor = colors.bgInput; + gram.innerText = labelGrammeme(gramStr); + grams.appendChild(gram); + }); + dom.appendChild(grams); + + return { dom: dom }; +} + +/** + * Create DOM tooltip for {@link ISyntacticReference}. +*/ +export function domTooltipSyntacticReference(ref: ISyntacticReference) { + const DIMENSIONS = 'max-h-[25rem] max-w-[25rem] min-w-[10rem] w-fit z-tooltip px-2 py-2'; + const LAYOUT = 'flex flex-col gap-1 overflow-y-auto' + + const dom = document.createElement('div'); + dom.className = `${DIMENSIONS} ${LAYOUT} border shadow-md text-sm select-none cursor-auto`; + + const title = document.createElement('p'); + title.innerHTML = 'Синтаксическая ссылка'; + dom.appendChild(title); + + const offset = document.createElement('p'); + offset.innerHTML = `Смещение: ${ref.offset}`; + dom.appendChild(offset); + + const nominal = document.createElement('p'); + nominal.innerHTML = `Начальная форма: ${ref.nominal}`; + dom.appendChild(nominal); + + return { dom: dom }; +} \ No newline at end of file diff --git a/rsconcept/frontend/src/utils/color.ts b/rsconcept/frontend/src/utils/color.ts index c645b051..4df491b3 100644 --- a/rsconcept/frontend/src/utils/color.ts +++ b/rsconcept/frontend/src/utils/color.ts @@ -1,6 +1,6 @@ // =========== Modules contains all dynamic color definitions ========== -import { Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '../models/language' +import { GramData, Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '../models/language' import { CstClass, ExpressionStatus } from '../models/rsform' import { ISyntaxTreeNode, TokenID } from '../models/rslang' @@ -385,30 +385,31 @@ export function colorbgCstClass(cstClass: CstClass, colors: IColorTheme): string } } -export function colorfgGrammeme(gram: Grammeme, colors: IColorTheme): string { - if (PartOfSpeech.includes(gram)) { +export function colorfgGrammeme(gram: GramData, colors: IColorTheme): string { + if (PartOfSpeech.includes(gram as Grammeme)) { return colors.fgBlue; } - if (NounGrams.includes(gram)) { + if (NounGrams.includes(gram as Grammeme)) { return colors.fgGreen; } - if (VerbGrams.includes(gram)) { + if (VerbGrams.includes(gram as Grammeme)) { return colors.fgTeal; } - if (gram === Grammeme.UNKN) { + if (!Object.values(Grammeme).includes(gram as Grammeme)) { return colors.fgRed; + } else { + return colors.fgPurple; } - return colors.fgPurple; } -export function colorbgGrammeme(gram: Grammeme, colors: IColorTheme): string { - if (PartOfSpeech.includes(gram)) { +export function colorbgGrammeme(gram: GramData, colors: IColorTheme): string { + if (PartOfSpeech.includes(gram as Grammeme)) { return colors.bgBlue; } - if (NounGrams.includes(gram)) { + if (NounGrams.includes(gram as Grammeme)) { return colors.bgGreen; } - if (VerbGrams.includes(gram)) { + if (VerbGrams.includes(gram as Grammeme)) { return colors.bgTeal; } return colors.bgInput; diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index 902b660c..ba01d3f2 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -1,6 +1,6 @@ // =========== Modules contains all text descriptors ========== -import { Grammeme,IGramData } from '../models/language'; +import { GramData,Grammeme } from '../models/language'; import { CstMatchMode, DependencyMode, HelpTopic } from '../models/miscelanious'; import { CstClass, CstType, ExpressionStatus, IConstituenta } from '../models/rsform'; import { IFunctionArg, IRSErrorDescription, ISyntaxTreeNode, ParsingStatus, RSErrorType, TokenID } from '../models/rslang'; @@ -23,6 +23,17 @@ export function describeConstituenta(cst: IConstituenta): string { } } +export function describeConstituentaTerm(cst: IConstituenta | undefined): string { + if (!cst) { + return '!Конституента отсутствует!'; + } + if (!cst.term_resolved) { + return '!Пустой термин!'; + } else { + return cst.term_resolved; + } +} + export function labelConstituenta(cst: IConstituenta) { return `${cst.alias}: ${describeConstituenta(cst)}`; } @@ -346,8 +357,10 @@ export function labelSyntaxTree(node: ISyntaxTreeNode): string { return 'UNKNOWN ' + String(node.typeID); } -export function labelGrammeme(gram: IGramData): string { - switch (gram.type) { +export function labelGrammeme(gram: GramData): string { + switch (gram) { + default: return `Неизв: ${gram}`; + case Grammeme.NOUN: return 'ЧР: сущ'; case Grammeme.VERB: return 'ЧР: глагол'; case Grammeme.INFN: return 'ЧР: глагол инф'; @@ -411,8 +424,6 @@ export function labelGrammeme(gram: IGramData): string { case Grammeme.Slng: return 'Стиль: жаргон'; case Grammeme.Arch: return 'Стиль: устаревший'; case Grammeme.Litr: return 'Стиль: литературный'; - - case Grammeme.UNKN: return `Неизв: ${gram.data}`; } } diff --git a/rsconcept/frontend/src/utils/print-lezer-tree.ts b/rsconcept/frontend/src/utils/print-lezer-tree.ts deleted file mode 100644 index 4020f825..00000000 --- a/rsconcept/frontend/src/utils/print-lezer-tree.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NodeType, Tree, TreeCursor } from '@lezer/common' - -export type CursorNode = { - type: NodeType - from: number - to: number - isLeaf: boolean -} - -function cursorNode({ type, from, to }: TreeCursor, isLeaf = false): CursorNode { - return { type, from, to, isLeaf } -} - -type TreeTraversalOptions = { - beforeEnter?: (cursor: TreeCursor) => void - onEnter: (node: CursorNode) => false | void - onLeave?: (node: CursorNode) => false | void -} - -export function traverseTree(tree: Tree, { beforeEnter, onEnter, onLeave, }: TreeTraversalOptions) { - const cursor = tree.cursor(); - for (;;) { - let node = cursorNode(cursor) - let leave = false - const enter = !node.type.isAnonymous - if (enter && beforeEnter) beforeEnter(cursor) - node.isLeaf = !cursor.firstChild() - if (enter) { - leave = true - if (onEnter(node) === false) return - } - if (!node.isLeaf) continue - for (;;) { - node = cursorNode(cursor, node.isLeaf) - if (leave && onLeave) if (onLeave(node) === false) return; - leave = cursor.type.isAnonymous - node.isLeaf = false - if (cursor.nextSibling()) break; - if (!cursor.parent()) return; - leave = true - } - } -} - -export function printTree(tree: Tree): string { - const state = { - output: '', - prefixes: [] as string[] - } - traverseTree(tree, { - onEnter: node => { - state.output += '['; - state.output += node.type.name; - }, - onLeave: () => { - state.output += ']'; - }, - }) - return state.output; -} diff --git a/rsconcept/frontend/src/utils/selectors.ts b/rsconcept/frontend/src/utils/selectors.ts index f464f032..daedce1f 100644 --- a/rsconcept/frontend/src/utils/selectors.ts +++ b/rsconcept/frontend/src/utils/selectors.ts @@ -1,13 +1,15 @@ // Module: Selector maps import { LayoutTypes } from 'reagraph'; -import { Grammeme, IGramData } from '../models/language'; +import { compareGrammemes,type GramData, Grammeme } from '../models/language'; import { CstType } from '../models/rsform'; import { ColoringScheme } from '../pages/RSFormPage/EditorTermGraph'; import { labelGrammeme } from './labels'; import { labelCstType } from './labels'; - +/** + * Represents options for GraphLayout selector. +*/ export const SelectorGraphLayout: { value: LayoutTypes, label: string }[] = [ { value: 'treeTd2d', label: 'Граф: ДеревоВ 2D' }, { value: 'treeTd3d', label: 'Граф: ДеревоВ 3D' }, @@ -24,12 +26,18 @@ export const SelectorGraphLayout: { value: LayoutTypes, label: string }[] = [ // { value: 'hierarchicalLr', label: 'hierarchicalLr'} ]; +/** + * Represents options for {@link ColoringScheme} selector. +*/ export const SelectorGraphColoring: { value: ColoringScheme, label: string }[] = [ { value: 'none', label: 'Цвет: моно' }, { value: 'status', label: 'Цвет: статус' }, { value: 'type', label: 'Цвет: класс' }, ]; +/** + * Represents options for {@link CstType} selector. +*/ export const SelectorCstType = ( Object.values(CstType)).map( typeStr => ({ @@ -38,11 +46,24 @@ export const SelectorCstType = ( }) ); -export interface IGrammemeOption extends IGramData { - value: string +/** + * Represents single option for {@link Grammeme} selector. +*/ +export interface IGrammemeOption { + value: GramData label: string } +/** + * Compares {@link IGrammemeOption} based on Grammeme comparison. + */ +export function compareGrammemeOptions(left: IGrammemeOption, right: IGrammemeOption): number { + return compareGrammemes(left.value, right.value); +} + +/** + * Represents list of {@link Grammeme}s available in reference construction. +*/ export const SelectorGrammemesList = [ Grammeme.NOUN, Grammeme.VERB, @@ -62,11 +83,12 @@ export const SelectorGrammemesList = [ Grammeme.pssv, Grammeme.actv, ]; +/** + * Represents options for {@link Grammeme} selector. +*/ export const SelectorGrammems: IGrammemeOption[] = SelectorGrammemesList.map( gram => ({ - type: gram, - data: gram as string, - value: gram as string, - label: labelGrammeme({type: gram, data: ''} as IGramData) + value: gram, + label: labelGrammeme(gram) }));