2023-07-24 22:34:03 +03:00
|
|
|
|
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
2023-07-25 20:27:29 +03:00
|
|
|
|
import { toast } from 'react-toastify';
|
|
|
|
|
|
2023-07-20 17:11:03 +03:00
|
|
|
|
import Button from '../../components/Common/Button';
|
2023-07-16 20:25:55 +03:00
|
|
|
|
import Label from '../../components/Common/Label';
|
2023-07-25 20:27:29 +03:00
|
|
|
|
import { Loader } from '../../components/Common/Loader';
|
2023-08-10 18:35:49 +03:00
|
|
|
|
import { useAuth } from '../../context/AuthContext';
|
2023-07-16 20:25:55 +03:00
|
|
|
|
import { useRSForm } from '../../context/RSFormContext';
|
2023-07-20 17:11:03 +03:00
|
|
|
|
import useCheckExpression from '../../hooks/useCheckExpression';
|
2023-07-26 23:11:00 +03:00
|
|
|
|
import { TokenID } from '../../utils/enums';
|
2023-08-02 21:35:24 +03:00
|
|
|
|
import { IConstituenta, IRSErrorDescription, SyntaxTree } from '../../utils/models';
|
2023-08-06 23:13:45 +03:00
|
|
|
|
import { getCstExpressionPrefix, getTypificationLabel } from '../../utils/staticUI';
|
2023-07-28 00:03:37 +03:00
|
|
|
|
import ParsingResult from './elements/ParsingResult';
|
2023-08-10 18:35:49 +03:00
|
|
|
|
import RSInput from './elements/RSInput';
|
2023-07-28 00:03:37 +03:00
|
|
|
|
import RSLocalButton from './elements/RSLocalButton';
|
|
|
|
|
import RSTokenButton from './elements/RSTokenButton';
|
|
|
|
|
import StatusBar from './elements/StatusBar';
|
|
|
|
|
import { getSymbolSubstitute, TextWrapper } from './elements/textEditing';
|
2023-07-16 20:25:55 +03:00
|
|
|
|
|
2023-07-28 00:03:37 +03:00
|
|
|
|
interface EditorRSExpressionProps {
|
2023-07-16 20:25:55 +03:00
|
|
|
|
id: string
|
2023-08-02 21:35:24 +03:00
|
|
|
|
activeCst?: IConstituenta
|
2023-07-16 20:25:55 +03:00
|
|
|
|
label: string
|
2023-07-20 17:11:03 +03:00
|
|
|
|
isActive: boolean
|
2023-07-16 20:25:55 +03:00
|
|
|
|
disabled?: boolean
|
|
|
|
|
placeholder?: string
|
2023-08-01 21:55:18 +03:00
|
|
|
|
onShowAST: (expression: string, ast: SyntaxTree) => void
|
2023-07-20 17:11:03 +03:00
|
|
|
|
toggleEditMode: () => void
|
|
|
|
|
setTypification: (typificaiton: string) => void
|
2023-08-10 18:35:49 +03:00
|
|
|
|
value: string
|
|
|
|
|
onChange: (newValue: string) => void
|
2023-07-21 18:44:14 +03:00
|
|
|
|
setValue: (expression: string) => void
|
2023-07-16 20:25:55 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 00:03:37 +03:00
|
|
|
|
function EditorRSExpression({
|
2023-08-02 21:35:24 +03:00
|
|
|
|
id, activeCst, label, disabled, isActive, placeholder, value, setValue, onShowAST,
|
2023-07-20 17:11:03 +03:00
|
|
|
|
toggleEditMode, setTypification, onChange
|
2023-07-28 00:03:37 +03:00
|
|
|
|
}: EditorRSExpressionProps) {
|
2023-08-10 18:35:49 +03:00
|
|
|
|
const { user } = useAuth();
|
2023-08-02 21:35:24 +03:00
|
|
|
|
const { schema } = useRSForm();
|
2023-07-20 17:11:03 +03:00
|
|
|
|
const [isModified, setIsModified] = useState(false);
|
2023-07-25 20:27:29 +03:00
|
|
|
|
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema });
|
2023-07-21 18:44:14 +03:00
|
|
|
|
const expressionCtrl = useRef<HTMLTextAreaElement>(null);
|
2023-07-20 17:11:03 +03:00
|
|
|
|
|
2023-07-24 22:34:03 +03:00
|
|
|
|
useLayoutEffect(() => {
|
2023-07-20 17:11:03 +03:00
|
|
|
|
setIsModified(false);
|
|
|
|
|
resetParse();
|
2023-07-24 22:34:03 +03:00
|
|
|
|
}, [activeCst, resetParse]);
|
2023-07-20 17:11:03 +03:00
|
|
|
|
|
2023-07-31 22:38:58 +03:00
|
|
|
|
function handleFocusIn() {
|
|
|
|
|
toggleEditMode()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
2023-08-10 18:35:49 +03:00
|
|
|
|
onChange(event.target.value);
|
2023-07-31 22:38:58 +03:00
|
|
|
|
setIsModified(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCheckExpression() {
|
2023-07-25 20:27:29 +03:00
|
|
|
|
if (!activeCst) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-07-31 22:38:58 +03:00
|
|
|
|
const prefix = getCstExpressionPrefix(activeCst);
|
2023-07-20 17:11:03 +03:00
|
|
|
|
const expression = prefix + value;
|
2023-07-26 23:11:00 +03:00
|
|
|
|
checkExpression(expression, parse => {
|
2023-07-31 22:38:58 +03:00
|
|
|
|
if (parse.errors.length > 0) {
|
2023-07-26 23:11:00 +03:00
|
|
|
|
const errorPosition = parse.errors[0].position - prefix.length
|
|
|
|
|
expressionCtrl.current!.selectionStart = errorPosition;
|
|
|
|
|
expressionCtrl.current!.selectionEnd = errorPosition;
|
|
|
|
|
}
|
2023-07-31 22:38:58 +03:00
|
|
|
|
expressionCtrl.current!.focus();
|
2023-07-20 17:11:03 +03:00
|
|
|
|
setIsModified(false);
|
2023-08-06 23:13:45 +03:00
|
|
|
|
setTypification(getTypificationLabel({
|
|
|
|
|
isValid: parse.parseResult,
|
|
|
|
|
resultType: parse.typification,
|
|
|
|
|
args: parse.args
|
|
|
|
|
}));
|
2023-07-26 23:11:00 +03:00
|
|
|
|
});
|
2023-07-31 22:38:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onShowError = useCallback(
|
|
|
|
|
(error: IRSErrorDescription) => {
|
|
|
|
|
if (!activeCst || !expressionCtrl.current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const errorPosition = error.position - getCstExpressionPrefix(activeCst).length
|
|
|
|
|
expressionCtrl.current.selectionStart = errorPosition;
|
|
|
|
|
expressionCtrl.current.selectionEnd = errorPosition;
|
|
|
|
|
expressionCtrl.current.focus();
|
|
|
|
|
}, [activeCst]);
|
2023-07-20 17:11:03 +03:00
|
|
|
|
|
2023-07-22 03:18:48 +03:00
|
|
|
|
const handleEdit = useCallback((id: TokenID, key?: string) => {
|
2023-07-21 18:44:14 +03:00
|
|
|
|
if (!expressionCtrl.current) {
|
|
|
|
|
toast.error('Нет доступа к полю редактирования формального выражения');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-07-25 20:27:29 +03:00
|
|
|
|
const text = new TextWrapper(expressionCtrl.current);
|
2023-07-22 03:18:48 +03:00
|
|
|
|
if (id === TokenID.ID_LOCAL) {
|
2023-07-25 20:27:29 +03:00
|
|
|
|
text.insertChar(key ?? 'unknown_local');
|
2023-07-22 03:18:48 +03:00
|
|
|
|
} else {
|
|
|
|
|
text.insertToken(id);
|
|
|
|
|
}
|
2023-07-21 18:44:14 +03:00
|
|
|
|
text.finalize();
|
|
|
|
|
text.focus();
|
|
|
|
|
setValue(text.value);
|
2023-07-22 03:18:48 +03:00
|
|
|
|
setIsModified(true);
|
2023-07-21 18:44:14 +03:00
|
|
|
|
}, [setValue]);
|
2023-07-31 22:38:58 +03:00
|
|
|
|
|
|
|
|
|
const handleInput = useCallback(
|
|
|
|
|
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
2023-07-25 20:27:29 +03:00
|
|
|
|
if (!expressionCtrl.current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-07-29 03:31:21 +03:00
|
|
|
|
const text = new TextWrapper(expressionCtrl.current);
|
|
|
|
|
if (event.shiftKey && event.key === '*' && !event.altKey) {
|
|
|
|
|
text.insertToken(TokenID.DECART);
|
|
|
|
|
} else if (event.altKey) {
|
|
|
|
|
if (!text.processAltKey(event.key)) {
|
|
|
|
|
return;
|
2023-07-22 03:18:48 +03:00
|
|
|
|
}
|
|
|
|
|
} else if (!event.ctrlKey) {
|
|
|
|
|
const newSymbol = getSymbolSubstitute(event.key);
|
2023-07-29 03:31:21 +03:00
|
|
|
|
if (!newSymbol) {
|
|
|
|
|
return;
|
2023-07-22 03:18:48 +03:00
|
|
|
|
}
|
2023-07-29 03:31:21 +03:00
|
|
|
|
text.replaceWith(newSymbol);
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
2023-07-22 03:18:48 +03:00
|
|
|
|
}
|
2023-07-29 03:31:21 +03:00
|
|
|
|
event.preventDefault();
|
|
|
|
|
text.finalize();
|
|
|
|
|
setValue(text.value);
|
|
|
|
|
setIsModified(true);
|
2023-07-22 03:18:48 +03:00
|
|
|
|
}, [expressionCtrl, setValue]);
|
2023-07-20 17:11:03 +03:00
|
|
|
|
|
2023-07-25 20:27:29 +03:00
|
|
|
|
const EditButtons = useMemo(() => {
|
2023-07-22 03:18:48 +03:00
|
|
|
|
return (<div className='flex items-center justify-between w-full'>
|
|
|
|
|
<div className='text-sm w-fit'>
|
|
|
|
|
<div className='flex justify-start'>
|
|
|
|
|
<RSTokenButton id={TokenID.NT_DECLARATIVE_EXPR} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.NT_IMPERATIVE_EXPR} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.NT_RECURSIVE_FULL} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.BIGPR} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.SMALLPR} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.FILTER} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.REDUCE} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.CARD} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.BOOL} onInsert={handleEdit}/>
|
2023-07-25 20:27:29 +03:00
|
|
|
|
|
2023-07-22 03:18:48 +03:00
|
|
|
|
</div>
|
|
|
|
|
<div className='flex justify-start'>
|
|
|
|
|
<RSTokenButton id={TokenID.BOOLEAN} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.PUNC_PL} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.INTERSECTION} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.LIT_EMPTYSET} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.FORALL} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.NOT} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.IN} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.SUBSET_OR_EQ} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.AND} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.IMPLICATION} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.SET_MINUS} onInsert={handleEdit}/>
|
2023-07-25 20:27:29 +03:00
|
|
|
|
<RSTokenButton id={TokenID.PUNC_ITERATE} onInsert={handleEdit}/>
|
2023-07-22 03:18:48 +03:00
|
|
|
|
<RSTokenButton id={TokenID.SUBSET} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.DEBOOL} onInsert={handleEdit}/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className='flex justify-start'>
|
|
|
|
|
<RSTokenButton id={TokenID.DECART} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.PUNC_SL} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.UNION} onInsert={handleEdit}/>
|
2023-07-25 20:27:29 +03:00
|
|
|
|
<RSTokenButton id={TokenID.LIT_INTSET} onInsert={handleEdit}/>
|
2023-07-22 03:18:48 +03:00
|
|
|
|
<RSTokenButton id={TokenID.EXISTS} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.NOTEQUAL} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.NOTIN} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.NOTSUBSET} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.OR} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.EQUIVALENT} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.SYMMINUS} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.PUNC_ASSIGN} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.EQUAL} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.GREATER_OR_EQ} onInsert={handleEdit}/>
|
|
|
|
|
<RSTokenButton id={TokenID.LESSER_OR_EQ} onInsert={handleEdit}/>
|
|
|
|
|
</div>
|
2023-07-20 17:11:03 +03:00
|
|
|
|
</div>
|
2023-07-22 03:18:48 +03:00
|
|
|
|
<div className='text-xs w-fit'>
|
|
|
|
|
<div className='flex justify-start'>
|
|
|
|
|
<RSLocalButton text='μ' tooltip='q' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='ω' tooltip='w' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='ε' tooltip='e' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='ρ' tooltip='r' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='τ' tooltip='t' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='π' tooltip='y' onInsert={handleEdit}/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className='flex justify-start'>
|
|
|
|
|
<RSLocalButton text='α' tooltip='a' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='σ' tooltip='s' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='δ' tooltip='d' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='φ' tooltip='f' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='γ' tooltip='g' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='λ' tooltip='h' onInsert={handleEdit}/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className='flex justify-start'>
|
|
|
|
|
<RSLocalButton text='ζ' tooltip='z' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='ξ' tooltip='x' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='ψ' tooltip='c' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='θ' tooltip='v' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='β' tooltip='b' onInsert={handleEdit}/>
|
|
|
|
|
<RSLocalButton text='η' tooltip='n' onInsert={handleEdit}/>
|
|
|
|
|
</div>
|
2023-07-20 17:11:03 +03:00
|
|
|
|
</div>
|
|
|
|
|
</div>);
|
2023-07-31 22:38:58 +03:00
|
|
|
|
}, [handleEdit]);
|
2023-07-25 20:27:29 +03:00
|
|
|
|
|
2023-07-16 20:25:55 +03:00
|
|
|
|
return (
|
|
|
|
|
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3 w-full'>
|
2023-08-08 23:04:21 +03:00
|
|
|
|
<div className='relative w-full'>
|
|
|
|
|
<div className='absolute top-[-0.3rem] right-0'>
|
|
|
|
|
<StatusBar
|
|
|
|
|
isModified={isModified}
|
|
|
|
|
constituenta={activeCst}
|
|
|
|
|
parseData={parseData}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2023-07-25 20:27:29 +03:00
|
|
|
|
<Label
|
2023-07-16 20:25:55 +03:00
|
|
|
|
text={label}
|
|
|
|
|
required={false}
|
|
|
|
|
htmlFor={id}
|
|
|
|
|
/>
|
2023-07-21 18:44:14 +03:00
|
|
|
|
<textarea id={id} ref={expressionCtrl}
|
2023-08-10 18:35:49 +03:00
|
|
|
|
className='w-full px-3 py-2 mt-2 leading-tight border shadow clr-input'
|
|
|
|
|
rows={6}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
onFocus={handleFocusIn}
|
|
|
|
|
onKeyDown={handleInput}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
spellCheck={false}
|
2023-07-16 20:25:55 +03:00
|
|
|
|
/>
|
2023-08-10 18:35:49 +03:00
|
|
|
|
{ user?.is_staff && <RSInput
|
|
|
|
|
value={value}
|
|
|
|
|
setValue={setValue}
|
|
|
|
|
onChange={onChange}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
/> }
|
2023-07-22 03:18:48 +03:00
|
|
|
|
<div className='flex w-full gap-4 py-1 mt-1 justify-stretch'>
|
2023-07-20 17:11:03 +03:00
|
|
|
|
<div className='flex flex-col gap-2'>
|
|
|
|
|
<Button
|
|
|
|
|
tooltip='Проверить формальное выражение'
|
|
|
|
|
text='Проверить'
|
2023-08-08 23:04:21 +03:00
|
|
|
|
widthClass='h-full w-fit'
|
|
|
|
|
colorClass='clr-btn-default'
|
2023-07-20 17:11:03 +03:00
|
|
|
|
onClick={handleCheckExpression}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{isActive && EditButtons}
|
|
|
|
|
</div>
|
2023-07-29 03:31:21 +03:00
|
|
|
|
{ (loading || parseData) &&
|
|
|
|
|
<div className='w-full overflow-y-auto border mt-2 max-h-[14rem] min-h-[7rem]'>
|
|
|
|
|
{ loading && <Loader />}
|
2023-07-31 22:38:58 +03:00
|
|
|
|
{ !loading && parseData &&
|
|
|
|
|
<ParsingResult
|
|
|
|
|
data={parseData}
|
2023-08-01 21:55:18 +03:00
|
|
|
|
onShowAST={ast => onShowAST(value, ast)}
|
2023-07-31 22:38:58 +03:00
|
|
|
|
onShowError={onShowError}
|
|
|
|
|
/>}
|
2023-07-29 03:31:21 +03:00
|
|
|
|
</div>}
|
2023-07-16 20:25:55 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 00:03:37 +03:00
|
|
|
|
export default EditorRSExpression;
|