Compare commits

..

No commits in common. "2b97e109b4668676f52894042909e8e4ed9c60f8" and "9e9d8a3f08d5faa06bf16ea5439be45910eb7e66" have entirely different histories.

23 changed files with 350 additions and 1128 deletions

File diff suppressed because it is too large Load Diff

View File

@ -19,45 +19,45 @@
"axios": "^1.7.7",
"clsx": "^2.1.1",
"eslint-plugin-react-hooks": "^5.0.0",
"framer-motion": "^11.11.11",
"framer-motion": "^11.11.10",
"html-to-image": "^1.11.11",
"js-file-download": "^0.4.12",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2",
"react-icons": "^5.3.0",
"react-intl": "^6.8.7",
"react-intl": "^6.8.4",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.28.0",
"react-router-dom": "^6.27.0",
"react-select": "^5.8.2",
"react-tabs": "^6.0.2",
"react-toastify": "^10.0.6",
"react-tooltip": "^5.28.0",
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"reagraph": "^4.19.5",
"reagraph": "^4.19.4",
"use-debounce": "^10.0.4"
},
"devDependencies": {
"@lezer/generator": "^1.7.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.9.0",
"@types/node": "^22.8.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.14.0",
"eslint": "^9.13.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.12.0",
"globals": "^15.11.0",
"jest": "^29.7.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"ts-jest": "^29.2.5",
"typescript": "^5.6.3",
"typescript-eslint": "^8.13.0",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10"
},
"jest": {

View File

@ -4,15 +4,10 @@ import { HelpTopic } from '@/models/miscellaneous';
import TextURL from './TextURL';
interface TextURLProps {
/** Text to display. */
text: string;
/** Topic to link to. */
topic: HelpTopic;
}
/**
* Displays a link to a help topic.
*/
function LinkTopic({ text, topic }: TextURLProps) {
return <TextURL text={text} href={urls.help_topic(topic)} />;
}

View File

@ -1,47 +0,0 @@
'use client';
import { useMemo } from 'react';
import { toast } from 'react-toastify';
import { ReactFlowProvider } from 'reactflow';
import Modal, { ModalProps } from '@/components/ui/Modal';
import { IArgumentInfo } from '@/models/rslang';
import { TMGraph } from '@/models/TMGraph';
import { errors } from '@/utils/labels';
import MGraphFlow from './MGraphFlow';
interface DlgShowTypificationProps extends Pick<ModalProps, 'hideWindow'> {
alias: string;
resultTypification: string;
args: IArgumentInfo[];
}
function DlgShowTypification({ hideWindow, alias, resultTypification, args }: DlgShowTypificationProps) {
const graph = useMemo(() => {
const result = new TMGraph();
result.addConstituenta(alias, resultTypification, args);
return result;
}, [alias, resultTypification, args]);
if (graph.nodes.length === 0) {
toast.error(errors.typeStructureFailed);
hideWindow();
return null;
}
return (
<Modal
header='Структура типизации'
readonly
hideWindow={hideWindow}
className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]'
>
<ReactFlowProvider>
<MGraphFlow data={graph} />
</ReactFlowProvider>
</Modal>
);
}
export default DlgShowTypification;

View File

