@beyondwork/docx-react-component 1.0.59 → 1.0.61

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.
Files changed (46) hide show
  1. package/package.json +33 -44
  2. package/src/api/public-types.ts +43 -0
  3. package/src/core/state/editor-state.ts +2 -0
  4. package/src/io/docx-session.ts +167 -8
  5. package/src/io/export/serialize-footnotes.ts +36 -5
  6. package/src/io/export/serialize-headers-footers.ts +7 -0
  7. package/src/io/export/serialize-main-document.ts +25 -18
  8. package/src/io/export/serialize-paragraph-formatting.ts +6 -0
  9. package/src/io/export/serialize-settings.ts +130 -3
  10. package/src/io/normalize/normalize-text.ts +8 -4
  11. package/src/io/ooxml/parse-footnotes.ts +11 -0
  12. package/src/io/ooxml/parse-headers-footers.ts +117 -42
  13. package/src/io/ooxml/parse-main-document.ts +20 -8
  14. package/src/io/ooxml/parse-paragraph-formatting.ts +25 -1
  15. package/src/io/ooxml/parse-settings.ts +91 -1
  16. package/src/io/ooxml/workflow-payload.ts +6 -1
  17. package/src/model/canonical-document.ts +36 -2
  18. package/src/model/snapshot.ts +2 -0
  19. package/src/runtime/diagnostics/build-diagnostic.ts +2 -0
  20. package/src/runtime/diagnostics/code-metadata-table.ts +9 -0
  21. package/src/runtime/document-runtime.ts +770 -21
  22. package/src/runtime/footnote-resolver.ts +32 -8
  23. package/src/runtime/layout/layout-engine-version.ts +7 -1
  24. package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
  25. package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
  26. package/src/runtime/layout/paginated-layout-engine.ts +41 -8
  27. package/src/runtime/layout/resolved-formatting-document.ts +11 -9
  28. package/src/runtime/layout/resolved-formatting-state.ts +4 -0
  29. package/src/runtime/numbering-prefix.ts +26 -2
  30. package/src/runtime/query-scopes.ts +103 -2
  31. package/src/runtime/surface-projection.ts +75 -14
  32. package/src/runtime/table-schema.ts +26 -0
  33. package/src/ui/WordReviewEditor.tsx +25 -0
  34. package/src/ui/editor-runtime-boundary.ts +1 -0
  35. package/src/ui/editor-shell-view.tsx +8 -0
  36. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
  37. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
  38. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
  39. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  40. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
  41. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
  42. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
  43. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
  44. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
  45. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
  46. package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
