Compare commits
7 Commits
9e9d8a3f08
...
2b97e109b4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2b97e109b4 | ||
![]() |
527785a544 | ||
![]() |
c60b4398ed | ||
![]() |
abca112b28 | ||
![]() |
26a74202a1 | ||
![]() |
c0eac11539 | ||
![]() |
12afd5997e |
652
rsconcept/frontend/package-lock.json
generated
652
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -19,45 +19,45 @@
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"framer-motion": "^11.11.10",
|
"framer-motion": "^11.11.11",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^4.1.2",
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-intl": "^6.8.4",
|
"react-intl": "^6.8.7",
|
||||||
"react-loader-spinner": "^6.1.6",
|
"react-loader-spinner": "^6.1.6",
|
||||||
"react-router-dom": "^6.27.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"react-select": "^5.8.2",
|
"react-select": "^5.8.2",
|
||||||
"react-tabs": "^6.0.2",
|
"react-tabs": "^6.0.2",
|
||||||
"react-toastify": "^10.0.6",
|
"react-toastify": "^10.0.6",
|
||||||
"react-tooltip": "^5.28.0",
|
"react-tooltip": "^5.28.0",
|
||||||
"react-zoom-pan-pinch": "^3.6.1",
|
"react-zoom-pan-pinch": "^3.6.1",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"reagraph": "^4.19.4",
|
"reagraph": "^4.19.5",
|
||||||
"use-debounce": "^10.0.4"
|
"use-debounce": "^10.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.7.1",
|
"@lezer/generator": "^1.7.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.8.1",
|
"@types/node": "^22.9.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.13.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.12.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"typescript-eslint": "^8.11.0",
|
"typescript-eslint": "^8.13.0",
|
||||||
"vite": "^5.4.10"
|
"vite": "^5.4.10"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
|
@ -4,10 +4,15 @@ import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import TextURL from './TextURL';
|
import TextURL from './TextURL';
|
||||||
|
|
||||||
interface TextURLProps {
|
interface TextURLProps {
|
||||||
|
/** Text to display. */
|
||||||
text: string;
|
text: string;
|
||||||
|
/** Topic to link to. */
|
||||||
topic: HelpTopic;
|
topic: HelpTopic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a link to a help topic.
|
||||||
|
*/
|
||||||
function LinkTopic({ text, topic }: TextURLProps) {
|
function LinkTopic({ text, topic }: TextURLProps) {
|
||||||
return <TextURL text={text} href={urls.help_topic(topic)} />;
|
return <TextURL text={text} href={urls.help_topic(topic)} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
'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;
|
|
@ -0,0 +1,77 @@
|
||||||
|
'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;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { StraightEdge } from 'reactflow';
|
||||||
|
|
||||||
|
import { MGraphEdgeInternal } from '@/models/miscellaneous';
|
||||||
|
|
||||||
|
function BooleanEdge(props: MGraphEdgeInternal) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StraightEdge {...props} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BooleanEdge;
|
|
@ -0,0 +1,20 @@
|
||||||
|
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;
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { EdgeTypes } from 'reactflow';
|
||||||
|
|
||||||
|
import BooleanEdge from './BooleanEdge';
|
||||||
|
import CartesianEdge from './CartesianEdge';
|
||||||
|
|
||||||
|
export const TMGraphEdgeTypes: EdgeTypes = {
|
||||||
|
boolean: BooleanEdge,
|
||||||
|
cartesian: CartesianEdge
|
||||||
|
};
|
|
@ -0,0 +1,171 @@
|
||||||
|
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)!
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
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;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { NodeTypes } from 'reactflow';
|
||||||
|
|
||||||
|
import MGraphNode from './MGraphNode';
|
||||||
|
|
||||||
|
export const TMGraphNodeTypes: NodeTypes = {
|
||||||
|
step: MGraphNode
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './DlgShowTypification';
|
|
@ -6,8 +6,11 @@
|
||||||
* Represents single node of a {@link Graph}, as implemented by storing outgoing and incoming connections.
|
* Represents single node of a {@link Graph}, as implemented by storing outgoing and incoming connections.
|
||||||
*/
|
*/
|
||||||
export class GraphNode {
|
export class GraphNode {
|
||||||
|
/** Unique identifier of the node. */
|
||||||
id: number;
|
id: number;
|
||||||
|
/** List of outgoing nodes. */
|
||||||
outputs: number[];
|
outputs: number[];
|
||||||
|
/** List of incoming nodes. */
|
||||||
inputs: number[];
|
inputs: number[];
|
||||||
|
|
||||||
constructor(id: number) {
|
constructor(id: number) {
|
||||||
|
@ -48,6 +51,7 @@ export class GraphNode {
|
||||||
* This class is optimized for TermGraph use case and not supposed to be used as generic graph implementation.
|
* This class is optimized for TermGraph use case and not supposed to be used as generic graph implementation.
|
||||||
*/
|
*/
|
||||||
export class Graph {
|
export class Graph {
|
||||||
|
/** Map of nodes. */
|
||||||
nodes = new Map<number, GraphNode>();
|
nodes = new Map<number, GraphNode>();
|
||||||
|
|
||||||
constructor(arr?: number[][]) {
|
constructor(arr?: number[][]) {
|
||||||
|
|
72
rsconcept/frontend/src/models/TMGraph.test.ts
Normal file
72
rsconcept/frontend/src/models/TMGraph.test.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
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']);
|
||||||
|
});
|
||||||
|
});
|
215
rsconcept/frontend/src/models/TMGraph.ts
Normal file
215
rsconcept/frontend/src/models/TMGraph.ts
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
/**
|
||||||
|
* 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()!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,10 +2,11 @@
|
||||||
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Node } from 'reactflow';
|
import { EdgeProps, Node } from 'reactflow';
|
||||||
|
|
||||||
import { LibraryItemType, LocationHead } from './library';
|
import { LibraryItemType, LocationHead } from './library';
|
||||||
import { IOperation } from './oss';
|
import { IOperation } from './oss';
|
||||||
|
import { TMGraphNode } from './TMGraph';
|
||||||
import { UserID } from './user';
|
import { UserID } from './user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,6 +46,24 @@ export interface OssNodeInternal {
|
||||||
yPos: number;
|
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.
|
* Represents graph node coloring scheme.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -14,7 +14,7 @@ function HelpSubstitutions() {
|
||||||
учетом других отождествлений
|
учетом других отождествлений
|
||||||
</li>
|
</li>
|
||||||
<li>логические выражения могут замещать только другие логические выражения</li>
|
<li>логические выражения могут замещать только другие логические выражения</li>
|
||||||
<li>при отождествлении параметризованных конституент количество и типизации операндов должно совпадать</li>
|
<li>при отождествлении параметризованных конституент количество и типизации аргументов должно совпадать</li>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
|
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
|
||||||
|
import { CProps } from '@/components/props';
|
||||||
import RefsInput from '@/components/RefsInput';
|
import RefsInput from '@/components/RefsInput';
|
||||||
import Indicator from '@/components/ui/Indicator';
|
import Indicator from '@/components/ui/Indicator';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
@ -13,9 +14,11 @@ import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
|
import DlgShowTypification from '@/dialogs/DlgShowTypification';
|
||||||
import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform';
|
import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform';
|
||||||
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
|
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
|
||||||
import { information, labelCstTypification } from '@/utils/labels';
|
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
|
||||||
|
import { errors, information, labelCstTypification } from '@/utils/labels';
|
||||||
|
|
||||||
import EditorRSExpression from '../EditorRSExpression';
|
import EditorRSExpression from '../EditorRSExpression';
|
||||||
import ControlsOverlay from './ControlsOverlay';
|
import ControlsOverlay from './ControlsOverlay';
|
||||||
|
@ -55,6 +58,8 @@ function FormConstituenta({
|
||||||
const [expression, setExpression] = useState('');
|
const [expression, setExpression] = useState('');
|
||||||
const [convention, setConvention] = useState('');
|
const [convention, setConvention] = useState('');
|
||||||
const [typification, setTypification] = useState('N/A');
|
const [typification, setTypification] = useState('N/A');
|
||||||
|
const [showTypification, setShowTypification] = useState(false);
|
||||||
|
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
|
||||||
|
|
||||||
const [forceComment, setForceComment] = useState(false);
|
const [forceComment, setForceComment] = useState(false);
|
||||||
|
|
||||||
|
@ -98,6 +103,7 @@ function FormConstituenta({
|
||||||
setExpression(state.definition_formal || '');
|
setExpression(state.definition_formal || '');
|
||||||
setTypification(state ? labelCstTypification(state) : 'N/A');
|
setTypification(state ? labelCstTypification(state) : 'N/A');
|
||||||
setForceComment(false);
|
setForceComment(false);
|
||||||
|
setLocalParse(undefined);
|
||||||
}
|
}
|
||||||
}, [state, schema, toggleReset]);
|
}, [state, schema, toggleReset]);
|
||||||
|
|
||||||
|
@ -127,8 +133,28 @@ function FormConstituenta({
|
||||||
cstUpdate(data, () => toast.success(information.changesSaved));
|
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 (
|
return (
|
||||||
<AnimateFade className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
|
<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 ? (
|
{state ? (
|
||||||
<ControlsOverlay
|
<ControlsOverlay
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -161,10 +187,13 @@ function FormConstituenta({
|
||||||
dense
|
dense
|
||||||
noResize
|
noResize
|
||||||
noBorder
|
noBorder
|
||||||
disabled
|
noOutline
|
||||||
|
readOnly
|
||||||
label='Типизация'
|
label='Типизация'
|
||||||
|
title='Отобразить структуру типизации'
|
||||||
value={typification}
|
value={typification}
|
||||||
colors='clr-app clr-text-default'
|
colors='clr-app clr-text-default cursor-pointer'
|
||||||
|
onClick={event => handleTypificationClick(event)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{state ? (
|
{state ? (
|
||||||
|
@ -192,6 +221,7 @@ function FormConstituenta({
|
||||||
toggleReset={toggleReset}
|
toggleReset={toggleReset}
|
||||||
onChange={newValue => setExpression(newValue)}
|
onChange={newValue => setExpression(newValue)}
|
||||||
setTypification={setTypification}
|
setTypification={setTypification}
|
||||||
|
setLocalParse={setLocalParse}
|
||||||
onOpenEdit={onOpenEdit}
|
onOpenEdit={onOpenEdit}
|
||||||
/>
|
/>
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
@ -40,6 +40,7 @@ interface EditorRSExpressionProps {
|
||||||
toggleReset?: boolean;
|
toggleReset?: boolean;
|
||||||
|
|
||||||
setTypification: (typification: string) => void;
|
setTypification: (typification: string) => void;
|
||||||
|
setLocalParse: React.Dispatch<React.SetStateAction<IExpressionParse | undefined>>;
|
||||||
onChange: (newValue: string) => void;
|
onChange: (newValue: string) => void;
|
||||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||||
}
|
}
|
||||||
|
@ -50,6 +51,7 @@ function EditorRSExpression({
|
||||||
value,
|
value,
|
||||||
toggleReset,
|
toggleReset,
|
||||||
setTypification,
|
setTypification,
|
||||||
|
setLocalParse,
|
||||||
onChange,
|
onChange,
|
||||||
onOpenEdit,
|
onOpenEdit,
|
||||||
...restProps
|
...restProps
|
||||||
|
@ -78,6 +80,7 @@ function EditorRSExpression({
|
||||||
|
|
||||||
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
|
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
|
||||||
parser.checkConstituenta(value, activeCst, parse => {
|
parser.checkConstituenta(value, activeCst, parse => {
|
||||||
|
setLocalParse(parse);
|
||||||
if (parse.errors.length > 0) {
|
if (parse.errors.length > 0) {
|
||||||
onShowError(parse.errors[0], parse.prefixLen);
|
onShowError(parse.errors[0], parse.prefixLen);
|
||||||
} else {
|
} else {
|
||||||
|
@ -137,7 +140,6 @@ function EditorRSExpression({
|
||||||
toast.error(errors.astFailed);
|
toast.error(errors.astFailed);
|
||||||
} else {
|
} else {
|
||||||
setSyntaxTree(parse.ast);
|
setSyntaxTree(parse.ast);
|
||||||
// TODO: return prefix from parser API instead of prefixLength
|
|
||||||
setExpression(getDefinitionPrefix(activeCst) + value);
|
setExpression(getDefinitionPrefix(activeCst) + value);
|
||||||
setShowAST(true);
|
setShowAST(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { ExpressionStatus } from '@/models/rsform';
|
||||||
import { type IConstituenta } from '@/models/rsform';
|
import { type IConstituenta } from '@/models/rsform';
|
||||||
import { inferStatus } from '@/models/rsformAPI';
|
import { inferStatus } from '@/models/rsformAPI';
|
||||||
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
|
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
|
||||||
import { colorBgCstStatus } from '@/styling/color';
|
import { colorStatusBar } from '@/styling/color';
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
import { labelExpressionStatus, prepareTooltip } from '@/utils/labels';
|
import { labelExpressionStatus, prepareTooltip } from '@/utils/labels';
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }:
|
||||||
'focus-frame',
|
'focus-frame',
|
||||||
'duration-500 transition-colors'
|
'duration-500 transition-colors'
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: processing ? colors.bgDefault : colorBgCstStatus(status, colors) }}
|
style={{ backgroundColor: processing ? colors.bgDefault : colorStatusBar(status, colors) }}
|
||||||
data-tooltip-id={globals.tooltip}
|
data-tooltip-id={globals.tooltip}
|
||||||
data-tooltip-html={prepareTooltip('Проверить определение', 'Ctrl + Q')}
|
data-tooltip-html={prepareTooltip('Проверить определение', 'Ctrl + Q')}
|
||||||
onClick={onAnalyze}
|
onClick={onAnalyze}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { GramData, Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '@/models
|
||||||
import { GraphColoring } from '@/models/miscellaneous';
|
import { GraphColoring } from '@/models/miscellaneous';
|
||||||
import { CstClass, ExpressionStatus, IConstituenta } from '@/models/rsform';
|
import { CstClass, ExpressionStatus, IConstituenta } from '@/models/rsform';
|
||||||
import { ISyntaxTreeNode, TokenID } from '@/models/rslang';
|
import { ISyntaxTreeNode, TokenID } from '@/models/rslang';
|
||||||
|
import { TMGraphNode } from '@/models/TMGraph';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -441,6 +442,21 @@ export function colorBgSyntaxTree(node: ISyntaxTreeNode, colors: IColorTheme): s
|
||||||
* Determines background color for {@link ExpressionStatus}.
|
* Determines background color for {@link ExpressionStatus}.
|
||||||
*/
|
*/
|
||||||
export function colorBgCstStatus(status: ExpressionStatus, colors: IColorTheme): string {
|
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
|
// prettier-ignore
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ExpressionStatus.VERIFIED: return colors.bgGreen50;
|
case ExpressionStatus.VERIFIED: return colors.bgGreen50;
|
||||||
|
@ -548,3 +564,16 @@ export function colorBgGraphNode(cst: IConstituenta, coloringScheme: GraphColori
|
||||||
}
|
}
|
||||||
return '';
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -66,9 +66,15 @@
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-flow__edge {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.react-flow__attribution {
|
.react-flow__attribution {
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-right: 3px;
|
||||||
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--cl-fg-60);
|
color: var(--cl-fg-60);
|
||||||
|
@ -77,19 +83,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node-input,
|
[class*='react-flow__node-'] {
|
||||||
.react-flow__node-synthesis {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
border: 1px solid;
|
|
||||||
padding: 2px;
|
|
||||||
width: 150px;
|
|
||||||
height: 40px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
border-radius: 5px;
|
border: 1px solid;
|
||||||
background-color: var(--cl-bg-120);
|
|
||||||
|
|
||||||
|
background-color: var(--cl-bg-120);
|
||||||
color: var(--cl-fg-100);
|
color: var(--cl-fg-100);
|
||||||
border-color: var(--cl-bg-40);
|
border-color: var(--cl-bg-40);
|
||||||
background-color: var(--cl-bg-120);
|
background-color: var(--cl-bg-120);
|
||||||
|
@ -116,3 +115,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import {
|
||||||
} from '@/models/rslang';
|
} from '@/models/rslang';
|
||||||
import { UserLevel } from '@/models/user';
|
import { UserLevel } from '@/models/user';
|
||||||
|
|
||||||
|
import { PARAMETER } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove html tags from target string.
|
* Remove html tags from target string.
|
||||||
*/
|
*/
|
||||||
|
@ -366,7 +368,7 @@ export function labelHelpTopic(topic: HelpTopic): string {
|
||||||
|
|
||||||
case HelpTopic.THESAURUS: return '📖 Тезаурус';
|
case HelpTopic.THESAURUS: return '📖 Тезаурус';
|
||||||
|
|
||||||
case HelpTopic.INTERFACE: return '🦄 Интерфейс';
|
case HelpTopic.INTERFACE: return '🌀 Интерфейс';
|
||||||
case HelpTopic.UI_LIBRARY: return 'Библиотека';
|
case HelpTopic.UI_LIBRARY: return 'Библиотека';
|
||||||
case HelpTopic.UI_RS_MENU: return 'Меню схемы';
|
case HelpTopic.UI_RS_MENU: return 'Меню схемы';
|
||||||
case HelpTopic.UI_RS_CARD: return 'Карточка схемы';
|
case HelpTopic.UI_RS_CARD: return 'Карточка схемы';
|
||||||
|
@ -396,7 +398,7 @@ export function labelHelpTopic(topic: HelpTopic): string {
|
||||||
case HelpTopic.RSL_TEMPLATES: return 'Банк выражений';
|
case HelpTopic.RSL_TEMPLATES: return 'Банк выражений';
|
||||||
|
|
||||||
case HelpTopic.TERM_CONTROL: return '🪸 Терминологизация';
|
case HelpTopic.TERM_CONTROL: return '🪸 Терминологизация';
|
||||||
case HelpTopic.ACCESS: return '👀 Доступы';
|
case HelpTopic.ACCESS: return '🔐 Доступы';
|
||||||
case HelpTopic.VERSIONS: return '🏺 Версионирование';
|
case HelpTopic.VERSIONS: return '🏺 Версионирование';
|
||||||
|
|
||||||
case HelpTopic.INFO: return '📰 Информация';
|
case HelpTopic.INFO: return '📰 Информация';
|
||||||
|
@ -531,7 +533,7 @@ export function labelTypification({
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
}
|
}
|
||||||
if (resultType === '' || resultType === 'LOGIC') {
|
if (resultType === '' || resultType === PARAMETER.logicLabel) {
|
||||||
resultType = 'Logical';
|
resultType = 'Logical';
|
||||||
}
|
}
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
|
@ -1000,6 +1002,7 @@ export const information = {
|
||||||
*/
|
*/
|
||||||
export const errors = {
|
export const errors = {
|
||||||
astFailed: 'Невозможно построить дерево разбора',
|
astFailed: 'Невозможно построить дерево разбора',
|
||||||
|
typeStructureFailed: 'Структура отсутствует',
|
||||||
passwordsMismatch: 'Пароли не совпадают',
|
passwordsMismatch: 'Пароли не совпадают',
|
||||||
imageFailed: 'Ошибка при создании изображения',
|
imageFailed: 'Ошибка при создании изображения',
|
||||||
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении',
|
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user