This commit is contained in:
Ulle9 2023-08-15 16:54:50 +03:00
commit f258ab4a97
31 changed files with 777 additions and 139 deletions

View File

@ -1,3 +1,8 @@
# Docs
README.md
LICENSE
TODO.txt
# Git # Git
.git .git
.gitignore .gitignore

View File

@ -35,6 +35,7 @@ This readme file is used mostly to document project dependencies
- react-tooltip - react-tooltip
- @uiw/react-codemirror - @uiw/react-codemirror
- @uiw/codemirror-themes - @uiw/codemirror-themes
- @lezer/lr
</pre> </pre>
</details> </details>
<details> <details>
@ -45,6 +46,7 @@ This readme file is used mostly to document project dependencies
- jest - jest
- ts-jest - ts-jest
- @types/jest - @types/jest
- @lezer/generator
</pre> </pre>
</details> </details>
<details> <details>

View File

@ -0,0 +1 @@
**/parser.ts

View File

@ -8,6 +8,7 @@
"name": "frontend", "name": "frontend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@lezer/lr": "^1.3.9",
"@uiw/codemirror-themes": "^4.21.9", "@uiw/codemirror-themes": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9", "@uiw/react-codemirror": "^4.21.9",
"axios": "^1.4.0", "axios": "^1.4.0",
@ -26,6 +27,7 @@
"reagraph": "^4.11.1" "reagraph": "^4.11.1"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.4.0",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"@types/node": "^20.4.5", "@types/node": "^20.4.5",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
@ -3681,6 +3683,19 @@
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.3.tgz",
"integrity": "sha512-JH4wAXCgUOcCGNekQPLhVeUtIqjH0yPBs7vvUdSjyQama9618IOKFJwkv2kcqdhF0my8hQEgCTEJU0GIgnahvA==" "integrity": "sha512-JH4wAXCgUOcCGNekQPLhVeUtIqjH0yPBs7vvUdSjyQama9618IOKFJwkv2kcqdhF0my8hQEgCTEJU0GIgnahvA=="
}, },
"node_modules/@lezer/generator": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.4.0.tgz",
"integrity": "sha512-X/gB7vr7rEhtPQtgGxK61pMFOt60iXUBvANMq7DNO06PxrY6ZAmEEZIfSX8kfaVO/EVd9xARAsvguYDoShcMWA==",
"dev": true,
"dependencies": {
"@lezer/common": "^1.0.2",
"@lezer/lr": "^1.3.0"
},
"bin": {
"lezer-generator": "dist/lezer-generator.cjs"
}
},
"node_modules/@lezer/highlight": { "node_modules/@lezer/highlight": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz",

View File

@ -4,6 +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",
"test": "jest", "test": "jest",
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@ -11,6 +12,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@lezer/lr": "^1.3.9",
"@uiw/codemirror-themes": "^4.21.9", "@uiw/codemirror-themes": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9", "@uiw/react-codemirror": "^4.21.9",
"axios": "^1.4.0", "axios": "^1.4.0",
@ -29,6 +31,7 @@
"reagraph": "^4.11.1" "reagraph": "^4.11.1"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.4.0",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"@types/node": "^20.4.5", "@types/node": "^20.4.5",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",

Binary file not shown.

View File

@ -38,12 +38,21 @@ createTheme('customDark', {
} }
}, 'dark'); }, 'dark');
createTheme('customLight', {
divider: {
default: '#d1d5db'
},
striped: {
default: '#f0f2f7'
},
}, 'light');
function ConceptDataTable<T>({ theme, ...props }: TableProps<T>) { function ConceptDataTable<T>({ theme, ...props }: TableProps<T>) {
const { darkMode } = useConceptTheme(); const { darkMode } = useConceptTheme();
return ( return (
<DataTable<T> <DataTable<T>
theme={ theme ?? (darkMode ? 'customDark' : '')} theme={ theme ?? (darkMode ? 'customDark' : 'customLight')}
{...props} {...props}
/> />
); );

View File

@ -9,7 +9,7 @@ extends Omit<PropsWithRef<SelectProps<T>>, 'noDataLabel'> {
function ConceptSelect<T extends object | string>({ className, ...props }: ConceptSelectProps<T>) { function ConceptSelect<T extends object | string>({ className, ...props }: ConceptSelectProps<T>) {
return ( return (
<Select <Select
className={`overflow-ellipsis whitespace-nowrap ${className}`} className={`overflow-x-ellipsis whitespace-nowrap ${className}`}
{...props} {...props}
noDataLabel='Список пуст' noDataLabel='Список пуст'
/> />

View File

@ -1,9 +1,18 @@
import { ThreeDots } from 'react-loader-spinner'; import { ThreeDots } from 'react-loader-spinner';
export function Loader() { interface LoaderProps {
size?: number
}
export function Loader({size=10}: LoaderProps) {
return ( return (
<div className='flex justify-center w-full h-full'> <div className='flex justify-center w-full h-full'>
<ThreeDots color='rgb(96 165 250)' height='100' width='100' radius='10' /> <ThreeDots
color='rgb(96 165 250)'
height={size*10}
width={size*10}
radius={size}
/>
</div> </div>
); );
} }

View File

@ -0,0 +1,45 @@
import { bracketMatching, MatchResult } from '@codemirror/language';
import { Decoration, EditorView } from '@codemirror/view';
const matchingMark = Decoration.mark({class: "cc-matchingBracket"});
const nonmatchingMark = Decoration.mark({class: "cc-nonmatchingBracket"});
function bracketRender(match: MatchResult) {
const decorations = [];
const mark = match.matched ? matchingMark : nonmatchingMark;
decorations.push(mark.range(match.start.from, match.start.to));
if (match.end) {
decorations.push(mark.range(match.end.from, match.end.to));
}
return decorations;
}
const darkTheme = EditorView.baseTheme({
'.cc-matchingBracket': {
fontWeight: 600,
},
'.cc-nonmatchingBracket': {
color: '#ef4444',
fontWeight: 700,
},
'&.cm-focused .cc-matchingBracket': {
backgroundColor: '#734f00',
},
});
const lightTheme = EditorView.baseTheme({
'.cc-matchingBracket': {
fontWeight: 600,
},
'.cc-nonmatchingBracket': {
color: '#ef4444',
fontWeight: 700,
},
'&.cm-focused .cc-matchingBracket': {
backgroundColor: '#dae6f2',
},
});
export function ccBracketMatching(darkMode: boolean) {
return [bracketMatching({ renderMatch: bracketRender }), darkMode ? darkTheme : lightTheme];
}

View File

@ -1,17 +1,22 @@
import { bracketMatching } from '@codemirror/language';
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes'; import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror'; import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
import { Ref, useMemo } from 'react'; import { Ref, useMemo } from 'react';
import { useConceptTheme } from '../../../context/ThemeContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { ccBracketMatching } from './bracketMatching';
import { RSLanguage } from './rslang';
import { rshoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = { const editorSetup: BasicSetupOptions = {
highlightSpecialChars: true, highlightSpecialChars: false,
history: true, history: true,
drawSelection: true, drawSelection: false,
syntaxHighlighting: true, syntaxHighlighting: false,
defaultKeymap: true, defaultKeymap: true,
historyKeymap: true, historyKeymap: true,
@ -35,11 +40,6 @@ const editorSetup: BasicSetupOptions = {
lintKeymap: false lintKeymap: false
}; };
const editorExtensions = [
EditorView.lineWrapping,
bracketMatching()
];
interface RSInputProps interface RSInputProps
extends Omit<ReactCodeMirrorProps, 'onChange'> { extends Omit<ReactCodeMirrorProps, 'onChange'> {
innerref?: Ref<ReactCodeMirrorRef> | undefined innerref?: Ref<ReactCodeMirrorRef> | undefined
@ -49,10 +49,10 @@ extends Omit<ReactCodeMirrorProps, 'onChange'> {
function RSInput({ function RSInput({
innerref, onChange, editable, innerref, onChange, editable,
height='10rem',
...props ...props
}: RSInputProps) { }: RSInputProps) {
const { darkMode } = useConceptTheme(); const { darkMode } = useConceptTheme();
const { schema } = useRSForm();
const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]); const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]);
const lightTheme: Extension = useMemo( const lightTheme: Extension = useMemo(
@ -62,15 +62,17 @@ function RSInput({
fontFamily: 'inherit', fontFamily: 'inherit',
background: editable ? '#ffffff' : '#f0f2f7', background: editable ? '#ffffff' : '#f0f2f7',
foreground: '#000000', foreground: '#000000',
selection: '#036dd626', selection: '#aacef2',
selectionMatch: '#036dd626',
caret: '#5d00ff', caret: '#5d00ff',
}, },
styles: [ styles: [
// { tag: t.comment, color: '#787b8099' }, { tag: t.name, class: 'text-[#b266ff] cursor-default' }, // GlobalID
// { tag: t.variableName, color: '#0080ff' }, { tag: t.variableName, class: 'text-[#24821a]' }, // LocalID
// { tag: [t.string, t.special(t.brace)], color: '#5c6166' }, { tag: t.propertyName, class: '' }, // Radical
// { tag: t.definition(t.typeName), color: '#5c6166' }, { tag: t.keyword, class: 'text-[#001aff]' }, // keywords
{ tag: t.literal, class: 'text-[#001aff]' }, // literals
{ tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D
{ tag: t.unit, class: 'text-[0.75rem]' }, // indicies
] ]
}), [editable]); }), [editable]);
@ -81,27 +83,36 @@ function RSInput({
fontFamily: 'inherit', fontFamily: 'inherit',
background: editable ? '#070b12' : '#374151', background: editable ? '#070b12' : '#374151',
foreground: '#e4e4e7', foreground: '#e4e4e7',
selection: '#ffae00b0', selection: '#8c6000',
selectionMatch: '#ffae00b0',
caret: '#ffaa00' caret: '#ffaa00'
}, },
styles: [ styles: [
// { tag: t.comment, color: '#787b8099' }, { tag: t.name, class: 'text-[#dfbfff] cursor-default' }, // GlobalID
// { tag: t.variableName, color: '#0080ff' }, { tag: t.variableName, class: 'text-[#69bf60]' }, // LocalID
// { tag: [t.string, t.special(t.brace)], color: '#5c6166' }, { tag: t.propertyName, class: '' }, // Radical
// { tag: t.definition(t.typeName), color: '#5c6166' }, { tag: t.keyword, class: 'text-[#808dff]' }, // keywords
{ tag: t.literal, class: 'text-[#808dff]' }, // literals
{ tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D
{ tag: t.unit, class: 'text-[0.75rem]' }, // indicies
] ]
}), [editable]); }), [editable]);
const editorExtensions = useMemo(
() => [
EditorView.lineWrapping,
RSLanguage,
ccBracketMatching(darkMode),
rshoverTooltip(schema?.items || []),
], [darkMode, schema?.items]);
return ( return (
<div className={`w-full h-[${height}] ${cursor}`}> <div className={`w-full ${cursor} text-lg`}>
<CodeMirror <CodeMirror
ref={innerref} ref={innerref}
basicSetup={editorSetup} basicSetup={editorSetup}
extensions={editorExtensions}
height={height}
indentWithTab={false}
theme={darkMode ? darkTheme : lightTheme} theme={darkMode ? darkTheme : lightTheme}
extensions={editorExtensions}
indentWithTab={false}
onChange={value => onChange(value)} onChange={value => onChange(value)}
editable={editable} editable={editable}
{...props} {...props}

View File

@ -0,0 +1,19 @@
import {styleTags, tags} from '@lezer/highlight';
export const highlighting = styleTags({
Index: tags.unit,
ComplexIndex: tags.unit,
Literal: tags.literal,
Radical: tags.propertyName,
Function: tags.name,
Predicate: tags.name,
Global: tags.name,
Local: tags.variableName,
TextFunction: tags.keyword,
Filter: tags.keyword,
PrefixR: tags.controlKeyword,
PrefixI: tags.controlKeyword,
PrefixD: tags.controlKeyword
});

View File

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

View File

@ -0,0 +1,35 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Expression = 1,
LogicPredicate = 2,
Literal = 3,
Local = 4,
Index = 5,
Global = 6,
Radical = 7,
BinaryOperation = 8,
Enumeration = 21,
Tuple = 22,
Boolean = 23,
Declarative = 25,
PrefixD = 27,
Variable = 28,
VariableComposite = 29,
PrefixI = 30,
Imperative = 31,
ImperativeBlocks = 32,
ImperativeIteration = 33,
ImperativeAssignment = 35,
ImperativeCondition = 37,
PrefixR = 38,
Recursion = 39,
FunctionCall = 41,
Function = 42,
TextFunctionExpression = 44,
TextFunction = 45,
ComplexIndex = 46,
Filter = 47,
Predicate = 58,
QuantorVariable = 62,
LogicBinary = 63,
FunctionDeclaration = 68

View File

@ -0,0 +1,26 @@
import { printTree } from '../../../utils/print-lezer-tree';
import { parser } from './parser';
const testData = [
['a1', '[Expression[Local[Index]]]'],
['A1', '[Expression[Global]]'],
['∅', '[Expression[Literal]]'],
['Z', '[Expression[Literal]]'],
['1', '[Expression[Literal]]'],
['12=41', '[Expression[LogicPredicate[Literal][=][Literal]]]'],
['0-5', '[Expression[BinaryOperation[Literal][-][Literal]]]'],
['12+41', '[Expression[BinaryOperation[Literal][+][Literal]]]'],
['a1Z', '[Expression[BinaryOperation[Local[Index]][][Literal]]]'],
['(X1)', '[Expression[Boolean[][Boolean[][(][Global][)]]]]'],
['P2[S1]', '[Expression[PredicateCall[Predicate][[][Global][]]]]'],
['[σ∈R1×R1] F6[σ]', '[Expression[FunctionDeclaration[[][Local][∈][BinaryOperation[Radical][×][Radical]][]][FunctionCall[Function][[][Local][]]]]]'],
['D{ξ∈red(S1) | ξ=ξ}', '[Expression[Declarative[PrefixD][{][Variable[Local]][∈][TextFunctionExpression[TextFunction][(][Global][)]][LogicPredicate[Local][=][Local]][}]]]'],
];
describe('Testing RSParser', () => {
it.each(testData)('Parse %p',
(input: string, expectedTree: string) => {
const tree = parser.parse(input);
expect(printTree(tree)).toBe(expectedTree);
});
});

View File

@ -0,0 +1,22 @@
// 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: "7|O!pQPOOOOQO'#C_'#C_O!wQPO'#C`O&bQPO'#DvOVQPO'#CdO&iQPO'#CqO'yQPO'#CsO(RQPO'#CuO(WQPO'#C{O(]QPO'#DTOOQO'#D}'#D}O(bQPO'#DVO(gQQO'#DZOOQO'#DZ'#DZO(lQQO'#D]O(qQPO'#DYO(vQPO'#DYOOQO'#Dx'#DxO({QPO'#DgO&iQPO'#DiO)QQPO'#DkOOQO'#E^'#E^O)YQPO'#EcOOQO'#Ec'#EcO)kQPOOOOQO'#Dw'#DwO)TQPO'#DrQOQPOOOOQO'#Ca'#CaOOQO,58z,58zO&iQPO,59OO&iQPO,59OO&iQPO,59OO&iQPO,59OO&iQPO,59OO&iQPO,59OO&iQPO,59OO&iQPO,59OO&iQPO,58xO)yQPO'#EOO+[QPO,59OO+fQPO'#EPO+kQPO,59^O+sQPO,5:zO,pQPO'#EOO&iQPO'#CdO,}QPO,59]OOQO'#EO'#EOO-qQPO,59aO&iQPO,59_OOQO,59_,59_O)QQPO,59aO&iQPO,59gO)QQPO,59oO&iQPO,59qOOQO,59u,59uOOQO,59w,59wO&iQPO,59tO&iQPO,59tO&iQPO,5:RO%WQPO'#C^O.OQPO'#E_OOQO,5:T,5:TOOQO'#Cx'#CxO)QQPO'#CxO.ZQPO,5:VOOQO'#Dl'#DlOVQPO,5:XOVQPO,5:XOVQPO,5:XOVQPO,5:XO.cQPO'#EeOOQO'#Ed'#EdO.hQPO,5:^OOQO1G.j1G.jO0sQPO1G.jO0zQPO1G.jO3UQPO1G.jO5`QPO1G.jO5gQPO1G.jO5nQPO1G.jO6WQPO1G.jO8PQPO1G.dO&iQPO,5:kOOQO1G.x1G.xOOQO1G0f1G0fOOQO1G.w1G.wO&iQPO1G.{O8yQPO1G.yO9QQPO1G.{O9VQPO1G/RO9^QPO1G/ZO9cQPO1G/]O9kQPO1G/`O9rQPO1G/`O9zQPO1G/mO:SQPO,5:yOOQO'#ES'#ESO:XQPO,59dO:aQPO'#CyO)QQPO,5:|O:fQPO1G/qOOQO1G/s1G/sO<bQPO1G/sO<iQPO1G/sO<pQPO1G/sO&iQPO,5;PO)TQPO,5;OOVQPO1G/xO=_QPO1G0VO=oQPO7+$gOOQO7+$e7+$eO&iQPO7+$gOVQPO7+$mO&iQPO7+$uOOQO7+$w7+$wOOQO7+$z7+$zO=vQPO7+$zOOQO7+%X7+%XOVQPO'#E`OOQO1G0e1G0eOOQO1G/O1G/OO)QQPO,59eOOQO1G0h1G0hOOQO'#C`'#C`O={QPO7+%]O>iQPO1G0kOOQO1G0j1G0jOOQO7+%d7+%dOVQPO<<HRO>sQPO<<HRO>zQPO'#DxO?UQPO'#DROOQO'#ET'#ETOOQO'#C|'#C|O?jQPO<<HXO?rQPO<<HaO&iQPO<<HfOOQO1G/P1G/POOQO<<Hw<<HwO?yQPOAN=mOVQPOAN=mO&iQPO,59iO&iQPO,59kOVQPO,59hOOQOAN=sAN=sOVQPOAN={O@[QPOAN>QOOQOG23XG23XO@cQPOG23XO@tQPO1G/TOAOQPO1G/VOOQO1G/S1G/SOAYQPOG23gOAaQPOG23gOOQOG23lG23lOOQOLD(sLD(sOOQOLD)RLD)RO&iQPOLD)ROArQPO!$'LmOOQO!)9BX!)9BXO:fQPO,59OO:fQPO,59OO:fQPO,59OO:fQPO,59OO:fQPO,59OO:fQPO,59OO:fQPO,59OO:fQPO,59OOAyQPO1G.jOCfQPO1G.jOFxQPO1G.jOHvQPO1G.jOH}QPO1G.jOIUQPO1G.jOI]QPO1G.j",
stateData: "KZ~O!iOS~OUaOVaOaSOdTOhUOkVOnWOvXOzZO![bO!^cO!mPO!nPO!oPO!pQO!y[O!z[O!{]O!|]O!}]O#O]O#P^O#TdO~O{jO~PVO!mlOXSXYSXZSX[SX]SX^SX_SX`SXjSX!QSX!RSX!SSX!TSX!USX!VSX!WSX!XSX!YSX!gSX!tSXcSX!bSX!cSX!dSX!eSXbSX!uSXtSXxSX![SX!^SX#TSXrSX!xSX~OXnOYoOZpO[qO]rO^sO_tO`uOjvO!QvO!RvO!SvO!TvO!UvO!VvO!WvO!XvO!YvO~O!g!jX~P%WOUaOVaOa}OdTOhUOkVOnWOvXOzZO!mPO!nPO!oPO!pQO!y[O!z[O!{]O!|]O!}]O#O]O#P^O~Oa!ROhUO~Od!TO~Od!UO~Od!VO~O{!WO~O!O!XO~O!O!YO~Oa!ZO~O{![O~O{!]O~Oa!bO!pQO~O!b#VX!c#VX!d#VX!e#VX!g!jX~O!b!eO!c!fO!d!gO!e!hO~O!t!rX~P%WOX!lXY!lXZ!lX[!lX]!lX^!lX_!lX`!lXj!lX!Q!lX!R!lX!S!lX!T!lX!U!lX!V!lX!W!lX!X!lX!Y!lX~Ob!lO!t!lX~P*QO!t!uO~Ob!vO!t!rX~Ob!wO!b#VX!c#VX!d#VX!e#VX~OXnOYoOZpO[qO]rO^sO_tO`uO~Oc!rX!t!rXx!rX~P,UOc!xO!t!uO~OX!lXY!lXZ!lX[!lX]!lX^!lX_!lX`!lX~Oj!yOc!lX!t!lX~P-VO![bO!^cO#TdO~Oj#XO!t#WO~Oj#^O~Ox#`O!t#_O~OXnOZWi[Wi]Wi^Wi_Wi`WijWi!QWi!RWi!SWi!TWi!UWi!VWi!WWi!XWi!YWi!gWibWi!tWicWi!bWi!cWi!dWi!eWi!uWixWi![Wi!^Wi#TWi!xWi~OYWi~P.pOYoO~P.pOXnOYoOZpO`uO]Wi^Wi_WijWi!QWi!RWi!SWi!TWi!UWi!VWi!WWi!XWi!YWi!gWibWi!tWicWi!bWi!cWi!dWi!eWi!uWixWi![Wi!^Wi#TWi!xWi~O[Wi~P1ROXnOYoOZpO[qO_tO`uO^WijWi!QWi!RWi!SWi!TWi!UWi!VWi!WWi!XWi!YWi!gWibWi!tWicWi!bWi!cWi!dWi!eWi!uWixWi![Wi!^Wi#TWi!xWi~O]Wi~P3]O]rO~P3]O[qO~P1RO[Wi]Wi^Wi_Wi`Wi~OXnOYoOZpOjWi!QWi!RWi!SWi!TWi!UWi!VWi!WWi!XWi!YWi!gWibWi!tWicWi!bWi!cWi!dWi!eWi!uWixWi![Wi!^Wi#TWi!xWi~P5uO!bQi!cQi!dQi!eQi!gQibQi![Qi!^Qi#TQicQi!xQi!uQi~P,UOb#cO~P,UOj#dO~O!u#eO~P,UOt#fO~Ox#gO!t!uO~Ob#hO~P,UOx#iO!t!uO~Ox#jO!t!uO~Oa#kO~Ob#mO!t!vX~O!t#nO~OUaOVaOa}OdTOhUOkVOnWOvXOzZO!mPO!nPO!oPO!p#pO!y[O!z[O!{]O!|]O!}]O#O]O#P^O~O!b!eO!d!ai!e!ai!g!aib!aic!ai!x!ai!u!ai~O!c!ai~P;vO!c!fO~P;vO!b!eO!c!fO!d!gO!e!ai!g!aib!aic!ai!x!ai!u!ai~Ob!si!t!sic!six!si~P,UO!u#uO~P,UOa#}O~OX$gOY$hOZ$iO[$jO]$kO^$lO_$mO`$nO~P&iOx#Xi!t#Xi~P,UO!u$RO~P,UOr$SOt$TO~P*QOcuX!b#VX!c#VX!d#VX!e#VX!xuX~Oc$VO!x$UO~O!u$WO~P,UOc$YO!b#VX!c#VX!d#VX!e#VX~Ob$aO~P,UOc$bO!b#VX!c#VX!d#VX!e#VX~Ocqi!xqi~P,UOcsi!xsi~P,UOc$cO~P%WO!u$dO!b#VX!c#VX!d#VX!e#VX~Oc$fO~P,UOX$gOUWiVWiYWiZWiaWidWihWikWinWivWizWi!mWi!nWi!oWi!pWi!yWi!zWi!{Wi!|Wi!}Wi#OWi#PWi~P5uOX$gOY$hOUWiVWiZWiaWidWihWikWinWivWizWi!mWi!nWi!oWi!pWi!yWi!zWi!{Wi!|Wi!}Wi#OWi#PWi~P5uOX$gOY$hOZ$iO`$nOUWiVWi]Wi^Wi_WiaWidWihWikWinWivWizWi!mWi!nWi!oWi!pWi!yWi!zWi!{Wi!|Wi!}Wi#OWi#PWi~O[Wi~PEROX$gOY$hOZ$iO[$jO_$mO`$nOUWiVWi^WiaWidWihWikWinWivWizWi!mWi!nWi!oWi!pWi!yWi!zWi!{Wi!|Wi!}Wi#OWi#PWi~O]Wi~PGPO]$kO~PGPO[$jO~PEROX$gOY$hOZ$iOUWiVWiaWidWihWikWinWivWizWi!mWi!nWi!oWi!pWi!yWi!zWi!{Wi!|Wi!}Wi#OWi#PWi~P5uO#P!y![zUV!{!|!}#O!z!pvnkn~",
goto: "3Y#YPP#Z#n$t&[PP&_PPPPPPPPPPPP'g'g(mP'gPP)v*YP'g*]*`P*`P*`P'gP#nPP#n*dP+jPPPPPPPPP,pP,pP,p-Q-TPPPP-dPPP-g-m.YPPPP#n0t1UPP1`1cPPPPPPPP1i1{2RP2e2h3P3SjiOS!e!f!g!h#`#e#k#u$R$U$WT!_c#q#XaOSTcnopqrstuv}!R!U!W!Z![!]!e!f!g!h!u!y#X#^#`#d#e#f#k#q#u#}$R$S$T$U$W$d$g$h$i$j$k$l$m$n#QaOScnopqrstuv}!R!U!W!Z![!]!e!f!g!h!u!y#X#^#`#d#f#k#q#u#}$R$S$T$W$d$g$h$i$j$k$l$m$nQ!QT[!ad!T!V!b#W#nS!ij#_T#w#e$URmQ#SaOTcnopqrstuv!R!U!W!Z![!]!e!f!g!h!u!y#X#^#`#d#e#f#k#q#u#}$R$S$T$U$W$d$g$h$i$j$k$l$m$nTxS}#XYOSTcnopqrstuv}!R!U!W!Z![!]!e!f!g!h!u!y#X#^#`#d#e#f#k#q#u#}$R$S$T$U$W$d$g$h$i$j$k$l$m$n#WYOSTcnopqrstuv}!R!U!W!Z![!]!e!f!g!h!u!y#X#^#`#d#e#f#k#q#u#}$R$S$T$U$W$d$g$h$i$j$k$l$m$nR!SUQ!ddQ!{!TQ!}!VQ#T!bQ#o#WR$O#nR#U!bR#{#eT#y#e$U#X_OSTcnopqrstuv}!R!U!W!Z![!]!e!f!g!h!u!y#X#^#`#d#e#f#k#q#u#}$R$S$T$U$W$d$g$h$i$j$k$l$m$n#X`OSTcnopqrstuv}!R!U!W!Z![!]!e!f!g!h!u!y#X#^#`#d#e#f#k#q#u#}$R$S$T$U$W$d$g$h$i$j$k$l$m$nmeOS!_!e!f!g!h#`#e#k#u$R$U$WR!cdkiOS!e!f!g!h#`#e#k#u$R$U$WRkOQkOR#t#`SfO#`Wg!e!f!g!hS{S#kS#x#e$UQ$Q#uQ$Z$RR$`$WSRO#`QwSY|T}!W![!]f!^c!e!f!g!h#e#k#q#u$R$US!ln$gQ!moQ!npQ!oqQ!prQ!qsQ!rtQ!suQ!tvQ!z!RQ!|!UQ#P!ZQ#a!uQ#b!yQ#q#XQ#r#^Q#v#dQ#|#fQ$X#}Q$[$SQ$]$TQ$_$WQ$e$dQ$o$hQ$p$iQ$q$jQ$r$kQ$s$lQ$t$mR$u$nSyS}Q!OTQ#O!WQ#Q![R#R!]SzS}X!PT!W![!]R#V!bQ#z#eR$^$UjiOS!e!f!g!h#`#e#k#u$R$U$WR#S!_Q!`cR$P#qjgOS!e!f!g!h#`#e#k#u$R$U$WR#l#SR!ddbhOS#`#e#k#u$R$U$WQ#Y!eQ#Z!fQ#[!gR#]!hR!kjQ!jjR#s#_",
nodeNames: "⚠ Expression LogicPredicate Literal Local Index Global Radical BinaryOperation + - * \\ ∆ ∩ × ( ) } { Enumeration Tuple Boolean Declarative ∈ PrefixD Variable VariableComposite PrefixI Imperative ImperativeBlocks ImperativeIteration :∈ ImperativeAssignment := ImperativeCondition PrefixR Recursion ] FunctionCall Function [ TextFunctionExpression TextFunction ComplexIndex Filter ∉ ⊆ ⊄ ⊂ > ≥ ≤ ≠ = PredicateCall Predicate LogicNegation ¬ LogicQuantor QuantorVariable LogicBinary ⇔ ⇒ & FunctionDeclaration",
maxTerm: 101,
nodeProps: [
["closedBy", 17,")",20,"}"],
["openedBy", 18,"(",19,"{"]
],
propSources: [highlighting],
skippedNodes: [0],
repeatNodeCount: 0,
tokenData: "4X~R!iX^%ppq%pvw&exy&jyz&oz{&t{|&y|}'O}!O'T!Q!R'Y!R!['b![!]'}!]!^(b!_!`(g!`!a(l!c!d(q!e!f(q!f!g)P!h!i)X!k!l)o!r!s)t!t!u*[!u!v(q!v!w(q!z!{(q!|!}*l!}#O*q#O#P*v#P#Q*{#R#S+Q#T#U+Q#U#V+]#V#W,o#W#X.O#X#d+Q#d#e0U#e#f+Q#f#g0t#g#o+Q#o#p1u#p#q1z#q#r2P#y#z%p$f$g%p$r$s2U%o%p2Z5i6S+Q#BY#BZ%p$IS$I_%p$I|$JO%p$JT$JU%p$KV$KW%p% l% m2`%%Y%%Z2e%%[%%]2j%&Y%&Z2o%&]%&^2o%&_%&`2t%&`%&a2y%&b%&c3O%&c%&d3T%'S%'T3Y%'T%'U3_%'U%'V3d%(^%(_3i%(b%(c3n%(c%(d3s%)Q%)R3x%)S%)T3}%)U%)V4S&FU&FV%p~%uY!i~X^%ppq%p#y#z%p$f$g%p#BY#BZ%p$IS$I_%p$I|$JO%p$JT$JU%p$KV$KW%p&FU&FV%p~&jO!e~~&oOa~~&tOb~~&yOZ~~'OOX~~'TO!t~~'YOY~P'_P!mP!Q!['YR'iQ!OQ!mP|}'o!Q!['YQ'rP!R!['uQ'zP!OQ|}'o~(QQ!_!`(W%&b%&c(]~(]Ot~~(bOr~~(gO!x~~(lO!Y~~(qO!U~~(tP!Q![(w~(|PU~!Q![(w~)UPk~!Q![(w~)[Q!Q![)b#]#^)j~)gPz~!Q![)b~)oO#P~~)tOn~~)wQ!Q![)}#f#g*V~*SP![~!Q![)}~*[O!y~~*aPv~!Q![*d~*iPV~!Q![*d~*qO!o~~*vO{~~*{O]~~+QOx~~+VQ!p~#T#o+Q5i6S+Q~+bS!p~#T#c+Q#c#d+n#d#o+Q5i6S+Q~+sS!p~#T#c+Q#c#d,P#d#o+Q5i6S+Q~,US!p~#T#`+Q#`#a,b#a#o+Q5i6S+Q~,iQ!|~!p~#T#o+Q5i6S+Q~,tR!p~#T#U,}#U#o+Q5i6S+Q~-SS!p~#T#f+Q#f#g-`#g#o+Q5i6S+Q~-eS!p~#T#W+Q#W#X-q#X#o+Q5i6S+Q~-xQ!{~!p~#T#o+Q5i6S+Q~.TS!p~#T#X+Q#X#Y.a#Y#o+Q5i6S+Q~.fS!p~#T#U+Q#U#V.r#V#o+Q5i6S+Q~.wS!p~#T#c+Q#c#d/T#d#o+Q5i6S+Q~/YS!p~#T#c+Q#c#d/f#d#o+Q5i6S+Q~/kS!p~#T#`+Q#`#a/w#a#o+Q5i6S+Q~0OQ!}~!p~#T#o+Q5i6S+Q~0ZS!p~#T#f+Q#f#g0g#g#o+Q5i6S+Q~0nQ!z~!p~#T#o+Q5i6S+Q~0yS!p~#T#X+Q#X#Y1V#Y#o+Q5i6S+Q~1[S!p~#T#W+Q#W#X1h#X#o+Q5i6S+Q~1oQ#O~!p~#T#o+Q5i6S+Q~1zOd~~2PO!u~~2UOc~~2ZO!^~~2`O`~~2eOh~~2jO!c~~2oO!b~~2tO#T~~2yO!n~~3OO^~~3TOj~~3YO!Q~~3_O!d~~3dO_~~3iO[~~3nO!X~~3sO!W~~3xO!V~~3}O!T~~4SO!S~~4XO!R~",
tokenizers: [0, 1],
topRules: {"Expression":[0,1]},
tokenPrec: 1926
})

View File

@ -0,0 +1,200 @@
@precedence {
plus @left minus @left,
times @left,
not @right,
log_equiv @left,
log_impl @left,
log_or @left,
log_and @left,
set_decart @left set_union @left set_intersect @left set_minus @left set_symminus @left,
set_bool @right,
p1, p2
}
@top Expression {
term_or_logic |
FunctionDeclaration
}
@skip { space }
@tokens {
space { @whitespace+ }
ComplexIndex { $[1-9](","$[1-9])* }
integer { $[0-9]+ }
emptySet { "∅" }
integerSet { "Z" }
bigPr { "Pr" }
smallPr { "pr" }
filter { "Fi" }
card { "card" }
bool { "bool" }
debool { "debool" }
red { "red" }
quantifier { $[∀∃] }
Global { $[XCSDAT]$[0-9]+ }
Function { "F"$[0-9]+ }
Predicate { "P"$[0-9]+ }
Radical { "R"$[0-9]+ }
local { $[_a-zα-ω]($[a-zα-ω])* }
PrefixR { "R" }
PrefixI { "I" }
PrefixD { "D" }
"¬"
"⇔" "⇒" "" "&"
""
"+" "-" "*" "" "\\" "∆" "∩" "×"
"∈" "∉" "⊆" "⊄" "⊂" ">" "≥" "≤" "≠" "="
":∈" ":="
"[" "]"
"{" "}"
"(" ")"
@precedence { filter bigPr Predicate Function Global Radical PrefixR PrefixI PrefixD }
@precedence { card bool debool red smallPr local }
}
Index { integer }
Local {
!p1 local |
!p2 local Index
}
Literal { integer | emptySet | integerSet }
TextFunction {
bigPr ComplexIndex |
smallPr ComplexIndex |
card | bool | debool | red
}
Filter { filter ComplexIndex }
term_or_logic { logic | term }
FunctionDeclaration { "[" arg_declaration "]" term_or_logic }
arg_declaration {
declaration |
arg_declaration "," declaration
}
declaration { Local "∈" term }
logic {
LogicPredicate |
logicUnary |
LogicBinary
}
logic_all { logic | logic_par }
logic_par { "(" logic ")" }
logic_no_binary {
LogicPredicate
logicUnary
logic_par
}
logicUnary {
PredicateCall { Predicate "[" term_enum "]" } |
LogicNegation { !not "¬" logic_no_binary } |
LogicQuantor { quantifier QuantorVariable "∈" term logic_no_binary }
}
LogicPredicate {
term "∈" term |
term "∉" term |
term "⊆" term |
term "⊄" term |
term "⊂" term |
term ">" term |
term "≥" term |
term "≤" term |
term "≠" term |
term "=" term
}
LogicBinary {
logic_all !log_equiv "⇔" logic_all |
logic_all !log_impl "⇒" logic_all |
logic_all !log_or "" logic_all |
logic_all !log_and "&" logic_all
}
QuantorVariable { Variable | quant_var_enum }
quant_var_enum { QuantorVariable "," Variable }
Variable { Local | "(" VariableComposite ")" }
VariableComposite { var_all "," Variable }
var_all { Variable | VariableComposite }
term {
Literal | Local | Global | Radical |
BinaryOperation |
typed_constructors |
FunctionCall |
TextFunctionExpression
}
FunctionCall { Function "[" term_enum "]" }
TextFunctionExpression {
TextFunction "(" term ")" |
Filter "[" term_enum "]" "(" term ")"
}
BinaryOperation {
term !plus "+" term |
term !minus "-" term |
term !times "*" term |
term !set_union "" term |
term !set_minus "\\" term |
term !set_symminus "∆" term |
term !set_intersect "∩" term |
term !set_decart "×" term |
"(" BinaryOperation ")"
}
typed_constructors {
Enumeration |
Tuple |
Boolean |
Declarative |
Imperative |
Recursion
}
Enumeration { "{" term_enum "}"}
Tuple { "(" term_enum_min2 ")"}
Boolean {
!set_bool "" "(" term ")" |
!set_bool "" Boolean
}
term_enum { term | term_enum_min2 }
term_enum_min2 { term_enum "," term }
Declarative {
"{" Local "∈" term "|" logic "}" |
PrefixD "{" Variable "∈" term "|" logic "}"
}
Recursion {
PrefixR "{" Variable ":=" term ("|" logic)? "|" term "}"
}
Imperative {
PrefixI "{" term "|" ImperativeBlocks "}"
}
ImperativeBlocks {
imp_block |
ImperativeBlocks ";" imp_block
}
imp_block {
ImperativeIteration |
ImperativeAssignment |
ImperativeCondition
}
ImperativeIteration { Local ":∈" term }
ImperativeAssignment { Local ":=" term }
ImperativeCondition { logic }
@detectDelim
@external propSource highlighting from "./highlight.ts"

View File

@ -0,0 +1,54 @@
@top Expression { token* }
@skip { space }
@tokens {
space { @whitespace+ }
Index { $[0-9]+ }
ComplexIndex { $[1-9](","$[1-9])* }
Integer { space$[0-9]+space? }
bigPr { "Pr" }
smallPr { "pr" }
filter { "Fi" }
card { "card" }
bool { "bool" }
debool { "debool" }
red { "red" }
ConstructPrefix { "D" | "R" | "I" }
Global { $[XCSDAPTF]$[0-9]+ }
Radical { "R"$[0-9]+ }
local { $[_a-zα-ω]$[a-zα-ω]* }
"(" ")"
"[" "]"
"{" "}"
@precedence { filter bigPr Global Radical ConstructPrefix }
@precedence { card bool debool red smallPr local }
@precedence { Integer space }
}
TextFunction {
bigPr ComplexIndex |
smallPr ComplexIndex |
filter ComplexIndex |
card | bool | debool | red
}
Local { local Index? }
token {
TextFunction |
ConstructPrefix |
Integer |
Global |
Radical |
Local
}
@detectDelim
@external propSource highlighting from "./highlight.ts"

View File

@ -2,7 +2,7 @@
import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { TokenID } from '../../../utils/enums'; import { TokenID } from '../../utils/enums';
export function getSymbolSubstitute(input: string): string | undefined { export function getSymbolSubstitute(input: string): string | undefined {
switch (input) { switch (input) {
@ -68,6 +68,7 @@ export class TextWrapper {
} }
insertToken(tokenID: TokenID): boolean { insertToken(tokenID: TokenID): boolean {
const hasSelection = this.ref.view.state.selection.main.from !== this.ref.view.state.selection.main.to
switch (tokenID) { switch (tokenID) {
case TokenID.NT_DECLARATIVE_EXPR: this.envelopeWith('D{ξ∈X1 | P1[ξ]', '}'); return true; case TokenID.NT_DECLARATIVE_EXPR: this.envelopeWith('D{ξ∈X1 | P1[ξ]', '}'); return true;
case TokenID.NT_IMPERATIVE_EXPR: this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]', '}'); return true; case TokenID.NT_IMPERATIVE_EXPR: this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]', '}'); return true;
@ -84,7 +85,7 @@ export class TextWrapper {
this.envelopeWith('(', ')'); this.envelopeWith('(', ')');
this.ref.view.dispatch({ this.ref.view.dispatch({
selection: { selection: {
anchor: this.ref.view.state.selection.main.from + 1, anchor: hasSelection ? this.ref.view.state.selection.main.to: this.ref.view.state.selection.main.from + 1,
} }
}); });
return true; return true;
@ -93,15 +94,14 @@ export class TextWrapper {
this.envelopeWith('[', ']'); this.envelopeWith('[', ']');
this.ref.view.dispatch({ this.ref.view.dispatch({
selection: { selection: {
anchor: this.ref.view.state.selection.main.from + 1, anchor: hasSelection ? this.ref.view.state.selection.main.to: this.ref.view.state.selection.main.from + 1,
} }
}); });
return true; return true;
} }
case TokenID.BOOLEAN: { case TokenID.BOOLEAN: {
const selStart = this.ref.view.state.selection.main.from; const selStart = this.ref.view.state.selection.main.from;
if (selStart !== this.ref.view.state.selection.main.to && if (hasSelection && this.ref.view.state.sliceDoc(selStart, selStart + 1) === '') {
this.ref.view.state.sliceDoc(selStart, selStart + 1) === '') {
this.envelopeWith('', ''); this.envelopeWith('', '');
} else { } else {
this.envelopeWith('(', ')'); this.envelopeWith('(', ')');

View File

@ -0,0 +1,64 @@
import { Extension } from "@codemirror/state";
import { hoverTooltip } from "@codemirror/view";
import { IConstituenta } from '../../utils/models';
import { getCstTypificationLabel } from '../../utils/staticUI';
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-20 text-sm';
const alias = document.createElement('h1');
alias.className = 'text-sm text-left';
alias.textContent = `${cst.alias}: ${getCstTypificationLabel(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.text.resolved) {
const definition = document.createElement('p');
definition.innerHTML = `<b>Определение:</b> ${cst.definition.text.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

@ -19,7 +19,7 @@
} }
.cm-editor { .cm-editor {
@apply border shadow rounded clr-border px-2 @apply border shadow rounded clr-border px-1
} }
.cm-editor.cm-focused { .cm-editor.cm-focused {
@apply border shadow rounded outline-2 outline @apply border shadow rounded outline-2 outline

View File

@ -35,6 +35,7 @@ function DlgCreateCst({ hideWindow, defaultType, onCreate }: DlgCreateCstProps)
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='fixed h-fit w-[15rem] px-2'>
<ConceptSelect <ConceptSelect
className='my-4' className='my-4'
options={CstTypeSelector} options={CstTypeSelector}
@ -42,6 +43,8 @@ function DlgCreateCst({ hideWindow, defaultType, onCreate }: DlgCreateCstProps)
values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []} values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []}
onChange={data => { setSelectedType(data.length > 0 ? data[0].value : undefined); }} onChange={data => { setSelectedType(data.length > 0 ? data[0].value : undefined); }}
/> />
</div>
<div className='h-[4rem]'></div>
</Modal> </Modal>
); );
} }

View File

@ -13,6 +13,9 @@ import { getCstTypeLabel, getCstTypificationLabel, mapStatusInfo } from '../../u
import EditorRSExpression from './EditorRSExpression'; import EditorRSExpression from './EditorRSExpression';
import ViewSideConstituents from './elements/ViewSideConstituents'; import ViewSideConstituents from './elements/ViewSideConstituents';
// Max height of content for left enditor pane
const UNFOLDED_HEIGHT = '59.1rem';
interface EditorConstituentaProps { interface EditorConstituentaProps {
activeID?: number activeID?: number
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
@ -109,8 +112,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
} }
return ( return (
<div className='flex items-stretch w-full gap-2 mb-2'> <div className='flex items-stretch w-full gap-2 mb-2 justify-stretch'>
<form onSubmit={handleSubmit} className='flex-grow min-w-[50rem] max-w-min max-h-fit px-4 py-2 border'> <form onSubmit={handleSubmit} className='min-w-[50rem] max-w-min px-4 py-2 border'>
<div className='flex items-start justify-between'> <div className='flex items-start justify-between'>
<button type='submit' <button type='submit'
title='Сохранить изменения' title='Сохранить изменения'
@ -238,11 +241,14 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
/> />
</div> </div>
</form> </form>
<ViewSideConstituents <div className='self-stretch border w-full pb-1'>
expression={expression} <ViewSideConstituents
activeID={activeID} expression={expression}
onOpenEdit={onOpenEdit} baseHeight={UNFOLDED_HEIGHT}
/> activeID={activeID}
onOpenEdit={onOpenEdit}
/>
</div>
</div> </div>
); );
} }

View File

@ -4,17 +4,17 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import Label from '../../components/Common/Label'; import Label from '../../components/Common/Label';
import { Loader } from '../../components/Common/Loader'; import { Loader } from '../../components/Common/Loader';
import RSInput from '../../components/RSInput';
import { getSymbolSubstitute, TextWrapper } from '../../components/RSInput/textEditing';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression'; import useCheckExpression from '../../hooks/useCheckExpression';
import { TokenID } from '../../utils/enums'; import { TokenID } from '../../utils/enums';
import { IConstituenta, IRSErrorDescription, SyntaxTree } from '../../utils/models'; import { IConstituenta, IRSErrorDescription, SyntaxTree } from '../../utils/models';
import { getCstExpressionPrefix, getTypificationLabel } from '../../utils/staticUI'; import { getCstExpressionPrefix, getTypificationLabel } from '../../utils/staticUI';
import ParsingResult from './elements/ParsingResult'; import ParsingResult from './elements/ParsingResult';
import RSInput from './elements/RSInput';
import RSLocalButton from './elements/RSLocalButton'; import RSLocalButton from './elements/RSLocalButton';
import RSTokenButton from './elements/RSTokenButton'; import RSTokenButton from './elements/RSTokenButton';
import StatusBar from './elements/StatusBar'; import StatusBar from './elements/StatusBar';
import { getSymbolSubstitute, TextWrapper } from './elements/textEditing';
interface EditorRSExpressionProps { interface EditorRSExpressionProps {
id: string id: string
@ -227,6 +227,7 @@ function EditorRSExpression({
/> />
<RSInput innerref={rsInput} <RSInput innerref={rsInput}
className='mt-2' className='mt-2'
height='10.1rem'
value={value} value={value}
placeholder={placeholder} placeholder={placeholder}
editable={!disabled} editable={!disabled}
@ -246,9 +247,9 @@ function EditorRSExpression({
</div> </div>
{isActive && !disabled && EditButtons} {isActive && !disabled && EditButtons}
</div> </div>
{ (loading || parseData) && { (isActive || loading || parseData) &&
<div className='w-full overflow-y-auto border mt-2 max-h-[14rem] min-h-[7rem]'> <div className='w-full overflow-y-auto border mt-2 max-h-[14rem] min-h-[4.2rem]'>
{ loading && <Loader />} { loading && <Loader size={6} />}
{ !loading && parseData && { !loading && parseData &&
<ParsingResult <ParsingResult
data={parseData} data={parseData}

View File

@ -107,6 +107,7 @@ function EditorTermGraph() {
onClick={handleCenter} onClick={handleCenter}
/> />
<ConceptSelect <ConceptSelect
className='min-w-[9.5rem]'
options={GraphLayoutSelector} options={GraphLayoutSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []} values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []}

View File

@ -33,9 +33,8 @@ function ParsingResult({ data, onShowAST, onShowError }: ParsingResultProps) {
title='отобразить дерево разбора' title='отобразить дерево разбора'
onClick={handleShowAST} onClick={handleShowAST}
> >
Дерево разбора: Дерево разбора
</button> </button>
<span> {data.astText}</span>
</p>} </p>}
</div> </div>
) )

View File

@ -11,14 +11,18 @@ import ConstituentaTooltip from './ConstituentaTooltip';
import DependencyModePicker from './DependencyModePicker'; import DependencyModePicker from './DependencyModePicker';
import MatchModePicker from './MatchModePicker'; import MatchModePicker from './MatchModePicker';
// Height that should be left to accomodate navigation panel + bottom margin
const LOCAL_NAVIGATION_H = '2.6rem';
interface ViewSideConstituentsProps { interface ViewSideConstituentsProps {
expression: string expression: string
baseHeight: string
activeID?: number activeID?: number
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
} }
function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideConstituentsProps) { function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) {
const { darkMode } = useConceptTheme(); const { darkMode, noNavigation } = useConceptTheme();
const { schema } = useRSForm(); const { schema } = useRSForm();
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL); const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
@ -27,7 +31,8 @@ function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideCons
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []); const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
useEffect(() => { useEffect(
() => {
if (!schema?.items) { if (!schema?.items) {
setFilteredData([]); setFilteredData([]);
return; return;
@ -67,8 +72,8 @@ function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideCons
} }
}, [onOpenEdit]); }, [onOpenEdit]);
const conditionalRowStyles = useMemo(() => const conditionalRowStyles = useMemo(
[ () => [
{ {
when: (cst: IConstituenta) => cst.id === activeID, when: (cst: IConstituenta) => cst.id === activeID,
style: { style: {
@ -77,92 +82,101 @@ function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideCons
} }
], [activeID, darkMode]); ], [activeID, darkMode]);
const columns = useMemo(() => const columns = useMemo(
[ () => [
{ {
id: 'id', id: 'id',
selector: (cst: IConstituenta) => cst.id, selector: (cst: IConstituenta) => cst.id,
omit: true omit: true
},
{
name: 'ID',
id: 'alias',
cell: (cst: IConstituenta) => {
const info = mapStatusInfo.get(cst.status)!;
return (<>
<div
id={`${prefixes.cst_list}${cst.alias}`}
className={`w-full rounded-md text-center ${info.color}`}
>
{cst.alias}
</div>
<ConstituentaTooltip data={cst} anchor={`#${prefixes.cst_list}${cst.alias}`} />
</>);
}, },
{ width: '65px',
name: 'ID', maxWidth: '65px',
id: 'alias', conditionalCellStyles: [
cell: (cst: IConstituenta) => { {
const info = mapStatusInfo.get(cst.status)!; when: (cst: IConstituenta) => cst.id <= 0,
return (<> classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]']
<div }
id={`${prefixes.cst_list}${cst.alias}`} ]
className={`w-full rounded-md text-center ${info.color}`} },
> {
{cst.alias} name: 'Описание',
</div> id: 'description',
<ConstituentaTooltip data={cst} anchor={`#${prefixes.cst_list}${cst.alias}`} /> selector: (cst: IConstituenta) => getCstDescription(cst),
</>); minWidth: '350px',
}, wrap: true,
width: '65px', conditionalCellStyles: [
maxWidth: '65px', {
conditionalCellStyles: [ when: (cst: IConstituenta) => cst.id <= 0,
{ classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]']
when: (cst: IConstituenta) => cst.id <= 0, }
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] ]
} },
] {
}, name: 'Выражение',
{ id: 'expression',
name: 'Описание', selector: (cst: IConstituenta) => cst.definition?.formal ?? '',
id: 'description', minWidth: '200px',
selector: (cst: IConstituenta) => getCstDescription(cst), hide: 1600,
minWidth: '350px', grow: 2,
wrap: true, wrap: true,
conditionalCellStyles: [ conditionalCellStyles: [
{ {
when: (cst: IConstituenta) => cst.id <= 0, when: (cst: IConstituenta) => cst.id <= 0,
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]']
} }
] ]
}, }
{ ], []);
name: 'Выражение',
id: 'expression', const maxHeight = useMemo(
selector: (cst: IConstituenta) => cst.definition?.formal ?? '', () => {
minWidth: '200px', const siblingHeight = `${baseHeight} - ${LOCAL_NAVIGATION_H}`
hide: 1600, return (noNavigation ?
grow: 2, `calc(min(100vh - 5.2rem, ${siblingHeight}))`
wrap: true, : `calc(min(100vh - 8.7rem, ${siblingHeight}))`);
conditionalCellStyles: [ }, [noNavigation, baseHeight]);
{
when: (cst: IConstituenta) => cst.id <= 0, return (<>
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] <div className='sticky top-0 left-0 right-0 z-10 flex items-center justify-between w-full gap-1 px-2 py-1 bg-white border-b rounded clr-bg-pop clr-border'>
} <MatchModePicker
] value={filterMatch}
} onChange={setFilterMatch}
], [] />
); <input type='text'
className='w-full px-2 bg-white outline-none hover:text-clip clr-bg-pop clr-border'
return ( placeholder='наберите текст фильтра'
<div className='max-h-[calc(100vh-10.3rem)] min-h-[40rem] overflow-y-scroll border flex-grow w-full'> value={filterText}
<div className='sticky top-0 left-0 right-0 z-10 flex items-center justify-between w-full gap-1 px-2 py-1 bg-white border-b rounded clr-bg-pop clr-border'> onChange={event => setFilterText(event.target.value)}
<div className='flex items-center justify-between w-full'> />
<MatchModePicker value={filterMatch} onChange={setFilterMatch}/> <DependencyModePicker value={filterSource} onChange={setFilterSource}/>
<input type='text' </div>
className='w-full px-2 bg-white outline-none hover:text-clip clr-bg-pop clr-border' <div className='overflow-y-auto' style={{maxHeight : `${maxHeight}`}}>
placeholder='наберите текст фильтра'
value={filterText}
onChange={event => { setFilterText(event.target.value); }}
/>
<DependencyModePicker value={filterSource} onChange={setFilterSource}/>
</div>
</div>
<ConceptDataTable <ConceptDataTable
data={filteredData} data={filteredData}
columns={columns} columns={columns}
conditionalRowStyles={conditionalRowStyles}
keyField='id' keyField='id'
noContextMenu conditionalRowStyles={conditionalRowStyles}
noDataComponent={<span className='flex flex-col justify-center p-2 text-center'> noDataComponent={
<p>Список конституент пуст</p> <span className='flex flex-col justify-center p-2 text-center min-h-[5rem]'>
<p>Измените параметры фильтра</p> <p>Список конституент пуст</p>
</span>} <p>Измените параметры фильтра</p>
</span>
}
striped striped
highlightOnHover highlightOnHover
@ -173,7 +187,7 @@ function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideCons
dense dense
/> />
</div> </div>
); </>);
} }
export default ViewSideConstituents; export default ViewSideConstituents;

View File

@ -0,0 +1,60 @@
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;
}

View File

@ -6,5 +6,5 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts", "package.json"]
} }

View File

@ -1,10 +1,33 @@
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { dependencies } from './package.json'
const exclVendors = ['react', 'react-router-dom', 'react-dom']
function renderChunks(deps: Record<string, string>) {
const chunks = {}
Object.keys(deps).forEach((key) => {
if (exclVendors.includes(key)) return
chunks[key] = [key]
})
return chunks
}
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
port: 3000 port: 3000
},
build: {
chunkSizeWarningLimit: 4000, // KB
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
...renderChunks(dependencies),
},
},
},
} }
}) })

3
updateProd.sh Normal file
View File

@ -0,0 +1,3 @@
git pull
docker compose -f "docker-compose-prod.yml" up --build -d
docker image prune -f