@formulaxjs/editor 0.1.1 → 0.3.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/dist/index.js CHANGED
@@ -1,434 +1,164 @@
1
- // src/dom-renderer.ts
2
- var joinPath = (path) => path.join(".");
3
- var renderInteractiveHtml = (doc, activePath) => `
4
- <div class="fx-editor-surface" data-role="surface">
5
- ${renderChildren(doc.body, [], activePath)}
6
- </div>
7
- `;
8
- var renderChildren = (nodes, basePath, activePath) => {
9
- const html = [];
10
- for (let index = 0; index <= nodes.length; index += 1) {
11
- const path = joinPath([...basePath, index]);
12
- const isActive = path === joinPath(activePath);
13
- html.push(
14
- `<button class="fx-slot${isActive ? " is-active" : ""}" data-path="${path}" type="button" title="${path}"></button>`
15
- );
16
- if (index < nodes.length) {
17
- html.push(renderNode(nodes[index], [...basePath, index], activePath));
18
- }
19
- }
20
- return html.join("");
21
- };
22
- var renderNode = (node, path, activePath) => {
23
- const pathValue = joinPath(path);
24
- const isActive = (p) => joinPath(p) === joinPath(activePath);
25
- switch (node.type) {
26
- case "text":
27
- return `<span class="fx-node fx-text" data-node-path="${pathValue}">${node.value}</span>`;
28
- case "group":
29
- return `<span class="fx-node fx-group" data-node-path="${pathValue}">${renderChildren(node.body, [...path, 0], activePath)}</span>`;
30
- case "frac":
31
- return `<span class="fx-node fx-frac" data-node-path="${pathValue}">
32
- <span class="fx-frac-num${isActive([...path, 0]) ? " is-active" : ""}" data-path="${joinPath([...path, 0])}">${renderChildren(node.numerator, [...path, 0], activePath)}</span>
33
- <span class="fx-frac-line"></span>
34
- <span class="fx-frac-den${isActive([...path, 1]) ? " is-active" : ""}" data-path="${joinPath([...path, 1])}">${renderChildren(node.denominator, [...path, 1], activePath)}</span>
35
- </span>`;
36
- case "supsub":
37
- return `<span class="fx-node fx-supsub" data-node-path="${pathValue}">
38
- <span class="fx-supsub-base">${renderChildren(node.base, [...path, 0], activePath)}</span>
39
- <span class="fx-supsub-stack">
40
- <span class="fx-sup${isActive([...path, 1]) ? " is-active" : ""}" data-path="${joinPath([...path, 1])}">${renderChildren(node.sup ?? [], [...path, 1], activePath)}</span>
41
- <span class="fx-sub${isActive([...path, 2]) ? " is-active" : ""}" data-path="${joinPath([...path, 2])}">${renderChildren(node.sub ?? [], [...path, 2], activePath)}</span>
42
- </span>
43
- </span>`;
44
- case "sqrt":
45
- return `<span class="fx-node fx-sqrt" data-node-path="${pathValue}">
46
- <span class="fx-sqrt-symbol">\u221A</span>
47
- <span class="fx-sqrt-body${isActive([...path, 0]) ? " is-active" : ""}" data-path="${joinPath([...path, 0])}">${renderChildren(node.value, [...path, 0], activePath)}</span>
48
- </span>`;
49
- case "fenced":
50
- return `<span class="fx-node fx-fenced" data-node-path="${pathValue}">
51
- <span class="fx-fence">${node.left}</span>
52
- <span class="fx-fenced-body${isActive([...path, 0]) ? " is-active" : ""}" data-path="${joinPath([...path, 0])}">${renderChildren(node.body, [...path, 0], activePath)}</span>
53
- <span class="fx-fence">${node.right}</span>
54
- </span>`;
55
- case "doc":
56
- return renderChildren(node.body, [0], activePath);
57
- }
58
- throw new Error(`Unsupported node type: ${String(node.type)}`);
59
- };
60
-
61
- // src/editor.ts
1
+ // src/formula-modal.ts
2
+ import { createEmptyState, parseLatex } from "@formulaxjs/core";
3
+ import { mountKityEditor } from "@formulaxjs/kity-runtime";
4
+ import { escapeHtml, ensureFormulaXBaseStyles } from "@formulaxjs/renderer";
62
5
  import {
63
- applyCommand,
64
- backspace,
65
- createEmptyState,
66
- insertFenced,
67
- insertFraction,
68
- insertSqrt,
69
- insertSubscript,
70
- insertSuperscript,
71
- insertText
72
- } from "@formulaxjs/core";
73
-
74
- // src/styles.ts
75
- var editorStyles = `
76
- @keyframes fx-blink {
77
- 0%, 50% { opacity: 1; }
78
- 51%, 100% { opacity: 0; }
79
- }
80
- .fx-editor {
81
- border: 1px solid #d8d4c7;
82
- border-radius: 2px;
83
- background: #fffefb;
84
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7);
85
- padding: 24px 28px;
86
- font-family: Cambria, 'Times New Roman', serif;
87
- font-size: 25px;
88
- line-height: 1.7;
89
- min-height: 120px;
90
- position: relative;
91
- }
92
- .fx-editor:focus-within {
93
- border-color: #7bbb59;
94
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7), 0 0 0 3px rgba(123, 187, 89, 0.18);
95
- }
96
- .fx-editor-surface {
97
- display: flex;
98
- flex-wrap: wrap;
99
- align-items: baseline;
100
- gap: 3px;
101
- cursor: text;
102
- }
103
- .fx-node {
104
- position: relative;
105
- display: inline-flex;
106
- align-items: center;
107
- padding: 2px 3px;
108
- border-radius: 2px;
109
- transition: background 0.12s ease;
110
- }
111
- .fx-node:hover {
112
- background: rgba(122, 186, 89, 0.12);
113
- }
114
- .fx-text {
115
- color: #2b2925;
116
- }
117
- .fx-group {
118
- background: rgba(243, 241, 230, 0.82);
119
- border: 1px dashed #c8c0a8;
120
- }
121
- .fx-frac {
122
- display: inline-flex;
123
- flex-direction: column;
124
- align-items: center;
125
- vertical-align: middle;
126
- padding: 0 5px;
127
- }
128
- .fx-frac-num, .fx-frac-den {
129
- display: flex;
130
- flex-wrap: wrap;
131
- align-items: center;
132
- justify-content: center;
133
- padding: 3px 8px;
134
- min-width: 24px;
135
- min-height: 28px;
136
- border-radius: 2px;
137
- transition: all 0.15s ease;
138
- }
139
- .fx-frac-num {
140
- border-bottom: 2px solid #2e2c29;
141
- padding-bottom: 6px;
142
- }
143
- .fx-frac-den {
144
- border-top: 2px solid #2e2c29;
145
- padding-top: 6px;
146
- }
147
- .fx-frac-num:hover, .fx-frac-den:hover {
148
- background: rgba(122, 186, 89, 0.1);
149
- }
150
- .fx-frac-num.is-active, .fx-frac-den.is-active {
151
- background: rgba(122, 186, 89, 0.16);
152
- box-shadow: inset 0 0 0 1px rgba(83, 184, 86, 0.5);
153
- }
154
- .fx-supsub {
155
- display: inline-flex;
156
- align-items: baseline;
157
- gap: 0;
158
- }
159
- .fx-supsub-base {
160
- display: inline-flex;
161
- align-items: center;
162
- padding: 0 2px;
163
- }
164
- .fx-supsub-stack {
165
- display: inline-flex;
166
- flex-direction: column;
167
- align-items: flex-start;
168
- font-size: 0.65em;
169
- margin-left: 2px;
170
- }
171
- .fx-sup, .fx-sub {
172
- display: flex;
173
- align-items: center;
174
- min-width: 16px;
175
- min-height: 20px;
176
- padding: 0 4px;
177
- border-radius: 2px;
178
- transition: all 0.15s ease;
179
- }
180
- .fx-sup {
181
- vertical-align: super;
182
- }
183
- .fx-sub {
184
- vertical-align: sub;
185
- }
186
- .fx-sup:hover, .fx-sub:hover {
187
- background: rgba(122, 186, 89, 0.1);
188
- }
189
- .fx-sup.is-active, .fx-sub.is-active {
190
- background: rgba(122, 186, 89, 0.16);
191
- box-shadow: inset 0 0 0 1px rgba(83, 184, 86, 0.5);
192
- }
193
- .fx-sqrt {
194
- display: inline-flex;
195
- align-items: baseline;
196
- }
197
- .fx-sqrt-symbol {
198
- font-size: 1.1em;
199
- color: #2e2c29;
200
- margin-right: 2px;
201
- }
202
- .fx-sqrt-body {
203
- display: inline-flex;
204
- align-items: center;
205
- border-top: 2px solid #2e2c29;
206
- padding: 4px 6px 0;
207
- margin-left: 2px;
208
- }
209
- .fx-sqrt-body:hover {
210
- background: rgba(122, 186, 89, 0.1);
211
- }
212
- .fx-sqrt-body.is-active {
213
- background: rgba(122, 186, 89, 0.16);
214
- }
215
- .fx-fenced {
216
- display: inline-flex;
217
- align-items: center;
218
- background: rgba(243, 241, 230, 0.78);
219
- border-radius: 3px;
220
- padding: 0 4px;
221
- }
222
- .fx-fence {
223
- color: #59554d;
224
- font-size: 1.2em;
225
- padding: 0 2px;
226
- }
227
- .fx-fenced-body {
228
- display: inline-flex;
229
- align-items: center;
230
- padding: 0 6px;
231
- }
232
- .fx-fenced-body:hover {
233
- background: rgba(122, 186, 89, 0.1);
234
- border-radius: 2px;
235
- }
236
- .fx-fenced-body.is-active {
237
- background: rgba(122, 186, 89, 0.16);
238
- border-radius: 2px;
6
+ serializeKityFormulaFromRoot,
7
+ waitForKityFormulaSvgLayout
8
+ } from "@formulaxjs/renderer-kity";
9
+
10
+ // src/perf.ts
11
+ import { ensureKityRuntime } from "@formulaxjs/kity-runtime";
12
+ function getPerfHost() {
13
+ return globalThis;
14
+ }
15
+ function getPerfState() {
16
+ const host = getPerfHost();
17
+ host.__FORMULAX_PERF_STATE__ ??= {
18
+ reportedMeasureCount: 0,
19
+ reportScheduled: false
20
+ };
21
+ return host.__FORMULAX_PERF_STATE__;
239
22
  }
240
- .fx-slot {
241
- width: 3px;
242
- height: 1.2em;
243
- border: none;
244
- background: #53b856;
245
- padding: 0;
246
- margin: 0 1px;
247
- cursor: text;
248
- border-radius: 1px;
249
- vertical-align: middle;
23
+ function hasPerfSupport() {
24
+ return typeof performance !== "undefined" && typeof performance.mark === "function" && typeof performance.measure === "function" && typeof performance.getEntriesByType === "function";
250
25
  }
251
- .fx-slot.is-active {
252
- animation: fx-blink 1s infinite;
26
+ function isPerfDebugEnabled() {
27
+ return getPerfHost().__FORMULAX_PERF__ === true;
253
28
  }
254
- .fx-slot:hover {
255
- background: #77c75b;
29
+ function schedulePerfReport() {
30
+ if (!hasPerfSupport() || !isPerfDebugEnabled()) {
31
+ return;
32
+ }
33
+ const state = getPerfState();
34
+ if (state.reportScheduled) {
35
+ return;
36
+ }
37
+ state.reportScheduled = true;
38
+ queueMicrotask(() => {
39
+ state.reportScheduled = false;
40
+ const entries = performance.getEntriesByType("measure").filter((entry) => entry.name.startsWith("fx:")).sort((left, right) => left.startTime - right.startTime);
41
+ const nextEntries = entries.slice(state.reportedMeasureCount);
42
+ state.reportedMeasureCount = entries.length;
43
+ if (!nextEntries.length) {
44
+ return;
45
+ }
46
+ console.table(nextEntries.map((entry) => ({
47
+ name: entry.name,
48
+ duration: Number(entry.duration.toFixed(2)),
49
+ startTime: Number(entry.startTime.toFixed(2))
50
+ })));
51
+ });
256
52
  }
257
- `;
258
-
259
- // src/editor.ts
260
- var FormulaEditor = class {
261
- state;
262
- root;
263
- onChange;
264
- locale;
265
- constructor(options) {
266
- this.root = options.root;
267
- this.state = options.initialState ?? createEmptyState();
268
- this.onChange = options.onChange;
269
- this.locale = options.locale ?? "en";
270
- this.root.classList.add("fx-editor");
271
- this.root.tabIndex = 0;
272
- this.ensureStyles();
273
- this.bindEvents();
274
- this.render();
53
+ function markFormulaXPerf(name) {
54
+ if (!hasPerfSupport()) {
55
+ return null;
275
56
  }
276
- getState() {
277
- return structuredClone(this.state);
57
+ const markName = `${name}::${Date.now()}::${Math.random().toString(36).slice(2, 8)}`;
58
+ performance.mark(markName);
59
+ return markName;
60
+ }
61
+ function measureFormulaXPerf(name, startMark, endMark) {
62
+ if (!hasPerfSupport() || !startMark) {
63
+ return null;
278
64
  }
279
- getLocale() {
280
- return this.locale;
65
+ const resolvedEndMark = endMark ?? markFormulaXPerf(`${name}:end`);
66
+ if (!resolvedEndMark) {
67
+ return null;
281
68
  }
282
- setState(state) {
283
- this.state = structuredClone(state);
284
- this.render();
69
+ performance.measure(name, startMark, resolvedEndMark);
70
+ schedulePerfReport();
71
+ return resolvedEndMark;
72
+ }
73
+ function recordFormulaXPerfPoint(name) {
74
+ const markName = markFormulaXPerf(name);
75
+ if (!markName) {
76
+ return;
285
77
  }
286
- dispatch(command) {
287
- this.state = applyCommand(this.state, command);
288
- this.render();
289
- this.onChange?.(this.getState());
78
+ measureFormulaXPerf(name, markName, markName);
79
+ clearFormulaXPerfMarks(markName);
80
+ }
81
+ function clearFormulaXPerfMarks(...marks) {
82
+ if (!hasPerfSupport()) {
83
+ return;
290
84
  }
291
- ensureStyles() {
292
- if (document.getElementById("fx-editor-styles")) return;
293
- const style = document.createElement("style");
294
- style.id = "fx-editor-styles";
295
- style.textContent = editorStyles;
296
- document.head.appendChild(style);
85
+ for (const mark of marks) {
86
+ if (!mark) {
87
+ continue;
88
+ }
89
+ performance.clearMarks(mark);
297
90
  }
298
- bindEvents() {
299
- this.root.addEventListener("click", (event) => {
300
- const target = event.target;
301
- const pathValue = target.dataset.path;
302
- if (pathValue === void 0) return;
303
- const path = pathValue === "" ? [] : pathValue.split(".").map(Number);
304
- this.moveSelection(path);
305
- });
306
- this.root.addEventListener("keydown", (event) => {
307
- if (event.key === "Backspace") {
308
- event.preventDefault();
309
- this.dispatch(backspace());
310
- return;
311
- }
312
- if (event.key === "/") {
313
- event.preventDefault();
314
- this.dispatch(insertFraction());
315
- return;
316
- }
317
- if (event.key === "^") {
318
- event.preventDefault();
319
- this.dispatch(insertSuperscript());
320
- return;
321
- }
322
- if (event.key === "_") {
323
- event.preventDefault();
324
- this.dispatch(insertSubscript());
325
- return;
326
- }
327
- if (event.key === "r" && event.ctrlKey) {
328
- event.preventDefault();
329
- this.dispatch(insertSqrt());
330
- return;
331
- }
332
- if (event.key === "(") {
333
- event.preventDefault();
334
- this.dispatch(insertFenced("(", ")"));
335
- return;
336
- }
337
- if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
338
- event.preventDefault();
339
- this.dispatch(insertText(event.key));
340
- }
341
- });
91
+ }
92
+ async function preloadFormulaXEditor() {
93
+ await ensureKityRuntime();
94
+ }
95
+ function scheduleFormulaXEditorPreload(mode, target) {
96
+ if (mode === false || typeof window === "undefined") {
97
+ return () => void 0;
342
98
  }
343
- moveSelection(path) {
344
- this.state = {
345
- ...this.state,
346
- selection: {
347
- anchor: [...path],
348
- focus: [...path]
99
+ let disposed = false;
100
+ let triggered = false;
101
+ const cleanupCallbacks = [];
102
+ const trigger = () => {
103
+ if (disposed || triggered) {
104
+ return;
105
+ }
106
+ triggered = true;
107
+ while (cleanupCallbacks.length) {
108
+ cleanupCallbacks.pop()?.();
109
+ }
110
+ void preloadFormulaXEditor();
111
+ };
112
+ if (mode === "idle") {
113
+ const host = getPerfHost();
114
+ if (typeof host.requestIdleCallback === "function") {
115
+ const handle = host.requestIdleCallback(() => {
116
+ trigger();
117
+ });
118
+ cleanupCallbacks.push(() => {
119
+ host.cancelIdleCallback?.(handle);
120
+ });
121
+ } else {
122
+ const handle = window.setTimeout(() => {
123
+ trigger();
124
+ }, 1);
125
+ cleanupCallbacks.push(() => {
126
+ window.clearTimeout(handle);
127
+ });
128
+ }
129
+ return () => {
130
+ disposed = true;
131
+ while (cleanupCallbacks.length) {
132
+ cleanupCallbacks.pop()?.();
349
133
  }
350
134
  };
351
- this.render();
352
- this.onChange?.(this.getState());
353
135
  }
354
- render() {
355
- this.root.innerHTML = renderInteractiveHtml(this.state.doc, this.state.selection.focus);
136
+ if (target && "addEventListener" in target && "removeEventListener" in target) {
137
+ const eventTarget = target;
138
+ const onActivate = () => {
139
+ trigger();
140
+ };
141
+ eventTarget.addEventListener("pointerenter", onActivate, { once: true, passive: true });
142
+ eventTarget.addEventListener("focusin", onActivate, { once: true });
143
+ cleanupCallbacks.push(() => {
144
+ eventTarget.removeEventListener("pointerenter", onActivate);
145
+ eventTarget.removeEventListener("focusin", onActivate);
146
+ });
356
147
  }
357
- };
358
-
359
- // src/formula-modal.ts
360
- import { createEmptyState as createEmptyState2, parseLatex } from "@formulaxjs/core";
361
- import { mountKityEditor } from "@formulaxjs/kity-runtime";
362
-
363
- // src/formula-node.ts
364
- var DEFAULT_FORMULA_ATTRIBUTE = "data-formulax-latex";
365
- var FORMULA_FLAG_ATTRIBUTE = "data-formulax";
366
- var DEFAULT_FORMULA_CLASS = "formulax-math";
367
- function escapeAttribute(value) {
368
- return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
369
- }
370
- function escapeHtml(value) {
371
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
372
- }
373
- function createFormulaMarkup(latex, options = {}) {
374
- const attributeName = options.attributeName ?? DEFAULT_FORMULA_ATTRIBUTE;
375
- const className = options.className ?? DEFAULT_FORMULA_CLASS;
376
- const displayClass = options.displayMode ? `${className} ${className}--block` : className;
377
- const safeLatex = escapeAttribute(latex);
378
- const cursorStyle = options.cursorStyle?.trim() || "pointer";
379
- const extraAttributes = {
380
- ...options.extraAttributes ?? {},
381
- style: mergeInlineStyles(
382
- typeof options.extraAttributes?.style === "string" ? options.extraAttributes.style : "",
383
- cursorStyle ? `cursor: ${cursorStyle}` : ""
384
- )
148
+ return () => {
149
+ disposed = true;
150
+ while (cleanupCallbacks.length) {
151
+ cleanupCallbacks.pop()?.();
152
+ }
385
153
  };
386
- const serializedAttributes = Object.entries(extraAttributes).filter(([, value]) => value !== null && value !== void 0 && value !== false).map(([key, value]) => value === true ? key : `${key}="${escapeAttribute(String(value))}"`);
387
- return [
388
- "<span",
389
- ` class="${escapeAttribute(displayClass)}"`,
390
- ` ${FORMULA_FLAG_ATTRIBUTE}="true"`,
391
- ` ${attributeName}="${safeLatex}"`,
392
- ` data-latex="${safeLatex}"`,
393
- ' contenteditable="false"',
394
- ' role="button"',
395
- ' tabindex="0"',
396
- serializedAttributes.length ? ` ${serializedAttributes.join(" ")}` : "",
397
- ">",
398
- options.renderHtml ?? `<span class="${escapeAttribute(className)}__render">${escapeHtml(latex || "\\square")}</span>`,
399
- "</span>"
400
- ].join("");
401
- }
402
- function mergeInlineStyles(existingStyle, nextStyle) {
403
- const existing = existingStyle.trim().replace(/;+\s*$/, "");
404
- const next = nextStyle.trim().replace(/;+\s*$/, "");
405
- if (!existing) return next;
406
- if (!next) return existing;
407
- return `${existing}; ${next}`;
408
- }
409
- function createFormulaElement(ownerDocument, latex, options = {}) {
410
- const wrapper = ownerDocument.createElement("span");
411
- wrapper.innerHTML = createFormulaMarkup(latex, options);
412
- return wrapper.firstElementChild;
413
- }
414
- function replaceFormulaElement(target, latex, options = {}) {
415
- const next = createFormulaElement(target.ownerDocument ?? document, latex, options);
416
- if (!next) return null;
417
- target.replaceWith(next);
418
- return next;
419
- }
420
- function getFormulaLatexFromElement(element, attributeName = DEFAULT_FORMULA_ATTRIBUTE) {
421
- return element.getAttribute(attributeName) ?? element.getAttribute("data-latex") ?? "";
422
154
  }
423
- function isFormulaElement(node) {
424
- if (!node || typeof node !== "object") return false;
425
- const element = node;
426
- return typeof element.getAttribute === "function" && element.getAttribute(FORMULA_FLAG_ATTRIBUTE) === "true";
427
- }
428
- function findFormulaElement(node) {
429
- if (!node) return null;
430
- const element = node.nodeType === 1 ? node : node.parentElement;
431
- return element?.closest?.(`[${FORMULA_FLAG_ATTRIBUTE}="true"]`);
155
+ function waitForFormulaXAnimationFrame() {
156
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
157
+ return Promise.resolve();
158
+ }
159
+ return new Promise((resolve) => {
160
+ window.requestAnimationFrame(() => resolve());
161
+ });
432
162
  }
433
163
 
434
164
  // src/formula-modal.ts
@@ -569,8 +299,7 @@ var formulaXModalStyles = `
569
299
  .fx-formula-kity-host .kf-editor svg text,
570
300
  .fx-formula-kity-host .kf-editor-ui-area-item-text,
571
301
  .fx-formula-kity-host .kf-editor-ui-box-item-text,
572
- .fx-formula-kity-host .kf-editor-ui-box-item-val,
573
- .formulax-math__render {
302
+ .fx-formula-kity-host .kf-editor-ui-box-item-val {
574
303
  font-family: "KF AMS MAIN", "Cambria Math", "Latin Modern Math", "Times New Roman", serif !important;
575
304
  }
576
305
 
@@ -645,59 +374,34 @@ var formulaXModalStyles = `
645
374
  background: #2563eb;
646
375
  color: #fff;
647
376
  }
648
-
649
- .formulax-math {
650
- display: inline-flex;
651
- align-items: center;
652
- vertical-align: middle;
653
- line-height: 1;
654
- padding: 0 2px;
655
- margin: 0 1px;
656
- border-radius: 3px;
657
- background: transparent;
658
- cursor: pointer;
659
- user-select: none;
660
- }
661
-
662
- .formulax-math:hover {
663
- outline: 1px solid rgba(37, 99, 235, 0.35);
664
- background: rgba(37, 99, 235, 0.06);
665
- }
666
-
667
- .formulax-math__svg {
668
- display: inline-block;
669
- flex: 0 0 auto;
670
- max-width: 100%;
671
- vertical-align: -0.35em;
672
- pointer-events: none;
673
- }
674
-
675
- .formulax-math__image {
676
- display: inline-block;
677
- max-width: 100%;
678
- height: auto;
679
- vertical-align: middle;
680
- pointer-events: none;
681
- }
682
377
  `;
683
378
  function ensureFormulaXModalStyles(doc = document) {
379
+ ensureFormulaXBaseStyles(doc);
684
380
  if (doc.getElementById(STYLE_ID)) return;
685
381
  const style = doc.createElement("style");
686
382
  style.id = STYLE_ID;
687
383
  style.textContent = formulaXModalStyles;
688
384
  doc.head.appendChild(style);
689
385
  }
690
- function mountFormulaXKityEditor(root, options = {}) {
691
- let destroyed = false;
692
- let latestLatex = options.initialLatex ?? "";
693
- let handle = null;
694
- const initialLatex = latestLatex.trim() ? latestLatex : EMPTY_FORMULA_PLACEHOLDER;
386
+ function renderFormulaXEditorLoadingState(root) {
695
387
  root.classList.add("fx-formula-kity-host");
696
388
  root.innerHTML = `
697
389
  <div class="fx-formula-editor-loading" role="status" aria-live="polite">
698
390
  Loading FormulaX editor...
699
391
  </div>
700
392
  `;
393
+ }
394
+ function mountFormulaXEditor(root, options = {}) {
395
+ recordFormulaXPerfPoint("fx:formula-editor:mount:start");
396
+ const mountStart = markFormulaXPerf("fx:formula-editor:mount:start:scope");
397
+ let destroyed = false;
398
+ let latestLatex = options.initialLatex ?? "";
399
+ let handle = null;
400
+ const initialLatex = latestLatex.trim() ? latestLatex : EMPTY_FORMULA_PLACEHOLDER;
401
+ renderFormulaXEditorLoadingState(root);
402
+ const loadingVisibleMark = markFormulaXPerf("fx:formula-editor:loading-visible");
403
+ measureFormulaXPerf("fx:formula-editor:loading-visible", mountStart, loadingVisibleMark);
404
+ clearFormulaXPerfMarks(loadingVisibleMark);
701
405
  const readyPromise = mountKityEditor(root, {
702
406
  initialLatex,
703
407
  height: options.height ?? "100%",
@@ -711,6 +415,9 @@ function mountFormulaXKityEditor(root, options = {}) {
711
415
  nextHandle.destroy();
712
416
  throw new Error("FormulaX editor mount cancelled");
713
417
  }
418
+ const readyMark = markFormulaXPerf("fx:kity-editor:ready");
419
+ measureFormulaXPerf("fx:kity-editor:ready", mountStart, readyMark);
420
+ clearFormulaXPerfMarks(readyMark);
714
421
  handle = nextHandle;
715
422
  return nextHandle;
716
423
  }).catch((error) => {
@@ -724,6 +431,8 @@ function mountFormulaXKityEditor(root, options = {}) {
724
431
  `;
725
432
  }
726
433
  throw error;
434
+ }).finally(() => {
435
+ clearFormulaXPerfMarks(mountStart);
727
436
  });
728
437
  const getCurrentLatex = async () => {
729
438
  const readyHandle = handle ?? await readyPromise;
@@ -740,17 +449,17 @@ function mountFormulaXKityEditor(root, options = {}) {
740
449
  const latex = await getCurrentLatex();
741
450
  try {
742
451
  return {
743
- ...createEmptyState2(),
452
+ ...createEmptyState(),
744
453
  doc: parseLatex(latex)
745
454
  };
746
455
  } catch {
747
- return createEmptyState2();
456
+ return createEmptyState();
748
457
  }
749
458
  },
750
459
  async getRenderHtml() {
751
460
  await readyPromise;
752
- await waitForFormulaSvgLayout(root);
753
- return renderCurrentFormulaAsSvgHtml(root);
461
+ await waitForKityFormulaSvgLayout(root);
462
+ return serializeKityFormulaFromRoot(root);
754
463
  },
755
464
  destroy() {
756
465
  if (destroyed) return;
@@ -800,399 +509,17 @@ async function tryReadLatexFromKityHandle(handle) {
800
509
  }
801
510
  return null;
802
511
  }
803
- function renderCurrentFormulaAsSvgHtml(root) {
804
- const svg = findFormulaSvg(root);
805
- if (!svg) {
806
- return "";
807
- }
808
- return serializeSvgForInsertion(svg);
809
- }
810
- async function waitForFormulaSvgLayout(root) {
811
- const doc = root.ownerDocument ?? document;
812
- const view = doc.defaultView ?? window;
813
- await waitForDocumentFonts(doc);
814
- let previous = readRenderedFormulaBox(root);
815
- for (let attempt = 0; attempt < 4; attempt += 1) {
816
- await waitForAnimationFrame(view);
817
- const current = readRenderedFormulaBox(root);
818
- if (previous && current && areSvgBoxesClose(previous, current)) {
819
- return;
820
- }
821
- previous = current;
822
- }
823
- }
824
- function findFormulaSvg(root) {
825
- return root.querySelector(
826
- ".kf-editor-edit-area svg, .kf-editor-canvas-container svg, svg"
827
- );
828
- }
829
- function readRenderedFormulaBox(root) {
830
- const svg = findFormulaSvg(root);
831
- if (!svg) {
832
- return null;
833
- }
834
- return getInlineSvgContent(svg)?.box ?? readSvgBox(svg);
835
- }
836
- function areSvgBoxesClose(left, right) {
837
- return Math.abs(left.x - right.x) < 0.01 && Math.abs(left.y - right.y) < 0.01 && Math.abs(left.width - right.width) < 0.01 && Math.abs(left.height - right.height) < 0.01;
838
- }
839
- async function waitForDocumentFonts(doc) {
840
- if (!doc.fonts?.ready) {
841
- return;
842
- }
843
- try {
844
- await doc.fonts.ready;
845
- } catch {
846
- }
847
- }
848
- function waitForAnimationFrame(view) {
849
- return new Promise((resolve) => {
850
- view.requestAnimationFrame(() => resolve());
851
- });
852
- }
853
- function serializeSvgForInsertion(svg) {
854
- const content = getInlineSvgContent(svg);
855
- const inlineViewport = content ? createInlineSvgViewport(content.box) : null;
856
- const clone = content && inlineViewport ? createInlineSvgClone(svg, content, inlineViewport) : svg.cloneNode(true);
857
- uniquifySvgIds(clone);
858
- sizeSvgForInlineDisplay(clone, svg, inlineViewport);
859
- clone.removeAttribute("id");
860
- clone.removeAttribute("xmlns");
861
- clone.removeAttribute("xmlns:xlink");
862
- clone.setAttribute("class", mergeClassNames(clone.getAttribute("class"), "formulax-math__svg"));
863
- clone.setAttribute("focusable", "false");
864
- clone.setAttribute("aria-hidden", "true");
865
- clone.setAttribute("preserveAspectRatio", clone.getAttribute("preserveAspectRatio") || "xMinYMin meet");
866
- return new XMLSerializer().serializeToString(clone);
867
- }
868
- function getInlineSvgContent(svg) {
869
- const candidates = [
870
- '[data-root="true"] > g[data-type="kf-editor-exp-content-box"]',
871
- 'g[data-type="kf-editor-exp-content-box"]',
872
- 'g[data-type="kf-container"]',
873
- "svg > g, g"
874
- ];
875
- for (const selector of candidates) {
876
- const content = svg.querySelector(selector);
877
- const rootSpace = content ? readSvgBoxInRootSpace(content) : null;
878
- if (content && rootSpace) {
879
- return {
880
- root: content,
881
- box: rootSpace.box,
882
- matrix: rootSpace.matrix
883
- };
884
- }
885
- }
886
- return null;
887
- }
888
- function readSvgBoxInRootSpace(element) {
889
- const box = readSvgBox(element);
890
- const matrix = getSvgRootSpaceMatrix(element);
891
- if (!box || !matrix) {
892
- return null;
893
- }
894
- const points = [
895
- { x: box.x, y: box.y },
896
- { x: box.x + box.width, y: box.y },
897
- { x: box.x, y: box.y + box.height },
898
- { x: box.x + box.width, y: box.y + box.height }
899
- ].map((point) => ({
900
- x: matrix.a * point.x + matrix.c * point.y + matrix.e,
901
- y: matrix.b * point.x + matrix.d * point.y + matrix.f
902
- }));
903
- const xs = points.map((point) => point.x);
904
- const ys = points.map((point) => point.y);
905
- const x = Math.min(...xs);
906
- const y = Math.min(...ys);
907
- const width = Math.max(...xs) - x;
908
- const height = Math.max(...ys) - y;
909
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
910
- return null;
911
- }
912
- return {
913
- box: { x, y, width, height },
914
- matrix
915
- };
916
- }
917
- function getSvgRootSpaceMatrix(element) {
918
- const elementMatrix = typeof element.getCTM === "function" ? element.getCTM() : null;
919
- const rootMatrix = typeof element.ownerSVGElement?.getCTM === "function" ? element.ownerSVGElement.getCTM() : null;
920
- if (!elementMatrix) {
921
- return null;
922
- }
923
- return rootMatrix ? multiplySvgMatrices(invertSvgMatrix(rootMatrix), elementMatrix) : toSvgMatrixLike(elementMatrix);
924
- }
925
- function invertSvgMatrix(matrix) {
926
- const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
927
- if (!Number.isFinite(determinant) || determinant === 0) {
928
- return {
929
- a: 1,
930
- b: 0,
931
- c: 0,
932
- d: 1,
933
- e: 0,
934
- f: 0
935
- };
936
- }
937
- return {
938
- a: matrix.d / determinant,
939
- b: -matrix.b / determinant,
940
- c: -matrix.c / determinant,
941
- d: matrix.a / determinant,
942
- e: (matrix.c * matrix.f - matrix.d * matrix.e) / determinant,
943
- f: (matrix.b * matrix.e - matrix.a * matrix.f) / determinant
944
- };
945
- }
946
- function multiplySvgMatrices(left, right) {
947
- return {
948
- a: left.a * right.a + left.c * right.b,
949
- b: left.b * right.a + left.d * right.b,
950
- c: left.a * right.c + left.c * right.d,
951
- d: left.b * right.c + left.d * right.d,
952
- e: left.a * right.e + left.c * right.f + left.e,
953
- f: left.b * right.e + left.d * right.f + left.f
954
- };
955
- }
956
- function toSvgMatrixLike(matrix) {
957
- return {
958
- a: matrix.a,
959
- b: matrix.b,
960
- c: matrix.c,
961
- d: matrix.d,
962
- e: matrix.e,
963
- f: matrix.f
964
- };
965
- }
966
- function readSvgBox(element) {
967
- if (typeof element.getBBox !== "function") {
968
- return null;
969
- }
970
- try {
971
- const box = element.getBBox();
972
- if (!Number.isFinite(box.width) || !Number.isFinite(box.height) || box.width <= 0 || box.height <= 0) {
973
- return null;
974
- }
975
- return {
976
- x: box.x,
977
- y: box.y,
978
- width: box.width,
979
- height: box.height
980
- };
981
- } catch {
982
- return null;
983
- }
984
- }
985
- function createInlineSvgViewport(contentBox) {
986
- const edgePadding = Math.max(0.5, Math.min(contentBox.width, contentBox.height) * 6e-3);
987
- const inset = edgePadding / 2;
988
- return {
989
- x: contentBox.x - inset,
990
- y: contentBox.y - inset,
991
- width: contentBox.width + edgePadding,
992
- height: contentBox.height + edgePadding
993
- };
994
- }
995
- function createInlineSvgClone(source, content, viewport) {
996
- const clone = source.cloneNode(false);
997
- const ownerDocument = source.ownerDocument;
998
- copySvgRootAttributes(source, clone);
999
- clone.setAttribute(
1000
- "viewBox",
1001
- `0 0 ${roundLength(viewport.width)} ${roundLength(viewport.height)}`
1002
- );
1003
- Array.from(source.children).forEach((child) => {
1004
- if (child.tagName.toLowerCase() === "defs") {
1005
- clone.appendChild(child.cloneNode(true));
1006
- }
1007
- });
1008
- const wrapper = ownerDocument.createElementNS("http://www.w3.org/2000/svg", "g");
1009
- wrapper.setAttribute(
1010
- "transform",
1011
- `translate(${roundLength(-viewport.x)} ${roundLength(-viewport.y)})`
1012
- );
1013
- const flattened = ownerDocument.createElementNS("http://www.w3.org/2000/svg", "g");
1014
- flattened.setAttribute(
1015
- "transform",
1016
- `matrix(${roundLength(content.matrix.a)} ${roundLength(content.matrix.b)} ${roundLength(content.matrix.c)} ${roundLength(content.matrix.d)} ${roundLength(content.matrix.e)} ${roundLength(content.matrix.f)})`
1017
- );
1018
- flattened.appendChild(content.root.cloneNode(true));
1019
- wrapper.appendChild(flattened);
1020
- clone.appendChild(wrapper);
1021
- return clone;
1022
- }
1023
- function copySvgRootAttributes(source, target) {
1024
- const excluded = /* @__PURE__ */ new Set([
1025
- "id",
1026
- "width",
1027
- "height",
1028
- "viewBox",
1029
- "class",
1030
- "focusable",
1031
- "aria-hidden",
1032
- "xmlns",
1033
- "xmlns:xlink"
1034
- ]);
1035
- Array.from(source.attributes).forEach((attribute) => {
1036
- if (excluded.has(attribute.name)) return;
1037
- target.setAttribute(attribute.name, attribute.value);
1038
- });
1039
- }
1040
- function sizeSvgForInlineDisplay(clone, source, viewport) {
1041
- const viewBox = clone.viewBox?.baseVal;
1042
- const rect = source.getBoundingClientRect();
1043
- const width = viewport?.width || viewBox?.width || rect.width || Number(clone.getAttribute("width")) || 1;
1044
- const height = viewport?.height || viewBox?.height || rect.height || Number(clone.getAttribute("height")) || 1;
1045
- const ratio = Math.max(0.1, width / Math.max(1, height));
1046
- const inlineHeightEm = 0.875;
1047
- const inlineWidthEm = Math.min(40, Math.max(0.75, ratio * inlineHeightEm));
1048
- clone.setAttribute("width", roundLength(width));
1049
- clone.setAttribute("height", roundLength(height));
1050
- clone.setAttribute(
1051
- "style",
1052
- mergeInlineStyles2(
1053
- clone.getAttribute("style"),
1054
- `width:${roundLength(inlineWidthEm)}em`,
1055
- `height:${inlineHeightEm}em`
1056
- )
1057
- );
1058
- }
1059
- function roundLength(value) {
1060
- return String(Math.round(value * 1e3) / 1e3);
1061
- }
1062
- function uniquifySvgIds(svg) {
1063
- const idMap = /* @__PURE__ */ new Map();
1064
- const prefix = `fx-${randomIdPrefix()}-`;
1065
- const elementsWithId = svg.querySelectorAll("[id]");
1066
- elementsWithId.forEach((element) => {
1067
- const id = element.getAttribute("id");
1068
- if (!id) return;
1069
- const nextId = `${prefix}${id}`;
1070
- idMap.set(id, nextId);
1071
- element.setAttribute("id", nextId);
1072
- });
1073
- if (!idMap.size) return;
1074
- svg.querySelectorAll("*").forEach((element) => {
1075
- Array.from(element.attributes).forEach((attribute) => {
1076
- const nextValue = rewriteSvgReferences(attribute.value, idMap);
1077
- if (nextValue !== attribute.value) {
1078
- element.setAttribute(attribute.name, nextValue);
1079
- }
1080
- });
1081
- });
1082
- }
1083
- function randomIdPrefix() {
1084
- return Math.random().toString(36).slice(2, 5).padEnd(3, "0");
1085
- }
1086
- function rewriteSvgReferences(value, idMap) {
1087
- let nextValue = value;
1088
- idMap.forEach((nextId, id) => {
1089
- nextValue = nextValue.replaceAll(`#${id}`, `#${nextId}`).replaceAll(`url(${id})`, `url(${nextId})`).replaceAll(`url(#${id})`, `url(#${nextId})`);
1090
- });
1091
- return nextValue;
1092
- }
1093
- function mergeClassNames(...values) {
1094
- return values.flatMap((value) => value?.split(/\s+/) ?? []).filter(Boolean).filter((value, index, list) => list.indexOf(value) === index).join(" ");
1095
- }
1096
- function mergeInlineStyles2(...values) {
1097
- return values.flatMap((value) => value?.split(";") ?? []).map((value) => value.trim()).filter(Boolean).join("; ");
1098
- }
1099
-
1100
- // src/i18n.ts
1101
- var translations = {
1102
- en: {
1103
- equation: "Equation",
1104
- structures: "Structures",
1105
- symbols: "Symbols",
1106
- matrices: "Matrices",
1107
- templates: "Templates",
1108
- insert: "Insert",
1109
- greek: "Greek",
1110
- operators: "Operators",
1111
- relations: "Relations",
1112
- fraction: "Fraction",
1113
- superscript: "Superscript",
1114
- subscript: "Subscript",
1115
- squareRoot: "Square Root",
1116
- parentheses: "Parentheses",
1117
- plusMinus: "Plus Minus",
1118
- multiply: "Multiply",
1119
- divide: "Divide",
1120
- dot: "Dot",
1121
- union: "Union",
1122
- intersect: "Intersection",
1123
- lessOrEqual: "Less or Equal",
1124
- greaterOrEqual: "Greater or Equal",
1125
- notEqual: "Not Equal",
1126
- approximate: "Approximate",
1127
- infinity: "Infinity",
1128
- arrow: "Arrow",
1129
- limit: "Limit",
1130
- sine: "Sine",
1131
- logarithm: "Logarithm",
1132
- matrix: "Matrix",
1133
- summation: "Summation",
1134
- integral: "Integral",
1135
- placeholder: "WPS-inspired ribbon layout. Some tiles are placeholders for future SDK features."
1136
- },
1137
- zh: {
1138
- equation: "\u516C\u5F0F",
1139
- structures: "\u7ED3\u6784",
1140
- symbols: "\u7B26\u53F7",
1141
- matrices: "\u77E9\u9635",
1142
- templates: "\u6A21\u677F",
1143
- insert: "\u63D2\u5165",
1144
- greek: "\u5E0C\u814A\u5B57\u6BCD",
1145
- operators: "\u8FD0\u7B97\u7B26",
1146
- relations: "\u5173\u7CFB",
1147
- fraction: "\u5206\u6570",
1148
- superscript: "\u4E0A\u6807",
1149
- subscript: "\u4E0B\u6807",
1150
- squareRoot: "\u5E73\u65B9\u6839",
1151
- parentheses: "\u62EC\u53F7",
1152
- plusMinus: "\u52A0\u51CF",
1153
- multiply: "\u4E58",
1154
- divide: "\u9664",
1155
- dot: "\u70B9\u4E58",
1156
- union: "\u5E76\u96C6",
1157
- intersect: "\u4EA4\u96C6",
1158
- lessOrEqual: "\u5C0F\u4E8E\u7B49\u4E8E",
1159
- greaterOrEqual: "\u5927\u4E8E\u7B49\u4E8E",
1160
- notEqual: "\u4E0D\u7B49\u4E8E",
1161
- approximate: "\u7EA6\u7B49\u4E8E",
1162
- infinity: "\u65E0\u7A77",
1163
- arrow: "\u7BAD\u5934",
1164
- limit: "\u6781\u9650",
1165
- sine: "\u6B63\u5F26",
1166
- logarithm: "\u5BF9\u6570",
1167
- matrix: "\u77E9\u9635",
1168
- summation: "\u6C42\u548C",
1169
- integral: "\u79EF\u5206",
1170
- placeholder: "WPS \u98CE\u683C\u7684\u5DE5\u5177\u680F\u5E03\u5C40\u3002\u90E8\u5206\u6309\u94AE\u4ECD\u662F\u672A\u6765 SDK \u529F\u80FD\u7684\u5360\u4F4D\u9879\u3002"
1171
- }
1172
- };
1173
- function t(locale, key) {
1174
- return translations[locale][key] ?? translations.en[key];
1175
- }
1176
512
  export {
1177
- DEFAULT_FORMULA_ATTRIBUTE,
1178
- DEFAULT_FORMULA_CLASS,
1179
- FORMULA_FLAG_ATTRIBUTE,
1180
- FormulaEditor,
1181
- createFormulaElement,
1182
- createFormulaMarkup,
1183
- editorStyles,
513
+ clearFormulaXPerfMarks,
1184
514
  ensureFormulaXModalStyles,
1185
- escapeAttribute,
1186
- escapeHtml,
1187
- findFormulaElement,
1188
515
  formulaXModalStyles,
1189
- getFormulaLatexFromElement,
1190
- isFormulaElement,
1191
- mountFormulaXKityEditor,
1192
- renderInteractiveHtml,
1193
- replaceFormulaElement,
1194
- serializeSvgForInsertion,
1195
- t,
1196
- translations
516
+ markFormulaXPerf,
517
+ measureFormulaXPerf,
518
+ mountFormulaXEditor,
519
+ preloadFormulaXEditor,
520
+ recordFormulaXPerfPoint,
521
+ renderFormulaXEditorLoadingState,
522
+ scheduleFormulaXEditorPreload,
523
+ waitForFormulaXAnimationFrame
1197
524
  };
1198
525
  //# sourceMappingURL=index.js.map