@@ -0,0 +1,514 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ } from "react";
7
+
8
+ import type { WordReviewEditorRef } from "../../api/public-types";
9
+ import type { DocumentRuntime } from "../../runtime/document-runtime";
10
+
11
+ export interface TwRuntimeReplDialogProps {
12
+ runtime: DocumentRuntime | null;
13
+ /**
14
+ * Optional editor ref. When provided, the REPL exposes it to evaluated
15
+ * expressions as `ref` — e.g. `ref.getRenderSnapshot()`. The REPL reads
16
+ * the live `.current` at evaluation time, so a stable refObject is fine.
17
+ */
18
+ editorRef?: React.RefObject<WordReviewEditorRef | null>;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ interface ReplEntry {
23
+ id: number;
24
+ input: string;
25
+ status: "ok" | "error";
26
+ output: string;
27
+ }
28
+
29
+ const MAX_HISTORY = 30;
30
+ const MAX_ENTRIES = 50;
31
+ const MAX_OUTPUT_CHARS = 8000;
32
+ const HISTORY_STORAGE_KEY = "wre-runtime-repl-history";
33
+ const HISTORY_STORAGE_VERSION = 1;
34
+
35
+ export function TwRuntimeReplDialog(props: TwRuntimeReplDialogProps): React.JSX.Element | null {
36
+ const { runtime, editorRef, disabled = false } = props;
37
+ const [open, setOpen] = useState(false);
38
+ const [input, setInput] = useState("");
39
+ const [entries, setEntries] = useState<readonly ReplEntry[]>([]);
40
+ const [history, setHistory] = useState<readonly string[]>(() =>
41
+ loadPersistedHistory(),
42
+ );
43
+ const [historyIndex, setHistoryIndex] = useState<number | null>(null);
44
+ const openRef = useRef(false);
45
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
46
+ const outputRef = useRef<HTMLDivElement | null>(null);
47
+ const nextEntryIdRef = useRef(1);
48
+
49
+ const appendToHistory = useCallback((code: string) => {
50
+ setHistory((prev) => {
51
+ const next = appendHistoryEntry(prev, code);
52
+ if (next === prev) return prev;
53
+ savePersistedHistory(next);
54
+ return next;
55
+ });
56
+ }, []);
57
+
58
+ useEffect(() => {
59
+ if (disabled) return;
60
+ if (typeof window === "undefined") return;
61
+ const handler = (event: KeyboardEvent): void => {
62
+ if (!isReplToggleShortcut(event)) return;
63
+ event.preventDefault();
64
+ event.stopPropagation();
65
+ const next = !openRef.current;
66
+ openRef.current = next;
67
+ setOpen(next);
68
+ };
69
+ window.addEventListener("keydown", handler, true);
70
+ return () => {
71
+ window.removeEventListener("keydown", handler, true);
72
+ };
73
+ }, [disabled]);
74
+
75
+ useEffect(() => {
76
+ openRef.current = open;
77
+ if (open) {
78
+ const id = requestAnimationFrame(() => {
79
+ textareaRef.current?.focus();
80
+ });
81
+ return () => cancelAnimationFrame(id);
82
+ }
83
+ return undefined;
84
+ }, [open]);
85
+
86
+ useEffect(() => {
87
+ if (!open) return;
88
+ const el = outputRef.current;
89
+ if (el) el.scrollTop = el.scrollHeight;
90
+ }, [entries, open]);
91
+
92
+ const handleClose = useCallback(() => {
93
+ openRef.current = false;
94
+ setOpen(false);
95
+ }, []);
96
+
97
+ const handleClear = useCallback(() => {
98
+ setEntries([]);
99
+ }, []);
100
+
101
+ const pushEntry = useCallback((entry: Omit<ReplEntry, "id">) => {
102
+ setEntries((prev) => {
103
+ const next = [...prev, { ...entry, id: nextEntryIdRef.current }];
104
+ nextEntryIdRef.current += 1;
105
+ return next.length > MAX_ENTRIES ? next.slice(next.length - MAX_ENTRIES) : next;
106
+ });
107
+ }, []);
108
+
109
+ const runEval = useCallback(
110
+ async (code: string): Promise<void> => {
111
+ const trimmed = code.trim();
112
+ if (trimmed.length === 0) return;
113
+ appendToHistory(code);
114
+ if (!runtime) {
115
+ pushEntry({
116
+ input: code,
117
+ status: "error",
118
+ output: "runtime is not available yet",
119
+ });
120
+ return;
121
+ }
122
+ try {
123
+ const value = await evaluateReplExpression(
124
+ code,
125
+ runtime,
126
+ editorRef?.current ?? null,
127
+ );
128
+ pushEntry({
129
+ input: code,
130
+ status: "ok",
131
+ output: formatReplValue(value),
132
+ });
133
+ } catch (error) {
134
+ pushEntry({
135
+ input: code,
136
+ status: "error",
137
+ output: formatReplError(error),
138
+ });
139
+ }
140
+ },
141
+ [appendToHistory, editorRef, pushEntry, runtime],
142
+ );
143
+
144
+ const handleKeyDown = useCallback(
145
+ (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
146
+ if (event.key === "Escape") {
147
+ event.preventDefault();
148
+ handleClose();
149
+ return;
150
+ }
151
+ if (event.key === "Enter" && !event.shiftKey) {
152
+ event.preventDefault();
153
+ const snapshot = input;
154
+ setInput("");
155
+ setHistoryIndex(null);
156
+ void runEval(snapshot);
157
+ return;
158
+ }
159
+ if (event.key === "ArrowUp" && !event.shiftKey && !event.altKey) {
160
+ const textarea = event.currentTarget;
161
+ const isFirstLine = textarea.selectionStart <= (textarea.value.indexOf("\n") === -1 ? textarea.value.length : textarea.value.indexOf("\n"));
162
+ if (!isFirstLine || history.length === 0) return;
163
+ event.preventDefault();
164
+ const nextIndex =
165
+ historyIndex === null
166
+ ? history.length - 1
167
+ : Math.max(0, historyIndex - 1);
168
+ setHistoryIndex(nextIndex);
169
+ setInput(history[nextIndex] ?? "");
170
+ return;
171
+ }
172
+ if (event.key === "ArrowDown" && !event.shiftKey && !event.altKey && historyIndex !== null) {
173
+ const textarea = event.currentTarget;
174
+ const afterLastNewline = textarea.value.lastIndexOf("\n");
175
+ const isLastLine = textarea.selectionStart > afterLastNewline;
176
+ if (!isLastLine) return;
177
+ event.preventDefault();
178
+ const nextIndex = historyIndex + 1;
179
+ if (nextIndex >= history.length) {
180
+ setHistoryIndex(null);
181
+ setInput("");
182
+ } else {
183
+ setHistoryIndex(nextIndex);
184
+ setInput(history[nextIndex] ?? "");
185
+ }
186
+ }
187
+ },
188
+ [handleClose, history, historyIndex, input, runEval],
189
+ );
190
+
191
+ if (!open) return null;
192
+
193
+ return (
194
+ <div
195
+ className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh]"
196
+ role="dialog"
197
+ aria-modal="true"
198
+ aria-labelledby="tw-runtime-repl__title"
199
+ data-testid="tw-runtime-repl"
200
+ >
201
+ <div
202
+ className="absolute inset-0 bg-[var(--color-bg-overlay)] backdrop-blur-sm transition-opacity duration-[var(--motion-fast)]"
203
+ onClick={handleClose}
204
+ data-testid="tw-runtime-repl__backdrop"
205
+ />
206
+ <div
207
+ className={[
208
+ "relative mx-4 flex w-full max-w-3xl flex-col",
209
+ "rounded-[var(--radius-sm)]",
210
+ "bg-[var(--color-bg-elevated)]",
211
+ "shadow-[var(--shadow-float)]",
212
+ "ring-1 ring-[var(--color-border-subtle)]",
213
+ ].join(" ")}
214
+ data-testid="tw-runtime-repl__card"
215
+ >
216
+ <div className="flex items-center justify-between border-b border-[var(--color-border-subtle)] px-4 py-3">
217
+ <div className="flex items-baseline gap-2">
218
+ <h3
219
+ id="tw-runtime-repl__title"
220
+ className="text-sm font-semibold text-[var(--color-text-primary)]"
221
+ >
222
+ Runtime REPL
223
+ </h3>
224
+ <span className="text-xs text-[var(--color-text-tertiary)]">
225
+ evaluates against <code>runtime</code>
226
+ {editorRef ? (
227
+ <>
228
+ {" "}and <code>ref</code>
229
+ </>
230
+ ) : null}
231
+ </span>
232
+ </div>
233
+ <div className="flex items-center gap-2">
234
+ <button
235
+ type="button"
236
+ onClick={handleClear}
237
+ className={[
238
+ "rounded-[var(--radius-sm)] px-2 py-1 text-xs font-medium",
239
+ "text-[var(--color-text-secondary)]",
240
+ "hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-primary)]",
241
+ "transition-colors duration-[var(--motion-fast)]",
242
+ "focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
243
+ ].join(" ")}
244
+ data-testid="tw-runtime-repl__clear"
245
+ >
246
+ Clear
247
+ </button>
248
+ <button
249
+ type="button"
250
+ onClick={handleClose}
251
+ className={[
252
+ "rounded-[var(--radius-sm)] px-2 py-1 text-xs font-medium",
253
+ "text-[var(--color-text-secondary)]",
254
+ "hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-primary)]",
255
+ "transition-colors duration-[var(--motion-fast)]",
256
+ "focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
257
+ ].join(" ")}
258
+ data-testid="tw-runtime-repl__close"
259
+ aria-label="Close REPL"
260
+ >
261
+ Esc
262
+ </button>
263
+ </div>
264
+ </div>
265
+
266
+ <div
267
+ ref={outputRef}
268
+ className="max-h-[50vh] min-h-[160px] overflow-y-auto px-4 py-3 font-mono text-xs leading-relaxed text-[var(--color-text-primary)]"
269
+ data-testid="tw-runtime-repl__output"
270
+ >
271
+ {entries.length === 0 ? (
272
+ <p className="text-[var(--color-text-tertiary)]">
273
+ Evaluate JavaScript against the active runtime
274
+ {editorRef ? " and editor ref" : ""}. Example:
275
+ {" "}
276
+ <code>
277
+ {editorRef
278
+ ? "ref.getRenderSnapshot().surface?.blocks.length"
279
+ : "runtime.getRenderSnapshot().surface?.blocks.length"}
280
+ </code>
281
+ </p>
282
+ ) : (
283
+ entries.map((entry) => (
284
+ <div key={entry.id} className="mb-3 last:mb-0">
285
+ <div className="flex items-start gap-2">
286
+ <span
287
+ aria-hidden="true"
288
+ className="select-none text-[var(--color-text-tertiary)]"
289
+ >
290
+ &gt;
291
+ </span>
292
+ <pre
293
+ className="whitespace-pre-wrap break-all text-[var(--color-text-primary)]"
294
+ data-testid="tw-runtime-repl__entry-input"
295
+ >
296
+ {entry.input}
297
+ </pre>
298
+ </div>
299
+ <pre
300
+ data-testid={
301
+ entry.status === "error"
302
+ ? "tw-runtime-repl__entry-error"
303
+ : "tw-runtime-repl__entry-output"
304
+ }
305
+ className={[
306
+ "mt-1 whitespace-pre-wrap break-all pl-4",
307
+ entry.status === "error"
308
+ ? "text-[var(--color-semantic-error)]"
309
+ : "text-[var(--color-text-secondary)]",
310
+ ].join(" ")}
311
+ >
312
+ {entry.output}
313
+ </pre>
314
+ </div>
315
+ ))
316
+ )}
317
+ </div>
318
+
319
+ <div className="border-t border-[var(--color-border-subtle)] px-4 py-3">
320
+ <label className="sr-only" htmlFor="tw-runtime-repl__input">
321
+ Runtime expression
322
+ </label>
323
+ <textarea
324
+ ref={textareaRef}
325
+ id="tw-runtime-repl__input"
326
+ data-testid="tw-runtime-repl__input"
327
+ value={input}
328
+ onChange={(event) => {
329
+ setInput(event.target.value);
330
+ setHistoryIndex(null);
331
+ }}
332
+ onKeyDown={handleKeyDown}
333
+ rows={3}
334
+ spellCheck={false}
335
+ placeholder={
336
+ editorRef
337
+ ? "ref.getRenderSnapshot().selection"
338
+ : "runtime.getRenderSnapshot().selection"
339
+ }
340
+ className={[
341
+ "w-full resize-y rounded-[var(--radius-sm)] bg-[var(--color-bg-muted)]",
342
+ "px-3 py-2 font-mono text-xs leading-relaxed",
343
+ "text-[var(--color-text-primary)]",
344
+ "placeholder:text-[var(--color-text-tertiary)]",
345
+ "ring-1 ring-[var(--color-border-subtle)]",
346
+ "focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
347
+ ].join(" ")}
348
+ />
349
+ <div className="mt-1.5 flex items-center justify-between text-[10px] text-[var(--color-text-tertiary)]">
350
+ <span>Enter to evaluate · Shift+Enter for newline · ↑/↓ history · Esc to close</span>
351
+ <span>{entries.length > 0 ? `${entries.length} entries` : ""}</span>
352
+ </div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ );
357
+ }
358
+
359
+ export function isReplToggleShortcut(event: KeyboardEvent): boolean {
360
+ const isP =
361
+ event.code === "KeyP" ||
362
+ event.key === "p" ||
363
+ event.key === "P" ||
364
+ event.key === "π";
365
+ if (!isP) return false;
366
+ if (!event.altKey) return false;
367
+ if (event.shiftKey) return false;
368
+ return event.metaKey || event.ctrlKey;
369
+ }
370
+
371
+ export async function evaluateReplExpression(
372
+ code: string,
373
+ runtime: DocumentRuntime,
374
+ ref: WordReviewEditorRef | null = null,
375
+ ): Promise<unknown> {
376
+ type ReplFn = (
377
+ runtime: DocumentRuntime,
378
+ ref: WordReviewEditorRef | null,
379
+ ) => Promise<unknown>;
380
+ let fn: ReplFn | null = null;
381
+ try {
382
+ fn = new Function(
383
+ "runtime",
384
+ "ref",
385
+ `return (async () => { return (${code}); })();`,
386
+ ) as ReplFn;
387
+ } catch (exprError) {
388
+ if (!(exprError instanceof SyntaxError)) throw exprError;
389
+ }
390
+ if (fn === null) {
391
+ fn = new Function(
392
+ "runtime",
393
+ "ref",
394
+ `return (async () => { ${code}\n })();`,
395
+ ) as ReplFn;
396
+ }
397
+ return await fn(runtime, ref);
398
+ }
399
+
400
+ export function formatReplValue(value: unknown): string {
401
+ if (value === undefined) return "undefined";
402
+ if (value === null) return "null";
403
+ const type = typeof value;
404
+ if (type === "string") return JSON.stringify(value);
405
+ if (type === "number" || type === "boolean" || type === "bigint") {
406
+ return String(value);
407
+ }
408
+ if (type === "symbol") return value.toString();
409
+ if (type === "function") {
410
+ const fn = value as (...args: unknown[]) => unknown;
411
+ return `[Function${fn.name ? `: ${fn.name}` : ""}]`;
412
+ }
413
+ try {
414
+ const serialized = JSON.stringify(value, createCircularReplacer(), 2);
415
+ if (serialized === undefined) {
416
+ return String(value);
417
+ }
418
+ return truncate(serialized, MAX_OUTPUT_CHARS);
419
+ } catch (error) {
420
+ return `[unserializable value: ${formatReplError(error)}]`;
421
+ }
422
+ }
423
+
424
+ export function formatReplError(error: unknown): string {
425
+ if (error instanceof Error) {
426
+ const name = error.name || "Error";
427
+ return truncate(`${name}: ${error.message}`, MAX_OUTPUT_CHARS);
428
+ }
429
+ return truncate(String(error), MAX_OUTPUT_CHARS);
430
+ }
431
+
432
+ function createCircularReplacer(): (key: string, value: unknown) => unknown {
433
+ const seen = new WeakSet<object>();
434
+ return function replacer(_key: string, value: unknown): unknown {
435
+ if (typeof value === "bigint") return `${value.toString()}n`;
436
+ if (typeof value === "function") {
437
+ const fn = value as (...args: unknown[]) => unknown;
438
+ return `[Function${fn.name ? `: ${fn.name}` : ""}]`;
439
+ }
440
+ if (typeof value === "symbol") return value.toString();
441
+ if (value !== null && typeof value === "object") {
442
+ if (seen.has(value as object)) return "[Circular]";
443
+ seen.add(value as object);
444
+ }
445
+ return value;
446
+ };
447
+ }
448
+
449
+ function truncate(text: string, max: number): string {
450
+ if (text.length <= max) return text;
451
+ return `${text.slice(0, max)}\n... [truncated ${text.length - max} chars]`;
452
+ }
453
+
454
+ export function appendHistoryEntry(
455
+ history: readonly string[],
456
+ code: string,
457
+ ): readonly string[] {
458
+ const trimmed = code.trim();
459
+ if (trimmed.length === 0) return history;
460
+ if (history.length > 0 && history[history.length - 1] === trimmed) {
461
+ return history;
462
+ }
463
+ return history.length >= MAX_HISTORY
464
+ ? [...history.slice(history.length - MAX_HISTORY + 1), trimmed]
465
+ : [...history, trimmed];
466
+ }
467
+
468
+ export function loadPersistedHistory(): readonly string[] {
469
+ if (typeof window === "undefined") return [];
470
+ try {
471
+ const raw = window.localStorage.getItem(HISTORY_STORAGE_KEY);
472
+ if (!raw) return [];
473
+ const parsed: unknown = JSON.parse(raw);
474
+ if (
475
+ typeof parsed !== "object" ||
476
+ parsed === null ||
477
+ !("version" in parsed) ||
478
+ !("entries" in parsed)
479
+ ) {
480
+ return [];
481
+ }
482
+ const record = parsed as { version: unknown; entries: unknown };
483
+ if (record.version !== HISTORY_STORAGE_VERSION) return [];
484
+ if (!Array.isArray(record.entries)) return [];
485
+ const strings = record.entries.filter(
486
+ (entry): entry is string => typeof entry === "string" && entry.length > 0,
487
+ );
488
+ return strings.slice(-MAX_HISTORY);
489
+ } catch {
490
+ return [];
491
+ }
492
+ }
493
+
494
+ export function savePersistedHistory(history: readonly string[]): void {
495
+ if (typeof window === "undefined") return;
496
+ try {
497
+ const payload = JSON.stringify({
498
+ version: HISTORY_STORAGE_VERSION,
499
+ entries: history.slice(-MAX_HISTORY),
500
+ });
501
+ window.localStorage.setItem(HISTORY_STORAGE_KEY, payload);
502
+ } catch {
503
+ return;
504
+ }
505
+ }
506
+
507
+ export function clearPersistedHistory(): void {
508
+ if (typeof window === "undefined") return;
509
+ try {
510
+ window.localStorage.removeItem(HISTORY_STORAGE_KEY);
511
+ } catch {
512
+ return;
513
+ }
514
+ }
@@ -498,6 +498,8 @@ export const editorSchema = new Schema({
498
498
  wrapMode: { default: null },
499
499
  distMargins: { default: null },
500
500
  positionH: { default: null },
501
+ positionV: { default: null },
502
+ renderInPageOverlay: { default: false },
501
503
  // Lane 6d N9.b — polygon clip for tight/through wrap.
502
504
  wrapPolygon: { default: null },
503
505
  // Lane 6d N11.b — CSS filter effects (soft-edge, outer shadow, glow).
@@ -508,9 +510,21 @@ export const editorSchema = new Schema({
508
510
  toDOM(node) {
509
511
  const isMissing = node.attrs.state === "missing";
510
512
  const isFloating = node.attrs.display === "floating";
513
+ const renderInPageOverlay = Boolean(node.attrs.renderInPageOverlay);
511
514
  const src = node.attrs.src as string | null;
512
515
  const widthEmu = node.attrs.widthEmu as number | null;
513
516
  const heightEmu = node.attrs.heightEmu as number | null;
517
+ if (renderInPageOverlay && isFloating) {
518
+ return [
519
+ "span",
520
+ {
521
+ class: "inline-block h-0 w-0 overflow-hidden align-middle",
522
+ "data-node-type": "image-floating-anchor",
523
+ title: (node.attrs.detail as string) ?? (node.attrs.altText as string) ?? "Floating image anchor",
524
+ "aria-hidden": "true",
525
+ },
526
+ ];
527
+ }
514
528
  if (!isMissing && src) {
515
529
  const widthPx = widthEmu ? Math.max(24, Math.round(widthEmu / EMU_PER_PX)) : undefined;
516
530
  const heightPx = heightEmu ? Math.max(24, Math.round(heightEmu / EMU_PER_PX)) : undefined;