@ -1,77 +0,0 @@
'use client';
import { useLayoutEffect } from 'react';
import { Edge, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
import { TMGraph } from '@/models/TMGraph';
import { PARAMETER } from '@/utils/constants';
import { TMGraphEdgeTypes } from './graph/MGraphEdgeTypes';
import { applyLayout } from './graph/MGraphLayout';
import { TMGraphNodeTypes } from './graph/MGraphNodeTypes';
interface MGraphFlowProps {
data: TMGraph;
}
function MGraphFlow({ data }: MGraphFlowProps) {
const [nodes, setNodes] = useNodesState([]);
const [edges, setEdges] = useEdgesState([]);
const flow = useReactFlow();
useLayoutEffect(() => {
const newNodes = data.nodes.map(node => ({
id: String(node.id),
data: node,
position: { x: 0, y: 0 },
type: 'step'
}));
const newEdges: Edge[] = [];
data.nodes.forEach(node => {
const visited = new Set<number>();
const edges = new Map<number, number>();
node.parents.forEach((parent, index) => {
if (visited.has(parent)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
newEdges.at(edges.get(parent)!)!.data!.indices.push(index + 1);
} else {
newEdges.push({
id: String(newEdges.length),
data: node.parents.length > 1 ? { indices: [index + 1] } : undefined,
source: String(parent),
target: String(node.id),
type: node.parents.length > 1 ? 'cartesian' : 'boolean'
});
edges.set(parent, newEdges.length - 1);
visited.add(parent);
}
});
});
applyLayout(newNodes);
setNodes(newNodes);
setEdges(newEdges);
flow.fitView({ duration: PARAMETER.zoomDuration });
}, [data, setNodes, setEdges, flow]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
edgesFocusable={false}
nodesFocusable={false}
nodeTypes={TMGraphNodeTypes}
edgeTypes={TMGraphEdgeTypes}
fitView
maxZoom={2}
minZoom={0.5}
nodesConnectable={false}
snapToGrid={true}
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
/>
);
}
export default MGraphFlow;

View File

@ -1,13 +0,0 @@
import { StraightEdge } from 'reactflow';
import { MGraphEdgeInternal } from '@/models/miscellaneous';
function BooleanEdge(props: MGraphEdgeInternal) {
return (
<>
<StraightEdge {...props} />
</>
);
}
export default BooleanEdge;

View File

@ -1,20 +0,0 @@
import { SimpleBezierEdge } from 'reactflow';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { MGraphEdgeInternal } from '@/models/miscellaneous';
function CartesianEdge({ data, ...restProps }: MGraphEdgeInternal) {
const { colors } = useConceptOptions();
return (
<>
<SimpleBezierEdge
{...restProps}
label={data?.indices.join(', ')}
labelBgStyle={{ fill: colors.bgDefault }}
labelStyle={{ fill: colors.fgDefault }}
/>
</>
);
}
export default CartesianEdge;

View File

@ -1,9 +0,0 @@
import { EdgeTypes } from 'reactflow';
import BooleanEdge from './BooleanEdge';
import CartesianEdge from './CartesianEdge';
export const TMGraphEdgeTypes: EdgeTypes = {
boolean: BooleanEdge,
cartesian: CartesianEdge
};

View File

@ -1,171 +0,0 @@
import { Node } from 'reactflow';
import { Graph } from '@/models/Graph';
import { TMGraphNode } from '@/models/TMGraph';
export function applyLayout(nodes: Node<TMGraphNode>[]) {
new LayoutManager(nodes).execute();
}
const UNIT_HEIGHT = 100;
const UNIT_WIDTH = 100;
const MIN_NODE_DISTANCE = 80;
const LAYOUT_ITERATIONS = 8;
class LayoutManager {
nodes: Node<TMGraphNode>[];
graph = new Graph();
ranks = new Map<number, number>();
posX = new Map<number, number>();
posY = new Map<number, number>();
maxRank = 0;
virtualCount = 0;
layers: number[][] = [];
constructor(nodes: Node<TMGraphNode>[]) {
this.nodes = nodes;
this.prepareGraph();
}
/** Prepares graph for layout calculations.
*
* Assumes that nodes are already topologically sorted.
* 1. Adds nodes to graph.
* 2. Adds elementary edges to graph.
* 3. Splits non-elementary edges via virtual nodes.
*/
private prepareGraph(): void {
this.nodes.forEach(node => {
if (this.maxRank < node.data.rank) {
this.maxRank = node.data.rank;
}
const nodeID = node.data.id;
this.ranks.set(nodeID, node.data.rank);
this.graph.addNode(nodeID);
if (node.data.parents.length === 0) {
return;
}
const visited = new Set<number>();
node.data.parents.forEach(parent => {
if (!visited.has(parent)) {
visited.add(parent);
let target = nodeID;
let currentRank = node.data.rank;
const parentRank = this.ranks.get(parent)!;
while (currentRank - 1 > parentRank) {
currentRank = currentRank - 1;
this.virtualCount = this.virtualCount + 1;
this.ranks.set(-this.virtualCount, currentRank);
this.graph.addEdge(-this.virtualCount, target);
target = -this.virtualCount;
}
this.graph.addEdge(parent, target);
}
});
});
}
execute(): void {
this.calculateLayers();
this.calculatePositions();
this.savePositions();
}
private calculateLayers(): void {
this.initLayers();
// TODO: implement ordering algorithm iterations
}
private initLayers(): void {
this.layers = Array.from({ length: this.maxRank + 1 }, () => []);
const visited = new Set<number>();
const dfs = (nodeID: number) => {
if (visited.has(nodeID)) {
return;
}
visited.add(nodeID);
this.layers[this.ranks.get(nodeID)!].push(nodeID);
this.graph.at(nodeID)!.outputs.forEach(dfs);
};
const simpleNodes = this.nodes
.filter(node => node.data.rank === 0)
.sort((a, b) => a.data.text.localeCompare(b.data.text))
.map(node => node.data.id);
simpleNodes.forEach(dfs);
}
private calculatePositions(): void {
this.initPositions();
for (let i = 0; i < LAYOUT_ITERATIONS; i++) {
this.fixLayersPositions();
}
}
private fixLayersPositions(): void {
for (let rank = 1; rank <= this.maxRank; rank++) {
this.layers[rank].reverse().forEach(nodeID => {
const inputs = this.graph.at(nodeID)!.inputs;
const currentPos = this.posX.get(nodeID)!;
if (inputs.length === 1) {
const parent = inputs[0];
const parentPos = this.posX.get(parent)!;
if (currentPos === parentPos) {
return;
}
if (currentPos > parentPos) {
this.tryMoveNodeX(parent, currentPos);
} else {
this.tryMoveNodeX(nodeID, parentPos);
}
} else if (inputs.length % 2 === 1) {
const median = inputs[Math.floor(inputs.length / 2)];
const medianPos = this.posX.get(median)!;
if (currentPos === medianPos) {
return;
}
this.tryMoveNodeX(nodeID, medianPos);
} else {
const median1 = inputs[Math.floor(inputs.length / 2)];
const median2 = inputs[Math.floor(inputs.length / 2) - 1];
const medianPos = (this.posX.get(median1)! + this.posX.get(median2)!) / 2;
this.tryMoveNodeX(nodeID, medianPos);
}
});
}
}
private tryMoveNodeX(nodeID: number, targetX: number) {
const rank = this.ranks.get(nodeID)!;
if (this.layers[rank].some(id => id !== nodeID && Math.abs(targetX - this.posX.get(id)!) < MIN_NODE_DISTANCE)) {
return;
}
this.posX.set(nodeID, targetX);
}
private initPositions(): void {
this.layers.forEach((layer, rank) => {
layer.forEach((nodeID, index) => {
this.posX.set(nodeID, index * UNIT_WIDTH);
this.posY.set(nodeID, -rank * UNIT_HEIGHT);
});
});
}
private savePositions(): void {
this.nodes.forEach(node => {
const nodeID = node.data.id;
node.position = {
x: this.posX.get(nodeID)!,
y: this.posY.get(nodeID)!
};
});
}
}

View File

@ -1,27 +0,0 @@
import { Handle, Position } from 'reactflow';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { MGraphNodeInternal } from '@/models/miscellaneous';
import { colorBgTMGraphNode } from '@/styling/color';
import { globals } from '@/utils/constants';
function MGraphNode(node: MGraphNodeInternal) {
const { colors } = useConceptOptions();
return (
<>
<Handle type='source' position={Position.Top} style={{ opacity: 0 }} />
<div
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
data-tooltip-id={globals.tooltip}
data-tooltip-content={node.data.text}
style={{ backgroundColor: colorBgTMGraphNode(node.data, colors) }}
>
{node.data.rank === 0 ? node.data.text : ''}
</div>
<Handle type='target' position={Position.Bottom} style={{ opacity: 0 }} />
</>
);
}
export default MGraphNode;

View File

@ -1,7 +0,0 @@
import { NodeTypes } from 'reactflow';
import MGraphNode from './MGraphNode';
export const TMGraphNodeTypes: NodeTypes = {
step: MGraphNode
};

View File

@ -1 +0,0 @@
export { default } from './DlgShowTypification';

View File

@ -6,11 +6,8 @@
* Represents single node of a {@link Graph}, as implemented by storing outgoing and incoming connections.
*/
export class GraphNode {
/** Unique identifier of the node. */
id: number;
/** List of outgoing nodes. */
outputs: number[];
/** List of incoming nodes. */
inputs: number[];
constructor(id: number) {
@ -51,7 +48,6 @@ export class GraphNode {
* This class is optimized for TermGraph use case and not supposed to be used as generic graph implementation.
*/
export class Graph {
/** Map of nodes. */
nodes = new Map<number, GraphNode>();
constructor(arr?: number[][]) {

View File

@ -1,72 +0,0 @@
import { TMGraph } from './TMGraph';
const typificationData = [
['', ''],
['X1', 'X1'],
['Z', 'Z'],
['R1', 'R1'],
['C1', 'C1'],
['C1×X1', 'C1 X1 C1×X1'],
['X1×X1', 'X1 X1×X1'],
['X1×X1×X1', 'X1 X1×X1×X1'],
['(X1)', 'X1 (X1)'],
['(X1)', 'X1 (X1) (X1)'],
['(X1×X2)', 'X1 X2 X1×X2 (X1×X2) (X1×X2)'],
['((X1×X1)×X2)', 'X1 X1×X1 X2 (X1×X1)×X2 ((X1×X1)×X2)'],
['((X1)×(X1))', 'X1 (X1) (X1)×(X1) ((X1)×(X1))'],
[
'(((X1×(X1))×X1)×X2)',
'X1 (X1) X1×(X1) (X1×(X1))×X1 ((X1×(X1))×X1) X2 ((X1×(X1))×X1)×X2 (((X1×(X1))×X1)×X2)'
]
];
describe('Testing parsing typifications', () => {
it.each(typificationData)('Typification parsing %p', (input: string, expected: string) => {
const graph = new TMGraph();
graph.addConstituenta('X1', input, []);
const nodeText = graph.nodes.map(node => node.text).join(' ');
expect(nodeText).toBe(expected);
});
});
describe('Testing constituents parsing', () => {
test('simple expression no arguments', () => {
const graph = new TMGraph();
graph.addConstituenta('X1', '(X1)', []);
expect(graph.nodes.length).toBe(2);
expect(graph.nodes.at(-1)?.annotations).toStrictEqual(['X1']);
});
test('no expression with single argument', () => {
const graph = new TMGraph();
graph.addConstituenta('X1', '', [{ alias: 'a', typification: 'X1' }]);
const nodeText = graph.nodes.map(node => node.text).join(' ');
expect(nodeText).toBe('X1 (X1)');
expect(graph.nodes.at(-1)?.annotations).toStrictEqual(['X1']);
});
test('no expression with multiple arguments', () => {
const graph = new TMGraph();
graph.addConstituenta('X1', '', [
{ alias: 'a', typification: 'X1' },
{ alias: 'b', typification: 'R1×X1' }
]);
const nodeText = graph.nodes.map(node => node.text).join(' ');
expect(nodeText).toBe('X1 R1 R1×X1 X1×(R1×X1) (X1×(R1×X1))');
expect(graph.nodes.at(-1)?.annotations).toStrictEqual(['X1']);
});
test('expression with multiple arguments', () => {
const graph = new TMGraph();
graph.addConstituenta('X1', '(X2×Z)', [
{ alias: 'a', typification: 'X1' },
{ alias: 'b', typification: 'R1×X1' }
]);
const nodeText = graph.nodes.map(node => node.text).join(' ');
expect(nodeText).toBe('X1 R1 R1×X1 X1×(R1×X1) X2 Z X2×Z (X2×Z) (X1×(R1×X1))×(X2×Z) ((X1×(R1×X1))×(X2×Z))');
expect(graph.nodes.at(-1)?.annotations).toStrictEqual(['X1']);
});
});

View File

@ -1,215 +0,0 @@
/**
* Module: Multi-graph for typifications.
*/
import { PARAMETER } from '@/utils/constants';
import { IArgumentInfo } from './rslang';
/**
* Represents a single node of a {@link TMGraph}.
*/
export interface TMGraphNode {
id: number;
rank: number;
text: string;
parents: number[];
annotations: string[];
}
/**
* Represents a typification multi-graph.
*/
export class TMGraph {
/** List of nodes. */
nodes: TMGraphNode[] = [];
/** Map of nodes by ID. */
nodeById = new Map<number, TMGraphNode>();
/** Map of nodes by alias. */
nodeByAlias = new Map<string, TMGraphNode>();
/**
* Adds a constituent to the graph.
*
* @param alias - The alias of the constituent.
* @param result - typification of the formal definition.
* @param args - arguments for term or predicate function.
*/
addConstituenta(alias: string, result: string, args: IArgumentInfo[]): void {
const argsNode = this.processArguments(args);
const resultNode = this.processResult(result);
const combinedNode = this.combineResults(resultNode, argsNode);
if (!combinedNode) {
return;
}
this.addAliasAnnotation(combinedNode.id, alias);
}
addBaseNode(baseAlias: string): TMGraphNode {
const existingNode = this.nodes.find(node => node.text === baseAlias);
if (existingNode) {
return existingNode;
}
const node: TMGraphNode = {
id: this.nodes.length,
text: baseAlias,
rank: 0,
parents: [],
annotations: []
};
this.nodes.push(node);
this.nodeById.set(node.id, node);
return node;
}
addBooleanNode(parent: number): TMGraphNode {
const existingNode = this.nodes.find(node => node.parents.length === 1 && node.parents[0] === parent);
if (existingNode) {
return existingNode;
}
const parentNode = this.nodeById.get(parent);
if (!parentNode) {
throw new Error(`Parent node ${parent} not found`);
}
const text = parentNode.parents.length === 1 ? `${parentNode.text}` : `(${parentNode.text})`;
const node: TMGraphNode = {
id: this.nodes.length,
rank: parentNode.rank + 1,
text: text,
parents: [parent],
annotations: []
};
this.nodes.push(node);
this.nodeById.set(node.id, node);
return node;
}
addCartesianNode(parents: number[]): TMGraphNode {
const existingNode = this.nodes.find(
node => node.parents.length === parents.length && node.parents.every((p, i) => p === parents[i])
);
if (existingNode) {
return existingNode;
}
const parentNodes = parents.map(parent => this.nodeById.get(parent));
if (parentNodes.some(parent => !parent) || parents.length < 2) {
throw new Error(`Parent nodes ${parents.join(', ')} not found`);
}
const text = parentNodes.map(node => (node!.parents.length > 1 ? `(${node!.text})` : node!.text)).join('×');
const node: TMGraphNode = {
id: this.nodes.length,
text: text,
rank: Math.max(...parentNodes.map(parent => parent!.rank)) + 1,
parents: parents,
annotations: []
};
this.nodes.push(node);
this.nodeById.set(node.id, node);
return node;
}
addAliasAnnotation(node: number, alias: string): void {
const nodeToAnnotate = this.nodeById.get(node);
if (!nodeToAnnotate) {
throw new Error(`Node ${node} not found`);
}
nodeToAnnotate.annotations.push(alias);
this.nodeByAlias.set(alias, nodeToAnnotate);
}
private processArguments(args: IArgumentInfo[]): TMGraphNode | undefined {
if (args.length === 0) {
return undefined;
}
const argsNodes = args.map(argument => this.parseToNode(argument.typification));
if (args.length === 1) {
return argsNodes[0];
}
return this.addCartesianNode(argsNodes.map(node => node.id));
}
private processResult(result: string): TMGraphNode | undefined {
if (!result || result === PARAMETER.logicLabel) {
return undefined;
}
return this.parseToNode(result);
}
private combineResults(result: TMGraphNode | undefined, args: TMGraphNode | undefined): TMGraphNode | undefined {
if (!result && !args) {
return undefined;
}
if (!result) {
return this.addBooleanNode(args!.id);
}
if (!args) {
return result;
}
const argsAndResult = this.addCartesianNode([args.id, result.id]);
return this.addBooleanNode(argsAndResult.id);
}
private parseToNode(typification: string): TMGraphNode {
const tokens = this.tokenize(typification);
return this.parseTokens(tokens);
}
private tokenize(expression: string): string[] {
const tokens = [];
let currentToken = '';
for (const char of expression) {
if (['(', ')', '×', ''].includes(char)) {
if (currentToken) {
tokens.push(currentToken);
currentToken = '';
}
tokens.push(char);
} else {
currentToken += char;
}
}
if (currentToken) {
tokens.push(currentToken);
}
return tokens;
}
private parseTokens(tokens: string[], isBoolean: boolean = false): TMGraphNode {
const stack: TMGraphNode[] = [];
let isCartesian = false;
while (tokens.length > 0) {
const token = tokens.shift();
if (!token) {
throw new Error('Unexpected end of expression');
}
if (isBoolean && token === '(') {
return this.parseTokens(tokens);
}
if (token === ')') {
break;
} else if (token === '') {
const innerNode = this.parseTokens(tokens, true);
stack.push(this.addBooleanNode(innerNode.id));
} else if (token === '×') {
isCartesian = true;
} else if (token === '(') {
stack.push(this.parseTokens(tokens));
} else {
stack.push(this.addBaseNode(token));
}
}
if (isCartesian) {
return this.addCartesianNode(stack.map(node => node.id));
} else {
return stack.pop()!;
}
}
}

View File

@ -2,11 +2,10 @@
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
*/
import { EdgeProps, Node } from 'reactflow';
import { Node } from 'reactflow';
import { LibraryItemType, LocationHead } from './library';
import { IOperation } from './oss';
import { TMGraphNode } from './TMGraph';
import { UserID } from './user';
/**
@ -46,24 +45,6 @@ export interface OssNodeInternal {
yPos: number;
}
/**
* Represents graph TMGraph node internal data.
*/
export interface MGraphNodeInternal {
id: string;
data: TMGraphNode;
dragging: boolean;
xPos: number;
yPos: number;
}
/**
* Represents graph TMGraph edge internal data.
*/
export interface MGraphEdgeInternal extends EdgeProps {
data?: { indices: number[] };
}
/**
* Represents graph node coloring scheme.
*/

View File

@ -14,7 +14,7 @@ function HelpSubstitutions() {
учетом других отождествлений
</li>
<li>логические выражения могут замещать только другие логические выражения</li>
<li>при отождествлении параметризованных конституент количество и типизации аргументов должно совпадать</li>
<li>при отождествлении параметризованных конституент количество и типизации операндов должно совпадать</li>
</p>
</div>
);

View File

@ -6,7 +6,6 @@ import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
import { CProps } from '@/components/props';
import RefsInput from '@/components/RefsInput';
import Indicator from '@/components/ui/Indicator';
import Overlay from '@/components/ui/Overlay';
@ -14,11 +13,9 @@ import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useRSForm } from '@/context/RSFormContext';
import DlgShowTypification from '@/dialogs/DlgShowTypification';
import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform';
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { errors, information, labelCstTypification } from '@/utils/labels';
import { information, labelCstTypification } from '@/utils/labels';
import EditorRSExpression from '../EditorRSExpression';
import ControlsOverlay from './ControlsOverlay';
@ -58,8 +55,6 @@ function FormConstituenta({
const [expression, setExpression] = useState('');
const [convention, setConvention] = useState('');
const [typification, setTypification] = useState('N/A');
const [showTypification, setShowTypification] = useState(false);
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
const [forceComment, setForceComment] = useState(false);
@ -103,7 +98,6 @@ function FormConstituenta({
setExpression(state.definition_formal || '');
setTypification(state ? labelCstTypification(state) : 'N/A');
setForceComment(false);
setLocalParse(undefined);
}
}, [state, schema, toggleReset]);
@ -133,28 +127,8 @@ function FormConstituenta({
cstUpdate(data, () => toast.success(information.changesSaved));
}
function handleTypificationClick(event: CProps.EventMouse) {
if (!state || (localParse && !localParse.parseResult) || state.parse.status !== ParsingStatus.VERIFIED) {
toast.error(errors.typeStructureFailed);
return;
}
event.stopPropagation();
event.preventDefault();
setShowTypification(true);
}
return (
<AnimateFade className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
<AnimatePresence>
{showTypification && state ? (
<DlgShowTypification
alias={state.alias}
resultTypification={localParse ? localParse.typification : state.parse.typification}
args={localParse ? localParse.args : state.parse.args}
hideWindow={() => setShowTypification(false)}
/>
) : null}
</AnimatePresence>
{state ? (
<ControlsOverlay
disabled={disabled}
@ -187,13 +161,10 @@ function FormConstituenta({
dense
noResize
noBorder
noOutline
readOnly
disabled
label='Типизация'
title='Отобразить структуру типизации'
value={typification}
colors='clr-app clr-text-default cursor-pointer'
onClick={event => handleTypificationClick(event)}
colors='clr-app clr-text-default'
/>
) : null}
{state ? (
@ -221,7 +192,6 @@ function FormConstituenta({
toggleReset={toggleReset}
onChange={newValue => setExpression(newValue)}
setTypification={setTypification}
setLocalParse={setLocalParse}
onOpenEdit={onOpenEdit}
/>
</AnimateFade>

View File

@ -2,7 +2,7 @@
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { AnimatePresence } from 'framer-motion';
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import BadgeHelp from '@/components/info/BadgeHelp';
@ -40,7 +40,6 @@ interface EditorRSExpressionProps {
toggleReset?: boolean;
setTypification: (typification: string) => void;
setLocalParse: React.Dispatch<React.SetStateAction<IExpressionParse | undefined>>;
onChange: (newValue: string) => void;
onOpenEdit?: (cstID: ConstituentaID) => void;
}
@ -51,7 +50,6 @@ function EditorRSExpression({
value,
toggleReset,
setTypification,
setLocalParse,
onChange,
onOpenEdit,
...restProps
@ -80,7 +78,6 @@ function EditorRSExpression({
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
parser.checkConstituenta(value, activeCst, parse => {
setLocalParse(parse);
if (parse.errors.length > 0) {
onShowError(parse.errors[0], parse.prefixLen);
} else {
@ -140,6 +137,7 @@ function EditorRSExpression({
toast.error(errors.astFailed);
} else {
setSyntaxTree(parse.ast);
// TODO: return prefix from parser API instead of prefixLength
setExpression(getDefinitionPrefix(activeCst) + value);
setShowAST(true);
}

View File

@ -11,7 +11,7 @@ import { ExpressionStatus } from '@/models/rsform';
import { type IConstituenta } from '@/models/rsform';
import { inferStatus } from '@/models/rsformAPI';
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { colorStatusBar } from '@/styling/color';
import { colorBgCstStatus } from '@/styling/color';
import { globals } from '@/utils/constants';
import { labelExpressionStatus, prepareTooltip } from '@/utils/labels';
@ -48,7 +48,7 @@ function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }:
'focus-frame',
'duration-500 transition-colors'
)}
style={{ backgroundColor: processing ? colors.bgDefault : colorStatusBar(status, colors) }}
style={{ backgroundColor: processing ? colors.bgDefault : colorBgCstStatus(status, colors) }}
data-tooltip-id={globals.tooltip}
data-tooltip-html={prepareTooltip('Проверить определение', 'Ctrl + Q')}
onClick={onAnalyze}

View File

@ -6,7 +6,6 @@ import { GramData, Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '@/models
import { GraphColoring } from '@/models/miscellaneous';
import { CstClass, ExpressionStatus, IConstituenta } from '@/models/rsform';
import { ISyntaxTreeNode, TokenID } from '@/models/rslang';
import { TMGraphNode } from '@/models/TMGraph';
import { PARAMETER } from '@/utils/constants';
/**
@ -442,21 +441,6 @@ export function colorBgSyntaxTree(node: ISyntaxTreeNode, colors: IColorTheme): s
* Determines background color for {@link ExpressionStatus}.
*/
export function colorBgCstStatus(status: ExpressionStatus, colors: IColorTheme): string {
// prettier-ignore
switch (status) {
case ExpressionStatus.VERIFIED: return colors.bgGreen;
case ExpressionStatus.INCORRECT: return colors.bgRed;
case ExpressionStatus.INCALCULABLE: return colors.bgOrange;
case ExpressionStatus.PROPERTY: return colors.bgTeal;
case ExpressionStatus.UNKNOWN: return colors.bgSelected;
case ExpressionStatus.UNDEFINED: return colors.bgBlue;
}
}
/**
* Determines statusbar color for {@link ExpressionStatus}.
*/
export function colorStatusBar(status: ExpressionStatus, colors: IColorTheme): string {
// prettier-ignore
switch (status) {
case ExpressionStatus.VERIFIED: return colors.bgGreen50;
@ -564,16 +548,3 @@ export function colorBgGraphNode(cst: IConstituenta, coloringScheme: GraphColori
}
return '';
}
/**
* Determines m-graph color for {@link TMGraphNode}.
*/
export function colorBgTMGraphNode(node: TMGraphNode, colors: IColorTheme): string {
if (node.rank === 0) {
return colors.bgControls;
}
if (node.parents.length === 1) {
return colors.bgTeal;
}
return colors.bgOrange;
}

View File

@ -66,15 +66,9 @@
cursor: default;
}
.react-flow__edge {
cursor: default;
}
.react-flow__attribution {
font-size: var(--font-size-sm);
font-family: var(--font-ui);
margin-left: 3px;
margin-right: 3px;
background-color: transparent;
color: var(--cl-fg-60);
@ -83,12 +77,19 @@
}
}
[class*='react-flow__node-'] {
font-size: 14px;
.react-flow__node-input,
.react-flow__node-synthesis {
cursor: pointer;
border: 1px solid;
padding: 2px;
width: 150px;
height: 40px;
font-size: 14px;
border-radius: 5px;
background-color: var(--cl-bg-120);
color: var(--cl-fg-100);
border-color: var(--cl-bg-40);
background-color: var(--cl-bg-120);
@ -115,21 +116,3 @@
}
}
}
.react-flow__node-input,
.react-flow__node-synthesis {
cursor: pointer;
border-radius: 5px;
padding: 2px;
width: 150px;
height: 40px;
}
.react-flow__node-step {
cursor: default;
border-radius: 100%;
width: 40px;
height: 40px;
}

View File

@ -22,8 +22,6 @@ import {
} from '@/models/rslang';
import { UserLevel } from '@/models/user';
import { PARAMETER } from './constants';
/**
* Remove html tags from target string.
*/
@ -368,7 +366,7 @@ export function labelHelpTopic(topic: HelpTopic): string {
case HelpTopic.THESAURUS: return '📖 Тезаурус';
case HelpTopic.INTERFACE: return '🌀 Интерфейс';
case HelpTopic.INTERFACE: return '🦄 Интерфейс';
case HelpTopic.UI_LIBRARY: return 'Библиотека';
case HelpTopic.UI_RS_MENU: return 'Меню схемы';
case HelpTopic.UI_RS_CARD: return 'Карточка схемы';
@ -398,7 +396,7 @@ export function labelHelpTopic(topic: HelpTopic): string {
case HelpTopic.RSL_TEMPLATES: return 'Банк выражений';
case HelpTopic.TERM_CONTROL: return '🪸 Терминологизация';
case HelpTopic.ACCESS: return '🔐 Доступы';
case HelpTopic.ACCESS: return '👀 Доступы';
case HelpTopic.VERSIONS: return '🏺 Версионирование';
case HelpTopic.INFO: return '📰 Информация';
@ -533,7 +531,7 @@ export function labelTypification({
if (!isValid) {
return 'N/A';
}
if (resultType === '' || resultType === PARAMETER.logicLabel) {
if (resultType === '' || resultType === 'LOGIC') {
resultType = 'Logical';
}
if (args.length === 0) {
@ -1002,7 +1000,6 @@ export const information = {
*/
export const errors = {
astFailed: 'Невозможно построить дерево разбора',
typeStructureFailed: 'Структура отсутствует',
passwordsMismatch: 'Пароли не совпадают',
imageFailed: 'Ошибка при создании изображения',
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении',