@elabs-ai/components-editor 4.0.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/chunk-LBC5VJBD.js +3748 -0
- package/dist/chunk-LBC5VJBD.js.map +1 -0
- package/dist/index.css +233 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/monaco-environment.d.ts +2 -0
- package/dist/lib/monaco-environment.js +44 -0
- package/dist/lib/monaco-environment.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +11 -0
- package/dist/markdown/frontmatter.js +33 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/index.css +233 -0
- package/dist/markdown/index.css.map +1 -0
- package/dist/markdown/index.d.ts +1486 -0
- package/dist/markdown/index.js +4882 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/markdown/parse.d.ts +25 -0
- package/dist/markdown/parse.js +14 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
- package/package.json +101 -0
- package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
- package/src/ai-objects/ai-objects.stories.tsx +233 -0
- package/src/ai-objects/ai-objects.test.tsx +205 -0
- package/src/ai-objects/decision-card.tsx +178 -0
- package/src/ai-objects/directives.ts +136 -0
- package/src/ai-objects/entity.tsx +181 -0
- package/src/ai-objects/index.ts +34 -0
- package/src/ai-objects/knowledge-card.tsx +143 -0
- package/src/calc-block/calc-block.stories.tsx +297 -0
- package/src/calc-block/calc-block.test.tsx +164 -0
- package/src/calc-block/calc-block.tsx +398 -0
- package/src/calc-block/calc-editor-monaco.ts +214 -0
- package/src/calc-block/calc-editor-prose.ts +128 -0
- package/src/calc-block/calc-editor.css +80 -0
- package/src/calc-block/calc-editor.stories.tsx +272 -0
- package/src/calc-block/calc-editor.test.ts +228 -0
- package/src/calc-block/calc-editor.ts +270 -0
- package/src/calc-block/calc-inline.stories.tsx +44 -0
- package/src/calc-block/calc-inline.test.tsx +44 -0
- package/src/calc-block/calc-inline.tsx +75 -0
- package/src/calc-block/index.ts +20 -0
- package/src/calc-block/types.ts +164 -0
- package/src/code-editor/code-editor.stories.tsx +218 -0
- package/src/code-editor/code-editor.test.tsx +134 -0
- package/src/code-editor/code-editor.tsx +250 -0
- package/src/code-editor/index.ts +6 -0
- package/src/code-workspace/code-workspace.stories.tsx +51 -0
- package/src/code-workspace/code-workspace.test.tsx +146 -0
- package/src/code-workspace/code-workspace.tsx +253 -0
- package/src/code-workspace/index.ts +6 -0
- package/src/copy-button/copy-button.tsx +51 -0
- package/src/copy-button/index.ts +1 -0
- package/src/css.d.ts +3 -0
- package/src/diff-editor/diff-editor.stories.tsx +44 -0
- package/src/diff-editor/diff-editor.test.tsx +68 -0
- package/src/diff-editor/diff-editor.tsx +151 -0
- package/src/diff-editor/index.ts +1 -0
- package/src/editor-context-menu/editor-context-menu.tsx +120 -0
- package/src/editor-context-menu/index.ts +1 -0
- package/src/editor-toolbar/editor-toolbar.tsx +83 -0
- package/src/editor-toolbar/index.ts +1 -0
- package/src/index.ts +48 -0
- package/src/lib/editor-completions-monaco.test.ts +206 -0
- package/src/lib/editor-completions-monaco.ts +127 -0
- package/src/lib/editor-completions.test.ts +156 -0
- package/src/lib/editor-completions.ts +153 -0
- package/src/lib/editor-content-access-prose.test.ts +411 -0
- package/src/lib/editor-content-access-prose.ts +172 -0
- package/src/lib/editor-content-access.test.ts +182 -0
- package/src/lib/editor-content-access.ts +109 -0
- package/src/lib/languages.ts +32 -0
- package/src/lib/markdown/diff.test.ts +80 -0
- package/src/lib/markdown/diff.ts +194 -0
- package/src/lib/markdown/directives.ts +342 -0
- package/src/lib/markdown/frontmatter.ts +50 -0
- package/src/lib/markdown/markdown-scale.test.ts +65 -0
- package/src/lib/markdown/markdown-scale.ts +62 -0
- package/src/lib/markdown/merge.test.ts +64 -0
- package/src/lib/markdown/merge.ts +158 -0
- package/src/lib/markdown/slugify.test.ts +156 -0
- package/src/lib/markdown/slugify.ts +47 -0
- package/src/lib/monaco-environment.ts +103 -0
- package/src/lib/monaco-theme-bridge.ts +293 -0
- package/src/lib/use-data-theme.ts +61 -0
- package/src/markdown/frontmatter.ts +14 -0
- package/src/markdown/index.ts +323 -0
- package/src/markdown/parse.test.ts +60 -0
- package/src/markdown/parse.ts +38 -0
- package/src/markdown-academic/citations.test.ts +77 -0
- package/src/markdown-academic/citations.tsx +442 -0
- package/src/markdown-academic/footnotes.tsx +235 -0
- package/src/markdown-academic/index.ts +32 -0
- package/src/markdown-academic/math.tsx +163 -0
- package/src/markdown-academic/toc.tsx +88 -0
- package/src/markdown-editor/completions/completions-menu.tsx +96 -0
- package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
- package/src/markdown-editor/completions/completions-prose.ts +313 -0
- package/src/markdown-editor/completions/completions-widget.tsx +75 -0
- package/src/markdown-editor/completions/index.ts +37 -0
- package/src/markdown-editor/directive-nodes.ts +151 -0
- package/src/markdown-editor/directive-views.test.tsx +189 -0
- package/src/markdown-editor/directive-views.tsx +795 -0
- package/src/markdown-editor/exit-keymap.test.ts +88 -0
- package/src/markdown-editor/exit-keymap.ts +87 -0
- package/src/markdown-editor/index.ts +6 -0
- package/src/markdown-editor/markdown-editor.css +250 -0
- package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
- package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
- package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
- package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
- package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
- package/src/markdown-editor/markdown-editor.tsx +560 -0
- package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
- package/src/markdown-editor/milkdown-react/index.ts +8 -0
- package/src/markdown-editor/milkdown-react/types.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
- package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
- package/src/markdown-editor/paste-embed.ts +355 -0
- package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
- package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
- package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
- package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
- package/src/markdown-editor/slash/index.ts +107 -0
- package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
- package/src/markdown-editor/slash/insert-directive.ts +345 -0
- package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
- package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
- package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
- package/src/markdown-editor/slash/shortcut.test.ts +157 -0
- package/src/markdown-editor/slash/shortcut.ts +57 -0
- package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
- package/src/markdown-editor/slash/slash-menu.tsx +115 -0
- package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
- package/src/markdown-editor/slash/slash-widget.tsx +97 -0
- package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
- package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
- package/src/markdown-editor/table-view.tsx +249 -0
- package/src/markdown-iteration/directive.tsx +92 -0
- package/src/markdown-iteration/edit-context.ts +56 -0
- package/src/markdown-iteration/index.ts +24 -0
- package/src/markdown-iteration/iteration-block.test.tsx +138 -0
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
- package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
- package/src/markdown-iteration/iteration-builder.test.ts +252 -0
- package/src/markdown-iteration/iteration-builder.ts +303 -0
- package/src/markdown-iteration/iteration.test.ts +56 -0
- package/src/markdown-iteration/iteration.tsx +333 -0
- package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
- package/src/markdown-iteration/template-dialog.test.tsx +104 -0
- package/src/markdown-iteration/template-dialog.tsx +116 -0
- package/src/markdown-outline/document-outline.stories.tsx +76 -0
- package/src/markdown-outline/document-outline.tsx +83 -0
- package/src/markdown-outline/index.ts +2 -0
- package/src/markdown-outline/markdown-outline.test.ts +49 -0
- package/src/markdown-outline/markdown-outline.ts +60 -0
- package/src/markdown-preview/code-fence.tsx +193 -0
- package/src/markdown-preview/index.ts +8 -0
- package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
- package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
- package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
- package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
- package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
- package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
- package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
- package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
- package/src/markdown-preview/markdown-preview.test.tsx +411 -0
- package/src/markdown-preview/markdown-preview.tsx +1541 -0
- package/src/markdown-toolbar/index.ts +8 -0
- package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
- package/src/markdown-toolbar/markdown-commands.ts +104 -0
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
- package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
- package/src/markdown-workspace/focus-writing.test.ts +52 -0
- package/src/markdown-workspace/focus-writing.ts +38 -0
- package/src/markdown-workspace/index.ts +6 -0
- package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
- package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
- package/src/markdown-workspace/markdown-workspace.tsx +787 -0
- package/src/mermaid-diagram/index.ts +1 -0
- package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
- package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
- package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
- package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
- package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
- package/src/mermaid-diagram/real-parse.test.ts +42 -0
- package/src/mermaid-diagram/remediate.test.ts +60 -0
- package/src/mermaid-diagram/remediate.ts +124 -0
- package/src/mermaid-workspace/index.ts +1 -0
- package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
- package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
- package/src/metric-block/index.ts +1 -0
- package/src/metric-block/metric-block.stories.tsx +40 -0
- package/src/metric-block/metric-block.tsx +14 -0
- package/src/monaco-workers.d.ts +10 -0
- package/src/prose/index.ts +14 -0
- package/src/prose/prose.stories.tsx +44 -0
- package/src/prose/prose.test.tsx +44 -0
- package/src/prose/prose.tsx +21 -0
- package/src/timeline/index.ts +18 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the engine-neutral calc authoring helpers (#220).
|
|
3
|
+
*
|
|
4
|
+
* These are the load-bearing pure functions both editor surfaces share — fence
|
|
5
|
+
* detection, the column/position math, and hook resolution. Monaco + Milkdown can't
|
|
6
|
+
* render in jsdom, so the live decoration passes are covered by `test-storybook`;
|
|
7
|
+
* here we lock the math + the degrade-gracefully behavior.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
calcDecorationSpecs,
|
|
13
|
+
calcInlaySpecs,
|
|
14
|
+
calcLineLayout,
|
|
15
|
+
calcTokenClassName,
|
|
16
|
+
findCalcFences,
|
|
17
|
+
identifierPrefix,
|
|
18
|
+
resolveHighlight,
|
|
19
|
+
resolveInlays,
|
|
20
|
+
resolveResults,
|
|
21
|
+
} from "./calc-editor";
|
|
22
|
+
import type { CalcEditorHooks, CalcSheet, CalcToken } from "./types";
|
|
23
|
+
|
|
24
|
+
const doc = [
|
|
25
|
+
"# Notes", // 1
|
|
26
|
+
"", // 2
|
|
27
|
+
"```calc", // 3
|
|
28
|
+
"items = 3", // 4
|
|
29
|
+
"price = 4.50", // 5
|
|
30
|
+
"total = items * price", // 6
|
|
31
|
+
"```", // 7
|
|
32
|
+
"", // 8
|
|
33
|
+
"prose", // 9
|
|
34
|
+
].join("\n");
|
|
35
|
+
|
|
36
|
+
describe("findCalcFences", () => {
|
|
37
|
+
it("locates a ```calc fence with 1-based body line numbers", () => {
|
|
38
|
+
const fences = findCalcFences(doc);
|
|
39
|
+
expect(fences).toHaveLength(1);
|
|
40
|
+
expect(fences[0]).toMatchObject({
|
|
41
|
+
openLine: 3,
|
|
42
|
+
closeLine: 7,
|
|
43
|
+
bodyStartLine: 4,
|
|
44
|
+
bodyEndLine: 6,
|
|
45
|
+
});
|
|
46
|
+
expect(fences[0]?.source).toBe("items = 3\nprice = 4.50\ntotal = items * price");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("ignores non-calc fences", () => {
|
|
50
|
+
expect(findCalcFences("```ts\nconst a = 1;\n```")).toHaveLength(0);
|
|
51
|
+
expect(findCalcFences("```\nplain\n```")).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("treats an unterminated trailing fence as running to EOF (mid-typing)", () => {
|
|
55
|
+
const fences = findCalcFences("```calc\na = 1\nb = 2");
|
|
56
|
+
expect(fences).toHaveLength(1);
|
|
57
|
+
expect(fences[0]?.bodyStartLine).toBe(2);
|
|
58
|
+
expect(fences[0]?.bodyEndLine).toBe(3);
|
|
59
|
+
expect(fences[0]?.source).toBe("a = 1\nb = 2");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("tolerates ~~~ fences and trailing whitespace on the info line", () => {
|
|
63
|
+
const fences = findCalcFences("~~~calc \nx = 1\n~~~");
|
|
64
|
+
expect(fences).toHaveLength(1);
|
|
65
|
+
expect(fences[0]?.source).toBe("x = 1");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("finds multiple fences", () => {
|
|
69
|
+
expect(findCalcFences("```calc\na=1\n```\ntext\n```calc\nb=2\n```")).toHaveLength(2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("calcLineLayout", () => {
|
|
74
|
+
it("returns per-line text + char offset (newlines counted)", () => {
|
|
75
|
+
expect(calcLineLayout("ab\nc\n")).toEqual([
|
|
76
|
+
{ text: "ab", offset: 0 },
|
|
77
|
+
{ text: "c", offset: 3 },
|
|
78
|
+
{ text: "", offset: 5 },
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const tok = (start: number, end: number, kind: CalcToken["kind"], resolved = true): CalcToken => ({
|
|
84
|
+
start,
|
|
85
|
+
end,
|
|
86
|
+
kind,
|
|
87
|
+
resolved,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("resolveHighlight", () => {
|
|
91
|
+
it("prefers the per-line tokenize hook", () => {
|
|
92
|
+
const hooks: CalcEditorHooks = {
|
|
93
|
+
tokenize: (line) => (line ? [tok(0, line.length, "number")] : []),
|
|
94
|
+
};
|
|
95
|
+
const result = resolveHighlight(hooks, "12\n");
|
|
96
|
+
expect(result[0]).toEqual([tok(0, 2, "number")]);
|
|
97
|
+
expect(result[1]).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("falls back to evaluate's per-line tokens when tokenize is absent", () => {
|
|
101
|
+
const sheet: CalcSheet = {
|
|
102
|
+
results: [
|
|
103
|
+
{ line: 1, tokens: [tok(0, 1, "var-def")] },
|
|
104
|
+
{ line: 2, tokens: [tok(0, 2, "number")] },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
const result = resolveHighlight({ evaluate: () => sheet }, "a\n12");
|
|
108
|
+
expect(result[0]).toEqual([tok(0, 1, "var-def")]);
|
|
109
|
+
expect(result[1]).toEqual([tok(0, 2, "number")]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns empty arrays when neither hook is supplied", () => {
|
|
113
|
+
expect(resolveHighlight({}, "a\nb")).toEqual([[], []]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("degrades to [] when a hook throws (never blanks the editor)", () => {
|
|
117
|
+
const hooks: CalcEditorHooks = {
|
|
118
|
+
tokenize: () => {
|
|
119
|
+
throw new Error("boom");
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
expect(resolveHighlight(hooks, "x")).toEqual([[]]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("resolveResults / resolveInlays", () => {
|
|
127
|
+
const hooks: CalcEditorHooks = {
|
|
128
|
+
evaluate: () => ({
|
|
129
|
+
results: [
|
|
130
|
+
{ line: 1, tokens: [], value: { kind: "number", raw: 3, display: "3" } },
|
|
131
|
+
{ line: 2, tokens: [], error: { message: "bad" } },
|
|
132
|
+
{ line: 3, tokens: [] }, // no value/error → no inlay
|
|
133
|
+
],
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
it("maps results by 1-based line", () => {
|
|
138
|
+
const map = resolveResults(hooks, "a\nb\nc");
|
|
139
|
+
expect(map.get(1)?.value?.display).toBe("3");
|
|
140
|
+
expect(map.get(2)?.error?.message).toBe("bad");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("builds inlays for value + error lines only, sorted by line", () => {
|
|
144
|
+
expect(resolveInlays(hooks, "a\nb\nc")).toEqual([
|
|
145
|
+
{ lineNumber: 1, text: "= 3", isError: false },
|
|
146
|
+
{ lineNumber: 2, text: "error: bad", isError: true },
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("returns no results when evaluate throws", () => {
|
|
151
|
+
expect(
|
|
152
|
+
resolveResults(
|
|
153
|
+
{
|
|
154
|
+
evaluate: () => {
|
|
155
|
+
throw new Error("x");
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
"a",
|
|
159
|
+
).size,
|
|
160
|
+
).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("calcTokenClassName", () => {
|
|
165
|
+
it("emits the compound base + kind class, adding unresolved for warnings", () => {
|
|
166
|
+
expect(calcTokenClassName("number", true)).toBe("brand-calc-tok brand-calc-tok--number");
|
|
167
|
+
expect(calcTokenClassName("var-def", false)).toBe(
|
|
168
|
+
"brand-calc-tok brand-calc-tok--var-def brand-calc-tok--unresolved",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("identifierPrefix", () => {
|
|
174
|
+
it("returns the identifier immediately before the column", () => {
|
|
175
|
+
expect(identifierPrefix("total = ite", 11)).toBe("ite");
|
|
176
|
+
expect(identifierPrefix("3 + ", 4)).toBe("");
|
|
177
|
+
expect(identifierPrefix("foo.bar", 7)).toBe("bar");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("calcDecorationSpecs (Monaco column math)", () => {
|
|
182
|
+
const hooks: CalcEditorHooks = {
|
|
183
|
+
tokenize: (line) => (line.startsWith("total") ? [tok(0, 5, "var-def")] : [tok(0, 2, "number")]),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
it("maps 0-based token columns to 1-based Monaco columns at the right model line", () => {
|
|
187
|
+
const specs = calcDecorationSpecs(hooks, 4, ["12", "total = 1"]);
|
|
188
|
+
expect(specs).toEqual([
|
|
189
|
+
{
|
|
190
|
+
lineNumber: 4,
|
|
191
|
+
startColumn: 1,
|
|
192
|
+
endColumn: 3,
|
|
193
|
+
className: "brand-calc-tok brand-calc-tok--number",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
lineNumber: 5,
|
|
197
|
+
startColumn: 1,
|
|
198
|
+
endColumn: 6,
|
|
199
|
+
className: "brand-calc-tok brand-calc-tok--var-def",
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("clamps token columns to the line length and drops empty spans", () => {
|
|
205
|
+
const over: CalcEditorHooks = { tokenize: () => [tok(0, 99, "number"), tok(2, 2, "operator")] };
|
|
206
|
+
const specs = calcDecorationSpecs(over, 1, ["abc"]);
|
|
207
|
+
expect(specs).toEqual([
|
|
208
|
+
{
|
|
209
|
+
lineNumber: 1,
|
|
210
|
+
startColumn: 1,
|
|
211
|
+
endColumn: 4,
|
|
212
|
+
className: "brand-calc-tok brand-calc-tok--number",
|
|
213
|
+
},
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("calcInlaySpecs (Monaco inlay placement)", () => {
|
|
219
|
+
it("positions each inlay at the end-of-line column of its model line", () => {
|
|
220
|
+
const hooks: CalcEditorHooks = {
|
|
221
|
+
evaluate: () => ({
|
|
222
|
+
results: [{ line: 2, tokens: [], value: { kind: "number", raw: 7, display: "7" } }],
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
const specs = calcInlaySpecs(hooks, 4, ["a = 1", "b = 2 + 5"]);
|
|
226
|
+
expect(specs).toEqual([{ lineNumber: 5, column: 10, text: "= 7", isError: false }]);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, engine-agnostic helpers for the calc AUTHORING surfaces (#220).
|
|
3
|
+
*
|
|
4
|
+
* Both editor surfaces — the Monaco source editor and the Milkdown WYSIWYG
|
|
5
|
+
* editor — need the same things: find the ```calc fences, ask the consumer's
|
|
6
|
+
* hooks for highlight tokens + results, and turn those into decorations. The
|
|
7
|
+
* decoration *application* differs per engine (Monaco `inlineClassName` vs a
|
|
8
|
+
* ProseMirror `DecorationSet`), but the *logic* here is pure and shared, so it is
|
|
9
|
+
* unit-testable without rendering either engine (Monaco can't render in jsdom).
|
|
10
|
+
*
|
|
11
|
+
* Governing rule (D5): the library decorates, the consumer computes. Nothing here
|
|
12
|
+
* does any math — it only calls the consumer-supplied {@link CalcEditorHooks} and
|
|
13
|
+
* shapes the results. A throwing hook degrades to "no decorations" rather than
|
|
14
|
+
* blanking the editor (mirrors how `CalcBlock` never throws on a bad block).
|
|
15
|
+
*/
|
|
16
|
+
import type {
|
|
17
|
+
CalcContext,
|
|
18
|
+
CalcEditorHooks,
|
|
19
|
+
CalcLineResult,
|
|
20
|
+
CalcSheet,
|
|
21
|
+
CalcToken,
|
|
22
|
+
CalcTokenKind,
|
|
23
|
+
} from "./types";
|
|
24
|
+
|
|
25
|
+
/** The fenced-code-block info string this feature decorates. */
|
|
26
|
+
export const CALC_FENCE_LANG = "calc";
|
|
27
|
+
|
|
28
|
+
/** A located ```calc fence within a markdown document. Lines are 1-based. */
|
|
29
|
+
export interface CalcFence {
|
|
30
|
+
/** 1-based line of the opening fence (` ```calc `). */
|
|
31
|
+
openLine: number;
|
|
32
|
+
/** 1-based line of the closing fence, or `lines + 1` when unterminated. */
|
|
33
|
+
closeLine: number;
|
|
34
|
+
/** 1-based line of the first body line (`openLine + 1`). */
|
|
35
|
+
bodyStartLine: number;
|
|
36
|
+
/** 1-based line of the last body line (`closeLine - 1`); `< bodyStartLine` if empty. */
|
|
37
|
+
bodyEndLine: number;
|
|
38
|
+
/** The fence body (body lines joined by `"\n"`). */
|
|
39
|
+
source: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const OPEN_FENCE = /^(\s*)(`{3,}|~{3,})[ \t]*calc\b[ \t]*$/i;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scan a markdown string for ```calc fences. Tolerant of leading indentation,
|
|
46
|
+
* `~~~` fences, and an unterminated trailing fence (treated as running to EOF) —
|
|
47
|
+
* which is the normal state while the author is still typing the block.
|
|
48
|
+
*/
|
|
49
|
+
export function findCalcFences(text: string): CalcFence[] {
|
|
50
|
+
const lines = text.split("\n");
|
|
51
|
+
const fences: CalcFence[] = [];
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < lines.length) {
|
|
54
|
+
const open = OPEN_FENCE.exec(lines[i] ?? "");
|
|
55
|
+
if (!open) {
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const run = open[2] ?? "```";
|
|
60
|
+
const marker = run[0] ?? "`";
|
|
61
|
+
const closeRe = new RegExp(`^\\s*\\${marker}{${run.length},}\\s*$`);
|
|
62
|
+
let j = i + 1;
|
|
63
|
+
while (j < lines.length && !closeRe.test(lines[j] ?? "")) j++;
|
|
64
|
+
const openLine = i + 1;
|
|
65
|
+
const closeLine = j < lines.length ? j + 1 : lines.length + 1;
|
|
66
|
+
fences.push({
|
|
67
|
+
openLine,
|
|
68
|
+
closeLine,
|
|
69
|
+
bodyStartLine: openLine + 1,
|
|
70
|
+
bodyEndLine: closeLine - 1,
|
|
71
|
+
source: lines.slice(i + 1, j).join("\n"),
|
|
72
|
+
});
|
|
73
|
+
i = j + 1;
|
|
74
|
+
}
|
|
75
|
+
return fences;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One body line's text plus its character offset from the fence-body start. */
|
|
79
|
+
export interface CalcLineSpan {
|
|
80
|
+
text: string;
|
|
81
|
+
/** Character offset of this line's first char within `source` (newlines counted). */
|
|
82
|
+
offset: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Lay out a fence body into per-line `{ text, offset }` — used to map columns to positions. */
|
|
86
|
+
export function calcLineLayout(source: string): CalcLineSpan[] {
|
|
87
|
+
const spans: CalcLineSpan[] = [];
|
|
88
|
+
let offset = 0;
|
|
89
|
+
for (const text of source.split("\n")) {
|
|
90
|
+
spans.push({ text, offset });
|
|
91
|
+
offset += text.length + 1; // + the newline
|
|
92
|
+
}
|
|
93
|
+
return spans;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Evaluate `source` to a per-line result map (1-based line → result). Never throws. */
|
|
97
|
+
export function resolveResults(
|
|
98
|
+
hooks: CalcEditorHooks,
|
|
99
|
+
source: string,
|
|
100
|
+
ctx?: CalcContext,
|
|
101
|
+
): Map<number, CalcLineResult> {
|
|
102
|
+
const map = new Map<number, CalcLineResult>();
|
|
103
|
+
if (!hooks.evaluate) return map;
|
|
104
|
+
let sheet: CalcSheet;
|
|
105
|
+
try {
|
|
106
|
+
sheet = hooks.evaluate(source, ctx);
|
|
107
|
+
} catch {
|
|
108
|
+
return map;
|
|
109
|
+
}
|
|
110
|
+
for (const r of sheet.results ?? []) map.set(r.line, r);
|
|
111
|
+
return map;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve highlight tokens for every body line. Prefers the per-line `tokenize`
|
|
116
|
+
* hook (cheap, works mid-typing on a line that doesn't yet evaluate); falls back
|
|
117
|
+
* to `evaluate`'s per-line `tokens` so a consumer who only wrote `evaluate` still
|
|
118
|
+
* gets highlighting. Returns one `CalcToken[]` per line. Never throws.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveHighlight(
|
|
121
|
+
hooks: CalcEditorHooks,
|
|
122
|
+
source: string,
|
|
123
|
+
ctx?: CalcContext,
|
|
124
|
+
): CalcToken[][] {
|
|
125
|
+
const lines = source.split("\n");
|
|
126
|
+
if (hooks.tokenize) {
|
|
127
|
+
const { tokenize } = hooks;
|
|
128
|
+
return lines.map((line) => {
|
|
129
|
+
try {
|
|
130
|
+
return tokenize(line, ctx) ?? [];
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (hooks.evaluate) {
|
|
137
|
+
const byLine = resolveResults(hooks, source, ctx);
|
|
138
|
+
return lines.map((_line, i) => byLine.get(i + 1)?.tokens ?? []);
|
|
139
|
+
}
|
|
140
|
+
return lines.map(() => []);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A resolved result inlay for one body line (the computed answer shown after it). */
|
|
144
|
+
export interface CalcInlay {
|
|
145
|
+
/** 1-based line within the fence body. */
|
|
146
|
+
lineNumber: number;
|
|
147
|
+
/** The inlay text — `"= <display>"` for a value, `"error: …"` for a per-line error. */
|
|
148
|
+
text: string;
|
|
149
|
+
/** True when the line evaluated to an error rather than a value. */
|
|
150
|
+
isError: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The inlay text for one result line, or `null` when there's nothing to show. */
|
|
154
|
+
export function inlayTextForResult(result: CalcLineResult | undefined): {
|
|
155
|
+
text: string;
|
|
156
|
+
isError: boolean;
|
|
157
|
+
} | null {
|
|
158
|
+
if (!result) return null;
|
|
159
|
+
if (result.error) return { text: `error: ${result.error.message}`, isError: true };
|
|
160
|
+
if (result.value) return { text: `= ${result.value.display}`, isError: false };
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Resolve every body line's result inlay (skips lines with no value/error). */
|
|
165
|
+
export function resolveInlays(
|
|
166
|
+
hooks: CalcEditorHooks,
|
|
167
|
+
source: string,
|
|
168
|
+
ctx?: CalcContext,
|
|
169
|
+
): CalcInlay[] {
|
|
170
|
+
const byLine = resolveResults(hooks, source, ctx);
|
|
171
|
+
const out: CalcInlay[] = [];
|
|
172
|
+
for (const [lineNumber, result] of byLine) {
|
|
173
|
+
const inlay = inlayTextForResult(result);
|
|
174
|
+
if (inlay) out.push({ lineNumber, text: inlay.text, isError: inlay.isError });
|
|
175
|
+
}
|
|
176
|
+
out.sort((a, b) => a.lineNumber - b.lineNumber);
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Class string for a highlight token. Hue comes from the `--calc-*` tokens (via
|
|
182
|
+
* `calc-editor.css`), but — exactly like `CalcBlock.tokenClass` — hue is never
|
|
183
|
+
* the SOLE cue: a `var-def` carries weight and an unresolved token a dotted
|
|
184
|
+
* underline, so roles stay distinct in the high-contrast theme (where the calc
|
|
185
|
+
* colors collapse toward the foreground).
|
|
186
|
+
*/
|
|
187
|
+
export function calcTokenClassName(kind: CalcTokenKind, resolved: boolean): string {
|
|
188
|
+
const base = `brand-calc-tok brand-calc-tok--${kind}`;
|
|
189
|
+
return resolved ? base : `${base} brand-calc-tok--unresolved`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The identifier prefix immediately before `column` in `line` (the word being typed). */
|
|
193
|
+
export function identifierPrefix(line: string, column: number): string {
|
|
194
|
+
const upto = line.slice(0, Math.max(0, column));
|
|
195
|
+
const m = /[A-Za-z_][A-Za-z0-9_]*$/.exec(upto);
|
|
196
|
+
return m ? m[0] : "";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const clamp = (n: number, lo: number, hi: number): number => Math.min(hi, Math.max(lo, n));
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* An engine-neutral highlight span: a 1-based model line + 1-based start/end
|
|
203
|
+
* columns (Monaco-style; `end` exclusive) + the CSS class. The Monaco layer maps
|
|
204
|
+
* these to `monaco.Range` decorations; kept pure here so the column math is
|
|
205
|
+
* unit-testable without rendering Monaco.
|
|
206
|
+
*/
|
|
207
|
+
export interface CalcDecorationSpec {
|
|
208
|
+
lineNumber: number;
|
|
209
|
+
startColumn: number;
|
|
210
|
+
endColumn: number;
|
|
211
|
+
className: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build highlight specs for one fence body. `bodyStartLine` is the 1-based model
|
|
216
|
+
* line of the first body line; `bodyLineTexts` are the EOL-free line strings
|
|
217
|
+
* (read from the model, so columns stay accurate regardless of CRLF/LF).
|
|
218
|
+
*/
|
|
219
|
+
export function calcDecorationSpecs(
|
|
220
|
+
hooks: CalcEditorHooks,
|
|
221
|
+
bodyStartLine: number,
|
|
222
|
+
bodyLineTexts: string[],
|
|
223
|
+
ctx?: CalcContext,
|
|
224
|
+
): CalcDecorationSpec[] {
|
|
225
|
+
const tokensByLine = resolveHighlight(hooks, bodyLineTexts.join("\n"), ctx);
|
|
226
|
+
const specs: CalcDecorationSpec[] = [];
|
|
227
|
+
for (let i = 0; i < bodyLineTexts.length; i++) {
|
|
228
|
+
const lineText = bodyLineTexts[i] ?? "";
|
|
229
|
+
for (const t of tokensByLine[i] ?? []) {
|
|
230
|
+
const start = clamp(t.start, 0, lineText.length);
|
|
231
|
+
const end = clamp(t.end, start, lineText.length);
|
|
232
|
+
if (end <= start) continue;
|
|
233
|
+
specs.push({
|
|
234
|
+
lineNumber: bodyStartLine + i,
|
|
235
|
+
startColumn: start + 1,
|
|
236
|
+
endColumn: end + 1,
|
|
237
|
+
className: calcTokenClassName(t.kind, t.resolved),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return specs;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** An engine-neutral inlay placement: a 1-based model line + end-of-line column. */
|
|
245
|
+
export interface CalcInlaySpec {
|
|
246
|
+
lineNumber: number;
|
|
247
|
+
/** 1-based column at the end of the line (where the inlay sits). */
|
|
248
|
+
column: number;
|
|
249
|
+
text: string;
|
|
250
|
+
isError: boolean;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Build result-inlay specs for one fence body (positioned at each line's end). */
|
|
254
|
+
export function calcInlaySpecs(
|
|
255
|
+
hooks: CalcEditorHooks,
|
|
256
|
+
bodyStartLine: number,
|
|
257
|
+
bodyLineTexts: string[],
|
|
258
|
+
ctx?: CalcContext,
|
|
259
|
+
): CalcInlaySpec[] {
|
|
260
|
+
return resolveInlays(hooks, bodyLineTexts.join("\n"), ctx).map((inlay) => {
|
|
261
|
+
const i = inlay.lineNumber - 1;
|
|
262
|
+
const lineText = bodyLineTexts[i] ?? "";
|
|
263
|
+
return {
|
|
264
|
+
lineNumber: bodyStartLine + i,
|
|
265
|
+
column: lineText.length + 1,
|
|
266
|
+
text: inlay.text,
|
|
267
|
+
isError: inlay.isError,
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { CalcInline } from "./calc-inline";
|
|
4
|
+
import type { CalcSheet, EvaluateCalc } from "./types";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Editor/CalcInline",
|
|
8
|
+
component: CalcInline,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: { layout: "padded" },
|
|
11
|
+
} satisfies Meta<typeof CalcInline>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
// Stand-in evaluator (the math engine is app/domain logic, outside the system).
|
|
17
|
+
const sheet = (display: string): CalcSheet => ({
|
|
18
|
+
results: [{ line: 1, tokens: [], value: { kind: "currency", raw: 0, display } }],
|
|
19
|
+
});
|
|
20
|
+
const evaluate: EvaluateCalc = (src) => {
|
|
21
|
+
if (src.includes("*")) return sheet("2,720 USD");
|
|
22
|
+
if (src.includes("/")) return sheet("31.3%");
|
|
23
|
+
return { results: [{ line: 1, tokens: [], error: { message: `Cannot evaluate "${src}"` } }] };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
args: { source: "85 USD * 32", evaluate },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** In prose: the chip sits inline, the result reads in `text-calc-result`. */
|
|
31
|
+
export const InProse: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<p className="max-w-prose text-body text-foreground">
|
|
34
|
+
The campaign spent <CalcInline source="85 USD * 32" evaluate={evaluate} /> last month, a{" "}
|
|
35
|
+
<CalcInline source="980 / 3130" evaluate={evaluate} /> conversion rate — both computed by the
|
|
36
|
+
consumer’s engine.
|
|
37
|
+
</p>
|
|
38
|
+
),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A bad expression renders a calm inline warning (icon + dotted underline), never throwing. */
|
|
42
|
+
export const ErrorState: Story = {
|
|
43
|
+
args: { source: "foo + bar", evaluate },
|
|
44
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { CalcInline } from "./calc-inline";
|
|
5
|
+
import type { CalcSheet, EvaluateCalc } from "./types";
|
|
6
|
+
|
|
7
|
+
const OK: CalcSheet = {
|
|
8
|
+
results: [{ line: 1, tokens: [], value: { kind: "currency", raw: 2720, display: "2,720 USD" } }],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// CalcInline is self-contained (native `title`, no Radix TooltipProvider needed).
|
|
12
|
+
function renderInline(source: string, evaluate: EvaluateCalc) {
|
|
13
|
+
return render(<CalcInline source={source} evaluate={evaluate} />);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("CalcInline", () => {
|
|
17
|
+
it("renders the evaluated result as the chip, with the expression in the accessible name", () => {
|
|
18
|
+
renderInline("85 USD * 32", () => OK);
|
|
19
|
+
const chip = screen.getByTestId("calc-inline");
|
|
20
|
+
expect(chip).toHaveTextContent("2,720 USD");
|
|
21
|
+
expect(chip).toHaveAttribute("aria-label", "85 USD * 32 = 2,720 USD");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("passes the verbatim expression to the evaluate hook (no bundled engine)", () => {
|
|
25
|
+
let seen = "";
|
|
26
|
+
renderInline("85 USD * 32", (src) => {
|
|
27
|
+
seen = src;
|
|
28
|
+
return OK;
|
|
29
|
+
});
|
|
30
|
+
expect(seen).toBe("85 USD * 32");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("renders a calm warning (never throws) for a bad expression", () => {
|
|
34
|
+
renderInline("oops", () => ({ results: [{ line: 1, tokens: [], error: { message: "bad" } }] }));
|
|
35
|
+
const chip = screen.getByTestId("calc-inline");
|
|
36
|
+
expect(chip).toHaveAttribute("data-calc-error");
|
|
37
|
+
expect(chip).toHaveAttribute("aria-label", "oops: bad");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("treats a missing result as a calm 'No result' warning", () => {
|
|
41
|
+
renderInline("", () => ({ results: [] }));
|
|
42
|
+
expect(screen.getByTestId("calc-inline")).toHaveAttribute("data-calc-error");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CalcInline — an inline `:calc[expr]` directive rendered as a result chip (#L).
|
|
5
|
+
*
|
|
6
|
+
* The inline sibling of {@link CalcBlock}, same contract: the library RENDERS, the
|
|
7
|
+
* consumer EVALUATES. `evaluate(expr)` returns a `CalcSheet`; the chip shows the
|
|
8
|
+
* first line's value (the expression's result) with the verbatim source as a
|
|
9
|
+
* native hover `title` and in the accessible name (`"85 USD * 32 = 2,720 USD"`). A
|
|
10
|
+
* bad expression renders a calm inline warning — it never throws or blanks the
|
|
11
|
+
* sentence. A native `title` (not a Radix Tooltip) keeps the chip self-contained,
|
|
12
|
+
* so it drops into rendered prose with no `TooltipProvider` ancestor.
|
|
13
|
+
*
|
|
14
|
+
* Color comes from the shared `--calc-*` tokens (#221): the result reads in
|
|
15
|
+
* `text-calc-result`; the warning cue is the icon + dotted underline, never hue
|
|
16
|
+
* alone (so it survives the high-contrast theme).
|
|
17
|
+
*/
|
|
18
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
19
|
+
import { TriangleAlert } from "lucide-react";
|
|
20
|
+
import { forwardRef, useMemo, type HTMLAttributes } from "react";
|
|
21
|
+
|
|
22
|
+
import type { EvaluateCalc } from "./types";
|
|
23
|
+
|
|
24
|
+
export interface CalcInlineProps extends Omit<HTMLAttributes<HTMLElement>, "children"> {
|
|
25
|
+
/** The inline expression, e.g. `85 USD * 32`. */
|
|
26
|
+
source: string;
|
|
27
|
+
/** Consumer-supplied evaluator (sync). The library bundles no math engine. */
|
|
28
|
+
evaluate: EvaluateCalc;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const CalcInline = forwardRef<HTMLSpanElement, CalcInlineProps>(function CalcInline(
|
|
32
|
+
{ source, evaluate, className, ...props },
|
|
33
|
+
ref,
|
|
34
|
+
) {
|
|
35
|
+
const sheet = useMemo(() => evaluate(source), [evaluate, source]);
|
|
36
|
+
const first = sheet.results[0];
|
|
37
|
+
const display = first?.value?.display;
|
|
38
|
+
const error = first?.error ?? (display == null ? { message: "No result" } : undefined);
|
|
39
|
+
|
|
40
|
+
if (error) {
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
ref={ref}
|
|
44
|
+
data-testid="calc-inline"
|
|
45
|
+
data-calc-error=""
|
|
46
|
+
title={error.message}
|
|
47
|
+
aria-label={`${source}: ${error.message}`}
|
|
48
|
+
className={cn(
|
|
49
|
+
"inline-flex items-center gap-0.5 align-baseline font-mono text-calc-warning underline decoration-dotted decoration-1 underline-offset-2",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
<TriangleAlert className="size-3" aria-hidden="true" />
|
|
55
|
+
<span>{source}</span>
|
|
56
|
+
</span>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<span
|
|
62
|
+
ref={ref}
|
|
63
|
+
data-testid="calc-inline"
|
|
64
|
+
title={source}
|
|
65
|
+
aria-label={`${source} = ${display}`}
|
|
66
|
+
className={cn(
|
|
67
|
+
"inline-flex items-center rounded-sm bg-calc-result/10 px-1 align-baseline font-mono tabular-nums text-calc-result",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
{display}
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { CalcBlock, type CalcBlockProps } from "./calc-block";
|
|
2
|
+
export { CalcInline, type CalcInlineProps } from "./calc-inline";
|
|
3
|
+
export type {
|
|
4
|
+
CalcComplete,
|
|
5
|
+
CalcCompletion,
|
|
6
|
+
CalcCompletionContext,
|
|
7
|
+
CalcCompletionKind,
|
|
8
|
+
CalcContext,
|
|
9
|
+
CalcEditorHooks,
|
|
10
|
+
CalcLineResult,
|
|
11
|
+
CalcRule,
|
|
12
|
+
CalcSheet,
|
|
13
|
+
CalcTint,
|
|
14
|
+
CalcToken,
|
|
15
|
+
CalcTokenize,
|
|
16
|
+
CalcTokenKind,
|
|
17
|
+
CalcValue,
|
|
18
|
+
CalcValueKind,
|
|
19
|
+
EvaluateCalc,
|
|
20
|
+
} from "./types";
|