@hwp-editor/react 1.3.0 → 1.4.1

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
@@ -26,10 +26,10 @@ function useHwpEditorContext() {
26
26
 
27
27
  // src/PageCanvas.tsx
28
28
  import { useMemo } from "react";
29
- import { base64, segmentAtRef, segmentText, selectionAction } from "@hwp-editor/core";
29
+ import { base64, segmentAtRef, segmentText as segmentText2, selectionAction } from "@hwp-editor/core";
30
30
 
31
31
  // src/geometry.ts
32
- import { segmentAtOffset, markdownLength } from "@hwp-editor/core";
32
+ import { segmentAtOffset, markdownLength, segmentIdPath } from "@hwp-editor/core";
33
33
  function clamp(value, min, max) {
34
34
  return Math.min(max, Math.max(min, value));
35
35
  }
@@ -81,21 +81,46 @@ function idIndex(envelope) {
81
81
  idIndexes.set(envelope, index);
82
82
  return index;
83
83
  }
84
+ var pathIndexes = /* @__PURE__ */ new WeakMap();
85
+ function pathIndex(envelope) {
86
+ const cached = pathIndexes.get(envelope);
87
+ if (cached !== void 0) return cached;
88
+ const index = /* @__PURE__ */ new Map();
89
+ for (const segment of envelope.segments) {
90
+ if (segment.id !== void 0 || segment.kind !== "para") continue;
91
+ const key = [segment.section, ...segment.indices ?? [segment.para]].join(".");
92
+ if (!index.has(key)) index.set(key, segment);
93
+ }
94
+ pathIndexes.set(envelope, index);
95
+ return index;
96
+ }
97
+ function rowSegment(envelope, rowId) {
98
+ const byId = idIndex(envelope).get(rowId);
99
+ if (byId !== void 0) return byId;
100
+ const path = segmentIdPath(rowId);
101
+ return path.length === 0 ? void 0 : pathIndex(envelope).get(path.join("."));
102
+ }
103
+ function hasIds(envelope) {
104
+ return envelope.segments.some((segment) => segment.id !== void 0);
105
+ }
106
+ function rowDraws(envelope, rowId, segment) {
107
+ if (segment.id !== void 0) return rowId === segment.id;
108
+ return rowSegment(envelope, rowId) === segment;
109
+ }
84
110
  function segmentOnPage(envelope, page, xFraction, yFraction, pageIndex, pageCount) {
85
- if (page.layout === void 0 || !envelope.segments.some((segment) => segment.id !== void 0)) {
111
+ if (page.layout === void 0 || !hasIds(envelope) && page.layout.rows.every((row) => rowSegment(envelope, row.id) === void 0)) {
86
112
  return nearestSegment(envelope, pageIndex, yFraction, pageCount);
87
113
  }
88
114
  const { width_pt, height_pt, rows } = page.layout;
89
115
  const x = clamp(xFraction, 0, 1) * width_pt;
90
116
  const y = clamp(yFraction, 0, 1) * height_pt;
91
- const byId = idIndex(envelope);
92
117
  let best;
93
118
  let bestVertical = Infinity;
94
119
  let bestHorizontal = Infinity;
95
120
  let bestArea = Infinity;
96
121
  for (const row of rows) {
97
122
  const box = row.box;
98
- const segment = byId.get(row.id);
123
+ const segment = rowSegment(envelope, row.id);
99
124
  if (box === null || segment === void 0) continue;
100
125
  const dx = Math.max(box.x0 - x, 0, x - box.x1);
101
126
  const dy = Math.max(box.y0 - y, 0, y - box.y1);
@@ -112,13 +137,13 @@ function segmentOnPage(envelope, page, xFraction, yFraction, pageIndex, pageCoun
112
137
  return best;
113
138
  }
114
139
  function bandsOnPage(envelope, segment, page, pageIndex, pageCount) {
115
- if (page.layout === void 0 || segment.id === void 0) {
140
+ if (page.layout === void 0) {
116
141
  const band = segmentBand(envelope, segment, pageCount);
117
- return band?.pageIndex === pageIndex ? [{ left: 0, top: band.top, width: 100, height: band.height }] : [];
142
+ return band?.pageIndex === pageIndex ? [{ left: 0, top: band.top, width: 100, height: band.height, approximate: true }] : [];
118
143
  }
119
144
  const target = ["run", "image", "field"].includes(segment.kind) && segment.indices !== void 0 ? envelope.segments.find((candidate) => candidate.kind === "para" && candidate.section === segment.section && candidate.indices !== void 0 && candidate.indices.length === segment.indices.length - 1 && candidate.indices.every((value, index) => value === segment.indices[index])) ?? segment : segment;
120
145
  const { width_pt, height_pt, rows } = page.layout;
121
- return rows.filter((row) => row.id === target.id && row.box !== null).map((row) => {
146
+ return rows.filter((row) => row.box !== null && rowDraws(envelope, row.id, target)).map((row) => {
122
147
  const box = row.box;
123
148
  return {
124
149
  left: box.x0 / width_pt * 100,
@@ -129,32 +154,8 @@ function bandsOnPage(envelope, segment, page, pageIndex, pageCount) {
129
154
  });
130
155
  }
131
156
 
132
- // src/sanitize.ts
133
- function sanitizeSvg(svgText) {
134
- const doc = new DOMParser().parseFromString(svgText, "image/svg+xml");
135
- const root = doc.documentElement;
136
- if (root === null || root.nodeName === "parsererror" || doc.getElementsByTagName("parsererror").length > 0) {
137
- return "";
138
- }
139
- const dangerous = root.querySelectorAll("script, foreignObject");
140
- for (const node of Array.from(dangerous)) node.remove();
141
- scrubElement(root);
142
- return root.outerHTML;
143
- }
144
- function scrubElement(el) {
145
- for (const attr of Array.from(el.attributes)) {
146
- const name = attr.name.toLowerCase();
147
- const value = attr.value.trim().toLowerCase();
148
- if (name.startsWith("on")) {
149
- el.removeAttribute(attr.name);
150
- } else if ((name === "href" || name === "xlink:href") && value.startsWith("javascript:")) {
151
- el.removeAttribute(attr.name);
152
- }
153
- }
154
- for (const child of Array.from(el.children)) scrubElement(child);
155
- }
156
-
157
157
  // src/text.ts
158
+ import { segmentIdPath as segmentIdPath2, segmentText } from "@hwp-editor/core";
158
159
  function plainSegmentText(markdownSlice) {
159
160
  return markdownSlice.split("\n").map(stripLine).join("\n").trim();
160
161
  }
@@ -168,6 +169,41 @@ function stripLine(line) {
168
169
  out = out.replace(/`([^`]*)`/g, "$1");
169
170
  return out;
170
171
  }
172
+ var LIST_MARKER = /^[ \t]*(?:\d{1,9}[.)]|[-*+])[ \t]+/;
173
+ function paragraphSourceText(envelope, para, pages = []) {
174
+ const plain = plainSegmentText(segmentText(envelope, para));
175
+ const marker = LIST_MARKER.exec(plain);
176
+ if (marker === null) return plain;
177
+ const stripped = plain.slice(marker[0].length);
178
+ if (para.indices !== void 0) {
179
+ const first = envelope.segments.find((candidate) => candidate.kind === "run" && candidate.section === para.section && candidate.indices !== void 0 && candidate.indices.length === para.indices.length + 1 && para.indices.every((value, index) => candidate.indices[index] === value));
180
+ if (first !== void 0) {
181
+ const lead = [...segmentText(envelope, para)].slice(0, Math.max(0, first.start - para.start)).join("");
182
+ return LIST_MARKER.test(lead) && lead.trim() === marker[0].trim() ? stripped : plain;
183
+ }
184
+ }
185
+ const length = sourceLength(para, pages);
186
+ if (length === void 0) return plain;
187
+ return stripped.length === length && plain.length !== length ? stripped : plain;
188
+ }
189
+ function sourceLength(para, pages) {
190
+ let start = Infinity;
191
+ let end = -Infinity;
192
+ for (const page of pages) {
193
+ for (const row of page.layout?.rows ?? []) {
194
+ if (row.kind !== "para" || row.source_chars === null || !rowMatches(row.id, para)) continue;
195
+ start = Math.min(start, row.source_chars.start);
196
+ end = Math.max(end, row.source_chars.end);
197
+ }
198
+ }
199
+ return end >= start ? end - start : void 0;
200
+ }
201
+ function rowMatches(rowId, segment) {
202
+ if (segment.id !== void 0) return rowId === segment.id;
203
+ const path = segmentIdPath2(rowId);
204
+ const own = [segment.section, ...segment.indices ?? [segment.para]];
205
+ return path.length === own.length && path.every((value, index) => value === own[index]);
206
+ }
171
207
 
172
208
  // src/PageCanvas.tsx
173
209
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -186,7 +222,7 @@ function PageCanvas() {
186
222
  if (segment !== void 0) selectedSegments.add(segment);
187
223
  }
188
224
  }
189
- if (pages.length === 0) {
225
+ if (pages.length === 0 || envelope === null) {
190
226
  return /* @__PURE__ */ jsx("div", { className: "hwped-canvas", role: "main", "aria-label": t("canvas.aria"), children: /* @__PURE__ */ jsx("div", { className: "hwped-empty", children: t("canvas.emptyPages") }) });
191
227
  }
192
228
  const selectAt = (pageIndex, xFraction, yFraction, modifiers) => {
@@ -240,7 +276,7 @@ function bandsForPage(envelope, page, pageIndex, pageCount, selected, pendingKey
240
276
  bands.push({
241
277
  ...band,
242
278
  key: `${isSelected ? "sel" : "pend"}-${key}-${bandIdentity(segment)}-${index}`,
243
- className: isSelected ? "hwped-band hwped-band-selected" : "hwped-band hwped-band-pending"
279
+ className: (isSelected ? "hwped-band hwped-band-selected" : "hwped-band hwped-band-pending") + (band.approximate ? " hwped-band-approximate" : "")
244
280
  });
245
281
  }
246
282
  }
@@ -250,13 +286,8 @@ function PageView(props) {
250
286
  const { page, onClick, onActivate, bands } = props;
251
287
  const { t } = useHwpEditorContext();
252
288
  const label = t("page.label", { page: page.page });
253
- const svgHtml = useMemo(
254
- () => page.format === "svg" ? sanitizeSvg(new TextDecoder().decode(page.data)) : null,
255
- [page]
256
- );
257
289
  const imgSrc = useMemo(() => {
258
- if (page.format === "svg") return null;
259
- const mime = page.format === "jpeg" ? "image/jpeg" : `image/${page.format}`;
290
+ const mime = page.format === "jpeg" ? "image/jpeg" : page.format === "svg" ? "image/svg+xml" : `image/${page.format}`;
260
291
  return `data:${mime};base64,${base64.encode(page.data)}`;
261
292
  }, [page]);
262
293
  return /* @__PURE__ */ jsxs(
@@ -275,17 +306,11 @@ function PageView(props) {
275
306
  }
276
307
  },
277
308
  children: [
278
- svgHtml !== null ? /* @__PURE__ */ jsx(
279
- "div",
280
- {
281
- className: "hwped-page-svg",
282
- dangerouslySetInnerHTML: { __html: svgHtml }
283
- }
284
- ) : /* @__PURE__ */ jsx(
309
+ /* @__PURE__ */ jsx(
285
310
  "img",
286
311
  {
287
312
  className: "hwped-page-img",
288
- src: imgSrc ?? "",
313
+ src: imgSrc,
289
314
  alt: label,
290
315
  width: page.width,
291
316
  height: page.height
@@ -295,6 +320,7 @@ function PageView(props) {
295
320
  "div",
296
321
  {
297
322
  className: band.className,
323
+ "data-approximate": band.className.includes("hwped-band-approximate") ? "true" : void 0,
298
324
  style: { top: `${band.top}%`, height: `${band.height}%`, left: `${band.left}%`, width: `${band.width}%` }
299
325
  },
300
326
  band.key
@@ -327,7 +353,7 @@ function targetsSegment(op, envelope, segment) {
327
353
  return "run" in at ? segment.kind === "run" && segment.indices?.length === 2 && segment.indices[1] === at.run : segment.kind === "para" && (segment.indices === void 0 || segment.indices.length === 1);
328
354
  }
329
355
  const target = opSearchText(op);
330
- return target !== null && target !== "" && plainSegmentText(segmentText(envelope, segment)).includes(target);
356
+ return target !== null && target !== "" && plainSegmentText(segmentText2(envelope, segment)).includes(target);
331
357
  }
332
358
 
333
359
  // src/unit-input.ts
@@ -338,7 +364,7 @@ function isPlainDecimal(value) {
338
364
 
339
365
  // src/SegmentInspector.tsx
340
366
  import { useEffect, useMemo as useMemo2, useRef, useState } from "react";
341
- import { compareSegments, documentFormat, formatTargets as collectFormatTargets, hasDirectFormatting, hasOwnRef, isSectionDefinitionParagraph, paragraphAddress, paragraphRuns, resolveSelection, segmentAtRef as segmentAtRef2, segmentIdPath, segmentPath, segmentRef, segmentText as segmentText2, selectSimilar, selectedParagraphs, selectionAction as selectionAction2, stylesVersionSupported } from "@hwp-editor/core";
367
+ import { compareSegments, documentFormat, formatTargets as collectFormatTargets, hasDirectFormatting, hasOwnRef, isSectionDefinitionParagraph, paragraphAddress, paragraphRuns, resolveSelection, segmentAtRef as segmentAtRef2, segmentIdPath as segmentIdPath3, segmentPath, segmentRef, segmentText as segmentText3, selectSimilar, selectedParagraphs, selectionAction as selectionAction2, stylesVersionSupported } from "@hwp-editor/core";
342
368
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
343
369
  var ALIGNMENTS = [
344
370
  { value: "left", key: "segment.alignLeft" },
@@ -354,6 +380,14 @@ var SHAPE_KEYS = [
354
380
  { value: "top", key: "segment.shape.top" },
355
381
  { value: "bottom", key: "segment.shape.bottom" }
356
382
  ];
383
+ var CLICK_REFUSAL_MESSAGE = {
384
+ cell: "segment.clickRefusedCell",
385
+ table: "segment.clickRefusedTable",
386
+ container: "segment.clickRefusedContainer",
387
+ unaddressable: "segment.clickRefusedUnaddressable",
388
+ unaddressableRuns: "segment.clickRefusedUnaddressableRuns",
389
+ noRuns: "segment.clickRefusedNoRuns"
390
+ };
357
391
  function SegmentInspector() {
358
392
  const { state, store, envelope, editable, capabilities, styles, stylesStatus, t, insertedHidden } = useHwpEditorContext();
359
393
  const { selection } = state;
@@ -378,11 +412,11 @@ function SegmentInspector() {
378
412
  })();
379
413
  const directStyleWhy = directStyleRow !== void 0 ? null : stylesStatus === "loading" ? "segment.directStyleLoading" : stylesStatus === "failed" ? "segment.directStyleFailed" : stylesStatus === "unsupported" ? "styles.unavailableHost" : capabilities === null ? "styles.checkingHost" : capabilities.styles !== "v1" ? stylesVersionSupported(capabilities.version) ? "styles.unavailableHost" : "segment.directStyleUnavailable" : "segment.directStyleUnknown";
380
414
  const refusalText = (reason) => reason === null || reason === void 0 ? null : reason === "cell" ? t("segment.cellSelectionRefused") : reason === "table" ? t("segment.tableSelectionRefused") : reason === "container" ? t("segment.containerSelectionRefused") : t("segment.noParagraphSelection");
381
- const clickRefusedText = (reason) => reason === null || reason === void 0 ? null : reason === "cell" ? t("segment.clickRefusedCell") : reason === "table" ? t("segment.clickRefusedTable") : reason === "container" ? t("segment.clickRefusedContainer") : t("segment.clickRefusedUnaddressable");
415
+ const clickRefusedText = (reason) => reason === null || reason === void 0 ? null : t(CLICK_REFUSAL_MESSAGE[reason]);
382
416
  const refusalReason2 = refusalText(targets?.refusal);
383
417
  const targetLabel = segment === void 0 ? "" : segment.kind === "cell" ? t("segment.selectedCell", { path: segmentPath(segment), count: paragraphTargets.length }) : singleParagraph === void 0 ? "" : t("segment.selectedPara", { path: segmentPath(singleParagraph) });
384
- const currentText = envelope !== null && paragraphTargets.length > 0 ? plainSegmentText(paragraphTargets.map((candidate) => segmentText2(envelope, candidate)).join("\n")) : "";
385
- const markdownText = envelope !== null && singleParagraph !== void 0 ? segmentText2(envelope, singleParagraph) : "";
418
+ const currentText = envelope !== null && paragraphTargets.length > 0 ? paragraphTargets.map((candidate) => paragraphSourceText(envelope, candidate, state.pages)).join("\n").trim() : "";
419
+ const markdownText = envelope !== null && singleParagraph !== void 0 ? segmentText3(envelope, singleParagraph) : "";
386
420
  const supplementaryStarts = useMemo2(() => {
387
421
  const positions = [];
388
422
  let offset = 0;
@@ -441,7 +475,7 @@ function SegmentInspector() {
441
475
  if (insertionRun === null || !insertionCurrent || runParent === void 0) return [];
442
476
  const section = insertionRun[0].section;
443
477
  return insertedHidden.ids.flatMap((id) => {
444
- const [idSection, ...indices] = segmentIdPath(id);
478
+ const [idSection, ...indices] = segmentIdPath3(id);
445
479
  if (idSection !== section || indices.length !== runParent.length + 1) return [];
446
480
  if (!runParent.every((index, at) => indices[at] === index)) return [];
447
481
  return [{ id, index: indices[indices.length - 1] }];
@@ -557,7 +591,10 @@ function SegmentInspector() {
557
591
  type: "selectMany",
558
592
  selections: refs,
559
593
  primary,
560
- ...state.selectionAnchor === null || state.selectionAnchor === void 0 ? { anchor: primary } : { anchor: state.selectionAnchor }
594
+ // A matching-character-format selection is a new run selection, so its
595
+ // anchor is its own primary (#71 rule 7), never a run left over from an
596
+ // earlier one. The paragraph level keeps an existing anchor as before.
597
+ ...level === "char" || state.selectionAnchor === null || state.selectionAnchor === void 0 ? { anchor: primary } : { anchor: state.selectionAnchor }
561
598
  });
562
599
  };
563
600
  const moveParagraph = (direction) => {
@@ -645,7 +682,7 @@ function SegmentInspector() {
645
682
  style: { maxHeight: "12rem", overflowY: "auto" },
646
683
  children: paragraphs.map((candidate, index) => {
647
684
  const isSelected = selected.some((item) => item === candidate || item.id !== void 0 && item.id === candidate.id || item.id === void 0 && candidate.id === void 0 && item.section === candidate.section && item.para === candidate.para);
648
- const snippet = plainSegmentText(segmentText2(envelope, candidate)).trim().replace(/\s+/g, " ");
685
+ const snippet = plainSegmentText(segmentText3(envelope, candidate)).trim().replace(/\s+/g, " ");
649
686
  const text = snippet.length > 96 ? `${snippet.slice(0, 93)}...` : snippet || t("segment.emptyPara");
650
687
  const label = t("segment.choiceLabel", { index: index + 1, text });
651
688
  return /* @__PURE__ */ jsx2(
@@ -671,9 +708,9 @@ function SegmentInspector() {
671
708
  // state holds and the targets the format op will address.
672
709
  children: hasRunSelection ? t("segment.selectedRunCount", { count: explicitSegments.length }) : t("segment.selectedCount", { count: selected.length })
673
710
  }),
674
- // A run-level selection formats runs, and a modifier click from it
675
- // starts a paragraph selection over instead of joining the runs.
676
- hasRunSelection && /* @__PURE__ */ jsx2("p", { className: "hwped-hint", role: "status", children: t("segment.runSelectionTerminal") }),
711
+ // A run-level selection formats runs, and a modifier click keeps it at
712
+ // run level: Ctrl toggles a unit's runs, Shift extends by run (#71).
713
+ hasRunSelection && /* @__PURE__ */ jsx2("p", { className: "hwped-hint", role: "status", children: t("segment.runSelectionGestures") }),
677
714
  // A modifier click that had no paragraph to add left the selection
678
715
  // alone, and reports the click here whatever the selection's own
679
716
  // reason is: the reader's action is what was refused, and a click
@@ -1542,7 +1579,7 @@ import { useEffect as useEffect3, useState as useState3 } from "react";
1542
1579
  import { tableCountMismatch } from "@hwp-editor/core";
1543
1580
 
1544
1581
  // src/tables.ts
1545
- import { segmentText as segmentText3 } from "@hwp-editor/core";
1582
+ import { segmentText as segmentText4 } from "@hwp-editor/core";
1546
1583
  var GFM_DELIMITER_CELL = /^:?-{2,}:?$/;
1547
1584
  function endsWithCellDelimiter(row) {
1548
1585
  if (!row.endsWith("|")) return false;
@@ -1609,7 +1646,7 @@ function findTables(envelope) {
1609
1646
  tables.push({
1610
1647
  tableIndex: tables.length,
1611
1648
  segment,
1612
- rows: parseMarkdownTable(segmentText3(envelope, segment))
1649
+ rows: parseMarkdownTable(segmentText4(envelope, segment))
1613
1650
  });
1614
1651
  }
1615
1652
  return tables;
@@ -1914,7 +1951,7 @@ function TableGrid() {
1914
1951
 
1915
1952
  // src/FieldsPanel.tsx
1916
1953
  import { useMemo as useMemo3, useState as useState4 } from "react";
1917
- import { segmentAtRef as segmentAtRef4, segmentText as segmentText4 } from "@hwp-editor/core";
1954
+ import { segmentAtRef as segmentAtRef4, segmentText as segmentText5 } from "@hwp-editor/core";
1918
1955
 
1919
1956
  // src/fields.ts
1920
1957
  import { resolveSelection as resolveSelection3, segmentRef as segmentRef3 } from "@hwp-editor/core";
@@ -1983,7 +2020,7 @@ function FieldsPanel() {
1983
2020
  legacy && /* @__PURE__ */ jsx5("p", { className: "hwped-hint", children: t("fields.legacySeparatorHint") }),
1984
2021
  slots.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "hwped-hint", children: t("fields.hint") }) : /* @__PURE__ */ jsx5("ul", { className: "hwped-fields", children: slots.map((slot) => {
1985
2022
  const segment = envelope === null ? void 0 : segmentAtRef4(envelope, slot.segment);
1986
- const snippet = envelope === null || segment === void 0 ? "" : segmentText4(envelope, segment).trim().slice(0, 40);
2023
+ const snippet = envelope === null || segment === void 0 ? "" : segmentText5(envelope, segment).trim().slice(0, 40);
1987
2024
  const draft = drafts[slot.name] ?? "";
1988
2025
  return /* @__PURE__ */ jsxs5("li", { className: "hwped-field-item", children: [
1989
2026
  /* @__PURE__ */ jsx5("div", { className: "hwped-row", children: /* @__PURE__ */ jsx5(
@@ -3834,7 +3871,7 @@ var en = {
3834
3871
  "segment.rangeTextEditHint": "A character range formats text but cannot rewrite it. Use whole paragraph to edit its text.",
3835
3872
  "segment.runTextEditHint": "A run selection formats runs only, so the text controls are off. Select a paragraph to edit its text.",
3836
3873
  "segment.runSelectionParagraphs": (p) => p.count === 1 ? "Alignment and spacing apply to the 1 paragraph containing a selected run." : `Alignment and spacing apply to all ${p.count} paragraphs containing a selected run.`,
3837
- "segment.runSelectionTerminal": "Starts a new paragraph selection",
3874
+ "segment.runSelectionGestures": "Ctrl adds or removes a paragraph's runs; Shift extends by run.",
3838
3875
  "segment.noParagraphSelection": "This selection carries no paragraph the engine can address, so paragraph edits are off. Pick a paragraph from the list above.",
3839
3876
  "segment.cellSelectionRefused": "This selection is a table cell. Alignment and spacing apply to every paragraph in the cell, but replace, insert and delete address exactly one paragraph, and the engine refuses to delete a cell's last one. Pick a paragraph from the list above.",
3840
3877
  "segment.tableSelectionRefused": "This selection is a table, not a paragraph. The paragraph that holds the table also holds its own text, so editing or deleting it would take the table with it. Pick a paragraph from the list above.",
@@ -3850,6 +3887,8 @@ var en = {
3850
3887
  "segment.clickRefusedTable": "The click added nothing: a table has no paragraph of its own, so there is none to add. Click a paragraph or a cell.",
3851
3888
  "segment.clickRefusedContainer": "The click added nothing: an image, a field or a bookmark has no text of its own, so there is no paragraph to add. Click a paragraph or a cell.",
3852
3889
  "segment.clickRefusedUnaddressable": "That unit does not name a paragraph the engine can address, so the click selected nothing. Click a paragraph or a cell.",
3890
+ "segment.clickRefusedUnaddressableRuns": "The clicked paragraph or cell has runs, but the engine published no address for them, so the click selected nothing. Click without Ctrl or Shift to select the whole paragraph, or click a paragraph or cell whose runs have addresses.",
3891
+ "segment.clickRefusedNoRuns": "The clicked paragraph or cell has no runs to add. Click a paragraph or cell that has text.",
3853
3892
  "segment.selectSimilar": "Select content with matching direct formatting",
3854
3893
  "segment.similarChar": "Matching character format",
3855
3894
  "segment.similarPara": "Matching paragraph format",
@@ -3976,6 +4015,7 @@ var en = {
3976
4015
  "error.staleCompose": "The open document changed while the new document was being composed; compose again.",
3977
4016
  "error.stalePreview": "The document changed while this preview was being made; preview again on the open document.",
3978
4017
  "error.unmatchedReplace": "The engine found no text to replace in that paragraph, so nothing was applied. Select the text again and retry.",
4018
+ "error.unresolvableAddress": "The engine could not address the edit's paragraph or run. Runs inside some nested paragraphs cannot be addressed. Format text in a top-level paragraph or a table cell, or edit this target in a word processor.",
3979
4019
  "error.runRangeConflict": "A run format change was refused because an earlier pending change in that paragraph (text, a link, a field, a bookmark, an image or a cell) may change its text, so the run might no longer name the same characters. Nothing was applied. Remove the format change from the pending list, apply the rest, then format the run again; or queue the format change before the others.",
3980
4020
  "error.rangeTagRefused": "This HWP document holds a range mark the engine cannot move, so a pending edit was refused and nothing was applied. Edits that move text around the mark are refused: changing a paragraph's text length, changing a field's value to one of a different length, inserting or deleting a link, field, bookmark or image, or deleting a table. A table cell that holds a mark refuses any change to its content. The engine does not say which edit. Remove those pending edits from the pending list (a same-length text replacement in a paragraph, or a same-length field value, is accepted) and apply the rest. To make those edits, convert the document to HWPX, which drops the range marks, or remove the mark in Hangul, then reopen it; reopening drops the pending list.",
3981
4021
  "error.stoppedWithoutDiagnostic": STOPPED_WITHOUT_DIAGNOSTIC,
@@ -4305,7 +4345,7 @@ var ko = {
4305
4345
  "segment.rangeTextEditHint": "\uAE00\uC790 \uBC94\uC704 \uC120\uD0DD\uC740 \uC11C\uC2DD\uB9CC \uBC14\uAFC0 \uC218 \uC788\uACE0 \uD14D\uC2A4\uD2B8\uB294 \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD14D\uC2A4\uD2B8\uB97C \uD3B8\uC9D1\uD558\uB824\uBA74 \uBB38\uB2E8 \uC804\uCCB4\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
4306
4346
  "segment.runTextEditHint": "run \uC120\uD0DD\uC740 \uC11C\uC2DD\uB9CC \uBC14\uAFC0 \uC218 \uC788\uC5B4 \uD14D\uC2A4\uD2B8 \uC785\uB825\uB780\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD14D\uC2A4\uD2B8\uB97C \uD3B8\uC9D1\uD558\uB824\uBA74 \uBB38\uB2E8\uC744 \uC120\uD0DD\uD558\uC138\uC694.",
4307
4347
  "segment.runSelectionParagraphs": (p) => `\uBB38\uB2E8 \uC815\uB82C\uACFC \uAC04\uACA9\uC740 \uC120\uD0DD\uD55C run\uC774 \uC788\uB294 \uBB38\uB2E8 ${p.count}\uAC1C \uC804\uCCB4\uC5D0 \uC801\uC6A9\uB429\uB2C8\uB2E4.`,
4308
- "segment.runSelectionTerminal": "\uC0C8 \uBB38\uB2E8 \uC120\uD0DD\uC774 \uC2DC\uC791\uB429\uB2C8\uB2E4",
4348
+ "segment.runSelectionGestures": "Ctrl\uB85C \uBB38\uB2E8\uC758 run\uC744 \uB354\uD558\uAC70\uB098 \uBE7C\uACE0, Shift\uB85C run \uB2E8\uC704\uB85C \uB113\uD799\uB2C8\uB2E4.",
4309
4349
  "segment.noParagraphSelection": "\uC774 \uC120\uD0DD\uC5D0\uB294 \uC5D4\uC9C4\uC774 \uC8FC\uC18C\uB85C \uC9C0\uC815\uD560 \uC218 \uC788\uB294 \uBB38\uB2E8\uC774 \uC5C6\uC5B4 \uBB38\uB2E8 \uD3B8\uC9D1\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC704 \uBAA9\uB85D\uC5D0\uC11C \uBB38\uB2E8\uC744 \uC120\uD0DD\uD558\uC138\uC694.",
4310
4350
  "segment.cellSelectionRefused": "\uC774 \uC120\uD0DD\uC740 \uD45C\uC758 \uC140\uC785\uB2C8\uB2E4. \uC815\uB82C\uACFC \uBB38\uB2E8 \uBAA8\uC591\uC740 \uC140 \uC548\uC758 \uBAA8\uB4E0 \uBB38\uB2E8\uC5D0 \uC801\uC6A9\uB418\uC9C0\uB9CC, \uAD50\uCCB4, \uC0BD\uC785, \uC0AD\uC81C\uB294 \uBB38\uB2E8 \uD558\uB098\uB9CC \uB300\uC0C1\uC73C\uB85C \uD558\uBA70, \uC5D4\uC9C4\uC740 \uC140\uC758 \uB9C8\uC9C0\uB9C9 \uBB38\uB2E8 \uC0AD\uC81C\uB3C4 \uAC70\uBD80\uD569\uB2C8\uB2E4. \uC704 \uBAA9\uB85D\uC5D0\uC11C \uBB38\uB2E8\uC744 \uC120\uD0DD\uD558\uC138\uC694.",
4311
4351
  "segment.tableSelectionRefused": "\uC774 \uC120\uD0DD\uC740 \uBB38\uB2E8\uC774 \uC544\uB2C8\uB77C \uD45C\uC785\uB2C8\uB2E4. \uD45C\uB97C \uB2F4\uACE0 \uC788\uB294 \uBB38\uB2E8\uC740 \uC790\uAE30 \uD14D\uC2A4\uD2B8\uB3C4 \uD568\uAED8 \uB2F4\uACE0 \uC788\uC5B4, \uADF8 \uBB38\uB2E8\uC744 \uD3B8\uC9D1\uD558\uAC70\uB098 \uC0AD\uC81C\uD558\uBA74 \uD45C\uAC00 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4. \uC704 \uBAA9\uB85D\uC5D0\uC11C \uBB38\uB2E8\uC744 \uC120\uD0DD\uD558\uC138\uC694.",
@@ -4314,6 +4354,8 @@ var ko = {
4314
4354
  "segment.clickRefusedTable": "\uD074\uB9AD\uC73C\uB85C \uCD94\uAC00\uB41C \uBB38\uB2E8\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uD45C\uB294 \uC790\uCCB4 \uBB38\uB2E8\uC774 \uC5C6\uC5B4 \uCD94\uAC00\uD560 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uB2E8\uC774\uB098 \uC140\uC744 \uD074\uB9AD\uD558\uC138\uC694.",
4315
4355
  "segment.clickRefusedContainer": "\uD074\uB9AD\uC73C\uB85C \uCD94\uAC00\uB41C \uBB38\uB2E8\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uADF8\uB9BC, \uD544\uB4DC \uB610\uB294 \uCC45\uAC08\uD53C\uB294 \uC790\uCCB4 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC5B4 \uCD94\uAC00\uD560 \uBB38\uB2E8\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uB2E8\uC774\uB098 \uC140\uC744 \uD074\uB9AD\uD558\uC138\uC694.",
4316
4356
  "segment.clickRefusedUnaddressable": "\uADF8 \uC694\uC18C\uB294 \uC5D4\uC9C4\uC774 \uC8FC\uC18C\uB85C \uC9C0\uC815\uD560 \uC218 \uC788\uB294 \uBB38\uB2E8\uC744 \uAC00\uB9AC\uD0A4\uC9C0 \uC54A\uC73C\uBBC0\uB85C \uD074\uB9AD\uC73C\uB85C \uC120\uD0DD\uB41C \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uB2E8\uC774\uB098 \uC140\uC744 \uD074\uB9AD\uD558\uC138\uC694.",
4357
+ "segment.clickRefusedUnaddressableRuns": "\uD074\uB9AD\uD55C \uBB38\uB2E8\uC774\uB098 \uC140\uC5D0 run\uC740 \uC788\uC9C0\uB9CC \uC5D4\uC9C4\uC774 \uADF8 run\uC758 \uC8FC\uC18C\uB97C \uC81C\uACF5\uD558\uC9C0 \uC54A\uC544 \uC120\uD0DD\uB41C \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. Ctrl\uC774\uB098 Shift \uC5C6\uC774 \uD074\uB9AD\uD574 \uBB38\uB2E8 \uC804\uCCB4\uB97C \uC120\uD0DD\uD558\uAC70\uB098, \uC8FC\uC18C\uAC00 \uC788\uB294 run\uC744 \uAC00\uC9C4 \uBB38\uB2E8\uC774\uB098 \uC140\uC744 \uD074\uB9AD\uD558\uC138\uC694.",
4358
+ "segment.clickRefusedNoRuns": "\uD074\uB9AD\uD55C \uBB38\uB2E8\uC774\uB098 \uC140\uC5D0 \uCD94\uAC00\uD560 run\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uD14D\uC2A4\uD2B8\uAC00 \uC788\uB294 \uBB38\uB2E8\uC774\uB098 \uC140\uC744 \uD074\uB9AD\uD558\uC138\uC694.",
4317
4359
  "segment.selectSimilar": "\uC9C1\uC811 \uC11C\uC2DD\uC774 \uAC19\uC740 \uB0B4\uC6A9 \uC120\uD0DD",
4318
4360
  "segment.similarChar": "\uAC19\uC740 \uAE00\uC790 \uC11C\uC2DD",
4319
4361
  "segment.similarPara": "\uAC19\uC740 \uBB38\uB2E8 \uC11C\uC2DD",
@@ -4431,6 +4473,7 @@ var ko = {
4431
4473
  "error.staleCompose": "\uC0C8 \uBB38\uC11C\uB97C \uC791\uC131\uD558\uB294 \uB3D9\uC548 \uC5F4\uB9B0 \uBB38\uC11C\uAC00 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC791\uC131\uD558\uC138\uC694.",
4432
4474
  "error.stalePreview": "\uBBF8\uB9AC\uBCF4\uAE30\uB97C \uB9CC\uB4DC\uB294 \uB3D9\uC548 \uBB38\uC11C\uAC00 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4. \uC5F4\uB9B0 \uBB38\uC11C\uC5D0\uC11C \uB2E4\uC2DC \uBBF8\uB9AC\uBCF4\uAE30\uD558\uC138\uC694.",
4433
4475
  "error.unmatchedReplace": "\uD574\uB2F9 \uBB38\uB2E8\uC5D0\uC11C \uBC14\uAFC0 \uD14D\uC2A4\uD2B8\uB97C \uC5D4\uC9C4\uC774 \uCC3E\uC9C0 \uBABB\uD574 \uC544\uBB34\uAC83\uB3C4 \uC801\uC6A9\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD14D\uC2A4\uD2B8\uB97C \uB2E4\uC2DC \uC120\uD0DD\uD55C \uB4A4 \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694.",
4476
+ "error.unresolvableAddress": "\uC5D4\uC9C4\uC774 \uD3B8\uC9D1 \uB300\uC0C1 \uBB38\uB2E8\uC774\uB098 run\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC77C\uBD80 \uC911\uCCA9 \uBB38\uB2E8 \uC548\uC758 run\uC740 \uC5D4\uC9C4\uC774 \uC8FC\uC18C\uB85C \uC9C0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD5C\uC0C1\uC704 \uBB38\uB2E8\uC774\uB098 \uD45C \uC140\uC758 \uD14D\uC2A4\uD2B8\uC5D0 \uC11C\uC2DD\uC744 \uC9C0\uC815\uD558\uAC70\uB098, \uC774 \uB300\uC0C1\uC740 \uC6CC\uB4DC\uD504\uB85C\uC138\uC11C\uC5D0\uC11C \uD3B8\uC9D1\uD558\uC138\uC694.",
4434
4477
  "error.runRangeConflict": "\uAC19\uC740 \uBB38\uB2E8\uC5D0 \uB300\uAE30 \uC911\uC778 \uC55E\uC120 \uBCC0\uACBD(\uD14D\uC2A4\uD2B8, \uB9C1\uD06C, \uD544\uB4DC, \uCC45\uAC08\uD53C, \uC774\uBBF8\uC9C0, \uC140)\uC774 \uBB38\uB2E8 \uD14D\uC2A4\uD2B8\uB97C \uBC14\uAFC0 \uC218 \uC788\uC5B4 run\uC774 \uB354 \uC774\uC0C1 \uAC19\uC740 \uAE00\uC790\uB97C \uAC00\uB9AC\uD0A4\uC9C0 \uC54A\uC744 \uC218 \uC788\uC73C\uBBC0\uB85C run \uC11C\uC2DD \uBCC0\uACBD\uC774 \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC544\uBB34\uAC83\uB3C4 \uC801\uC6A9\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uB300\uAE30 \uBAA9\uB85D\uC5D0\uC11C \uC11C\uC2DD \uBCC0\uACBD\uC744 \uC81C\uAC70\uD558\uACE0 \uB098\uBA38\uC9C0\uB97C \uC801\uC6A9\uD55C \uB4A4 run \uC11C\uC2DD\uC744 \uB2E4\uC2DC \uC9C0\uC815\uD558\uAC70\uB098, \uC11C\uC2DD \uBCC0\uACBD\uC744 \uB2E4\uB978 \uBCC0\uACBD\uBCF4\uB2E4 \uBA3C\uC800 \uB300\uAE30\uC5F4\uC5D0 \uB123\uC73C\uC138\uC694.",
4435
4478
  "error.rangeTagRefused": "\uC774 HWP \uBB38\uC11C\uC5D0\uB294 \uC5D4\uC9C4\uC774 \uC62E\uAE38 \uC218 \uC5C6\uB294 \uAD6C\uAC04 \uD45C\uC2DC\uAC00 \uC788\uC5B4 \uB300\uAE30 \uC911\uC778 \uD3B8\uC9D1\uC774 \uAC70\uBD80\uB418\uC5C8\uACE0, \uC544\uBB34\uAC83\uB3C4 \uC801\uC6A9\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uAD6C\uAC04 \uD45C\uC2DC \uC8FC\uBCC0\uC758 \uAE00\uC790\uB97C \uC6C0\uC9C1\uC774\uB294 \uD3B8\uC9D1\uC740 \uAC70\uBD80\uB429\uB2C8\uB2E4: \uBB38\uB2E8 \uD14D\uC2A4\uD2B8 \uAE38\uC774 \uBCC0\uACBD, \uAE38\uC774\uAC00 \uB2E4\uB978 \uD544\uB4DC \uAC12\uC73C\uB85C\uC758 \uBCC0\uACBD, \uB9C1\uD06C\xB7\uD544\uB4DC\xB7\uCC45\uAC08\uD53C\xB7\uC774\uBBF8\uC9C0\uC758 \uC0BD\uC785\uC774\uB098 \uC0AD\uC81C, \uD45C \uC0AD\uC81C. \uAD6C\uAC04 \uD45C\uC2DC\uAC00 \uC788\uB294 \uD45C \uC140\uC740 \uB0B4\uC6A9\uC744 \uC5B4\uB5BB\uAC8C \uBC14\uAFB8\uB4E0 \uAC70\uBD80\uD569\uB2C8\uB2E4. \uC5D4\uC9C4\uC740 \uC5B4\uB290 \uD3B8\uC9D1\uC778\uC9C0 \uC54C\uB824 \uC8FC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB300\uAE30 \uBAA9\uB85D\uC5D0\uC11C \uD574\uB2F9 \uD3B8\uC9D1\uC744 \uC81C\uAC70\uD558\uACE0(\uBB38\uB2E8 \uC548\uC758 \uAC19\uC740 \uAE38\uC774 \uD14D\uC2A4\uD2B8 \uCE58\uD658\uACFC \uAC19\uC740 \uAE38\uC774\uC758 \uD544\uB4DC \uAC12\uC740 \uD5C8\uC6A9\uB429\uB2C8\uB2E4) \uB098\uBA38\uC9C0\uB97C \uC801\uC6A9\uD558\uC138\uC694. \uADF8\uB7F0 \uD3B8\uC9D1\uC774 \uD544\uC694\uD558\uBA74 \uBB38\uC11C\uB97C HWPX\uB85C \uBCC0\uD658\uD558\uAC70\uB098(\uBCC0\uD658\uD558\uBA74 \uAD6C\uAC04 \uD45C\uC2DC\uAC00 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4) \uD55C\uAE00\uC5D0\uC11C \uAD6C\uAC04 \uD45C\uC2DC\uB97C \uC81C\uAC70\uD55C \uB4A4 \uB2E4\uC2DC \uC5EC\uC138\uC694. \uB2E4\uC2DC \uC5F4\uBA74 \uB300\uAE30 \uBAA9\uB85D\uC740 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4.",
4436
4479
  "error.noEngineCause": "\uC5D4\uC9C4\uC774 \uC6D0\uC778\uC744 \uBC1D\uD788\uC9C0 \uC54A\uACE0 \uC2E4\uD328\uD588\uC73C\uBBC0\uB85C(\uB300\uAC1C \uBE44\uC815\uC0C1 \uC885\uB8CC\uB41C \uACBD\uC6B0\uC785\uB2C8\uB2E4) \uC544\uBB34\uAC83\uB3C4 \uC801\uC6A9\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC801\uC6A9\uD558\uACE0, \uAC19\uC740 \uBC29\uC2DD\uC73C\uB85C \uC2E4\uD328\uD558\uBA74 \uD638\uC2A4\uD2B8 \uB85C\uADF8\uC640 \uD568\uAED8 \uC54C\uB824 \uC8FC\uC138\uC694.",
@@ -4608,7 +4651,8 @@ var RENDER_SVG = { format: "svg" };
4608
4651
  var REFUSAL_MESSAGE = {
4609
4652
  unmatched_replace: "error.unmatchedReplace",
4610
4653
  run_range_conflict: "error.runRangeConflict",
4611
- range_tag: "error.rangeTagRefused"
4654
+ range_tag: "error.rangeTagRefused",
4655
+ unresolvable_address: "error.unresolvableAddress"
4612
4656
  };
4613
4657
  var HwpEditor = forwardRef(
4614
4658
  function HwpEditor2(props, ref) {
@@ -5477,6 +5521,31 @@ var HwpEditor = forwardRef(
5477
5521
  ) });
5478
5522
  }
5479
5523
  );
5524
+
5525
+ // src/sanitize.ts
5526
+ function sanitizeSvg(svgText) {
5527
+ const doc = new DOMParser().parseFromString(svgText, "image/svg+xml");
5528
+ const root = doc.documentElement;
5529
+ if (root === null || root.nodeName === "parsererror" || doc.getElementsByTagName("parsererror").length > 0) {
5530
+ return "";
5531
+ }
5532
+ const dangerous = root.querySelectorAll("script, foreignObject");
5533
+ for (const node of Array.from(dangerous)) node.remove();
5534
+ scrubElement(root);
5535
+ return root.outerHTML;
5536
+ }
5537
+ function scrubElement(el) {
5538
+ for (const attr of Array.from(el.attributes)) {
5539
+ const name = attr.name.toLowerCase();
5540
+ const value = attr.value.trim().toLowerCase();
5541
+ if (name.startsWith("on")) {
5542
+ el.removeAttribute(attr.name);
5543
+ } else if ((name === "href" || name === "xlink:href") && value.startsWith("javascript:")) {
5544
+ el.removeAttribute(attr.name);
5545
+ }
5546
+ }
5547
+ for (const child of Array.from(el.children)) scrubElement(child);
5548
+ }
5480
5549
  export {
5481
5550
  COMPOSE_PRESETS,
5482
5551
  ComponentsPanel,