@qoretechnologies/reqraft 0.10.53 → 0.10.54-pr.115.g9bc96ed
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +25 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
- package/README.md +1 -1
- package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
- package/design/FORM_ENGINE_FIELD_MIGRATION.md +56 -1
- package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
- package/design/SMART_EDITOR_UX.md +21 -0
- package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
- package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
- package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
- package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlTags.js +5 -1
- package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
- package/dist/components/dpqlEditor/index.d.ts +4 -0
- package/dist/components/dpqlEditor/index.d.ts.map +1 -1
- package/dist/components/dpqlEditor/index.js +5 -1
- package/dist/components/dpqlEditor/index.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +42 -2
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/dpqlEditor/types.js +0 -4
- package/dist/components/dpqlEditor/types.js.map +1 -1
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
- package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
- package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
- package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +192 -45
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +5 -2
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +160 -12
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +376 -75
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
- package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +61 -11
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +42 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +74 -7
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rendererTypes.d.ts +13 -0
- package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
- package/dist/components/form/engine/rendererTypes.js.map +1 -1
- package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
- package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
- package/dist/components/form/engine/rowMenuContext.js +103 -0
- package/dist/components/form/engine/rowMenuContext.js.map +1 -0
- package/dist/components/form/engine/typeChoices.d.ts +37 -0
- package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
- package/dist/components/form/engine/typeChoices.js +25 -0
- package/dist/components/form/engine/typeChoices.js.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.js +53 -0
- package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
- package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +202 -45
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/argumentPresence.d.ts +97 -0
- package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
- package/dist/components/form/expressions/argumentPresence.js +203 -0
- package/dist/components/form/expressions/argumentPresence.js.map +1 -0
- package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
- package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts +15 -2
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +20 -58
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
- package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
- package/dist/components/form/expressions/dpqlMockLanguage.d.ts +76 -0
- package/dist/components/form/expressions/dpqlMockLanguage.d.ts.map +1 -0
- package/dist/components/form/expressions/dpqlMockLanguage.js +525 -0
- package/dist/components/form/expressions/dpqlMockLanguage.js.map +1 -0
- package/dist/components/form/expressions/dpqlMockLsp.d.ts +31 -2
- package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
- package/dist/components/form/expressions/dpqlMockLsp.js +86 -80
- package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +8 -4
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +95 -19
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/types.d.ts +7 -0
- package/dist/components/form/expressions/types.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +50 -126
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +3 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +32 -8
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/date/Date.d.ts +1 -1
- package/dist/components/form/fields/date/Date.d.ts.map +1 -1
- package/dist/components/form/fields/date/Date.js +16 -2
- package/dist/components/form/fields/date/Date.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +21 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +140 -8
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts +31 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +528 -50
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
- package/dist/components/smartEditor/SmartEditor.js +29 -8
- package/dist/components/smartEditor/SmartEditor.js.map +1 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +8 -0
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js +53 -10
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +5 -1
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +16 -2
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/helpers/dpqlDetection.d.ts +58 -0
- package/dist/helpers/dpqlDetection.d.ts.map +1 -0
- package/dist/helpers/dpqlDetection.js +164 -0
- package/dist/helpers/dpqlDetection.js.map +1 -0
- package/dist/helpers/optionUiTypes.d.ts +3 -3
- package/dist/helpers/options.d.ts +37 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +78 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/templateItems.d.ts +47 -0
- package/dist/helpers/templateItems.d.ts.map +1 -0
- package/dist/helpers/templateItems.js +88 -0
- package/dist/helpers/templateItems.js.map +1 -0
- package/dist/helpers/templateText.d.ts +37 -0
- package/dist/helpers/templateText.d.ts.map +1 -0
- package/dist/helpers/templateText.js +64 -0
- package/dist/helpers/templateText.js.map +1 -0
- package/dist/helpers/templates.d.ts +19 -11
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +46 -29
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +56 -9
- package/dist/helpers/validations.js.map +1 -1
- package/dist/hooks/useArgSchema.d.ts +2 -1
- package/dist/hooks/useArgSchema.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.d.ts +3 -1
- package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.js +47 -9
- package/dist/hooks/useFetch/useFetch.js.map +1 -1
- package/dist/hooks/useTemplates.d.ts +20 -0
- package/dist/hooks/useTemplates.d.ts.map +1 -1
- package/dist/hooks/useTemplates.js +34 -2
- package/dist/hooks/useTemplates.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/stories/storyNetwork.d.ts +36 -0
- package/dist/stories/storyNetwork.d.ts.map +1 -0
- package/dist/stories/storyNetwork.js +82 -0
- package/dist/stories/storyNetwork.js.map +1 -0
- package/dist/utils/fetch.d.ts +151 -1
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +256 -44
- package/dist/utils/fetch.js.map +1 -1
- package/dist/utils/lspClient.d.ts +6 -0
- package/dist/utils/lspClient.d.ts.map +1 -1
- package/dist/utils/lspClient.js +62 -11
- package/dist/utils/lspClient.js.map +1 -1
- package/dist/utils/websocket.d.ts +6 -0
- package/dist/utils/websocket.d.ts.map +1 -1
- package/dist/utils/websocket.js +15 -7
- package/dist/utils/websocket.js.map +1 -1
- package/package.json +2 -2
- package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
- package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
- package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
- package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
- package/src/components/dpqlEditor/dpqlTags.ts +13 -0
- package/src/components/dpqlEditor/index.ts +4 -0
- package/src/components/dpqlEditor/types.ts +38 -2
- package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
- package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
- package/src/components/form/engine/CompactRow.tsx +240 -51
- package/src/components/form/engine/CompactToolbar.tsx +10 -3
- package/src/components/form/engine/FormEngine.stories.tsx +423 -120
- package/src/components/form/engine/FormEngine.tsx +504 -87
- package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
- package/src/components/form/engine/OptionFieldMessages.tsx +89 -11
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
- package/src/components/form/engine/compactToolbarContext.ts +1 -1
- package/src/components/form/engine/readFirst.ts +86 -7
- package/src/components/form/engine/rendererTypes.ts +12 -0
- package/src/components/form/engine/rowMenuContext.ts +159 -0
- package/src/components/form/engine/typeChoices.ts +55 -0
- package/src/components/form/expressions/DpqlRendering.tsx +85 -0
- package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
- package/src/components/form/expressions/ExpressionField.tsx +265 -54
- package/src/components/form/expressions/argumentPresence.ts +200 -0
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +20 -4
- package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
- package/src/components/form/expressions/builder/index.tsx +39 -77
- package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
- package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
- package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
- package/src/components/form/expressions/mockExpressions.ts +8 -4
- package/src/components/form/expressions/renderExpressionToText.ts +112 -22
- package/src/components/form/expressions/types.ts +7 -0
- package/src/components/form/expressions/useRenderExpression.ts +43 -127
- package/src/components/form/fields/Field.tsx +3 -2
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +27 -20
- package/src/components/form/fields/auto/AutoFormField.tsx +33 -9
- package/src/components/form/fields/date/Date.tsx +17 -1
- package/src/components/form/fields/rich-text/RichText.tsx +199 -22
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
- package/src/components/form/fields/template/TemplateField.stories.tsx +31 -9
- package/src/components/form/fields/template/TemplateField.tsx +714 -122
- package/src/components/form/index.tsx +1 -0
- package/src/components/log/Log.stories.tsx +2 -1
- package/src/components/smartEditor/SmartEditor.tsx +41 -9
- package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
- package/src/components/smartEditor/useLspSession.ts +21 -3
- package/src/helpers/dpqlDetection.ts +209 -0
- package/src/helpers/options.ts +70 -0
- package/src/helpers/templateItems.ts +81 -0
- package/src/helpers/templateText.ts +92 -0
- package/src/helpers/templates.ts +47 -37
- package/src/helpers/validations.ts +59 -9
- package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
- package/src/hooks/useFetch/useFetch.tsx +53 -7
- package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
- package/src/hooks/useTemplates.ts +41 -0
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
- package/src/index.tsx +14 -1
- package/src/stores/currentUser/currentUser.stories.tsx +2 -1
- package/src/stories/storyNetwork.ts +92 -0
- package/src/utils/fetch.ts +311 -21
- package/src/utils/lspClient.ts +68 -11
- package/src/utils/websocket.ts +20 -9
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
// Copyright 2026 Qore Technologies, s.r.o.
|
|
2
|
+
// The DPQL the story language server speaks: tokens, parse, type analysis,
|
|
3
|
+
// serialize and render, as pure functions.
|
|
4
|
+
//
|
|
5
|
+
// A mock that answers every request with a fixed shape makes stories assert
|
|
6
|
+
// its artefacts instead of the product. The one this replaced parsed ANY text
|
|
7
|
+
// into `text == ""`, so `1 + 2 ==` — which the real server rejects — previewed
|
|
8
|
+
// as `"1 + 2 ==" == ""` and a bool field reported "returns string". Each
|
|
9
|
+
// answer here follows what the Qorus `/lsp` handler actually returned for the
|
|
10
|
+
// same input, recorded in `__tests__/dpqlMockLanguage.test.ts`; only the subset
|
|
11
|
+
// the stories exercise is modelled.
|
|
12
|
+
import { mockExpressions } from './mockExpressions';
|
|
13
|
+
import { IExpressionSchema } from './types';
|
|
14
|
+
|
|
15
|
+
/** One argument or node of a DPQL expression AST, as the server shapes it. */
|
|
16
|
+
export interface IDpqlMockNode {
|
|
17
|
+
exp?: string;
|
|
18
|
+
args?: IDpqlMockNode[];
|
|
19
|
+
type?: string;
|
|
20
|
+
value?: unknown;
|
|
21
|
+
is_expression?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IDpqlMockDiagnostic {
|
|
25
|
+
severity: 'error' | 'warning';
|
|
26
|
+
message: string;
|
|
27
|
+
code: string;
|
|
28
|
+
line: number;
|
|
29
|
+
column: number;
|
|
30
|
+
end_line: number;
|
|
31
|
+
end_column: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IDpqlMockParseResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
expression?: { is_expression: true; value: IDpqlMockNode };
|
|
37
|
+
inferred_type?: string;
|
|
38
|
+
target_type?: string;
|
|
39
|
+
type_compatible?: boolean;
|
|
40
|
+
auto_coercible?: boolean;
|
|
41
|
+
coercion_may_fail?: boolean;
|
|
42
|
+
suggested_fix?: { text: string; description: string };
|
|
43
|
+
diagnostics: IDpqlMockDiagnostic[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Catalogue ───────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/* The server parses, serializes and renders by the expression catalogue — an
|
|
49
|
+
expression's symbol, type, group and `render_template` — so this reads the
|
|
50
|
+
stories' catalogue for the same answers. */
|
|
51
|
+
const CATALOGUE = new Map<string, IExpressionSchema>(
|
|
52
|
+
mockExpressions.map((info) => [info.name, info])
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/** `DET_Function`: a rendering calls it by name. */
|
|
56
|
+
const DET_FUNCTION = 2;
|
|
57
|
+
/** `DET_Operator`. */
|
|
58
|
+
const DET_OPERATOR = 1;
|
|
59
|
+
|
|
60
|
+
const WORD = /^[A-Za-z_]\w*$/;
|
|
61
|
+
|
|
62
|
+
/** An expression's DPQL spelling: its symbol without a display `()`, else its name. */
|
|
63
|
+
const symbolOf = (name: string): string =>
|
|
64
|
+
CATALOGUE.get(name)?.symbol?.replace(/\(\)$/, '') || name;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A two-argument catalogue comparison is written infix by its symbol:
|
|
68
|
+
* `"test" startsWith "t"`. With any other number of arguments it is a call.
|
|
69
|
+
* `regex` is the exception the server spells `=~ /…/`, which is not modelled.
|
|
70
|
+
*/
|
|
71
|
+
const isInfixComparison = (name: string, argCount: number): boolean => {
|
|
72
|
+
const info = CATALOGUE.get(name);
|
|
73
|
+
return (
|
|
74
|
+
argCount === 2 &&
|
|
75
|
+
info?.type === DET_OPERATOR &&
|
|
76
|
+
!!info.groups?.includes('Comparison') &&
|
|
77
|
+
name !== 'regex'
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Expression names by the word DPQL spells them with: `startsWith` → `starts-with`. */
|
|
82
|
+
const NAME_BY_SYMBOL = new Map<string, string>(
|
|
83
|
+
mockExpressions
|
|
84
|
+
.filter((info) => WORD.test(symbolOf(info.name)))
|
|
85
|
+
.map((info) => [symbolOf(info.name), info.name])
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// ─── Semantic tokens ─────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const DPQL_KEYWORDS = new Set(['in', 'not', 'between', 'and', 'like', 'true', 'false', 'null']);
|
|
91
|
+
const DPQL_FUNCTIONS = new Set([
|
|
92
|
+
'abs', 'round', 'floor', 'ceil', 'trim', 'ltrim', 'rtrim', 'concat', 'split', 'substr',
|
|
93
|
+
'coalesce', 'nullif', 'now', 'days', 'hours', 'minutes', 'seconds', 'milliseconds',
|
|
94
|
+
'microseconds', 'years', 'months', 'weeks', 'get_year', 'get_month', 'get_day', 'get_hour',
|
|
95
|
+
'get_minute', 'get_second', 'format_date', 'format_number', 'map', 'hash_map', 'contains',
|
|
96
|
+
'ignore', 'case', 'toString', 'toInt', 'toFloat', 'toBool', 'startsWith', 'endsWith',
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `textDocument/semanticTokens/full` for `text`: the LSP delta-encoded 5-tuple
|
|
101
|
+
* array, with type indices into `SEMANTIC_TOKENS_LEGEND`. It covers readable
|
|
102
|
+
* renderings as well as DPQL, as the server's does — `contains` and
|
|
103
|
+
* `(ignore case)` in an explanation are coloured too.
|
|
104
|
+
*/
|
|
105
|
+
export function mockTokenizeDpql(text: string): number[] {
|
|
106
|
+
// Longer operators must come first so `==` isn't matched as two `=`.
|
|
107
|
+
const TOKEN_RE = new RegExp(
|
|
108
|
+
[
|
|
109
|
+
`("(?:\\\\.|[^"\\\\])*")`, // 1: double-quoted string
|
|
110
|
+
`('(?:\\\\.|[^'\\\\])*')`, // 2: single-quoted string
|
|
111
|
+
`(/(?:\\\\.|[^/\\\\])*/[gimsux]*)`, // 3: regex literal /…/flags
|
|
112
|
+
`(@"(?:\\\\.|[^"\\\\])*"|@[A-Za-z_][\\w.]*)`, // 4: @field
|
|
113
|
+
`(\\$[A-Za-z_-][\\w-]*:(?:\\{[^}]*\\}|[\\w.{}]+))`, // 5: $context:value
|
|
114
|
+
`(\\b\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)`, // 6: number
|
|
115
|
+
`(==|!=|<=|>=|&&|\\|\\||=~|!~|[+\\-*/%<>!=])`, // 7: operator
|
|
116
|
+
// 8: punctuation — matched so it is not misread, but the server leaves it uncoloured
|
|
117
|
+
`(\\.\\.|[,(){}\\[\\].])`,
|
|
118
|
+
`(\\b[A-Za-z_][\\w]*\\b)`, // 9: identifier (keyword check)
|
|
119
|
+
].join('|'),
|
|
120
|
+
'g'
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const tokens: Array<{ line: number; char: number; length: number; type: number }> = [];
|
|
124
|
+
text.split('\n').forEach((line, lineIdx) => {
|
|
125
|
+
TOKEN_RE.lastIndex = 0;
|
|
126
|
+
let match: RegExpExecArray | null;
|
|
127
|
+
while ((match = TOKEN_RE.exec(line)) !== null) {
|
|
128
|
+
let type = -1;
|
|
129
|
+
if (match[1] || match[2]) type = 11; // string
|
|
130
|
+
else if (match[3]) type = 13; // regexp
|
|
131
|
+
else if (match[4]) type = 4; // variable
|
|
132
|
+
else if (match[5]) type = 2; // class (template)
|
|
133
|
+
else if (match[6]) type = 12; // number
|
|
134
|
+
else if (match[7]) type = 14; // operator
|
|
135
|
+
else if (match[9]) {
|
|
136
|
+
const word = match[9];
|
|
137
|
+
if (DPQL_KEYWORDS.has(word.toLowerCase())) type = 8; // keyword
|
|
138
|
+
else if (DPQL_FUNCTIONS.has(word) || DPQL_FUNCTIONS.has(word.toLowerCase())) type = 6; // function
|
|
139
|
+
}
|
|
140
|
+
if (type >= 0) {
|
|
141
|
+
tokens.push({ line: lineIdx, char: match.index, length: match[0].length, type });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const data: number[] = [];
|
|
147
|
+
let prevLine = 0;
|
|
148
|
+
let prevChar = 0;
|
|
149
|
+
for (const t of tokens) {
|
|
150
|
+
const deltaLine = t.line - prevLine;
|
|
151
|
+
data.push(deltaLine, deltaLine === 0 ? t.char - prevChar : t.char, t.length, t.type, 0);
|
|
152
|
+
prevLine = t.line;
|
|
153
|
+
prevChar = t.char;
|
|
154
|
+
}
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── Parse ───────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
type TToken =
|
|
161
|
+
| { kind: 'number'; text: string; start: number }
|
|
162
|
+
| { kind: 'string'; text: string; start: number }
|
|
163
|
+
| { kind: 'template'; text: string; start: number }
|
|
164
|
+
| { kind: 'word'; text: string; start: number }
|
|
165
|
+
| { kind: 'op'; text: string; start: number };
|
|
166
|
+
|
|
167
|
+
const LEX_RE =
|
|
168
|
+
/\s*(?:("(?:\\.|[^"\\])*")|(\$[A-Za-z_-][\w-]*:[\w.{}]+)|(\d+(?:\.\d+)?)|([A-Za-z_]\w*)|(==|!=|<=|>=|&&|\|\||[-+*/<>!(),]))/y;
|
|
169
|
+
|
|
170
|
+
/** Binary operators, loosest first — the precedence the server parenthesises by. */
|
|
171
|
+
const BINARY_PRECEDENCE: Record<string, number> = {
|
|
172
|
+
'||': 1,
|
|
173
|
+
'&&': 2,
|
|
174
|
+
'==': 3,
|
|
175
|
+
'!=': 3,
|
|
176
|
+
'<': 3,
|
|
177
|
+
'>': 3,
|
|
178
|
+
'<=': 3,
|
|
179
|
+
'>=': 3,
|
|
180
|
+
'+': 4,
|
|
181
|
+
'-': 4,
|
|
182
|
+
'*': 5,
|
|
183
|
+
'/': 5,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const RETURN_TYPES: Record<string, string> = {
|
|
187
|
+
toString: 'string',
|
|
188
|
+
toInt: 'int',
|
|
189
|
+
toFloat: 'float',
|
|
190
|
+
toBool: 'bool',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
class DpqlMockSyntaxError extends Error {
|
|
194
|
+
constructor(
|
|
195
|
+
message: string,
|
|
196
|
+
readonly column: number
|
|
197
|
+
) {
|
|
198
|
+
super(message);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const tokenize = (text: string): TToken[] => {
|
|
203
|
+
const tokens: TToken[] = [];
|
|
204
|
+
LEX_RE.lastIndex = 0;
|
|
205
|
+
while (LEX_RE.lastIndex < text.length) {
|
|
206
|
+
const start = LEX_RE.lastIndex;
|
|
207
|
+
const m = LEX_RE.exec(text);
|
|
208
|
+
if (!m) {
|
|
209
|
+
if (text.slice(start).trim() === '') break;
|
|
210
|
+
throw new DpqlMockSyntaxError('Unexpected character', start + 1);
|
|
211
|
+
}
|
|
212
|
+
const tokenStart = start + m[0].length - m[0].trimStart().length;
|
|
213
|
+
if (m[1]) tokens.push({ kind: 'string', text: m[1], start: tokenStart });
|
|
214
|
+
else if (m[2]) tokens.push({ kind: 'template', text: m[2], start: tokenStart });
|
|
215
|
+
else if (m[3]) tokens.push({ kind: 'number', text: m[3], start: tokenStart });
|
|
216
|
+
else if (m[4]) tokens.push({ kind: 'word', text: m[4], start: tokenStart });
|
|
217
|
+
else if (m[5]) tokens.push({ kind: 'op', text: m[5], start: tokenStart });
|
|
218
|
+
}
|
|
219
|
+
return tokens;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const templateArg = (raw: string): IDpqlMockNode => {
|
|
223
|
+
const [, context, value] = raw.match(/^\$([^:]+):(.*)$/) ?? [];
|
|
224
|
+
return { type: 'auto', value: { tmpl_context: context, tmpl_value: value, raw } };
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** An operand: a bare argument, or a nested expression wrapped as the server wraps it. */
|
|
228
|
+
const asOperand = (node: IDpqlMockNode): IDpqlMockNode =>
|
|
229
|
+
node.exp ? { is_expression: true, value: node } : node;
|
|
230
|
+
|
|
231
|
+
const parseTokens = (tokens: TToken[], endColumn: number): IDpqlMockNode => {
|
|
232
|
+
let pos = 0;
|
|
233
|
+
const peek = () => tokens[pos];
|
|
234
|
+
const fail = (): never => {
|
|
235
|
+
const token = peek();
|
|
236
|
+
throw new DpqlMockSyntaxError(
|
|
237
|
+
token ? `Unexpected token '${token.text}'` : 'Unexpected end of input, expected value',
|
|
238
|
+
token ? token.start + 1 : endColumn
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const primary = (): IDpqlMockNode => {
|
|
243
|
+
const token = peek();
|
|
244
|
+
if (!token) return fail();
|
|
245
|
+
pos++;
|
|
246
|
+
switch (token.kind) {
|
|
247
|
+
case 'number':
|
|
248
|
+
return token.text.includes('.')
|
|
249
|
+
? { type: 'float', value: parseFloat(token.text) }
|
|
250
|
+
: { type: 'int', value: parseInt(token.text, 10) };
|
|
251
|
+
case 'string':
|
|
252
|
+
return { type: 'string', value: JSON.parse(token.text) };
|
|
253
|
+
case 'template':
|
|
254
|
+
return templateArg(token.text);
|
|
255
|
+
case 'word': {
|
|
256
|
+
if (token.text === 'true' || token.text === 'false') {
|
|
257
|
+
return { type: 'bool', value: token.text === 'true' };
|
|
258
|
+
}
|
|
259
|
+
if (token.text === 'null') {
|
|
260
|
+
return { type: 'any', value: null };
|
|
261
|
+
}
|
|
262
|
+
if (peek()?.text === '(') {
|
|
263
|
+
pos++;
|
|
264
|
+
const args: IDpqlMockNode[] = [];
|
|
265
|
+
while (peek() && peek()!.text !== ')') {
|
|
266
|
+
args.push(asOperand(binary(1)));
|
|
267
|
+
if (peek()?.text === ',') {
|
|
268
|
+
pos++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (peek()?.text !== ')') return fail();
|
|
272
|
+
pos++;
|
|
273
|
+
return { exp: NAME_BY_SYMBOL.get(token.text) ?? token.text, args };
|
|
274
|
+
}
|
|
275
|
+
return { type: 'string', value: token.text };
|
|
276
|
+
}
|
|
277
|
+
case 'op':
|
|
278
|
+
if (token.text === '(') {
|
|
279
|
+
const inner = binary(1);
|
|
280
|
+
if (peek()?.text !== ')') return fail();
|
|
281
|
+
pos++;
|
|
282
|
+
return inner;
|
|
283
|
+
}
|
|
284
|
+
if (token.text === '!') {
|
|
285
|
+
return { exp: '!', args: [asOperand(primary())] };
|
|
286
|
+
}
|
|
287
|
+
pos--;
|
|
288
|
+
return fail();
|
|
289
|
+
}
|
|
290
|
+
return fail();
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const binary = (minPrecedence: number): IDpqlMockNode => {
|
|
294
|
+
let left = primary();
|
|
295
|
+
for (;;) {
|
|
296
|
+
const op = peek();
|
|
297
|
+
// A word comparison (`"a" contains "b"`) binds as the symbol comparisons do.
|
|
298
|
+
const wordComparison =
|
|
299
|
+
op?.kind === 'word' && NAME_BY_SYMBOL.has(op.text) &&
|
|
300
|
+
isInfixComparison(NAME_BY_SYMBOL.get(op.text)!, 2)
|
|
301
|
+
? NAME_BY_SYMBOL.get(op.text)
|
|
302
|
+
: undefined;
|
|
303
|
+
const precedence =
|
|
304
|
+
op?.kind === 'op' ? BINARY_PRECEDENCE[op.text]
|
|
305
|
+
: wordComparison ? BINARY_PRECEDENCE['==']
|
|
306
|
+
: undefined;
|
|
307
|
+
if (precedence === undefined || precedence < minPrecedence) {
|
|
308
|
+
return left;
|
|
309
|
+
}
|
|
310
|
+
pos++;
|
|
311
|
+
const right = binary(precedence + 1);
|
|
312
|
+
left = { exp: wordComparison ?? op!.text, args: [asOperand(left), asOperand(right)] };
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const root = binary(1);
|
|
317
|
+
if (pos < tokens.length) fail();
|
|
318
|
+
return root;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const nodeOf = (operand: IDpqlMockNode): IDpqlMockNode =>
|
|
322
|
+
operand.is_expression ? (operand.value as IDpqlMockNode) : operand;
|
|
323
|
+
|
|
324
|
+
/** The type an expression returns, as far as the server can tell before it runs. */
|
|
325
|
+
export const inferDpqlMockType = (node: IDpqlMockNode): string => {
|
|
326
|
+
if (!node.exp) {
|
|
327
|
+
return node.type === 'auto' ? 'auto' : (node.type ?? 'auto');
|
|
328
|
+
}
|
|
329
|
+
if (node.exp === 'value') return 'auto';
|
|
330
|
+
if (RETURN_TYPES[node.exp]) return RETURN_TYPES[node.exp];
|
|
331
|
+
if (['==', '!=', '<', '>', '<=', '>=', '&&', '||', '!'].includes(node.exp)) return 'bool';
|
|
332
|
+
if (['+', '-', '*', '/'].includes(node.exp)) {
|
|
333
|
+
const operandTypes = (node.args ?? []).map((arg) => inferDpqlMockType(nodeOf(arg)));
|
|
334
|
+
if (operandTypes.includes('auto')) return 'auto';
|
|
335
|
+
if (node.exp === '+' && operandTypes.includes('string')) return 'string';
|
|
336
|
+
return operandTypes.includes('float') ? 'float' : 'int';
|
|
337
|
+
}
|
|
338
|
+
return 'auto';
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const capitalise = (word: string) => word[0].toUpperCase() + word.slice(1);
|
|
342
|
+
|
|
343
|
+
/** What a successful parse says about fitting `target`, shaped as the server's answer. */
|
|
344
|
+
const analyse = (
|
|
345
|
+
text: string,
|
|
346
|
+
inferred: string,
|
|
347
|
+
target: string
|
|
348
|
+
): Omit<IDpqlMockParseResult, 'success' | 'expression' | 'diagnostics'> & {
|
|
349
|
+
diagnostics: IDpqlMockDiagnostic[];
|
|
350
|
+
} => {
|
|
351
|
+
const base = { inferred_type: inferred, target_type: target };
|
|
352
|
+
if (inferred === 'auto' || target === 'auto' || target === 'any' || inferred === target) {
|
|
353
|
+
return { ...base, type_compatible: true, auto_coercible: true, coercion_may_fail: false, diagnostics: [] };
|
|
354
|
+
}
|
|
355
|
+
// Text can become anything but may not parse on the day; a number or a bool
|
|
356
|
+
// always becomes text; nothing else converts implicitly.
|
|
357
|
+
const coercible = inferred === 'string' || target === 'string';
|
|
358
|
+
const mayFail = inferred === 'string';
|
|
359
|
+
const span = { line: 1, column: 1, end_line: 1, end_column: text.length + 1 };
|
|
360
|
+
return {
|
|
361
|
+
...base,
|
|
362
|
+
type_compatible: false,
|
|
363
|
+
auto_coercible: coercible,
|
|
364
|
+
coercion_may_fail: mayFail,
|
|
365
|
+
...(coercible
|
|
366
|
+
? {
|
|
367
|
+
suggested_fix: {
|
|
368
|
+
text: `to${capitalise(target)}(${text})`,
|
|
369
|
+
description: `Convert ${inferred} to ${target} using to${capitalise(target)}()`,
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
: {}),
|
|
373
|
+
diagnostics: [
|
|
374
|
+
{
|
|
375
|
+
severity: coercible ? 'warning' : 'error',
|
|
376
|
+
message: coercible
|
|
377
|
+
? `Expression returns '${inferred}' but target expects '${target}' (auto-coercion available)`
|
|
378
|
+
: `Expression returns '${inferred}' but target expects '${target}'`,
|
|
379
|
+
code: 'TYPE_MISMATCH',
|
|
380
|
+
...span,
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/** `dpql/parse`: the AST for `text`, or the diagnostic the server reports. */
|
|
387
|
+
export const mockParseDpql = (text: string, target?: string): IDpqlMockParseResult => {
|
|
388
|
+
const trimmed = text.trim();
|
|
389
|
+
try {
|
|
390
|
+
const tokens = tokenize(trimmed);
|
|
391
|
+
const node = parseTokens(tokens, trimmed.length + 1);
|
|
392
|
+
// A lone value is wrapped in `value(...)`, which is how the server tells a
|
|
393
|
+
// literal apart from an expression.
|
|
394
|
+
const root: IDpqlMockNode = node.exp ? node : { exp: 'value', args: [node] };
|
|
395
|
+
const inferred = inferDpqlMockType(root);
|
|
396
|
+
return {
|
|
397
|
+
success: true,
|
|
398
|
+
expression: { is_expression: true, value: root },
|
|
399
|
+
...(target ? analyse(trimmed, inferred, target) : { inferred_type: inferred, diagnostics: [] }),
|
|
400
|
+
} as IDpqlMockParseResult;
|
|
401
|
+
} catch (err) {
|
|
402
|
+
const column = err instanceof DpqlMockSyntaxError ? err.column : 1;
|
|
403
|
+
return {
|
|
404
|
+
success: false,
|
|
405
|
+
diagnostics: [
|
|
406
|
+
{
|
|
407
|
+
severity: 'error',
|
|
408
|
+
message: err instanceof Error ? err.message : String(err),
|
|
409
|
+
code: 'DPQL-E001',
|
|
410
|
+
line: 1,
|
|
411
|
+
column,
|
|
412
|
+
end_line: 1,
|
|
413
|
+
end_column: column,
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// ─── Serialize and render ────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
const TEMPLATE_TOKEN = /^\$[A-Za-z_-][\w-]*:[\w.{}]+$/;
|
|
423
|
+
|
|
424
|
+
/** An argument's own value, for a rendering that asks whether it is set. */
|
|
425
|
+
const argumentValue = (arg: IDpqlMockNode | undefined): unknown =>
|
|
426
|
+
arg && !arg.exp && !arg.is_expression ? arg.value : arg;
|
|
427
|
+
|
|
428
|
+
const printLiteral = (value: unknown, readable: boolean): string => {
|
|
429
|
+
if (value && typeof value === 'object' && 'raw' in value) {
|
|
430
|
+
return String((value as { raw?: string }).raw);
|
|
431
|
+
}
|
|
432
|
+
if (typeof value === 'string') {
|
|
433
|
+
// Serialized DPQL keeps a string a string; a readable rendering shows a
|
|
434
|
+
// reference the string holds as the reference, which the server's does too.
|
|
435
|
+
return readable && TEMPLATE_TOKEN.test(value) ? value : JSON.stringify(value);
|
|
436
|
+
}
|
|
437
|
+
return value === null || value === undefined ? 'null' : String(value);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* `parentPrecedence` is the context: a nested operator binding looser than it
|
|
442
|
+
* is parenthesised. `0` never parenthesises (a call's own commas delimit its
|
|
443
|
+
* arguments); `Infinity` always does, which is how the server substitutes an
|
|
444
|
+
* argument into a `render_template`.
|
|
445
|
+
*/
|
|
446
|
+
const printArg = (arg: IDpqlMockNode, parentPrecedence: number, readable: boolean): string => {
|
|
447
|
+
if (arg.is_expression) {
|
|
448
|
+
const inner = arg.value as IDpqlMockNode;
|
|
449
|
+
const printed = printNode(inner, readable);
|
|
450
|
+
const precedence = BINARY_PRECEDENCE[inner.exp ?? ''];
|
|
451
|
+
return precedence !== undefined && precedence < parentPrecedence ? `(${printed})` : printed;
|
|
452
|
+
}
|
|
453
|
+
if (arg.exp) return printNode(arg, readable);
|
|
454
|
+
return printLiteral(arg.value, readable);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/** `render_template` substitution, in the server's order. */
|
|
458
|
+
const renderTemplate = (info: IExpressionSchema, args: IDpqlMockNode[]): string => {
|
|
459
|
+
const argument = (n: number): string => {
|
|
460
|
+
if (n < args.length) return printArg(args[n], Infinity, true);
|
|
461
|
+
// A missing argument reads as its default, or as nothing.
|
|
462
|
+
const fallback = info.args?.[n]?.default_value;
|
|
463
|
+
return fallback === undefined ? '' : printLiteral(fallback, true);
|
|
464
|
+
};
|
|
465
|
+
return info
|
|
466
|
+
.render_template!.split('$symbol')
|
|
467
|
+
.join(info.symbol)
|
|
468
|
+
.split('$args')
|
|
469
|
+
.join(args.map((_, n) => argument(n)).join(', '))
|
|
470
|
+
.replace(/\$arg\[(\d+)\]/g, (_, n) => argument(Number(n)))
|
|
471
|
+
.replace(
|
|
472
|
+
/\$\{arg\[(\d+)\](?:\s*\?([^:}]*):([^}]*))?\}/g,
|
|
473
|
+
(_, n, whenSet = '', whenUnset = '') => {
|
|
474
|
+
/* The argument decides when it was given, its default only when it was
|
|
475
|
+
not — as the expression evaluates. An explicit `false` on a
|
|
476
|
+
default-true argument reads as unset (Qore `5f3d9b491`). */
|
|
477
|
+
const given = args[Number(n)];
|
|
478
|
+
const isSet =
|
|
479
|
+
given !== undefined ? argumentValue(given) : info.args?.[Number(n)]?.default_value;
|
|
480
|
+
return isSet ? whenSet : whenUnset;
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
/** A node as `dpql/serialize` writes it: DPQL that parses back to the same node. */
|
|
486
|
+
const serializeNode = (args: IDpqlMockNode[], name: string): string => {
|
|
487
|
+
if (isInfixComparison(name, args.length) && WORD.test(symbolOf(name))) {
|
|
488
|
+
// Not in the server's precedence table, so its operands are rendered in the
|
|
489
|
+
// tightest context: any operator inside one is parenthesised.
|
|
490
|
+
return `${printArg(args[0], Infinity, false)} ${symbolOf(name)} ${printArg(args[1], Infinity, false)}`;
|
|
491
|
+
}
|
|
492
|
+
return `${symbolOf(name)}(${args.map((arg) => printArg(arg, 0, false)).join(', ')})`;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
/** A node as `dpql/renderExpression` reads it: a function by name, else its template. */
|
|
496
|
+
const renderNode = (args: IDpqlMockNode[], name: string): string | undefined => {
|
|
497
|
+
const info = CATALOGUE.get(name);
|
|
498
|
+
if (info?.type === DET_FUNCTION) {
|
|
499
|
+
return `${name}(${args.map((arg) => printArg(arg, 0, true)).join(', ')})`;
|
|
500
|
+
}
|
|
501
|
+
return info?.render_template ? renderTemplate(info, args) : undefined;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const printNode = (node: IDpqlMockNode, readable: boolean): string => {
|
|
505
|
+
const args = node.args ?? [];
|
|
506
|
+
const name = node.exp ?? '';
|
|
507
|
+
if (name === 'value' && args.length === 1) return printArg(args[0], 0, readable);
|
|
508
|
+
const rendered = readable ? renderNode(args, name) : undefined;
|
|
509
|
+
if (rendered !== undefined) return rendered;
|
|
510
|
+
const precedence = BINARY_PRECEDENCE[name];
|
|
511
|
+
if (precedence !== undefined && args.length === 2) {
|
|
512
|
+
return `${printArg(args[0], precedence, readable)} ${name} ${printArg(args[1], precedence + 1, readable)}`;
|
|
513
|
+
}
|
|
514
|
+
// The builder stores an AND/OR group as ONE node with every member as an
|
|
515
|
+
// argument; it reads as the members joined by the operator.
|
|
516
|
+
if ((name === '&&' || name === '||') && args.length !== 2) {
|
|
517
|
+
return args.map((arg) => printArg(arg, precedence + 1, readable)).join(` ${name} `);
|
|
518
|
+
}
|
|
519
|
+
if (name === '!') return `!${printArg(args[0], 6, readable)}`;
|
|
520
|
+
if (!readable) return serializeNode(args, name);
|
|
521
|
+
// Any other catalogue operator reads as its arguments joined by its symbol.
|
|
522
|
+
const info = CATALOGUE.get(name);
|
|
523
|
+
if (info) {
|
|
524
|
+
return args.length === 1 ?
|
|
525
|
+
`${info.symbol} ${printArg(args[0], 0, true)}`
|
|
526
|
+
: args.map((arg) => printArg(arg, precedence ?? 0, true)).join(` ${info.symbol} `);
|
|
527
|
+
}
|
|
528
|
+
return `${name}(${args.map((arg) => printArg(arg, 0, true)).join(', ')})`;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const rootOf = (expression: IDpqlMockNode | undefined): IDpqlMockNode =>
|
|
532
|
+
expression?.value && !expression.exp ? (expression.value as IDpqlMockNode) : (expression ?? {});
|
|
533
|
+
|
|
534
|
+
/** The richtext the server sends beside a rendering: each `$context:value` becomes a chip. */
|
|
535
|
+
export const dpqlMockRichtext = (text: string) => {
|
|
536
|
+
const children: unknown[] = [];
|
|
537
|
+
const reference = /\$([A-Za-z_-][\w-]*):([\w.{}]+)/g;
|
|
538
|
+
let last = 0;
|
|
539
|
+
let match: RegExpExecArray | null;
|
|
540
|
+
while ((match = reference.exec(text)) !== null) {
|
|
541
|
+
if (match.index > last) children.push({ text: text.slice(last, match.index) });
|
|
542
|
+
children.push({
|
|
543
|
+
type: 'tag',
|
|
544
|
+
value: match[0],
|
|
545
|
+
label: match[2],
|
|
546
|
+
children: [{ text: '' }],
|
|
547
|
+
metadata: { displayName: `${capitalise(match[1])} Context` },
|
|
548
|
+
});
|
|
549
|
+
last = match.index + match[0].length;
|
|
550
|
+
}
|
|
551
|
+
if (last < text.length || !children.length) children.push({ text: text.slice(last) });
|
|
552
|
+
return { type: 'richtext', value: [{ type: 'paragraph', children }] };
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
/** `dpql/serialize`: DPQL text that parses back to the same expression. */
|
|
556
|
+
export const mockSerializeDpql = (expression: IDpqlMockNode | undefined) => {
|
|
557
|
+
const dpql = printNode(rootOf(expression), false);
|
|
558
|
+
return { dpql, richtext: dpqlMockRichtext(dpql) };
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/** `dpql/renderExpression`: the readable rendering shown in previews and explanations. */
|
|
562
|
+
export const mockRenderDpql = (expression: IDpqlMockNode | undefined) => {
|
|
563
|
+
const rendered = printNode(rootOf(expression), true);
|
|
564
|
+
return { rendered, richtext: dpqlMockRichtext(rendered) };
|
|
565
|
+
};
|