@braincrew-lab/langchain-canvas 0.1.14 → 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,10 +1,12 @@
1
1
  "use client";
2
2
  import { loadOptional } from './chunk-YZZSJJMQ.js';
3
3
  import { resolveElements } from './chunk-6L3AL6W4.js';
4
- export { useArtifactPatch } from './chunk-TERWLUW3.js';
5
- import { useCanvasStoreApi, useCanvasStore } from './chunk-KKLWKR5G.js';
6
- export { CanvasProvider, createCanvasStore, emptyCanvasState, isCanvasEvent, isChatEvent, mergePatch, reduceCanvas, useCanvasStore, useCanvasStoreApi } from './chunk-KKLWKR5G.js';
7
- import { createContext, lazy, useRef, useCallback, useEffect, useContext, useState, useMemo, Suspense, Component } from 'react';
4
+ export { useArtifactPatch } from './chunk-FSFOURG5.js';
5
+ import { useCanvasStoreApi, useCanvasStore } from './chunk-ZLXAWRUP.js';
6
+ export { CanvasProvider, createCanvasStore, emptyCanvasState, isCanvasEvent, isChatEvent, mergePatch, reduceCanvas, useCanvasStore, useCanvasStoreApi } from './chunk-ZLXAWRUP.js';
7
+ import { useT, CanvasLocaleProvider } from './chunk-QMOJEGRH.js';
8
+ export { CanvasLocaleProvider, useLocale, useT } from './chunk-QMOJEGRH.js';
9
+ import { createContext, lazy, useRef, useCallback, useEffect, useMemo, useContext, useState, Suspense, Component } from 'react';
8
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
11
 
10
12
  // src/client/sse-client.ts
@@ -157,16 +159,47 @@ var INSPECTOR_SCRIPT = `
157
159
  for (var j = 0; j < marked.length; j++) scrub(marked[j]);
158
160
  parent.postMessage({ source: MARK, type: "doc_edit", self: !!selfApplied, html: "<!doctype html>\\n" + clone.outerHTML }, "*");
159
161
  }
162
+ // Copy the visual style of an existing element onto a new block, so inserts
163
+ // match the page's own design system instead of landing as bare UA-styled
164
+ // tags (a default grey <button> on a dark page reads as broken).
165
+ function adoptStyleFrom(el, sample) {
166
+ if (!sample) return false;
167
+ var cs = window.getComputedStyle(sample);
168
+ var props = ["background-color", "color", "border", "border-radius", "padding",
169
+ "font-family", "font-size", "font-weight", "letter-spacing", "box-shadow", "cursor"];
170
+ for (var i = 0; i < props.length; i++) el.style.setProperty(props[i], cs.getPropertyValue(props[i]));
171
+ return true;
172
+ }
160
173
  function newBlock(tag) {
161
174
  var el = document.createElement(tag);
162
175
  if (tag === "img") {
163
176
  el.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='270'%3E%3Crect width='100%25' height='100%25' fill='%23e5e7eb'/%3E%3Cpath d='M190 155l40-45 35 40 25-25 40 45z' fill='%23c3c8d0'/%3E%3Ccircle cx='300' cy='105' r='16' fill='%23c3c8d0'/%3E%3C/svg%3E";
164
177
  el.alt = "image"; el.style.maxWidth = "100%";
165
178
  }
166
- else if (tag === "button") el.textContent = "Button";
179
+ else if (tag === "button") {
180
+ el.textContent = "Button";
181
+ // Match an existing button (skipping editor chrome); else a clean accent default.
182
+ var sampleBtn = null;
183
+ var btns = document.querySelectorAll("button");
184
+ for (var sb = 0; sb < btns.length; sb++) {
185
+ if (!btns[sb].closest("[data-lcx]")) { sampleBtn = btns[sb]; break; }
186
+ }
187
+ if (!adoptStyleFrom(el, sampleBtn)) {
188
+ el.style.cssText = "padding:10px 18px;border:0;border-radius:9px;background:#6366f1;color:#fff;font:600 15px/1.2 inherit;cursor:pointer";
189
+ }
190
+ }
167
191
  else if (tag === "hr") { /* no content */ }
168
192
  else if (tag === "section") { var p = document.createElement("p"); p.textContent = "New section"; el.appendChild(p); }
169
- else el.textContent = tag === "h1" || tag === "h2" ? "New heading" : "New text";
193
+ else {
194
+ el.textContent = tag === "h1" || tag === "h2" ? "New heading" : "New text";
195
+ // Headings/text adopt an existing peer's look too (font, colour) so they
196
+ // don't appear in default serif-black on a styled page.
197
+ adoptStyleFrom(el, document.querySelector(tag === "p" ? "main p, section p, div p" : tag));
198
+ el.style.removeProperty("border");
199
+ el.style.removeProperty("box-shadow");
200
+ el.style.removeProperty("cursor");
201
+ el.style.removeProperty("background-color");
202
+ }
170
203
  return el;
171
204
  }
172
205
  // Floating rich-text toolbar shown while a text element is being edited.
@@ -263,16 +296,31 @@ var INSPECTOR_SCRIPT = `
263
296
  // into absolute positioning inside its own parent, and its final spot + size are
264
297
  // stored as percentages of that parent \u2014 so it stays put proportionally across
265
298
  // responsive breakpoints, instead of a fixed pixel offset that drifts off.
266
- var dragEls = null, dragStart = null, dragBases = null, groupSeq = 0;
299
+ var dragEls = null, dragStart = null, dragBases = null, parentMutated = false;
267
300
 
268
301
  function ensurePositioned(parent) {
269
- if (!parent || parent === document.body || parent === document.documentElement) return;
270
- if (window.getComputedStyle(parent).position === "static") parent.style.position = "relative";
302
+ if (!parent || parent === document.documentElement) return;
303
+ // body included: a static body makes absolute children resolve against the
304
+ // initial containing block (html), so a body margin or html padding shifts
305
+ // every free-dragged element \u2014 relative pins them to the body box itself.
306
+ if (window.getComputedStyle(parent).position === "static") {
307
+ parent.style.position = "relative";
308
+ parentMutated = true; // the parent must be persisted too, not just the child
309
+ }
310
+ }
311
+ // Persist a completed free-drag. A single element normally commits as a cheap
312
+ // node_edit \u2014 but when its parent was pulled to position:relative that parent
313
+ // lives only in the live DOM, so a node-only patch would store an absolute
314
+ // child inside a still-static parent (it jumps on the next reload). In that
315
+ // case persist the whole document instead.
316
+ function commitDrag(els) {
317
+ if (els.length === 1 && !parentMutated) emitEdit(els[0]); else emitDoc(true);
271
318
  }
272
319
  // Pull each element out into absolute positioning at its current spot (no visual
273
320
  // jump), so it can then be moved freely.
274
321
  function beginFreeDrag(els) {
275
322
  dragBases = [];
323
+ parentMutated = false;
276
324
  for (var i = 0; i < els.length; i++) {
277
325
  var el = els[i], parent = el.parentElement || document.body;
278
326
  ensurePositioned(parent);
@@ -295,13 +343,16 @@ var INSPECTOR_SCRIPT = `
295
343
  }
296
344
  // Commit the current position (in px, as set live by moveFree) as % of the
297
345
  // parent \u2014 position and width \u2014 so it scales with the layout. Falls back to px
298
- // only if the parent has collapsed to zero on that axis.
346
+ // only if the parent has collapsed to zero on that axis. The vertical axis is
347
+ // an exception: a container's *height* is content-driven and reflows once the
348
+ // dragged element leaves the flow, so a top stored as % of the old height
349
+ // lands somewhere else after reload \u2014 top always commits in px.
299
350
  function commitFree() {
300
351
  for (var i = 0; i < dragBases.length; i++) {
301
352
  var b = dragBases[i], pr = b.parent.getBoundingClientRect();
302
353
  var curLeft = parseFloat(b.el.style.left) || 0, curTop = parseFloat(b.el.style.top) || 0;
303
354
  b.el.style.left = pr.width ? ((curLeft / pr.width) * 100).toFixed(3) + "%" : curLeft + "px";
304
- b.el.style.top = pr.height ? ((curTop / pr.height) * 100).toFixed(3) + "%" : curTop + "px";
355
+ b.el.style.top = Math.round(curTop) + "px";
305
356
  if (pr.width) b.el.style.width = ((b.w / pr.width) * 100).toFixed(3) + "%";
306
357
  }
307
358
  }
@@ -458,8 +509,8 @@ var INSPECTOR_SCRIPT = `
458
509
  positionResize();
459
510
  // The new position is already shown in the iframe with every cid intact,
460
511
  // so persist without a reload (no flicker): one element \u2192 node_edit,
461
- // several \u2192 a self-applied doc_edit.
462
- if (els.length === 1) emitEdit(els[0]); else emitDoc(true);
512
+ // several (or a repositioned parent) \u2192 a self-applied doc_edit.
513
+ commitDrag(els);
463
514
  }
464
515
  dragBases = null;
465
516
  return;
@@ -510,7 +561,7 @@ var INSPECTOR_SCRIPT = `
510
561
  // Pointer left the frame mid-drag: commit the move at its last position so
511
562
  // it isn't lost (the element is already placed absolutely in the iframe).
512
563
  var els = dragEls; dragEls = null;
513
- if (moved && dragBases) { commitFree(); if (els.length === 1) emitEdit(els[0]); else emitDoc(true); }
564
+ if (moved && dragBases) { commitFree(); commitDrag(els); }
514
565
  dragBases = null;
515
566
  }
516
567
  if (dragging) {
@@ -584,6 +635,12 @@ var INSPECTOR_SCRIPT = `
584
635
  if (d.type === "set_src") { var ei = byCid(d.cid); if (ei) { ei.setAttribute("src", d.value); emitEdit(ei); } return; }
585
636
  if (d.type === "commit") { var el2 = byCid(d.cid); if (el2) emitEdit(el2); return; }
586
637
 
638
+ // No-selection inserts land in the slide root when there is one \u2014 a slide
639
+ // document is body > .slide-container (fixed 720px), so appending to body
640
+ // puts content below the visible slide where it silently never shows.
641
+ function insertRoot() {
642
+ return document.querySelector(".slide-container") || document.body;
643
+ }
587
644
  // Structural edits \u2014 mutate the tree, then persist the whole document.
588
645
  if (d.type === "insert") {
589
646
  var block = newBlock(d.block || "p");
@@ -591,7 +648,7 @@ var INSPECTOR_SCRIPT = `
591
648
  if (anchor && anchor.parentNode && anchor.parentNode !== document.documentElement) {
592
649
  anchor.parentNode.insertBefore(block, anchor.nextSibling);
593
650
  } else {
594
- document.body.appendChild(block);
651
+ insertRoot().appendChild(block);
595
652
  }
596
653
  emitDoc();
597
654
  return;
@@ -599,7 +656,7 @@ var INSPECTOR_SCRIPT = `
599
656
  if (d.type === "insert_html") {
600
657
  // A built-in section template (trusted markup from the toolbar).
601
658
  var anc = d.cid ? byCid(d.cid) : null;
602
- var container = (anc && anc.parentNode && anc.parentNode !== document.documentElement) ? anc.parentNode : document.body;
659
+ var container = (anc && anc.parentNode && anc.parentNode !== document.documentElement) ? anc.parentNode : insertRoot();
603
660
  var ref = (anc && anc.parentNode === container) ? anc.nextSibling : null;
604
661
  var frag = document.createElement("div");
605
662
  frag.innerHTML = d.html || "";
@@ -614,7 +671,16 @@ var INSPECTOR_SCRIPT = `
614
671
  var cids = d.cids || [];
615
672
  for (var g = 0; g < cids.length; g++) { var m = byCid(cids[g]); if (m) members.push(m); }
616
673
  if (members.length < 2) return;
617
- var gid = "g" + (groupSeq++);
674
+ // Next free id is derived from the document, not a counter \u2014 the counter
675
+ // reset on every reload, so a second group session reused "g0" and merged
676
+ // with the previously-persisted group.
677
+ var maxGid = -1;
678
+ var existing = document.querySelectorAll("[data-group-id]");
679
+ for (var q = 0; q < existing.length; q++) {
680
+ var mm = /^g(d+)$/.exec(existing[q].getAttribute("data-group-id") || "");
681
+ if (mm && Number(mm[1]) > maxGid) maxGid = Number(mm[1]);
682
+ }
683
+ var gid = "g" + (maxGid + 1);
618
684
  for (var w = 0; w < members.length; w++) members[w].setAttribute("data-group-id", gid);
619
685
  clearSelected();
620
686
  emitDoc();
@@ -777,7 +843,7 @@ function useCanvasReplay() {
777
843
  api.getState().applyEvent(event);
778
844
  }
779
845
  } finally {
780
- api.getState().setStreaming(false);
846
+ if (abortRef.current === controller) api.getState().setStreaming(false);
781
847
  }
782
848
  },
783
849
  [api]
@@ -786,6 +852,39 @@ function useCanvasReplay() {
786
852
  const reset = useCallback(() => api.getState().reset(), [api]);
787
853
  return { play, stop, reset, canvas, isPlaying };
788
854
  }
855
+ var DEFAULT_DEBOUNCE_MS = 800;
856
+ function useCanvasSave(onSave, debounceMs = DEFAULT_DEBOUNCE_MS) {
857
+ const timers = useRef(/* @__PURE__ */ new Map());
858
+ const latest = useRef(/* @__PURE__ */ new Map());
859
+ const handler = useRef(onSave);
860
+ handler.current = onSave;
861
+ const enabled = Boolean(onSave);
862
+ useEffect(() => {
863
+ const pending = timers.current;
864
+ return () => pending.forEach((t) => clearTimeout(t));
865
+ }, []);
866
+ return useMemo(() => {
867
+ if (!enabled) return null;
868
+ return (artifact2) => {
869
+ latest.current.set(artifact2.id, artifact2);
870
+ const existing = timers.current.get(artifact2.id);
871
+ if (existing) clearTimeout(existing);
872
+ timers.current.set(
873
+ artifact2.id,
874
+ setTimeout(() => {
875
+ timers.current.delete(artifact2.id);
876
+ const current = latest.current.get(artifact2.id);
877
+ if (!current || !handler.current) return;
878
+ void handler.current({
879
+ artifactId: current.id,
880
+ artifact: current,
881
+ baseRevision: typeof current.meta?.revision === "string" ? current.meta.revision : null
882
+ });
883
+ }, debounceMs)
884
+ );
885
+ };
886
+ }, [enabled, debounceMs]);
887
+ }
789
888
 
790
889
  // src/fixtures/scenarios.ts
791
890
  var PRICING_HTML = `<!doctype html>
@@ -957,7 +1056,107 @@ var slides = {
957
1056
  { type: "done" }
958
1057
  ]
959
1058
  };
960
- var scenarios = [htmlPage, document2, chart, table, slides];
1059
+ var VERSIONED_HTML = `<!doctype html><html><body style="font-family:sans-serif;padding:40px">
1060
+ <h1>Coffee history</h1><p>From the Ethiopian highlands to the espresso bar.</p>
1061
+ </body></html>`;
1062
+ var versions = {
1063
+ id: "versions",
1064
+ title: "Version history",
1065
+ description: "An agent builds a page, a user edit and an agent edit each land as described commits \u2014 open the version rail to browse and restore-view snapshots.",
1066
+ events: [
1067
+ { type: "message.delta", messageId: "m1", text: "Built the page \u2014 every change now lands in the version history." },
1068
+ { type: "message.end", messageId: "m1" },
1069
+ {
1070
+ type: "canvas.create",
1071
+ artifact: { id: "page", type: "html", title: "Coffee history", version: 1, status: "streaming", data: { html: VERSIONED_HTML } }
1072
+ },
1073
+ { type: "canvas.status", id: "page", status: "complete" },
1074
+ { type: "canvas.commit", id: "page", description: "Create page", revision: "v1" },
1075
+ // A human tweaks the headline by hand, then saves — one described commit.
1076
+ { type: "canvas.patch", id: "page", patch: { html: VERSIONED_HTML.replace("Coffee history", "A short history of coffee") } },
1077
+ { type: "canvas.commit", id: "page", description: "Manual edit: 1 change", revision: "v2" },
1078
+ // The agent applies a targeted follow-up edit on the current state.
1079
+ { type: "canvas.patch", id: "page", patch: { html: VERSIONED_HTML.replace("Coffee history", "A short history of coffee").replace("espresso bar", "third-wave caf\xE9") } },
1080
+ { type: "canvas.commit", id: "page", description: "Update closing phrase", revision: "v3" },
1081
+ { type: "done" }
1082
+ ]
1083
+ };
1084
+ var PDF_DATA_URL = "data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iajw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAyIDAgUj4+ZW5kb2JqCjIgMCBvYmo8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PmVuZG9iagozIDAgb2JqPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgNjEyIDc5Ml0vQ29udGVudHMgNCAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDUgMCBSPj4+Pj4+ZW5kb2JqCjQgMCBvYmo8PC9MZW5ndGggODA+PnN0cmVhbQpCVCAvRjEgMjggVGYgNzIgNzAwIFRkIChsYW5nY2hhaW4tY2FudmFzIFBERiB2aWV3ZXIpIFRqIEVUCmVuZHN0cmVhbQplbmRvYmoKNSAwIG9iajw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+ZW5kb2JqCnhyZWYKMCA2CnRyYWlsZXI8PC9TaXplIDYvUm9vdCAxIDAgUj4+CiUlRU9GCg==";
1085
+ var pdf = {
1086
+ id: "pdf",
1087
+ title: "PDF viewer",
1088
+ description: "A PDF artifact shown in the browser's built-in viewer \u2014 zero dependencies.",
1089
+ events: [
1090
+ { type: "message.delta", messageId: "m-pdf", text: "Here's the signed report as a PDF." },
1091
+ {
1092
+ type: "canvas.create",
1093
+ artifact: {
1094
+ id: "report-pdf",
1095
+ type: "pdf",
1096
+ title: "Signed report",
1097
+ version: 1,
1098
+ status: "complete",
1099
+ data: { src: PDF_DATA_URL, filename: "signed-report.pdf" }
1100
+ }
1101
+ },
1102
+ { type: "canvas.status", id: "report-pdf", status: "complete" },
1103
+ { type: "done" }
1104
+ ]
1105
+ };
1106
+ var HWP_HTML = `<!doctype html>
1107
+ <html lang="ko"><head><meta charset="utf-8"><style>
1108
+ body{margin:0;background:#eceef1;font-family:"Malgun Gothic","\uB9D1\uC740 \uACE0\uB515","Apple SD Gothic Neo",AppleGothic,sans-serif;color:#1a1a1a}
1109
+ .page{max-width:794px;margin:24px auto;padding:96px 72px;background:#fff;box-shadow:0 2px 14px rgba(0,0,0,.12);line-height:1.7}
1110
+ h1{font-size:22pt;text-align:center;margin:0 0 8px}
1111
+ .stamp{text-align:center;color:#555;font-size:10.5pt;margin:0 0 36px}
1112
+ h2{font-size:14pt;border-bottom:2px solid #2f5597;padding-bottom:4px;margin:28px 0 12px;color:#2f5597}
1113
+ p{margin:0 0 10px;font-size:11pt;text-align:justify}
1114
+ table{border-collapse:collapse;width:100%;margin:12px 0;font-size:10.5pt}
1115
+ th{background:#dbe5f1;border:1px solid #666;padding:7px 10px}
1116
+ td{border:1px solid #666;padding:7px 10px}
1117
+ .sign{margin-top:48px;text-align:right;font-size:12pt}
1118
+ .red{color:#c00000;font-weight:700}
1119
+ </style></head>
1120
+ <body><div class="page">
1121
+ <h1>\uC0AC\uC5C5 \uC218\uD589 \uACC4\uD68D\uC11C</h1>
1122
+ <p class="stamp">\uBB38\uC11C\uBC88\uD638 BC-2026-041 \xB7 2026. 7. 29.</p>
1123
+ <h2>1. \uAC1C\uC694</h2>
1124
+ <p>\uBCF8 \uBB38\uC11C\uB294 <b>\uB300\uD654\uD615 \uC5D0\uC774\uC804\uD2B8 \uBE4C\uB354 \uAD6C\uCD95</b> \uC0AC\uC5C5\uC758 \uC218\uD589 \uACC4\uD68D\uC744 \uC815\uB9AC\uD55C \uAC83\uC785\uB2C8\uB2E4. <span class="red">\uD55C\uAE00(.hwp/.hwpx) \uD30C\uC77C\uC744 \uCE94\uBC84\uC2A4\uC5D0 \uB04C\uC5B4\uB2E4 \uB193\uC73C\uBA74</span> \uC774 \uBB38\uC11C\uCC98\uB7FC <u>\uC11C\uC2DD\uC774 \uBCF4\uC874\uB41C \uD398\uC774\uC9C0</u>\uB85C \uC5F4\uB9BD\uB2C8\uB2E4 \u2014 \uAE00\uAF34 \uD06C\uAE30\xB7\uC0C9\xB7\uC815\uB82C\xB7\uD45C \uD14C\uB450\uB9AC\xB7\uC774\uBBF8\uC9C0\uAE4C\uC9C0.</p>
1125
+ <h2>2. \uCD94\uC9C4 \uC77C\uC815</h2>
1126
+ <table>
1127
+ <tr><th>\uB2E8\uACC4</th><th>\uAE30\uAC04</th><th>\uC0B0\uCD9C\uBB3C</th></tr>
1128
+ <tr><td>\uCC29\uC218</td><td style="text-align:center">2026-07</td><td>\uC0AC\uC5C5\uC218\uD589\uACC4\uD68D\uC11C</td></tr>
1129
+ <tr><td>\uBD84\uC11D/\uC124\uACC4</td><td style="text-align:center">2026-07 ~ 2026-08</td><td>\uC694\uAD6C\uC0AC\uD56D \uC815\uC758\uC11C</td></tr>
1130
+ <tr><td>\uAD6C\uCD95</td><td style="text-align:center">2026-08 ~ 2026-11</td><td>\uAE30\uB2A5 \uAD6C\uD604</td></tr>
1131
+ <tr><td>\uAC80\uC218</td><td style="text-align:center">2026-12</td><td>\uAC80\uC218\uD655\uC778\uC11C</td></tr>
1132
+ </table>
1133
+ <h2>3. \uB0B4\uBCF4\uB0B4\uAE30</h2>
1134
+ <p>\uD3B8\uC9D1\uD55C \uBB38\uC11C\uB294 <b>\uB0B4\uBCF4\uB0B4\uAE30</b> \uBA54\uB274\uC5D0\uC11C PDF\xB7HTML\uB85C \uC800\uC7A5\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC5D0\uC774\uC804\uD2B8\uAC00 \uB9CC\uB4E0 \uBB38\uC11C(document)\uB294 <b>\uD55C\uAE00 (HWPX)</b>\uB85C\uB3C4 \uB0B4\uBCF4\uB0C5\uB2C8\uB2E4.</p>
1135
+ <p class="sign">\uBE0C\uB808\uC778\uD06C\uB8E8 \uC8FC\uC2DD\uD68C\uC0AC</p>
1136
+ </div></body></html>`;
1137
+ var hwp = {
1138
+ id: "hwp",
1139
+ title: "\uD55C\uAE00 (HWP)",
1140
+ description: "A formatted Korean document \u2014 how .hwp/.hwpx files open on the canvas, styling preserved.",
1141
+ events: [
1142
+ { type: "message.delta", messageId: "m-hwp", text: "\uD55C\uAE00 \uD30C\uC77C\uC744 \uC11C\uC2DD \uADF8\uB300\uB85C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4." },
1143
+ {
1144
+ type: "canvas.create",
1145
+ artifact: {
1146
+ id: "hwp-doc",
1147
+ type: "html",
1148
+ title: "\uC0AC\uC5C5 \uC218\uD589 \uACC4\uD68D\uC11C.hwpx",
1149
+ version: 1,
1150
+ status: "complete",
1151
+ meta: { kind: "doc", source: "hwpx" },
1152
+ data: { html: HWP_HTML }
1153
+ }
1154
+ },
1155
+ { type: "canvas.status", id: "hwp-doc", status: "complete" },
1156
+ { type: "done" }
1157
+ ]
1158
+ };
1159
+ var scenarios = [htmlPage, document2, chart, table, slides, pdf, hwp, versions];
961
1160
  var RegistryContext = createContext({});
962
1161
  function CanvasRegistryProvider({ registry, children }) {
963
1162
  return /* @__PURE__ */ jsx(RegistryContext.Provider, { value: registry, children });
@@ -1103,40 +1302,202 @@ function display(value) {
1103
1302
  if (value == null) return "";
1104
1303
  if (typeof value === "object") {
1105
1304
  if (value.result != null) return display(value.result);
1305
+ if (Array.isArray(value.richText)) return value.richText.map((r) => r?.text ?? "").join("");
1106
1306
  if (typeof value.text === "string") return value.text;
1107
1307
  if (value instanceof Date) return formatDate(value, "yyyy-mm-dd");
1108
1308
  return "";
1109
1309
  }
1110
1310
  return String(value);
1111
1311
  }
1312
+ function splitSections(fmt) {
1313
+ const sections = [];
1314
+ let cur = "";
1315
+ for (let i = 0; i < fmt.length; i++) {
1316
+ const ch = fmt[i];
1317
+ if (ch === '"') {
1318
+ cur += ch;
1319
+ i++;
1320
+ while (i < fmt.length && fmt[i] !== '"') cur += fmt[i++];
1321
+ if (i < fmt.length) cur += fmt[i];
1322
+ continue;
1323
+ }
1324
+ if (ch === "[") {
1325
+ cur += ch;
1326
+ i++;
1327
+ while (i < fmt.length && fmt[i] !== "]") cur += fmt[i++];
1328
+ if (i < fmt.length) cur += fmt[i];
1329
+ continue;
1330
+ }
1331
+ if (ch === "\\") {
1332
+ cur += ch + (fmt[i + 1] ?? "");
1333
+ i++;
1334
+ continue;
1335
+ }
1336
+ if (ch === ";") {
1337
+ sections.push(cur);
1338
+ cur = "";
1339
+ continue;
1340
+ }
1341
+ cur += ch;
1342
+ }
1343
+ sections.push(cur);
1344
+ return sections;
1345
+ }
1346
+ function numberSkeleton(fmt) {
1347
+ let currency = "";
1348
+ let currencyTrails = false;
1349
+ let skeleton = "";
1350
+ let seenDigit = false;
1351
+ const found = (sym) => {
1352
+ if (currency) return;
1353
+ currency = sym;
1354
+ currencyTrails = seenDigit;
1355
+ };
1356
+ for (let i = 0; i < fmt.length; i++) {
1357
+ const ch = fmt[i];
1358
+ if (ch === '"') {
1359
+ let lit = "";
1360
+ i++;
1361
+ while (i < fmt.length && fmt[i] !== '"') lit += fmt[i++];
1362
+ if (/[$₩€£¥]/.test(lit)) found(lit);
1363
+ continue;
1364
+ }
1365
+ if (ch === "\\") {
1366
+ const c = fmt[i + 1] ?? "";
1367
+ if (/[$₩€£¥]/.test(c)) found(c);
1368
+ i++;
1369
+ continue;
1370
+ }
1371
+ if (ch === "[") {
1372
+ const end = fmt.indexOf("]", i);
1373
+ const group = fmt.slice(i + 1, end === -1 ? void 0 : end);
1374
+ const cur = /^\$(.*?)-/.exec(group)?.[1] ?? (group.startsWith("$") ? group.slice(1) : "");
1375
+ if (cur) found(cur);
1376
+ i = end === -1 ? fmt.length : end;
1377
+ continue;
1378
+ }
1379
+ if (ch === "_" || ch === "*") {
1380
+ i++;
1381
+ continue;
1382
+ }
1383
+ if (/[$₩€£¥]/.test(ch)) {
1384
+ found(ch);
1385
+ continue;
1386
+ }
1387
+ if (ch === "0" || ch === "#") seenDigit = true;
1388
+ skeleton += ch;
1389
+ }
1390
+ return { skeleton, currency, currencyTrails };
1391
+ }
1112
1392
  function formatNumber(value, numFmt) {
1113
- const fmt = numFmt && numFmt !== "General" ? numFmt : "";
1114
- if (!fmt) return String(value);
1115
- const decimals = (fmt.match(/\.([0#]+)/)?.[1] ?? "").length;
1116
- if (fmt.includes("%")) return `${(value * 100).toFixed(decimals)}%`;
1117
- const thousands = /[#0],[#0]/.test(fmt);
1118
- let out = thousands ? value.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) : value.toFixed(decimals);
1119
- const currency = fmt.match(/[$₩€£¥]/);
1120
- if (currency) out = value < 0 ? `-${currency[0]}${out.slice(1)}` : `${currency[0]}${out}`;
1393
+ const raw = numFmt && numFmt !== "General" ? numFmt : "";
1394
+ if (!raw) return String(value);
1395
+ const sections = splitSections(raw);
1396
+ const negSection = value < 0 && sections.length > 1 ? sections[1] : void 0;
1397
+ const section = negSection ?? (value === 0 && sections[2] ? sections[2] : sections[0]);
1398
+ const { skeleton, currency, currencyTrails } = numberSkeleton(section);
1399
+ if (!/[0#]/.test(skeleton)) return String(value);
1400
+ const decimals = (skeleton.match(/\.([0#]+)/)?.[1] ?? "").length;
1401
+ const thousands = /[#0],[#0]/.test(skeleton);
1402
+ const n = negSection ? Math.abs(value) : value;
1403
+ const percents = (skeleton.match(/%/g) ?? []).length;
1404
+ const scaled = percents ? n * 100 ** percents : n;
1405
+ let out = thousands ? scaled.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) : scaled.toFixed(decimals);
1406
+ if (percents) out += "%";
1407
+ if (currency) {
1408
+ if (currencyTrails) out = `${out}${currency}`;
1409
+ else if (out.startsWith("-")) out = `-${currency}${out.slice(1)}`;
1410
+ else out = `${currency}${out}`;
1411
+ }
1412
+ if (negSection && /\(/.test(skeleton)) out = `(${out})`;
1121
1413
  return out;
1122
1414
  }
1415
+ var EXCEL_EPOCH_MS = Date.UTC(1899, 11, 30);
1123
1416
  function formatDate(d, numFmt) {
1124
- const fmt = numFmt && numFmt !== "General" && /[ymdhs]/i.test(numFmt) ? numFmt : "yyyy-mm-dd";
1417
+ const fmt = numFmt && numFmt !== "General" && /[ymdhs]/i.test(numFmt) ? splitSections(numFmt)[0] : "yyyy-mm-dd";
1125
1418
  const p = (n, w = 2) => String(n).padStart(w, "0");
1419
+ const meridiem = /AM\/PM|A\/P/i.test(fmt);
1420
+ const H = d.getUTCHours();
1421
+ const hour12 = H % 12 === 0 ? 12 : H % 12;
1422
+ const hourText = (width) => meridiem ? p(hour12, width) : p(H, width);
1423
+ const elapsedMs = d.getTime() - EXCEL_EPOCH_MS;
1126
1424
  const map = {
1127
- yyyy: String(d.getFullYear()),
1128
- yy: p(d.getFullYear() % 100),
1129
- mmmm: d.toLocaleString("en-US", { month: "long" }),
1130
- mmm: d.toLocaleString("en-US", { month: "short" }),
1131
- mm: p(d.getMonth() + 1),
1132
- m: String(d.getMonth() + 1),
1133
- dd: p(d.getDate()),
1134
- d: String(d.getDate()),
1135
- hh: p(d.getHours()),
1136
- h: String(d.getHours()),
1137
- ss: p(d.getSeconds())
1425
+ yyyy: () => String(d.getUTCFullYear()),
1426
+ yy: () => p(d.getUTCFullYear() % 100),
1427
+ mmmm: () => d.toLocaleString("en-US", { month: "long", timeZone: "UTC" }),
1428
+ mmm: () => d.toLocaleString("en-US", { month: "short", timeZone: "UTC" }),
1429
+ dd: () => p(d.getUTCDate()),
1430
+ d: () => String(d.getUTCDate()),
1431
+ hh: () => hourText(2),
1432
+ h: () => hourText(1),
1433
+ ss: () => p(d.getUTCSeconds()),
1434
+ s: () => String(d.getUTCSeconds())
1138
1435
  };
1139
- return fmt.replace(/yyyy|yy|mmmm|mmm|mm|m|dd|d|hh|h|ss/g, (t) => map[t] ?? t);
1436
+ const month = (t) => t === "mm" ? p(d.getUTCMonth() + 1) : String(d.getUTCMonth() + 1);
1437
+ const minute = (t) => t === "mm" ? p(d.getUTCMinutes()) : String(d.getUTCMinutes());
1438
+ const TOKEN = /AM\/PM|A\/P|yyyy|yy|mmmm|mmm|mm|m|dd|d|hh|h|ss|s/iy;
1439
+ let out = "";
1440
+ let lastWasHours = false;
1441
+ for (let i = 0; i < fmt.length; ) {
1442
+ const ch = fmt[i];
1443
+ if (ch === '"') {
1444
+ i++;
1445
+ while (i < fmt.length && fmt[i] !== '"') out += fmt[i++];
1446
+ i++;
1447
+ continue;
1448
+ }
1449
+ if (ch === "\\") {
1450
+ out += fmt[i + 1] ?? "";
1451
+ i += 2;
1452
+ continue;
1453
+ }
1454
+ if (ch === "[") {
1455
+ const end = fmt.indexOf("]", i);
1456
+ const group = fmt.slice(i + 1, end === -1 ? fmt.length : end);
1457
+ i = end === -1 ? fmt.length : end + 1;
1458
+ if (/^h+$/i.test(group)) {
1459
+ out += p(Math.floor(elapsedMs / 36e5), group.length);
1460
+ lastWasHours = true;
1461
+ } else if (/^m+$/i.test(group)) out += p(Math.floor(elapsedMs / 6e4), group.length);
1462
+ else if (/^s+$/i.test(group)) out += p(Math.floor(elapsedMs / 1e3), group.length);
1463
+ continue;
1464
+ }
1465
+ if (ch === "_") {
1466
+ out += " ";
1467
+ i += 2;
1468
+ continue;
1469
+ }
1470
+ if (ch === "*") {
1471
+ i += 2;
1472
+ continue;
1473
+ }
1474
+ TOKEN.lastIndex = i;
1475
+ const m = TOKEN.exec(fmt);
1476
+ if (m) {
1477
+ const tok = m[0].toLowerCase();
1478
+ i += m[0].length;
1479
+ if (tok === "am/pm") {
1480
+ out += H < 12 ? "AM" : "PM";
1481
+ continue;
1482
+ }
1483
+ if (tok === "a/p") {
1484
+ out += H < 12 ? "A" : "P";
1485
+ continue;
1486
+ }
1487
+ if (tok === "m" || tok === "mm") {
1488
+ const minutes = lastWasHours || /^[\s:.,\-]*s/i.test(fmt.slice(i));
1489
+ out += minutes ? minute(tok) : month(tok);
1490
+ lastWasHours = false;
1491
+ continue;
1492
+ }
1493
+ lastWasHours = tok === "h" || tok === "hh";
1494
+ out += map[tok]();
1495
+ continue;
1496
+ }
1497
+ out += ch;
1498
+ i++;
1499
+ }
1500
+ return out;
1140
1501
  }
1141
1502
  function cellValue(cell) {
1142
1503
  const raw = cell.value;
@@ -1335,6 +1696,7 @@ function cellVal(v) {
1335
1696
  if (typeof v === "number" || typeof v === "string") return v;
1336
1697
  if (typeof v === "object") {
1337
1698
  if (v.result != null) return cellVal(v.result);
1699
+ if (Array.isArray(v.richText)) return v.richText.map((r) => r?.text ?? "").join("");
1338
1700
  if (typeof v.text === "string") return v.text;
1339
1701
  if (v instanceof Date) return v.toISOString().slice(0, 10);
1340
1702
  }
@@ -1377,8 +1739,1670 @@ function flatten(ws) {
1377
1739
  return { columns, rows };
1378
1740
  }
1379
1741
 
1742
+ // src/io/hwpx.ts
1743
+ var EOCD_SIG = 101010256;
1744
+ var CDIR_SIG = 33639248;
1745
+ var LOCAL_SIG = 67324752;
1746
+ async function inflateRaw(bytes) {
1747
+ if (typeof DecompressionStream === "undefined") {
1748
+ throw new Error("\uC774 \uBE0C\uB77C\uC6B0\uC800\uB294 \uC555\uCD95 \uD574\uC81C\uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (DecompressionStream unavailable).");
1749
+ }
1750
+ const ds = new DecompressionStream("deflate-raw");
1751
+ const writer = ds.writable.getWriter();
1752
+ const wrote = writer.write(bytes).then(() => writer.close());
1753
+ const reader = ds.readable.getReader();
1754
+ const chunks = [];
1755
+ let total = 0;
1756
+ for (; ; ) {
1757
+ const { done, value } = await reader.read();
1758
+ if (done) break;
1759
+ chunks.push(value);
1760
+ total += value.length;
1761
+ }
1762
+ await wrote;
1763
+ const out = new Uint8Array(total);
1764
+ let pos = 0;
1765
+ for (const c of chunks) {
1766
+ out.set(c, pos);
1767
+ pos += c.length;
1768
+ }
1769
+ return out;
1770
+ }
1771
+ async function readZip(buffer) {
1772
+ const view = new DataView(buffer);
1773
+ const bytes = new Uint8Array(buffer);
1774
+ let eocd = -1;
1775
+ const floor = Math.max(0, buffer.byteLength - 22 - 65535);
1776
+ for (let i = buffer.byteLength - 22; i >= floor; i--) {
1777
+ if (view.getUint32(i, true) === EOCD_SIG) {
1778
+ eocd = i;
1779
+ break;
1780
+ }
1781
+ }
1782
+ if (eocd === -1) throw new Error("ZIP \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4 (no end-of-central-directory).");
1783
+ const count = view.getUint16(eocd + 10, true);
1784
+ let offset = view.getUint32(eocd + 16, true);
1785
+ const entries = /* @__PURE__ */ new Map();
1786
+ const decoder = new TextDecoder();
1787
+ for (let i = 0; i < count; i++) {
1788
+ if (offset + 46 > buffer.byteLength || view.getUint32(offset, true) !== CDIR_SIG) {
1789
+ throw new Error("ZIP \uC911\uC559 \uB514\uB809\uD130\uB9AC\uAC00 \uC190\uC0C1\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (corrupt central directory).");
1790
+ }
1791
+ const method = view.getUint16(offset + 10, true);
1792
+ const compressedSize = view.getUint32(offset + 20, true);
1793
+ const uncompressedSize = view.getUint32(offset + 24, true);
1794
+ const nameLen = view.getUint16(offset + 28, true);
1795
+ const extraLen = view.getUint16(offset + 30, true);
1796
+ const commentLen = view.getUint16(offset + 32, true);
1797
+ const localOffset = view.getUint32(offset + 42, true);
1798
+ const name = decoder.decode(bytes.subarray(offset + 46, offset + 46 + nameLen));
1799
+ offset += 46 + nameLen + extraLen + commentLen;
1800
+ if (compressedSize === 4294967295 || uncompressedSize === 4294967295) {
1801
+ throw new Error("Zip64 \uD615\uC2DD\uC740 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (Zip64 not supported).");
1802
+ }
1803
+ if (name.endsWith("/")) continue;
1804
+ if (localOffset + 30 > buffer.byteLength || view.getUint32(localOffset, true) !== LOCAL_SIG) {
1805
+ throw new Error("ZIP \uB85C\uCEEC \uD5E4\uB354\uAC00 \uC190\uC0C1\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (corrupt local header).");
1806
+ }
1807
+ const localNameLen = view.getUint16(localOffset + 26, true);
1808
+ const localExtraLen = view.getUint16(localOffset + 28, true);
1809
+ const start = localOffset + 30 + localNameLen + localExtraLen;
1810
+ const data = bytes.subarray(start, start + compressedSize);
1811
+ if (method === 0) entries.set(name, data);
1812
+ else if (method === 8) entries.set(name, await inflateRaw(data));
1813
+ else throw new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC555\uCD95 \uBC29\uC2DD\uC785\uB2C8\uB2E4 (compression method ${method}).`);
1814
+ }
1815
+ return entries;
1816
+ }
1817
+ function parseXml(xml) {
1818
+ const doc = new DOMParser().parseFromString(xml, "application/xml");
1819
+ return doc.querySelector("parsererror") ? null : doc;
1820
+ }
1821
+ var IMAGE_BASE64_BUDGET = 8 * 1024 * 1024;
1822
+ var MIME_BY_EXT = {
1823
+ png: "image/png",
1824
+ jpg: "image/jpeg",
1825
+ jpeg: "image/jpeg",
1826
+ gif: "image/gif",
1827
+ bmp: "image/bmp"
1828
+ };
1829
+ function bytesToBase642(bytes) {
1830
+ let bin = "";
1831
+ for (let i = 0; i < bytes.length; i += 32768) {
1832
+ bin += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + 32768)));
1833
+ }
1834
+ return btoa(bin);
1835
+ }
1836
+ var ImageResolver = class {
1837
+ // ref id → ZIP entry name
1838
+ constructor(entries) {
1839
+ this.entries = entries;
1840
+ this.omitted = 0;
1841
+ this.used = 0;
1842
+ this.index = /* @__PURE__ */ new Map();
1843
+ for (const name of entries.keys()) {
1844
+ const m = /^(?:Contents\/)?BinData\/([^/]+)$/i.exec(name);
1845
+ if (!m) continue;
1846
+ this.index.set(m[1], name);
1847
+ this.index.set(m[1].replace(/\.[^.]+$/, ""), name);
1848
+ }
1849
+ const hpfName = [...entries.keys()].find((n) => /(^|\/)content\.hpf$/i.test(n));
1850
+ const doc = hpfName ? parseXml(new TextDecoder().decode(entries.get(hpfName))) : null;
1851
+ if (!doc) return;
1852
+ for (const item of Array.from(doc.getElementsByTagNameNS("*", "item"))) {
1853
+ const id = item.getAttribute("id");
1854
+ const href = item.getAttribute("href");
1855
+ if (!id || !href) continue;
1856
+ const target = this.entries.has(href) ? href : this.entries.has(`Contents/${href}`) ? `Contents/${href}` : null;
1857
+ if (target) this.index.set(id, target);
1858
+ }
1859
+ }
1860
+ /** Data URL for a binary item id, or null when unresolvable / over budget —
1861
+ * the shared resolution both the markdown and HTML emitters build on. */
1862
+ dataUrl(refId) {
1863
+ const name = this.index.get(refId);
1864
+ const bytes = name ? this.entries.get(name) : void 0;
1865
+ if (!name || !bytes) return null;
1866
+ const base64Length = Math.ceil(bytes.length / 3) * 4;
1867
+ if (this.used + base64Length > IMAGE_BASE64_BUDGET) {
1868
+ this.omitted++;
1869
+ return null;
1870
+ }
1871
+ this.used += base64Length;
1872
+ const ext = /\.([^.]+)$/.exec(name)?.[1]?.toLowerCase() ?? "";
1873
+ return `data:${MIME_BY_EXT[ext] ?? "image/png"};base64,${bytesToBase642(bytes)}`;
1874
+ }
1875
+ /** Markdown image block for a binary item id, or null when unresolvable /
1876
+ * over budget. */
1877
+ markdown(refId) {
1878
+ const url = this.dataUrl(refId);
1879
+ return url ? `![](${url})` : null;
1880
+ }
1881
+ };
1882
+ function binaryItemRef(pic) {
1883
+ const refOf = (el) => {
1884
+ for (const attr of Array.from(el.attributes)) {
1885
+ if (/^(binaryitemidref|bin-item-id|binitemidref)$/i.test(attr.localName)) return attr.value || null;
1886
+ }
1887
+ return null;
1888
+ };
1889
+ const descendants = [pic, ...Array.from(pic.getElementsByTagNameNS("*", "*"))];
1890
+ for (const el of descendants) {
1891
+ if (el.localName === "img") {
1892
+ const ref = refOf(el);
1893
+ if (ref) return ref;
1894
+ }
1895
+ }
1896
+ for (const el of descendants) {
1897
+ const ref = refOf(el);
1898
+ if (ref) return ref;
1899
+ }
1900
+ return null;
1901
+ }
1902
+ var HEADING_STYLE_NAME = /^(개요|Outline|Heading|제목)\s*([1-6])/i;
1903
+ function buildHeadingIndex(entries) {
1904
+ const byStyle = /* @__PURE__ */ new Map();
1905
+ const byParaPr = /* @__PURE__ */ new Map();
1906
+ const name = [...entries.keys()].find((n) => /^Contents\/header\.xml$/i.test(n));
1907
+ const doc = name ? parseXml(new TextDecoder().decode(entries.get(name))) : null;
1908
+ if (doc) {
1909
+ for (const style of Array.from(doc.getElementsByTagNameNS("*", "style"))) {
1910
+ const match = HEADING_STYLE_NAME.exec(style.getAttribute("name") ?? "");
1911
+ if (!match) continue;
1912
+ const level = Number(match[2]);
1913
+ const id = style.getAttribute("id");
1914
+ if (id !== null) byStyle.set(id, level);
1915
+ const paraPr = style.getAttribute("paraPrIDRef");
1916
+ if (paraPr !== null) byParaPr.set(paraPr, level);
1917
+ }
1918
+ }
1919
+ return { byStyle, byParaPr };
1920
+ }
1921
+ function paragraphText(p) {
1922
+ let out = "";
1923
+ const walk = (el) => {
1924
+ for (const child of Array.from(el.children)) {
1925
+ const tag = child.localName;
1926
+ if (tag === "tbl") continue;
1927
+ if (tag === "t") out += child.textContent ?? "";
1928
+ else if (tag === "lineBreak") out += "\n";
1929
+ else walk(child);
1930
+ }
1931
+ };
1932
+ walk(p);
1933
+ return out;
1934
+ }
1935
+ async function openHwpx(buffer) {
1936
+ const entries = await readZip(buffer);
1937
+ const names = [...entries.keys()].filter((n) => /^Contents\/section\d+\.xml$/i.test(n)).sort((a, b) => Number(/(\d+)/.exec(a)?.[1] ?? 0) - Number(/(\d+)/.exec(b)?.[1] ?? 0));
1938
+ if (!names.length) {
1939
+ throw new Error("HWPX \uBB38\uC11C\uAC00 \uC544\uB2D9\uB2C8\uB2E4 \u2014 \uBCF8\uBB38(section XML)\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (no Contents/section*.xml).");
1940
+ }
1941
+ const decoder = new TextDecoder();
1942
+ return {
1943
+ entries,
1944
+ sections: names.map((n) => decoder.decode(entries.get(n))),
1945
+ headings: buildHeadingIndex(entries),
1946
+ images: new ImageResolver(entries)
1947
+ };
1948
+ }
1949
+ function headingLevelOf(p, headings) {
1950
+ const styleId = p.getAttribute("styleIDRef");
1951
+ if (styleId !== null && headings.byStyle.has(styleId)) return headings.byStyle.get(styleId);
1952
+ const paraPrId = p.getAttribute("paraPrIDRef");
1953
+ return paraPrId !== null && headings.byParaPr.get(paraPrId) || 0;
1954
+ }
1955
+ function escapeHtml(text) {
1956
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1957
+ }
1958
+ var KOREAN_FONT_STACK = "'Malgun Gothic','\uB9D1\uC740 \uACE0\uB515','Apple SD Gothic Neo',AppleGothic,sans-serif";
1959
+ function koreanDocHtml(bodyHtml, title = "\uBB38\uC11C") {
1960
+ return `<!DOCTYPE html>
1961
+ <html lang="ko">
1962
+ <head>
1963
+ <meta charset="utf-8">
1964
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1965
+ <title>${escapeHtml(title)}</title>
1966
+ <style>
1967
+ body{margin:0;padding:32px 16px;background:#eceff1;color:#191919;font-family:${KOREAN_FONT_STACK};line-height:1.6;}
1968
+ .page{max-width:794px;margin:0 auto;background:#fff;padding:96px 72px;box-shadow:0 2px 12px rgba(0,0,0,.12);}
1969
+ .page p{margin:0 0 10px;}
1970
+ .page h1,.page h2,.page h3,.page h4,.page h5,.page h6{margin:18px 0 12px;line-height:1.35;}
1971
+ .page table{border-collapse:collapse;margin:0 0 10px;}
1972
+ .page td,.page th{border:1px solid #666;padding:6px 8px;vertical-align:top;}
1973
+ .page td p:last-child{margin-bottom:0;}
1974
+ .page img{max-width:100%;}
1975
+ </style>
1976
+ </head>
1977
+ <body>
1978
+ <div class="page">
1979
+ ${bodyHtml}
1980
+ </div>
1981
+ </body>
1982
+ </html>`;
1983
+ }
1984
+ var ALIGN_CSS = {
1985
+ LEFT: "left",
1986
+ CENTER: "center",
1987
+ RIGHT: "right",
1988
+ JUSTIFY: "justify",
1989
+ BOTH: "justify",
1990
+ DISTRIBUTE: "justify",
1991
+ DISTRIBUTE_SPACE: "justify"
1992
+ };
1993
+ var HEX_COLOR = /^#[0-9a-f]{6}$/i;
1994
+ var heightToPx = (height) => Math.round(height / 100 * (4 / 3) * 100) / 100;
1995
+ function charPrCss(charPr, faceByLangId) {
1996
+ let bold = false;
1997
+ let italic = false;
1998
+ let underline = false;
1999
+ let face;
2000
+ for (const child of Array.from(charPr.children)) {
2001
+ const tag = child.localName;
2002
+ if (tag === "bold") bold = true;
2003
+ else if (tag === "italic") italic = true;
2004
+ else if (tag === "underline") {
2005
+ underline = (child.getAttribute("type") ?? "SOLID").toUpperCase() !== "NONE";
2006
+ } else if (tag === "fontRef") {
2007
+ const ref = child.getAttribute("hangul") ?? child.getAttribute("latin");
2008
+ if (ref !== null) face = faceByLangId.get(`HANGUL:${ref}`) ?? faceByLangId.get(`LATIN:${ref}`);
2009
+ }
2010
+ }
2011
+ const rules = [];
2012
+ if (face) rules.push(`font-family:'${face.replace(/'/g, "")}',${KOREAN_FONT_STACK}`);
2013
+ const height = Number(charPr.getAttribute("height"));
2014
+ if (Number.isFinite(height) && height > 0) rules.push(`font-size:${heightToPx(height)}px`);
2015
+ const color = charPr.getAttribute("textColor");
2016
+ if (color && HEX_COLOR.test(color) && color.toLowerCase() !== "#000000") rules.push(`color:${color}`);
2017
+ if (bold) rules.push("font-weight:bold");
2018
+ if (italic) rules.push("font-style:italic");
2019
+ if (underline) rules.push("text-decoration:underline");
2020
+ return rules.join(";");
2021
+ }
2022
+ function buildStyleIndex(entries) {
2023
+ const index = { charCss: /* @__PURE__ */ new Map(), paraAlign: /* @__PURE__ */ new Map(), fillColor: /* @__PURE__ */ new Map() };
2024
+ const name = [...entries.keys()].find((n) => /^Contents\/header\.xml$/i.test(n));
2025
+ const doc = name ? parseXml(new TextDecoder().decode(entries.get(name))) : null;
2026
+ if (!doc) return index;
2027
+ const faceByLangId = /* @__PURE__ */ new Map();
2028
+ for (const fontface of Array.from(doc.getElementsByTagNameNS("*", "fontface"))) {
2029
+ const lang = fontface.getAttribute("lang") ?? "";
2030
+ for (const font of Array.from(fontface.getElementsByTagNameNS("*", "font"))) {
2031
+ const id = font.getAttribute("id");
2032
+ const face = font.getAttribute("face");
2033
+ if (id !== null && face) faceByLangId.set(`${lang}:${id}`, face);
2034
+ }
2035
+ }
2036
+ for (const charPr of Array.from(doc.getElementsByTagNameNS("*", "charPr"))) {
2037
+ const id = charPr.getAttribute("id");
2038
+ if (id !== null) index.charCss.set(id, charPrCss(charPr, faceByLangId));
2039
+ }
2040
+ for (const paraPr of Array.from(doc.getElementsByTagNameNS("*", "paraPr"))) {
2041
+ const id = paraPr.getAttribute("id");
2042
+ if (id === null) continue;
2043
+ let raw = paraPr.getAttribute("align") ?? paraPr.getAttribute("alignment");
2044
+ for (const child of Array.from(paraPr.children)) {
2045
+ if (child.localName === "align") raw = child.getAttribute("horizontal") ?? child.getAttribute("align") ?? raw;
2046
+ }
2047
+ const css = raw ? ALIGN_CSS[raw.toUpperCase()] : void 0;
2048
+ if (css) index.paraAlign.set(id, css);
2049
+ }
2050
+ for (const borderFill of Array.from(doc.getElementsByTagNameNS("*", "borderFill"))) {
2051
+ const id = borderFill.getAttribute("id");
2052
+ const color = borderFill.getElementsByTagNameNS("*", "winBrush")[0]?.getAttribute("faceColor");
2053
+ if (id !== null && color && HEX_COLOR.test(color)) index.fillColor.set(id, color);
2054
+ }
2055
+ return index;
2056
+ }
2057
+ function paragraphInlineHtml(p, ctx) {
2058
+ const segments = [];
2059
+ const walk = (el, css) => {
2060
+ for (const child of Array.from(el.children)) {
2061
+ const tag = child.localName;
2062
+ if (tag === "tbl" || tag === "pic") continue;
2063
+ if (tag === "t") {
2064
+ const text = child.textContent ?? "";
2065
+ if (text) segments.push({ css, html: escapeHtml(text) });
2066
+ } else if (tag === "lineBreak") {
2067
+ segments.push({ css, html: "<br>" });
2068
+ } else if (tag === "run") {
2069
+ walk(child, ctx.charCss(child.getAttribute("charPrIDRef")));
2070
+ } else {
2071
+ walk(child, css);
2072
+ }
2073
+ }
2074
+ };
2075
+ walk(p, "");
2076
+ let out = "";
2077
+ for (let i = 0; i < segments.length; ) {
2078
+ const css = segments[i].css;
2079
+ let html = "";
2080
+ for (; i < segments.length && segments[i].css === css; i++) html += segments[i].html;
2081
+ out += css ? `<span style="${escapeHtml(css)}">${html}</span>` : html;
2082
+ }
2083
+ return out;
2084
+ }
2085
+ function paragraphToHtmlBlocks(p, blocks, ctx) {
2086
+ if (paragraphText(p).trim()) {
2087
+ const level = ctx.headingLevel(p);
2088
+ const align = ctx.paraAlign(p.getAttribute("paraPrIDRef"));
2089
+ const style = align ? ` style="text-align:${align}"` : "";
2090
+ const inline = paragraphInlineHtml(p, ctx);
2091
+ blocks.push(level ? `<h${level}${style}>${inline}</h${level}>` : `<p${style}>${inline}</p>`);
2092
+ }
2093
+ collectEmbeddedHtml(p, blocks, ctx);
2094
+ }
2095
+ function collectEmbeddedHtml(el, blocks, ctx) {
2096
+ for (const child of Array.from(el.children)) {
2097
+ if (child.localName === "tbl") {
2098
+ const html = tableToHtml(child, ctx);
2099
+ if (html) blocks.push(html);
2100
+ } else if (child.localName === "pic") {
2101
+ const img = ctx.imageTag(child);
2102
+ if (img) blocks.push(img);
2103
+ } else {
2104
+ collectEmbeddedHtml(child, blocks, ctx);
2105
+ }
2106
+ }
2107
+ }
2108
+ function tableToHtml(tbl, ctx) {
2109
+ let rowsHtml = "";
2110
+ for (const tr of Array.from(tbl.getElementsByTagNameNS("*", "tr"))) {
2111
+ if (tr.closest("tbl") !== tbl) continue;
2112
+ let cells = "";
2113
+ for (const tc of Array.from(tr.children)) {
2114
+ if (tc.localName !== "tc") continue;
2115
+ let colSpan = 1;
2116
+ let rowSpan = 1;
2117
+ const readSpan = (el) => {
2118
+ for (const attr of Array.from(el.attributes)) {
2119
+ if (/^colspan$/i.test(attr.localName)) colSpan = Math.max(1, Number(attr.value) || 1);
2120
+ else if (/^rowspan$/i.test(attr.localName)) rowSpan = Math.max(1, Number(attr.value) || 1);
2121
+ }
2122
+ };
2123
+ readSpan(tc);
2124
+ for (const child of Array.from(tc.children)) {
2125
+ if (child.localName === "cellSpan") readSpan(child);
2126
+ }
2127
+ const inner = [];
2128
+ const walkCell = (el) => {
2129
+ for (const child of Array.from(el.children)) {
2130
+ if (child.localName === "p") paragraphToHtmlBlocks(child, inner, ctx);
2131
+ else if (child.localName === "tbl") inner.push(tableToHtml(child, ctx));
2132
+ else walkCell(child);
2133
+ }
2134
+ };
2135
+ walkCell(tc);
2136
+ const bg = ctx.cellBackground(tc.getAttribute("borderFillIDRef"));
2137
+ const attrs = (colSpan > 1 ? ` colspan="${colSpan}"` : "") + (rowSpan > 1 ? ` rowspan="${rowSpan}"` : "") + (bg ? ` style="background:${bg}"` : "");
2138
+ cells += `<td${attrs}>${inner.join("")}</td>`;
2139
+ }
2140
+ if (cells) rowsHtml += `<tr>${cells}</tr>`;
2141
+ }
2142
+ return rowsHtml ? `<table>${rowsHtml}</table>` : "";
2143
+ }
2144
+ function sectionToHtml(xml, ctx) {
2145
+ const doc = parseXml(xml);
2146
+ if (!doc) {
2147
+ throw new Error("HWPX \uBCF8\uBB38 XML\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (invalid section XML).");
2148
+ }
2149
+ const blocks = [];
2150
+ const walk = (el) => {
2151
+ for (const child of Array.from(el.children)) {
2152
+ if (child.localName === "p") paragraphToHtmlBlocks(child, blocks, ctx);
2153
+ else if (child.localName !== "tbl") walk(child);
2154
+ }
2155
+ };
2156
+ walk(doc.documentElement);
2157
+ return blocks;
2158
+ }
2159
+ async function hwpxToHtml(buffer) {
2160
+ const { entries, sections, headings, images } = await openHwpx(buffer);
2161
+ const styles2 = buildStyleIndex(entries);
2162
+ const ctx = {
2163
+ headingLevel: (p) => headingLevelOf(p, headings),
2164
+ charCss: (id) => id !== null ? styles2.charCss.get(id) ?? "" : "",
2165
+ paraAlign: (id) => id !== null ? styles2.paraAlign.get(id) ?? null : null,
2166
+ cellBackground: (id) => id !== null ? styles2.fillColor.get(id) ?? null : null,
2167
+ imageTag: (pic) => {
2168
+ const ref = binaryItemRef(pic);
2169
+ const url = ref ? images.dataUrl(ref) : null;
2170
+ return url ? `<img src="${url}" style="max-width:100%">` : null;
2171
+ }
2172
+ };
2173
+ const blocks = [];
2174
+ for (const xml of sections) {
2175
+ blocks.push(...sectionToHtml(xml, ctx));
2176
+ }
2177
+ if (images.omitted > 0) {
2178
+ blocks.push(`<p>(\uC774\uBBF8\uC9C0 ${images.omitted}\uC7A5 \uC0DD\uB7B5 / ${images.omitted} images omitted)</p>`);
2179
+ }
2180
+ return koreanDocHtml(blocks.join("\n"));
2181
+ }
2182
+
2183
+ // src/io/hwp.ts
2184
+ var ERR = {
2185
+ notCfb: "CFB(OLE) \uCEE8\uD14C\uC774\uB108 \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4 \u2014 \uC62C\uBC14\uB978 HWP \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4. / Not a CFB (OLE) compound file container \u2014 not a valid HWP file.",
2186
+ notHwp: '"HWP Document File" \uC2DC\uADF8\uB2C8\uCC98\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 HWP \uBB38\uC11C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. / Missing "HWP Document File" signature \u2014 not an HWP document.',
2187
+ encrypted: "\uC554\uD638\uB85C \uBCF4\uD638\uB41C HWP \uBB38\uC11C\uC785\uB2C8\uB2E4 \u2014 \uD55C\uAE00\uC5D0\uC11C \uC554\uD638\uB97C \uD574\uC81C\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694. / This HWP document is password-encrypted \u2014 remove the password in Hangul and try again.",
2188
+ drm: "\uBC30\uD3EC\uC6A9(DRM) HWP \uBB38\uC11C\uC785\uB2C8\uB2E4 \u2014 \uBCF8\uBB38\uC774 \uB09C\uB3C5\uD654\uB418\uC5B4 \uC788\uC5B4 \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. / This is a distribution (DRM) HWP document \u2014 its body text is obfuscated and cannot be extracted.",
2189
+ legacy: (version) => `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 HWP \uBC84\uC804\uC785\uB2C8\uB2E4 (${version} < 5.0) \u2014 \uD55C\uAE00\uC5D0\uC11C HWP 5.0 \uC774\uC0C1\uC73C\uB85C \uC800\uC7A5\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694. / Unsupported HWP version (${version} < 5.0) \u2014 re-save as HWP 5.0+ in Hangul and try again.`,
2190
+ corrupt: (detail) => `\uC190\uC0C1\uB41C HWP \uD30C\uC77C\uC785\uB2C8\uB2E4 (${detail}). / Corrupt HWP file (${detail}).`,
2191
+ noInflate: "\uC774 \uD658\uACBD\uC5D0\uC11C\uB294 DecompressionStream\uC744 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC544 \uC555\uCD95\uB41C HWP\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. / DecompressionStream is unavailable in this environment, so compressed HWP files cannot be opened."
2192
+ };
2193
+ var ENDOFCHAIN = 4294967294;
2194
+ var CFB_SIGNATURE = [208, 207, 17, 224, 161, 177, 26, 225];
2195
+ function parseCfb(bytes) {
2196
+ if (bytes.length < 512 || CFB_SIGNATURE.some((b, i) => bytes[i] !== b)) {
2197
+ throw new Error(ERR.notCfb);
2198
+ }
2199
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2200
+ const sectorSize = 1 << view.getUint16(30, true);
2201
+ const miniSectorSize = 1 << view.getUint16(32, true);
2202
+ const numFatSectors = view.getUint32(44, true);
2203
+ const firstDirSector = view.getUint32(48, true);
2204
+ const miniCutoff = view.getUint32(56, true);
2205
+ const firstMiniFatSector = view.getUint32(60, true);
2206
+ const firstDifatSector = view.getUint32(68, true);
2207
+ const totalSectors = Math.max(0, Math.ceil((bytes.length - 512) / sectorSize));
2208
+ const sectorBytes = (sector) => {
2209
+ const start = 512 + sector * sectorSize;
2210
+ if (sector >= totalSectors || start >= bytes.length) {
2211
+ throw new Error(ERR.corrupt("sector out of range"));
2212
+ }
2213
+ return bytes.subarray(start, start + sectorSize);
2214
+ };
2215
+ const fatSectorIds = [];
2216
+ for (let i = 0; i < 109 && fatSectorIds.length < numFatSectors; i++) {
2217
+ fatSectorIds.push(view.getUint32(76 + i * 4, true));
2218
+ }
2219
+ let difatSector = firstDifatSector;
2220
+ let difatHops = 0;
2221
+ while (difatSector !== ENDOFCHAIN && difatSector < 4294967290) {
2222
+ if (difatHops++ > totalSectors) throw new Error(ERR.corrupt("DIFAT chain loops"));
2223
+ const s = sectorBytes(difatSector);
2224
+ const sv = new DataView(s.buffer, s.byteOffset, s.byteLength);
2225
+ const perSector = sectorSize / 4 - 1;
2226
+ for (let i = 0; i < perSector && fatSectorIds.length < numFatSectors; i++) {
2227
+ fatSectorIds.push(sv.getUint32(i * 4, true));
2228
+ }
2229
+ difatSector = sv.getUint32(sectorSize - 4, true);
2230
+ }
2231
+ const fat = new Uint32Array(fatSectorIds.length * (sectorSize / 4));
2232
+ fatSectorIds.forEach((id, idx) => {
2233
+ const s = sectorBytes(id);
2234
+ const sv = new DataView(s.buffer, s.byteOffset, s.byteLength);
2235
+ for (let i = 0; i < sectorSize / 4; i++) fat[idx * (sectorSize / 4) + i] = sv.getUint32(i * 4, true);
2236
+ });
2237
+ const readChain = (start, size) => {
2238
+ const out = new Uint8Array(size);
2239
+ let sector = start;
2240
+ let written = 0;
2241
+ let hops = 0;
2242
+ while (sector !== ENDOFCHAIN && written < size) {
2243
+ if (hops++ > totalSectors || sector >= fat.length) throw new Error(ERR.corrupt("FAT chain loops"));
2244
+ const chunk = sectorBytes(sector);
2245
+ out.set(chunk.subarray(0, Math.min(sectorSize, size - written)), written);
2246
+ written += sectorSize;
2247
+ sector = fat[sector];
2248
+ }
2249
+ return out;
2250
+ };
2251
+ const dirSectors = [];
2252
+ let dirSector = firstDirSector;
2253
+ let dirHops = 0;
2254
+ while (dirSector !== ENDOFCHAIN) {
2255
+ if (dirHops++ > totalSectors || dirSector >= fat.length) throw new Error(ERR.corrupt("directory chain loops"));
2256
+ dirSectors.push(sectorBytes(dirSector));
2257
+ dirSector = fat[dirSector];
2258
+ }
2259
+ const entries = [];
2260
+ for (const sector of dirSectors) {
2261
+ const sv = new DataView(sector.buffer, sector.byteOffset, sector.byteLength);
2262
+ for (let off = 0; off + 128 <= sector.length; off += 128) {
2263
+ const nameLen = sv.getUint16(off + 64, true);
2264
+ const type = sv.getUint8(off + 66);
2265
+ if (type === 0 || nameLen < 2 || nameLen > 64) continue;
2266
+ let name = "";
2267
+ for (let i = 0; i < nameLen - 2; i += 2) name += String.fromCharCode(sv.getUint16(off + i, true));
2268
+ entries.push({
2269
+ name,
2270
+ type,
2271
+ startSector: sv.getUint32(off + 116, true),
2272
+ // Size is a uint64, but HWP streams are far below 4 GB — the low half suffices.
2273
+ size: sv.getUint32(off + 120, true)
2274
+ });
2275
+ }
2276
+ }
2277
+ const root = entries.find((e) => e.type === 5);
2278
+ if (!root) throw new Error(ERR.corrupt("missing root directory entry"));
2279
+ let miniStream = null;
2280
+ let miniFat = null;
2281
+ const lazyMini = () => {
2282
+ if (miniStream && miniFat) return;
2283
+ miniStream = readChain(root.startSector, root.size);
2284
+ const miniFatSectors = Math.ceil(root.size / miniSectorSize) + 1;
2285
+ const raw = readChain(firstMiniFatSector, miniFatSectors * 4 + sectorSize);
2286
+ const rv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
2287
+ miniFat = new Uint32Array(Math.floor(raw.length / 4));
2288
+ for (let i = 0; i < miniFat.length; i++) miniFat[i] = rv.getUint32(i * 4, true);
2289
+ };
2290
+ const readMiniChain = (start, size) => {
2291
+ lazyMini();
2292
+ const out = new Uint8Array(size);
2293
+ let sector = start;
2294
+ let written = 0;
2295
+ let hops = 0;
2296
+ const maxMini = Math.ceil(miniStream.length / miniSectorSize);
2297
+ while (sector !== ENDOFCHAIN && written < size) {
2298
+ if (hops++ > maxMini || sector >= miniFat.length || sector >= maxMini) {
2299
+ throw new Error(ERR.corrupt("mini FAT chain loops"));
2300
+ }
2301
+ const at = sector * miniSectorSize;
2302
+ out.set(miniStream.subarray(at, at + Math.min(miniSectorSize, size - written)), written);
2303
+ written += miniSectorSize;
2304
+ sector = miniFat[sector];
2305
+ }
2306
+ return out;
2307
+ };
2308
+ return {
2309
+ entries,
2310
+ readStream: (entry) => entry.size < miniCutoff && entry.type !== 5 ? readMiniChain(entry.startSector, entry.size) : readChain(entry.startSector, entry.size)
2311
+ };
2312
+ }
2313
+ var HWP_SIGNATURE = "HWP Document File";
2314
+ function parseFileHeader(bytes) {
2315
+ if (bytes.length < 40) throw new Error(ERR.notHwp);
2316
+ let sig = "";
2317
+ for (let i = 0; i < HWP_SIGNATURE.length; i++) sig += String.fromCharCode(bytes[i]);
2318
+ if (sig !== HWP_SIGNATURE) throw new Error(ERR.notHwp);
2319
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2320
+ const version = view.getUint32(32, true);
2321
+ const major = version >>> 24 & 255;
2322
+ if (major < 5) {
2323
+ throw new Error(ERR.legacy(`${major}.${version >>> 16 & 255}`));
2324
+ }
2325
+ const flags = view.getUint32(36, true);
2326
+ if (flags & 2) throw new Error(ERR.encrypted);
2327
+ if (flags & 4) throw new Error(ERR.drm);
2328
+ return { compressed: (flags & 1) !== 0 };
2329
+ }
2330
+ var HWPTAG_PARA_TEXT = 16 + 51;
2331
+ var HWPTAG_CTRL_HEADER = 16 + 55;
2332
+ var HWPTAG_LIST_HEADER = 16 + 56;
2333
+ var HWPTAG_TABLE = 16 + 61;
2334
+ var CTRL_ID_TABLE = 1952607264;
2335
+ function decodeParaText(view, offset, byteLength) {
2336
+ const paragraphs = [];
2337
+ let text = "";
2338
+ const count = byteLength >> 1;
2339
+ for (let i = 0; i < count; i++) {
2340
+ const code = view.getUint16(offset + i * 2, true);
2341
+ if (code >= 32) {
2342
+ text += String.fromCharCode(code);
2343
+ continue;
2344
+ }
2345
+ switch (code) {
2346
+ case 13:
2347
+ paragraphs.push(text);
2348
+ text = "";
2349
+ break;
2350
+ case 10:
2351
+ text += "\n";
2352
+ break;
2353
+ case 0:
2354
+ case 24:
2355
+ case 25:
2356
+ case 26:
2357
+ case 27:
2358
+ case 28:
2359
+ case 29:
2360
+ case 30:
2361
+ case 31:
2362
+ break;
2363
+ // 1-WCHAR char controls with no text representation
2364
+ case 9:
2365
+ i += 7;
2366
+ text += " ";
2367
+ break;
2368
+ default:
2369
+ i += 7;
2370
+ break;
2371
+ }
2372
+ }
2373
+ if (text) paragraphs.push(text);
2374
+ return paragraphs;
2375
+ }
2376
+ function parseRecords(section, view) {
2377
+ const records = [];
2378
+ let off = 0;
2379
+ while (off + 4 <= section.length) {
2380
+ const header = view.getUint32(off, true);
2381
+ off += 4;
2382
+ const tag = header & 1023;
2383
+ const level = header >>> 10 & 1023;
2384
+ let size = header >>> 20 & 4095;
2385
+ if (size === 4095) {
2386
+ if (off + 4 > section.length) throw new Error(ERR.corrupt("truncated record header"));
2387
+ size = view.getUint32(off, true);
2388
+ off += 4;
2389
+ }
2390
+ if (off + size > section.length) throw new Error(ERR.corrupt("record overruns section"));
2391
+ records.push({ tag, level, start: off, size });
2392
+ off += size;
2393
+ }
2394
+ return records;
2395
+ }
2396
+ function tableBlocks(view, subtree, ctrlLevel) {
2397
+ const tableRec = subtree.find((r) => r.tag === HWPTAG_TABLE);
2398
+ const cellLevel = ctrlLevel + 1;
2399
+ const cells = [];
2400
+ const flat = [];
2401
+ let current = null;
2402
+ let surprised = tableRec === void 0 || tableRec.size < 8;
2403
+ for (const rec of subtree) {
2404
+ if (rec.tag === HWPTAG_LIST_HEADER && rec.level === cellLevel) {
2405
+ current = [];
2406
+ cells.push(current);
2407
+ } else if (rec.tag === HWPTAG_PARA_TEXT) {
2408
+ const paragraphs = decodeParaText(view, rec.start, rec.size);
2409
+ flat.push(...paragraphs);
2410
+ if (current) current.push(...paragraphs);
2411
+ else surprised = true;
2412
+ }
2413
+ }
2414
+ if (!surprised) {
2415
+ const nRows = view.getUint16(tableRec.start + 4, true);
2416
+ const nCols = view.getUint16(tableRec.start + 6, true);
2417
+ if (nRows >= 1 && nCols >= 1 && cells.length === nRows * nCols) {
2418
+ const texts = cells.map((c) => c.filter((p) => p.length > 0).join("\n"));
2419
+ const rows = Array.from({ length: nRows }, (_, r) => texts.slice(r * nCols, (r + 1) * nCols));
2420
+ return [{ kind: "table", rows }];
2421
+ }
2422
+ }
2423
+ return flat.map((text) => ({ kind: "paragraph", text }));
2424
+ }
2425
+ function extractSectionBlocks(section) {
2426
+ const view = new DataView(section.buffer, section.byteOffset, section.byteLength);
2427
+ const records = parseRecords(section, view);
2428
+ const blocks = [];
2429
+ let i = 0;
2430
+ while (i < records.length) {
2431
+ const rec = records[i];
2432
+ if (rec.tag === HWPTAG_CTRL_HEADER && rec.size >= 4 && view.getUint32(rec.start, true) === CTRL_ID_TABLE) {
2433
+ let end = i + 1;
2434
+ while (end < records.length && records[end].level > rec.level) end++;
2435
+ blocks.push(...tableBlocks(view, records.slice(i + 1, end), rec.level));
2436
+ i = end;
2437
+ continue;
2438
+ }
2439
+ if (rec.tag === HWPTAG_PARA_TEXT) {
2440
+ for (const text of decodeParaText(view, rec.start, rec.size)) blocks.push({ kind: "paragraph", text });
2441
+ }
2442
+ i++;
2443
+ }
2444
+ return blocks;
2445
+ }
2446
+ function blocksToHtml(blocks) {
2447
+ const inline = (text) => escapeHtml(text).replace(/\n/g, "<br>");
2448
+ const out = [];
2449
+ for (const block of blocks) {
2450
+ if (block.kind === "paragraph") {
2451
+ if (block.text.length > 0) out.push(`<p>${inline(block.text)}</p>`);
2452
+ continue;
2453
+ }
2454
+ const rows = block.rows.map((row) => `<tr>${row.map((cell) => `<td>${inline(cell)}</td>`).join("")}</tr>`);
2455
+ out.push(`<table>${rows.join("")}</table>`);
2456
+ }
2457
+ return out;
2458
+ }
2459
+ async function inflateRaw2(data) {
2460
+ if (typeof DecompressionStream === "undefined") throw new Error(ERR.noInflate);
2461
+ const stream = new DecompressionStream("deflate-raw");
2462
+ const writer = stream.writable.getWriter();
2463
+ const writing = writer.write(data).then(() => writer.close());
2464
+ writing.catch(() => {
2465
+ });
2466
+ const reader = stream.readable.getReader();
2467
+ const chunks = [];
2468
+ let total = 0;
2469
+ for (; ; ) {
2470
+ const { done, value } = await reader.read();
2471
+ if (done) break;
2472
+ chunks.push(value);
2473
+ total += value.length;
2474
+ }
2475
+ const out = new Uint8Array(total);
2476
+ let at = 0;
2477
+ for (const chunk of chunks) {
2478
+ out.set(chunk, at);
2479
+ at += chunk.length;
2480
+ }
2481
+ return out;
2482
+ }
2483
+ async function extractBlocks(buffer) {
2484
+ const cfb = parseCfb(new Uint8Array(buffer));
2485
+ const headerEntry = cfb.entries.find((e) => e.type === 2 && e.name === "FileHeader");
2486
+ if (!headerEntry) throw new Error(ERR.notHwp);
2487
+ const header = parseFileHeader(cfb.readStream(headerEntry));
2488
+ const sections = cfb.entries.map((e) => ({ entry: e, match: e.type === 2 ? /^Section(\d+)$/.exec(e.name) : null })).filter((s) => s.match !== null).sort((a, b) => Number(a.match[1]) - Number(b.match[1]));
2489
+ const blocks = [];
2490
+ for (const { entry } of sections) {
2491
+ let bytes = cfb.readStream(entry);
2492
+ if (header.compressed) bytes = await inflateRaw2(bytes);
2493
+ blocks.push(...extractSectionBlocks(bytes));
2494
+ }
2495
+ return blocks;
2496
+ }
2497
+ async function hwpToHtml(buffer) {
2498
+ return koreanDocHtml(blocksToHtml(await extractBlocks(buffer)).join("\n"));
2499
+ }
2500
+
2501
+ // src/io/docx.ts
2502
+ function childOf(el, name) {
2503
+ for (const c of Array.from(el.children)) if (c.localName === name) return c;
2504
+ return null;
2505
+ }
2506
+ function attrOf(el, name) {
2507
+ for (const a of Array.from(el.attributes)) if (a.localName === name) return a.value;
2508
+ return null;
2509
+ }
2510
+ function toggleOn(rPr, name) {
2511
+ const prop = rPr && childOf(rPr, name);
2512
+ if (!prop) return false;
2513
+ const val = attrOf(prop, "val");
2514
+ return val !== "0" && val !== "false" && val !== "none";
2515
+ }
2516
+ function headingLevel(pPr) {
2517
+ const style = pPr && childOf(pPr, "pStyle");
2518
+ const id = style && attrOf(style, "val");
2519
+ const m = id && /^heading ?([1-6])$/i.exec(id);
2520
+ return m ? Number(m[1]) : 0;
2521
+ }
2522
+ function listLevel(pPr) {
2523
+ const numPr = pPr && childOf(pPr, "numPr");
2524
+ if (!numPr) return null;
2525
+ const ilvl = childOf(numPr, "ilvl");
2526
+ return Number(ilvl && attrOf(ilvl, "val")) || 0;
2527
+ }
2528
+ function runText(run) {
2529
+ let out = "";
2530
+ for (const c of Array.from(run.children)) {
2531
+ if (c.localName === "t") {
2532
+ const raw = c.textContent ?? "";
2533
+ out += attrOf(c, "space") === "preserve" ? raw : raw.trim();
2534
+ } else if (c.localName === "br") out += "\n";
2535
+ else if (c.localName === "tab") out += " ";
2536
+ }
2537
+ return out;
2538
+ }
2539
+ function paragraphSegments(p) {
2540
+ const segs = [];
2541
+ const walk = (el) => {
2542
+ for (const c of Array.from(el.children)) {
2543
+ if (c.localName === "pPr") continue;
2544
+ if (c.localName === "r") {
2545
+ const rPr = childOf(c, "rPr");
2546
+ const text = runText(c);
2547
+ if (text) segs.push({ text, bold: toggleOn(rPr, "b"), italic: toggleOn(rPr, "i") });
2548
+ } else walk(c);
2549
+ }
2550
+ };
2551
+ walk(p);
2552
+ return segs;
2553
+ }
2554
+ function renderSegments(segs) {
2555
+ const merged = [];
2556
+ for (const s of segs) {
2557
+ const last = merged[merged.length - 1];
2558
+ if (last && last.bold === s.bold && last.italic === s.italic) last.text += s.text;
2559
+ else merged.push({ ...s });
2560
+ }
2561
+ return merged.map(({ text, bold, italic }) => {
2562
+ if (!bold && !italic) return text;
2563
+ const [, lead, core, trail] = /^(\s*)([\s\S]*?)(\s*)$/.exec(text);
2564
+ if (!core) return text;
2565
+ const mark = bold && italic ? "***" : bold ? "**" : "*";
2566
+ return `${lead}${mark}${core}${mark}${trail}`;
2567
+ }).join("");
2568
+ }
2569
+ function paragraphToBlock(p) {
2570
+ const text = renderSegments(paragraphSegments(p)).trim();
2571
+ if (!text) return null;
2572
+ const pPr = childOf(p, "pPr");
2573
+ const heading = headingLevel(pPr);
2574
+ if (heading) return { kind: "other", text: `${"#".repeat(heading)} ${text}` };
2575
+ const level = listLevel(pPr);
2576
+ if (level !== null) return { kind: "list", text: `${" ".repeat(level)}- ${text}` };
2577
+ return { kind: "other", text };
2578
+ }
2579
+ function tableToMarkdown(tbl) {
2580
+ const rows = [];
2581
+ for (const tr of Array.from(tbl.children)) {
2582
+ if (tr.localName !== "tr") continue;
2583
+ const cells = [];
2584
+ for (const tc of Array.from(tr.children)) {
2585
+ if (tc.localName !== "tc") continue;
2586
+ const text = Array.from(tc.getElementsByTagNameNS("*", "p")).filter((p) => p.closest("tbl") === tbl).map((p) => renderSegments(paragraphSegments(p)).trim()).filter(Boolean).join(" ").replace(/\|/g, "\\|").replace(/\n/g, " ");
2587
+ cells.push(text);
2588
+ }
2589
+ if (cells.length) rows.push(cells);
2590
+ }
2591
+ if (!rows.length) return "";
2592
+ const width = Math.max(...rows.map((r) => r.length));
2593
+ const pad = (r) => Array.from({ length: width }, (_, i) => r[i] ?? "");
2594
+ const line = (r) => `| ${pad(r).join(" | ")} |`;
2595
+ const [head, ...body] = rows;
2596
+ return [line(head), `| ${Array(width).fill("---").join(" | ")} |`, ...body.map(line)].join("\n");
2597
+ }
2598
+ function documentToMarkdown(doc) {
2599
+ const blocks = [];
2600
+ const walk = (el) => {
2601
+ for (const c of Array.from(el.children)) {
2602
+ if (c.localName === "p") {
2603
+ const block = paragraphToBlock(c);
2604
+ if (block) blocks.push(block);
2605
+ } else if (c.localName === "tbl") {
2606
+ const md = tableToMarkdown(c);
2607
+ if (md) blocks.push({ kind: "other", text: md });
2608
+ } else {
2609
+ walk(c);
2610
+ }
2611
+ }
2612
+ };
2613
+ walk(doc.documentElement);
2614
+ const out = [];
2615
+ let prevKind = null;
2616
+ for (const { kind, text } of blocks) {
2617
+ if (kind === "list" && prevKind === "list") out[out.length - 1] += `
2618
+ ${text}`;
2619
+ else out.push(text);
2620
+ prevKind = kind;
2621
+ }
2622
+ return out.join("\n\n").trim();
2623
+ }
2624
+ async function docxToMarkdown(buffer) {
2625
+ const entries = await readZip(buffer);
2626
+ const body = entries.get("word/document.xml");
2627
+ if (!body) {
2628
+ throw new Error("DOCX \uBB38\uC11C\uAC00 \uC544\uB2D9\uB2C8\uB2E4 \u2014 \uBCF8\uBB38(word/document.xml)\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (Not a DOCX document \u2014 missing word/document.xml).");
2629
+ }
2630
+ const doc = new DOMParser().parseFromString(new TextDecoder().decode(body), "application/xml");
2631
+ if (doc.querySelector("parsererror")) {
2632
+ throw new Error("DOCX \uBCF8\uBB38 XML\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (invalid document XML).");
2633
+ }
2634
+ return documentToMarkdown(doc);
2635
+ }
2636
+
2637
+ // src/io/pptx.ts
2638
+ function parseXml2(xml) {
2639
+ const doc = new DOMParser().parseFromString(xml, "application/xml");
2640
+ return doc.querySelector("parsererror") ? null : doc;
2641
+ }
2642
+ function childOf2(el, name) {
2643
+ for (const c of Array.from(el.children)) if (c.localName === name) return c;
2644
+ return null;
2645
+ }
2646
+ function attrOf2(el, name) {
2647
+ for (const a of Array.from(el.attributes)) if (a.localName === name) return a.value;
2648
+ return null;
2649
+ }
2650
+ var isOn = (value) => value === "1" || value === "true";
2651
+ function relIdOf(el) {
2652
+ for (const a of Array.from(el.attributes)) {
2653
+ if (a.localName === "id" && /relationships/i.test(a.namespaceURI ?? "")) return a.value;
2654
+ }
2655
+ for (const a of Array.from(el.attributes)) {
2656
+ if (/:id$/i.test(a.name)) return a.value;
2657
+ }
2658
+ return null;
2659
+ }
2660
+ function resolveTarget(baseDir, target) {
2661
+ if (target.startsWith("/")) return target.slice(1);
2662
+ const parts = baseDir ? baseDir.split("/") : [];
2663
+ for (const seg of target.split("/")) {
2664
+ if (seg === "..") parts.pop();
2665
+ else if (seg && seg !== ".") parts.push(seg);
2666
+ }
2667
+ return parts.join("/");
2668
+ }
2669
+ function relsOf(entries, partName) {
2670
+ const rels = /* @__PURE__ */ new Map();
2671
+ const slash = partName.lastIndexOf("/");
2672
+ const dir = partName.slice(0, slash);
2673
+ const relsPart = entries.get(`${dir}/_rels/${partName.slice(slash + 1)}.rels`);
2674
+ const doc = relsPart ? parseXml2(new TextDecoder().decode(relsPart)) : null;
2675
+ if (!doc) return rels;
2676
+ for (const rel of Array.from(doc.getElementsByTagNameNS("*", "Relationship"))) {
2677
+ const id = rel.getAttribute("Id");
2678
+ const target = rel.getAttribute("Target");
2679
+ if (id && target) rels.set(id, { type: rel.getAttribute("Type") ?? "", target: resolveTarget(dir, target) });
2680
+ }
2681
+ return rels;
2682
+ }
2683
+ function relOfType(rels, suffix) {
2684
+ for (const rel of rels.values()) if (rel.type.endsWith(suffix)) return rel;
2685
+ return void 0;
2686
+ }
2687
+ var DEFAULT_SLIDE_CX = 12192e3;
2688
+ var DEFAULT_SLIDE_CY = 6858e3;
2689
+ function emuOf(parent, child, attr) {
2690
+ const el = childOf2(parent, child);
2691
+ const n = el ? Number(attrOf2(el, attr)) : NaN;
2692
+ return Number.isFinite(n) ? n : null;
2693
+ }
2694
+ function xfrmOf(pr) {
2695
+ const xfrm = childOf2(pr, "xfrm");
2696
+ if (!xfrm) return null;
2697
+ const x = emuOf(xfrm, "off", "x");
2698
+ const y = emuOf(xfrm, "off", "y");
2699
+ const cx = emuOf(xfrm, "ext", "cx");
2700
+ const cy = emuOf(xfrm, "ext", "cy");
2701
+ if (x === null || y === null || cx === null || cy === null) return null;
2702
+ return { x, y, cx, cy, rot: Number(attrOf2(xfrm, "rot")) || 0 };
2703
+ }
2704
+ var IDENTITY_MAP = { tx: 0, ty: 0, sx: 1, sy: 1, chx: 0, chy: 0 };
2705
+ function groupChildMap(grp, map) {
2706
+ const pr = childOf2(grp, "grpSpPr");
2707
+ const xfrm = pr && childOf2(pr, "xfrm");
2708
+ if (!pr || !xfrm) return map;
2709
+ const off = xfrmOf(pr);
2710
+ const chx = emuOf(xfrm, "chOff", "x");
2711
+ const chy = emuOf(xfrm, "chOff", "y");
2712
+ const chCx = emuOf(xfrm, "chExt", "cx");
2713
+ const chCy = emuOf(xfrm, "chExt", "cy");
2714
+ if (!off || chx === null || chy === null || !chCx || !chCy || chCx < 0 || chCy < 0) return map;
2715
+ return {
2716
+ tx: map.tx + (off.x - map.chx) * map.sx,
2717
+ ty: map.ty + (off.y - map.chy) * map.sy,
2718
+ sx: map.sx * (off.cx / chCx),
2719
+ sy: map.sy * (off.cy / chCy),
2720
+ chx,
2721
+ chy
2722
+ };
2723
+ }
2724
+ var round2 = (n) => Math.round(n * 100) / 100;
2725
+ var pct = (value, total) => round2(value / total * 100);
2726
+ var DEFAULT_CLR_MAP = [
2727
+ ["bg1", "lt1"],
2728
+ ["tx1", "dk1"],
2729
+ ["bg2", "lt2"],
2730
+ ["tx2", "dk2"]
2731
+ ];
2732
+ var clamp01 = (n) => Math.max(0, Math.min(1, n));
2733
+ var byteHex = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
2734
+ function rgbOf(hex) {
2735
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
2736
+ }
2737
+ function rgbToHsl(r, g, b) {
2738
+ r /= 255;
2739
+ g /= 255;
2740
+ b /= 255;
2741
+ const max = Math.max(r, g, b);
2742
+ const min = Math.min(r, g, b);
2743
+ const l = (max + min) / 2;
2744
+ if (max === min) return [0, 0, l];
2745
+ const d = max - min;
2746
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
2747
+ const h = max === r ? ((g - b) / d + (g < b ? 6 : 0)) / 6 : max === g ? ((b - r) / d + 2) / 6 : ((r - g) / d + 4) / 6;
2748
+ return [h, s, l];
2749
+ }
2750
+ function hslToRgb(h, s, l) {
2751
+ if (s === 0) return [l * 255, l * 255, l * 255];
2752
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
2753
+ const p = 2 * l - q;
2754
+ const channel = (t) => {
2755
+ if (t < 0) t += 1;
2756
+ if (t > 1) t -= 1;
2757
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
2758
+ if (t < 1 / 2) return q;
2759
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
2760
+ return p;
2761
+ };
2762
+ return [channel(h + 1 / 3) * 255, channel(h) * 255, channel(h - 1 / 3) * 255];
2763
+ }
2764
+ var ColorScheme = class {
2765
+ constructor(scheme, clrMap) {
2766
+ this.scheme = scheme;
2767
+ this.clrMap = clrMap;
2768
+ }
2769
+ resolve(clr) {
2770
+ let hex = null;
2771
+ if (clr.localName === "srgbClr") hex = attrOf2(clr, "val");
2772
+ else if (clr.localName === "sysClr") hex = attrOf2(clr, "lastClr");
2773
+ else if (clr.localName === "schemeClr") {
2774
+ const name = attrOf2(clr, "val") ?? "";
2775
+ hex = this.scheme.get(this.clrMap.get(name) ?? name) ?? null;
2776
+ }
2777
+ if (!hex || !/^[0-9a-fA-F]{6}$/.test(hex)) return void 0;
2778
+ let [r, g, b] = rgbOf(hex);
2779
+ for (const t of Array.from(clr.children)) {
2780
+ const raw = Number(attrOf2(t, "val"));
2781
+ if (!Number.isFinite(raw)) continue;
2782
+ const f = raw / 1e5;
2783
+ if (t.localName === "lumMod" || t.localName === "lumOff") {
2784
+ const [h, s, l] = rgbToHsl(r, g, b);
2785
+ [r, g, b] = hslToRgb(h, s, clamp01(t.localName === "lumMod" ? l * f : l + f));
2786
+ } else if (t.localName === "tint") {
2787
+ r = r * f + 255 * (1 - f);
2788
+ g = g * f + 255 * (1 - f);
2789
+ b = b * f + 255 * (1 - f);
2790
+ } else if (t.localName === "shade") {
2791
+ r *= f;
2792
+ g *= f;
2793
+ b *= f;
2794
+ }
2795
+ }
2796
+ return `#${byteHex(r)}${byteHex(g)}${byteHex(b)}`.toUpperCase();
2797
+ }
2798
+ };
2799
+ function solidFillColor(el, colors) {
2800
+ const fill = el && childOf2(el, "solidFill");
2801
+ if (!fill) return void 0;
2802
+ for (const c of Array.from(fill.children)) {
2803
+ const hex = colors.resolve(c);
2804
+ if (!hex) continue;
2805
+ const alphaEl = childOf2(c, "alpha");
2806
+ const alpha = alphaEl ? Number(attrOf2(alphaEl, "val")) : NaN;
2807
+ if (Number.isFinite(alpha) && alpha >= 0 && alpha < 1e5) {
2808
+ return hex + Math.round(alpha / 1e5 * 255).toString(16).padStart(2, "0").toUpperCase();
2809
+ }
2810
+ return hex;
2811
+ }
2812
+ return void 0;
2813
+ }
2814
+ function gradientFirstStop(el, colors) {
2815
+ const grad = el && childOf2(el, "gradFill");
2816
+ const lst = grad && childOf2(grad, "gsLst");
2817
+ const gs = lst && childOf2(lst, "gs");
2818
+ if (!gs) return void 0;
2819
+ for (const c of Array.from(gs.children)) {
2820
+ const hex = colors.resolve(c);
2821
+ if (hex) return hex;
2822
+ }
2823
+ return void 0;
2824
+ }
2825
+ function styleRefColor(sp, ref, colors) {
2826
+ const style = childOf2(sp, "style");
2827
+ const refEl = style && childOf2(style, ref);
2828
+ if (!refEl || attrOf2(refEl, "idx") === "0") return void 0;
2829
+ for (const c of Array.from(refEl.children)) {
2830
+ const hex = colors.resolve(c);
2831
+ if (hex) return hex;
2832
+ }
2833
+ return void 0;
2834
+ }
2835
+ function chainFor(entries, slideRels, cache) {
2836
+ const load = (name) => {
2837
+ if (!name) return null;
2838
+ let doc = cache.get(name);
2839
+ if (doc === void 0) {
2840
+ const part = entries.get(name);
2841
+ doc = part ? parseXml2(new TextDecoder().decode(part)) : null;
2842
+ cache.set(name, doc);
2843
+ }
2844
+ return doc;
2845
+ };
2846
+ const layoutName = relOfType(slideRels, "/slideLayout")?.target;
2847
+ const layoutDoc = load(layoutName);
2848
+ const masterName = layoutName ? relOfType(relsOf(entries, layoutName), "/slideMaster")?.target : void 0;
2849
+ const masterDoc = load(masterName);
2850
+ const themeDoc = masterName ? load(relOfType(relsOf(entries, masterName), "/theme")?.target) : null;
2851
+ const scheme = /* @__PURE__ */ new Map();
2852
+ const clrScheme = themeDoc?.getElementsByTagNameNS("*", "clrScheme")[0];
2853
+ for (const slot of Array.from(clrScheme?.children ?? [])) {
2854
+ const clr = slot.firstElementChild;
2855
+ const hex = clr?.localName === "sysClr" ? attrOf2(clr, "lastClr") : clr && attrOf2(clr, "val");
2856
+ if (hex && /^[0-9a-fA-F]{6}$/.test(hex)) scheme.set(slot.localName, hex);
2857
+ }
2858
+ const clrMap = new Map(DEFAULT_CLR_MAP);
2859
+ const clrMapEl = masterDoc?.getElementsByTagNameNS("*", "clrMap")[0];
2860
+ for (const a of Array.from(clrMapEl?.attributes ?? [])) clrMap.set(a.localName, a.value);
2861
+ const treeOf = (doc) => doc?.getElementsByTagNameNS("*", "spTree")[0] ?? null;
2862
+ return {
2863
+ colors: new ColorScheme(scheme, clrMap),
2864
+ layoutTree: treeOf(layoutDoc),
2865
+ masterTree: treeOf(masterDoc),
2866
+ layoutDoc,
2867
+ masterDoc,
2868
+ txStyles: masterDoc?.getElementsByTagNameNS("*", "txStyles")[0] ?? null
2869
+ };
2870
+ }
2871
+ var MEDIA_BASE64_BUDGET = 12 * 1024 * 1024;
2872
+ var MIME_BY_EXT2 = {
2873
+ png: "image/png",
2874
+ jpg: "image/jpeg",
2875
+ jpeg: "image/jpeg",
2876
+ gif: "image/gif",
2877
+ bmp: "image/bmp",
2878
+ webp: "image/webp",
2879
+ svg: "image/svg+xml"
2880
+ };
2881
+ function bytesToBase643(bytes) {
2882
+ let bin = "";
2883
+ for (let i = 0; i < bytes.length; i += 32768) {
2884
+ bin += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + 32768)));
2885
+ }
2886
+ return btoa(bin);
2887
+ }
2888
+ var MediaResolver = class {
2889
+ constructor(entries) {
2890
+ this.entries = entries;
2891
+ this.used = 0;
2892
+ }
2893
+ dataUrl(entryName) {
2894
+ const bytes = this.entries.get(entryName);
2895
+ if (!bytes) return null;
2896
+ const base64Length = Math.ceil(bytes.length / 3) * 4;
2897
+ if (this.used + base64Length > MEDIA_BASE64_BUDGET) return null;
2898
+ this.used += base64Length;
2899
+ const ext = /\.([^.]+)$/.exec(entryName)?.[1]?.toLowerCase() ?? "";
2900
+ return `data:${MIME_BY_EXT2[ext] ?? "image/png"};base64,${bytesToBase643(bytes)}`;
2901
+ }
2902
+ };
2903
+ function toPercentBox(xfrm, map, ctx) {
2904
+ return {
2905
+ x: pct(map.tx + (xfrm.x - map.chx) * map.sx, ctx.slideCx),
2906
+ y: pct(map.ty + (xfrm.y - map.chy) * map.sy, ctx.slideCy),
2907
+ w: pct(xfrm.cx * map.sx, ctx.slideCx),
2908
+ h: pct(xfrm.cy * map.sy, ctx.slideCy)
2909
+ };
2910
+ }
2911
+ function normalizedRotation(deg) {
2912
+ const n = Math.round((deg % 360 + 360) % 360 * 100) / 100;
2913
+ return n || void 0;
2914
+ }
2915
+ function rotationOf(xfrm) {
2916
+ if (!xfrm.rot) return void 0;
2917
+ const deg = Math.round(xfrm.rot / 6e4 * 100) / 100;
2918
+ return deg || void 0;
2919
+ }
2920
+ function txBodyText(txBody) {
2921
+ const paras = [];
2922
+ for (const p of Array.from(txBody.children)) {
2923
+ if (p.localName !== "p") continue;
2924
+ let line = "";
2925
+ for (const c of Array.from(p.children)) {
2926
+ if (c.localName === "r") line += childOf2(c, "t")?.textContent ?? "";
2927
+ else if (c.localName === "br") line += "\n";
2928
+ }
2929
+ paras.push(line);
2930
+ }
2931
+ return paras.join("\n");
2932
+ }
2933
+ function placeholderOf(sp) {
2934
+ for (const nv of Array.from(sp.children)) {
2935
+ if (!/^nv(Sp|Pic|GrpSp|Cxn|GraphicFrame)Pr$/.test(nv.localName ?? "")) continue;
2936
+ const nvPr = childOf2(nv, "nvPr");
2937
+ const ph = nvPr && childOf2(nvPr, "ph");
2938
+ if (ph) return { type: attrOf2(ph, "type") ?? "body", idx: attrOf2(ph, "idx") };
2939
+ }
2940
+ return null;
2941
+ }
2942
+ function placeholderType(sp) {
2943
+ return placeholderOf(sp)?.type ?? null;
2944
+ }
2945
+ function findPlaceholder(tree, want) {
2946
+ if (!tree) return null;
2947
+ const shapes = Array.from(tree.getElementsByTagNameNS("*", "sp"));
2948
+ for (const sp of shapes) if (placeholderOf(sp)?.type === want.type) return sp;
2949
+ if (want.idx !== null) {
2950
+ for (const sp of shapes) if (placeholderOf(sp)?.idx === want.idx) return sp;
2951
+ }
2952
+ return null;
2953
+ }
2954
+ function inheritedXfrm(sp, ctx) {
2955
+ const ph = placeholderOf(sp);
2956
+ if (!ph) return null;
2957
+ for (const tree of [ctx.chain.layoutTree, ctx.chain.masterTree]) {
2958
+ const match = findPlaceholder(tree, ph);
2959
+ const spPr = match && childOf2(match, "spPr");
2960
+ const xfrm = spPr && xfrmOf(spPr);
2961
+ if (xfrm) return xfrm;
2962
+ }
2963
+ return null;
2964
+ }
2965
+ function styleSources(sp, txBody, ctx) {
2966
+ const out = [];
2967
+ const own = childOf2(txBody, "lstStyle");
2968
+ if (own) out.push(own);
2969
+ const ph = placeholderOf(sp);
2970
+ if (!ph) return out;
2971
+ for (const tree of [ctx.chain.layoutTree, ctx.chain.masterTree]) {
2972
+ const match = findPlaceholder(tree, ph);
2973
+ const body = match && childOf2(match, "txBody");
2974
+ const lstStyle = body && childOf2(body, "lstStyle");
2975
+ if (lstStyle) out.push(lstStyle);
2976
+ }
2977
+ if (ctx.chain.txStyles) {
2978
+ const bucket = /^(title|ctrTitle)$/.test(ph.type) ? "titleStyle" : /^(body|subTitle|obj)$/.test(ph.type) ? "bodyStyle" : "otherStyle";
2979
+ const styles2 = childOf2(ctx.chain.txStyles, bucket);
2980
+ if (styles2) out.push(styles2);
2981
+ }
2982
+ return out;
2983
+ }
2984
+ function lvlPrOf(source, lvl) {
2985
+ return childOf2(source, `lvl${lvl + 1}pPr`) ?? childOf2(source, "lvl1pPr");
2986
+ }
2987
+ function bulletOf(pPr, sources, lvl) {
2988
+ for (const el of [pPr, ...sources.map((s) => lvlPrOf(s, lvl))]) {
2989
+ if (!el) continue;
2990
+ if (childOf2(el, "buNone")) return false;
2991
+ if (childOf2(el, "buChar") || childOf2(el, "buAutoNum")) return true;
2992
+ }
2993
+ return false;
2994
+ }
2995
+ function paragraphsOf(txBody, sp, ctx) {
2996
+ const colors = ctx.chain.colors;
2997
+ const sources = styleSources(sp, txBody, ctx);
2998
+ const shapeTextColor = styleRefColor(sp, "fontRef", colors);
2999
+ const out = [];
3000
+ for (const p of Array.from(txBody.children)) {
3001
+ if (p.localName !== "p") continue;
3002
+ let text = "";
3003
+ for (const c of Array.from(p.children)) {
3004
+ if (c.localName === "r") text += childOf2(c, "t")?.textContent ?? "";
3005
+ else if (c.localName === "br") text += "\n";
3006
+ }
3007
+ const pPr = childOf2(p, "pPr");
3008
+ const lvl = Math.max(0, Number(pPr && attrOf2(pPr, "lvl")) || 0);
3009
+ const firstRun = childOf2(p, "r");
3010
+ const rPr = firstRun && childOf2(firstRun, "rPr");
3011
+ const props = [];
3012
+ if (rPr) props.push(rPr);
3013
+ const pDef = pPr && childOf2(pPr, "defRPr");
3014
+ if (pDef) props.push(pDef);
3015
+ for (const src of sources) {
3016
+ const lvlPr = lvlPrOf(src, lvl);
3017
+ const def = lvlPr && childOf2(lvlPr, "defRPr");
3018
+ if (def) props.push(def);
3019
+ }
3020
+ const para = { text, bullet: bulletOf(pPr, sources, lvl) };
3021
+ for (const pr of props) {
3022
+ if (para.szCenti === void 0) {
3023
+ const sz = Number(attrOf2(pr, "sz"));
3024
+ if (Number.isFinite(sz) && sz > 0) para.szCenti = sz;
3025
+ }
3026
+ if (para.bold === void 0 && attrOf2(pr, "b") !== null) para.bold = isOn(attrOf2(pr, "b"));
3027
+ if (para.color === void 0) {
3028
+ const color = solidFillColor(pr, colors);
3029
+ if (color) para.color = color;
3030
+ }
3031
+ }
3032
+ if (para.color === void 0 && shapeTextColor) para.color = shapeTextColor;
3033
+ for (const el of [pPr, ...sources.map((s) => lvlPrOf(s, lvl))]) {
3034
+ const algn = el && attrOf2(el, "algn");
3035
+ if (!algn) continue;
3036
+ const align = algn === "l" ? "left" : algn === "ctr" ? "center" : algn === "r" ? "right" : void 0;
3037
+ if (align) para.align = align;
3038
+ break;
3039
+ }
3040
+ out.push(para);
3041
+ }
3042
+ return out;
3043
+ }
3044
+ var TITLE_BOX = { x: 8, y: 10, w: 84, h: 18 };
3045
+ var BODY_BOX = { x: 8, y: 32, w: 84, h: 55 };
3046
+ var MAX_SPLIT_PARAGRAPHS = 8;
3047
+ var DESIGN_WIDTH_PX = 1280;
3048
+ var DESIGN_HEIGHT_PX = 720;
3049
+ var DEFAULT_PARA_PX = 24;
3050
+ var LINE_HEIGHT = 1.3;
3051
+ var OVERFLOW_TOLERANCE = 1.05;
3052
+ var OVERFLOW_MIN_SCALE = 0.65;
3053
+ function textWidthEm(text) {
3054
+ let em = 0;
3055
+ for (const ch of text) {
3056
+ em += /[\u1100-\u11ff\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/.test(ch) ? 1 : 0.55;
3057
+ }
3058
+ return em;
3059
+ }
3060
+ function estimatedLines(text, sizePx, boxWPct) {
3061
+ const widthPx = Math.max(1, boxWPct / 100 * DESIGN_WIDTH_PX);
3062
+ let lines = 0;
3063
+ for (const seg of text.split("\n")) {
3064
+ lines += Math.max(1, Math.ceil(textWidthEm(seg) * sizePx / widthPx));
3065
+ }
3066
+ return lines;
3067
+ }
3068
+ function fitFontPx(sizePx, text, boxWPct, boxHPct) {
3069
+ const estimated = estimatedLines(text, sizePx, boxWPct) * sizePx * LINE_HEIGHT;
3070
+ const boxPx = boxHPct / 100 * DESIGN_HEIGHT_PX;
3071
+ if (boxPx <= 0 || estimated <= boxPx * OVERFLOW_TOLERANCE) return sizePx;
3072
+ const scale = Math.max(OVERFLOW_MIN_SCALE, boxPx / estimated);
3073
+ return Math.max(6, Math.round(sizePx * scale));
3074
+ }
3075
+ function fontPx(szCenti, fontScale, ctx) {
3076
+ return Math.max(6, Math.round(szCenti / 100 * fontScale * (4 / 3) * (12192e3 / ctx.slideCx)));
3077
+ }
3078
+ function textElements(sp, txBody, map, ctx) {
3079
+ const spPr = childOf2(sp, "spPr");
3080
+ const xfrm = (spPr && xfrmOf(spPr)) ?? inheritedXfrm(sp, ctx);
3081
+ const box = xfrm ? toPercentBox(xfrm, map, ctx) : /^(title|ctrTitle)$/.test(placeholderType(sp) ?? "") ? { ...TITLE_BOX } : { ...BODY_BOX };
3082
+ const rotate = xfrm ? rotationOf(xfrm) : void 0;
3083
+ const bodyPr = childOf2(txBody, "bodyPr");
3084
+ const autofit = bodyPr ? childOf2(bodyPr, "normAutofit") ?? childOf2(bodyPr, "spAutoFit") : null;
3085
+ const rawScale = autofit ? Number(attrOf2(autofit, "fontScale")) : NaN;
3086
+ const fontScale = Number.isFinite(rawScale) && rawScale > 0 ? rawScale / 1e5 : 1;
3087
+ const guard = (sizePx, text, hPct) => (
3088
+ // The guard runs regardless of autofit: PPT's own fontScale is already
3089
+ // applied, but our Korean wrap is wider than PowerPoint's, so even
3090
+ // autofitted boxes can overflow the model.
3091
+ fitFontPx(sizePx, text, box.w, hPct)
3092
+ );
3093
+ const paras = paragraphsOf(txBody, sp, ctx).map((p) => ({
3094
+ ...p,
3095
+ px: p.szCenti !== void 0 ? fontPx(p.szCenti, fontScale, ctx) : void 0
3096
+ }));
3097
+ const filled = paras.filter((p) => p.text.trim());
3098
+ const apply = (el, p) => {
3099
+ if (p.px !== void 0) el.fontSize = p.px;
3100
+ if (p.bold) el.bold = true;
3101
+ if (p.color) el.color = p.color;
3102
+ if (p.align) el.align = p.align;
3103
+ };
3104
+ const distinctSizes = new Set(filled.map((p) => p.px ?? DEFAULT_PARA_PX));
3105
+ const mixedBullets = new Set(filled.map((p) => p.bullet)).size > 1;
3106
+ const split = filled.length >= 2 && paras.length <= MAX_SPLIT_PARAGRAPHS && rotate === void 0 && (distinctSizes.size > 1 || mixedBullets);
3107
+ if (!split) {
3108
+ const el = {
3109
+ id: ctx.nextId(),
3110
+ type: "text",
3111
+ ...box,
3112
+ text: paras.map((p) => p.bullet && p.text.trim() ? `\u2022 ${p.text}` : p.text).join("\n")
3113
+ };
3114
+ const lead = filled[0] ?? paras[0];
3115
+ if (lead) apply(el, lead);
3116
+ if (el.fontSize !== void 0 && el.text) el.fontSize = guard(el.fontSize, el.text, box.h);
3117
+ if (rotate !== void 0) el.rotate = rotate;
3118
+ return [el];
3119
+ }
3120
+ const weights = paras.map((p) => estimatedLines(p.text, p.px ?? DEFAULT_PARA_PX, box.w) * (p.px ?? DEFAULT_PARA_PX));
3121
+ const total = weights.reduce((a, b) => a + b, 0) || 1;
3122
+ const out = [];
3123
+ let used = 0;
3124
+ paras.forEach((p, i) => {
3125
+ const y = box.y + box.h * used / total;
3126
+ used += weights[i];
3127
+ if (!p.text.trim()) return;
3128
+ const el = {
3129
+ id: ctx.nextId(),
3130
+ type: "text",
3131
+ x: box.x,
3132
+ y: round2(y),
3133
+ w: box.w,
3134
+ h: round2(box.h * weights[i] / total),
3135
+ text: p.bullet ? `\u2022 ${p.text}` : p.text
3136
+ };
3137
+ apply(el, p);
3138
+ if (el.fontSize !== void 0) el.fontSize = guard(el.fontSize, el.text ?? "", el.h);
3139
+ out.push(el);
3140
+ });
3141
+ return out;
3142
+ }
3143
+ function mappableShapeOf(prst, isConnector) {
3144
+ if (isConnector) return "line";
3145
+ if (!prst) return null;
3146
+ if (prst === "line" || /^straightConnector\d*$/.test(prst)) return "line";
3147
+ if (prst === "ellipse" || prst === "oval") return "ellipse";
3148
+ if (prst === "rect" || prst === "roundRect") return "rect";
3149
+ return null;
3150
+ }
3151
+ function shapeElement(sp, map, ctx, isConnector) {
3152
+ const colors = ctx.chain.colors;
3153
+ const spPr = childOf2(sp, "spPr");
3154
+ const xfrm = spPr && (xfrmOf(spPr) ?? inheritedXfrm(sp, ctx));
3155
+ if (!spPr || !xfrm) return null;
3156
+ const prst = attrOf2(childOf2(spPr, "prstGeom") ?? spPr, "prst");
3157
+ const shape = mappableShapeOf(prst, isConnector);
3158
+ if (!shape) return null;
3159
+ const el = { id: ctx.nextId(), type: "shape", shape, ...toPercentBox(xfrm, map, ctx) };
3160
+ const ln = childOf2(spPr, "ln");
3161
+ const areaFill = childOf2(spPr, "noFill") ? void 0 : solidFillColor(spPr, colors) ?? gradientFirstStop(spPr, colors) ?? styleRefColor(sp, "fillRef", colors);
3162
+ const lineFill = (ln && !childOf2(ln, "noFill") ? solidFillColor(ln, colors) : void 0) ?? styleRefColor(sp, "lnRef", colors);
3163
+ const fill = shape === "line" ? lineFill ?? areaFill : areaFill;
3164
+ if (fill) el.fill = fill;
3165
+ if (prst === "roundRect") el.radius = 8;
3166
+ el.rotate = rotationOf(xfrm);
3167
+ if (shape === "line" && !(el.w > 3 && el.h > 5)) {
3168
+ if (el.h >= el.w && el.w > 1.2) {
3169
+ el.x = Math.round((el.x + el.w / 2 - 0.3) * 100) / 100;
3170
+ el.w = 0.6;
3171
+ } else if (el.w > el.h && el.h > 1.2) {
3172
+ el.y = Math.round((el.y + el.h / 2 - 0.3) * 100) / 100;
3173
+ el.h = 0.6;
3174
+ }
3175
+ }
3176
+ if (shape === "line" && el.w > 3 && el.h > 5) {
3177
+ const wPx = el.w / 100 * 1280;
3178
+ const hPx = el.h / 100 * 720;
3179
+ const lenPct = Math.hypot(wPx, hPx) / 1280 * 100;
3180
+ const flip = attrOf2(childOf2(spPr, "xfrm") ?? spPr, "flipH") === "1" !== (attrOf2(childOf2(spPr, "xfrm") ?? spPr, "flipV") === "1");
3181
+ const angle = Math.atan2(hPx, wPx) * 180 / Math.PI;
3182
+ const cx = el.x + el.w / 2;
3183
+ const cy = el.y + el.h / 2;
3184
+ el.x = Math.round((cx - lenPct / 2) * 100) / 100;
3185
+ el.y = Math.round((cy - 0.3) * 100) / 100;
3186
+ el.w = Math.round(lenPct * 100) / 100;
3187
+ el.h = 0.6;
3188
+ el.rotate = normalizedRotation((el.rotate ?? 0) + (flip ? -angle : angle));
3189
+ }
3190
+ return el;
3191
+ }
3192
+ function pictureElement(pic, map, ctx) {
3193
+ const spPr = childOf2(pic, "spPr");
3194
+ const xfrm = (spPr && xfrmOf(spPr)) ?? inheritedXfrm(pic, ctx);
3195
+ if (!xfrm) return null;
3196
+ const blip = childOf2(pic, "blipFill") && childOf2(childOf2(pic, "blipFill"), "blip");
3197
+ const embed = blip && attrOf2(blip, "embed");
3198
+ const rel = embed ? ctx.rels.get(embed) : void 0;
3199
+ const src = rel ? ctx.media.dataUrl(rel.target) : null;
3200
+ if (!src) return null;
3201
+ const el = { id: ctx.nextId(), type: "image", ...toPercentBox(xfrm, map, ctx), src };
3202
+ el.rotate = rotationOf(xfrm);
3203
+ return el;
3204
+ }
3205
+ var MAX_TABLE_CELL_ELEMENTS = 60;
3206
+ var TABLE_FONT_PX_CAP = 14;
3207
+ var TABLE_FONT_PX_DEFAULT = 12;
3208
+ function cellText(tc) {
3209
+ const txBody = childOf2(tc, "txBody");
3210
+ return txBody ? txBodyText(txBody).trim() : "";
3211
+ }
3212
+ function tableElements(frame, map, ctx) {
3213
+ try {
3214
+ return tableGrid(frame, map, ctx);
3215
+ } catch {
3216
+ return [];
3217
+ }
3218
+ }
3219
+ function tableGrid(frame, map, ctx) {
3220
+ const graphic = childOf2(frame, "graphic");
3221
+ const data = graphic && childOf2(graphic, "graphicData");
3222
+ const tbl = data && childOf2(data, "tbl");
3223
+ const xfrm = xfrmOf(frame);
3224
+ if (!tbl || !xfrm) return [];
3225
+ const box = toPercentBox(xfrm, map, ctx);
3226
+ const rows = Array.from(tbl.children).filter((c) => c.localName === "tr");
3227
+ const cellsOf = (tr) => Array.from(tr.children).filter((c) => c.localName === "tc");
3228
+ if (!rows.length) return [];
3229
+ const grid = childOf2(tbl, "tblGrid");
3230
+ const colWidths = Array.from(grid?.children ?? []).filter((c) => c.localName === "gridCol").map((c) => Math.max(0, Number(attrOf2(c, "w")) || 0));
3231
+ const colCount = colWidths.length || Math.max(...rows.map((r) => cellsOf(r).length));
3232
+ if (!colCount || !Number.isFinite(colCount)) return [];
3233
+ if (rows.length * colCount > MAX_TABLE_CELL_ELEMENTS) {
3234
+ const text = rows.map((r) => cellsOf(r).map(cellText).join(" ")).join("\n").trim();
3235
+ if (!text) return [];
3236
+ return [{ id: ctx.nextId(), type: "text", ...box, text, fontSize: TABLE_FONT_PX_DEFAULT }];
3237
+ }
3238
+ const colSum = colWidths.reduce((a, b) => a + b, 0);
3239
+ const colFrac = colSum > 0 ? colWidths.map((w) => w / colSum) : Array(colCount).fill(1 / colCount);
3240
+ const rowHeights = rows.map((r) => Math.max(0, Number(attrOf2(r, "h")) || 0));
3241
+ const rowSum = rowHeights.reduce((a, b) => a + b, 0);
3242
+ const rowFrac = rowSum > 0 ? rowHeights.map((h) => h / rowSum) : rows.map(() => 1 / rows.length);
3243
+ const out = [];
3244
+ let yFrac = 0;
3245
+ rows.forEach((tr, ri) => {
3246
+ let col = 0;
3247
+ for (const tc of cellsOf(tr)) {
3248
+ const span = Math.max(1, Number(attrOf2(tc, "gridSpan")) || 1);
3249
+ const from = col;
3250
+ col += span;
3251
+ if (isOn(attrOf2(tc, "hMerge")) || isOn(attrOf2(tc, "vMerge")) || from >= colCount) continue;
3252
+ const text = cellText(tc);
3253
+ if (!text) continue;
3254
+ const xFrac = colFrac.slice(0, from).reduce((a, b) => a + b, 0);
3255
+ const wFrac = colFrac.slice(from, Math.min(from + span, colCount)).reduce((a, b) => a + b, 0);
3256
+ const el = {
3257
+ id: ctx.nextId(),
3258
+ type: "text",
3259
+ x: round2(box.x + box.w * xFrac),
3260
+ y: round2(box.y + box.h * yFrac),
3261
+ w: round2(box.w * wFrac),
3262
+ h: round2(box.h * rowFrac[ri]),
3263
+ text
3264
+ };
3265
+ const txBody = childOf2(tc, "txBody");
3266
+ const para = txBody ? paragraphsOf(txBody, tc, ctx).find((p) => p.text.trim()) : void 0;
3267
+ const sizePx = para?.szCenti !== void 0 ? fontPx(para.szCenti, 1, ctx) : TABLE_FONT_PX_DEFAULT;
3268
+ el.fontSize = Math.min(sizePx, TABLE_FONT_PX_CAP);
3269
+ if (para?.bold) el.bold = true;
3270
+ if (para?.color) el.color = para.color;
3271
+ if (para?.align) el.align = para.align;
3272
+ out.push(el);
3273
+ }
3274
+ yFrac += rowFrac[ri];
3275
+ });
3276
+ return out;
3277
+ }
3278
+ function collectElements(tree, map, ctx, out) {
3279
+ for (const child of Array.from(tree.children)) {
3280
+ if (child.localName === "sp") {
3281
+ const txBody = childOf2(child, "txBody");
3282
+ if (txBody && txBodyText(txBody).trim()) {
3283
+ out.push(...textElements(child, txBody, map, ctx));
3284
+ } else {
3285
+ const el = shapeElement(child, map, ctx, false);
3286
+ if (el) out.push(el);
3287
+ }
3288
+ } else if (child.localName === "cxnSp") {
3289
+ const el = shapeElement(child, map, ctx, true);
3290
+ if (el) out.push(el);
3291
+ } else if (child.localName === "pic") {
3292
+ const el = pictureElement(child, map, ctx);
3293
+ if (el) out.push(el);
3294
+ } else if (child.localName === "graphicFrame") {
3295
+ out.push(...tableElements(child, map, ctx));
3296
+ } else if (child.localName === "grpSp") {
3297
+ collectElements(child, groupChildMap(child, map), ctx, out);
3298
+ }
3299
+ }
3300
+ }
3301
+ function backgroundOf(doc, colors) {
3302
+ const cSld = doc?.getElementsByTagNameNS("*", "cSld")[0];
3303
+ const bg = cSld ? childOf2(cSld, "bg") : null;
3304
+ if (!bg) return void 0;
3305
+ const bgPr = childOf2(bg, "bgPr");
3306
+ if (bgPr) return solidFillColor(bgPr, colors) ?? gradientFirstStop(bgPr, colors);
3307
+ const bgRef = childOf2(bg, "bgRef");
3308
+ if (!bgRef || attrOf2(bgRef, "idx") === "0") return void 0;
3309
+ for (const c of Array.from(bgRef.children)) {
3310
+ const hex = colors.resolve(c);
3311
+ if (hex) return hex;
3312
+ }
3313
+ return void 0;
3314
+ }
3315
+ var NOTES_CHROME = /^(sldNum|sldImg|hdr|ftr|dt)$/;
3316
+ function notesFor(entries, slideName, rels) {
3317
+ let notesName = relOfType(rels, "/notesSlide")?.target;
3318
+ if (!notesName) {
3319
+ const num = /(\d+)\.xml$/.exec(slideName)?.[1];
3320
+ if (num) notesName = `ppt/notesSlides/notesSlide${num}.xml`;
3321
+ }
3322
+ const part = notesName ? entries.get(notesName) : void 0;
3323
+ const doc = part ? parseXml2(new TextDecoder().decode(part)) : null;
3324
+ if (!doc) return void 0;
3325
+ const texts = [];
3326
+ for (const sp of Array.from(doc.getElementsByTagNameNS("*", "sp"))) {
3327
+ if (NOTES_CHROME.test(placeholderType(sp) ?? "")) continue;
3328
+ const txBody = childOf2(sp, "txBody");
3329
+ const text = txBody ? txBodyText(txBody).trim() : "";
3330
+ if (text) texts.push(text);
3331
+ }
3332
+ return texts.length ? texts.join("\n") : void 0;
3333
+ }
3334
+ function orderedSlideNames(entries) {
3335
+ const presPart = entries.get("ppt/presentation.xml");
3336
+ const doc = presPart ? parseXml2(new TextDecoder().decode(presPart)) : null;
3337
+ if (doc) {
3338
+ const rels = relsOf(entries, "ppt/presentation.xml");
3339
+ const names = [];
3340
+ for (const sldId of Array.from(doc.getElementsByTagNameNS("*", "sldId"))) {
3341
+ const rel = relIdOf(sldId) ? rels.get(relIdOf(sldId)) : void 0;
3342
+ if (rel && entries.has(rel.target)) names.push(rel.target);
3343
+ }
3344
+ if (names.length) return names;
3345
+ }
3346
+ return [...entries.keys()].filter((n) => /^ppt\/slides\/slide\d+\.xml$/i.test(n)).sort((a, b) => Number(/(\d+)\.xml$/.exec(a)?.[1] ?? 0) - Number(/(\d+)\.xml$/.exec(b)?.[1] ?? 0));
3347
+ }
3348
+ function slideSizeOf(entries) {
3349
+ const presPart = entries.get("ppt/presentation.xml");
3350
+ const doc = presPart ? parseXml2(new TextDecoder().decode(presPart)) : null;
3351
+ const sldSz = doc && doc.getElementsByTagNameNS("*", "sldSz")[0];
3352
+ const cx = sldSz ? Number(attrOf2(sldSz, "cx")) : NaN;
3353
+ const cy = sldSz ? Number(attrOf2(sldSz, "cy")) : NaN;
3354
+ return { cx: cx > 0 ? cx : DEFAULT_SLIDE_CX, cy: cy > 0 ? cy : DEFAULT_SLIDE_CY };
3355
+ }
3356
+ async function pptxToSlides(buffer) {
3357
+ const entries = await readZip(buffer);
3358
+ const names = orderedSlideNames(entries);
3359
+ if (!names.length) {
3360
+ throw new Error("PPTX \uBB38\uC11C\uAC00 \uC544\uB2D9\uB2C8\uB2E4 \u2014 \uC2AC\uB77C\uC774\uB4DC(ppt/slides)\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (Not a PPTX document \u2014 no slides found).");
3361
+ }
3362
+ const { cx, cy } = slideSizeOf(entries);
3363
+ const media = new MediaResolver(entries);
3364
+ const partCache = /* @__PURE__ */ new Map();
3365
+ const decoder = new TextDecoder();
3366
+ let seq = 0;
3367
+ const slides2 = [];
3368
+ for (const name of names) {
3369
+ const doc = parseXml2(decoder.decode(entries.get(name)));
3370
+ if (!doc) continue;
3371
+ const rels = relsOf(entries, name);
3372
+ const chain = chainFor(entries, rels, partCache);
3373
+ const ctx = { slideCx: cx, slideCy: cy, rels, media, chain, nextId: () => `el_${++seq}` };
3374
+ const elements = [];
3375
+ const spTree = doc.getElementsByTagNameNS("*", "spTree")[0];
3376
+ if (spTree) collectElements(spTree, IDENTITY_MAP, ctx, elements);
3377
+ const slide = { layout: "blank", elements };
3378
+ const background = backgroundOf(doc, chain.colors) ?? backgroundOf(chain.layoutDoc, chain.colors) ?? backgroundOf(chain.masterDoc, chain.colors);
3379
+ if (background) slide.background = background;
3380
+ const notes = notesFor(entries, name, rels);
3381
+ if (notes) slide.notes = notes;
3382
+ slides2.push(slide);
3383
+ }
3384
+ if (!slides2.length) {
3385
+ throw new Error("PPTX \uC2AC\uB77C\uC774\uB4DC XML\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (Not a readable PPTX \u2014 every slide failed to parse).");
3386
+ }
3387
+ return { slides: slides2 };
3388
+ }
3389
+
1380
3390
  // src/io/importers.ts
1381
- var IMPORTABLE_EXTENSIONS = [".csv", ".md", ".markdown", ".txt", ".html", ".htm", ".json", ".xlsx"];
3391
+ var IMPORTABLE_EXTENSIONS = [
3392
+ ".csv",
3393
+ ".md",
3394
+ ".markdown",
3395
+ ".txt",
3396
+ ".html",
3397
+ ".htm",
3398
+ ".json",
3399
+ ".xlsx",
3400
+ ".pdf",
3401
+ ".hwpx",
3402
+ ".hwp",
3403
+ ".docx",
3404
+ ".pptx"
3405
+ ];
1382
3406
  var extensionOf = (name) => {
1383
3407
  const dot = name.lastIndexOf(".");
1384
3408
  return dot === -1 ? "" : name.slice(dot).toLowerCase();
@@ -1419,6 +3443,26 @@ async function importFile(file) {
1419
3443
  const { sheets, columns, rows } = await xlsxToSheets(await file.arrayBuffer(), () => loadOptional("exceljs", () => import('exceljs')));
1420
3444
  return toEvents(artifact(id, "table", title, { columns, rows, sheet: sheets }));
1421
3445
  }
3446
+ case ".pdf": {
3447
+ const src = await fileToDataUrl(file);
3448
+ return toEvents(artifact(id, "pdf", title, { src, filename: file.name }));
3449
+ }
3450
+ case ".hwpx": {
3451
+ const html = await hwpxToHtml(await file.arrayBuffer());
3452
+ return toEvents({ ...artifact(id, "html", title, { html }), meta: { kind: "doc" } });
3453
+ }
3454
+ case ".docx": {
3455
+ const content = await docxToMarkdown(await file.arrayBuffer());
3456
+ return toEvents(artifact(id, "document", title, { format: "markdown", content }));
3457
+ }
3458
+ case ".pptx": {
3459
+ const data = await pptxToSlides(await file.arrayBuffer());
3460
+ return toEvents(artifact(id, "slides", title, data));
3461
+ }
3462
+ case ".hwp": {
3463
+ const html = await hwpToHtml(await file.arrayBuffer());
3464
+ return toEvents({ ...artifact(id, "html", title, { html }), meta: { kind: "doc" } });
3465
+ }
1422
3466
  default:
1423
3467
  throw new Error(`Unsupported file type "${ext || file.name}". Supported: ${IMPORTABLE_EXTENSIONS.join(", ")}`);
1424
3468
  }
@@ -1426,6 +3470,14 @@ async function importFile(file) {
1426
3470
  function artifact(id, type, title, data) {
1427
3471
  return { id, type, title, version: 1, status: "complete", data };
1428
3472
  }
3473
+ function fileToDataUrl(file) {
3474
+ return new Promise((resolve, reject) => {
3475
+ const reader = new FileReader();
3476
+ reader.onload = () => resolve(reader.result);
3477
+ reader.onerror = () => reject(reader.error ?? new Error("Failed to read file."));
3478
+ reader.readAsDataURL(file);
3479
+ });
3480
+ }
1429
3481
  function importJson(text, id, title) {
1430
3482
  let parsed;
1431
3483
  try {
@@ -1498,6 +3550,8 @@ var DEVICES = [
1498
3550
  { id: "tablet", label: "Tablet", width: "768px" },
1499
3551
  { id: "mobile", label: "Mobile", width: "390px" }
1500
3552
  ];
3553
+ var BLOCK_KEYS = { h2: "heading", p: "text", button: "button", img: "image", hr: "divider" };
3554
+ var DEVICE_KEYS = { desktop: "desktop", tablet: "tablet", mobile: "mobile" };
1501
3555
  var BLOCKS = [
1502
3556
  { tag: "h2", label: "Heading" },
1503
3557
  { tag: "p", label: "Text" },
@@ -1743,8 +3797,8 @@ function checkA11y(html) {
1743
3797
  if (!doc.querySelector("h1")) issues.push("No <h1> \u2014 every page needs one top-level heading");
1744
3798
  return issues;
1745
3799
  }
1746
- var SLIDE_W = 1280;
1747
- var SCROLL_FIX = "<style>html,body{overflow:auto!important;height:auto!important;min-height:100%!important}</style>";
3800
+ var SLIDE_H = 720;
3801
+ var SCROLL_FIX = "<style data-lcx>html,body{overflow:auto!important;height:auto!important;min-height:100%!important}</style>";
1748
3802
  function withScrollableBody(html) {
1749
3803
  const i = html.lastIndexOf("</body>");
1750
3804
  return i === -1 ? html + SCROLL_FIX : html.slice(0, i) + SCROLL_FIX + html.slice(i);
@@ -1752,7 +3806,7 @@ function withScrollableBody(html) {
1752
3806
  function useSlideFit(ratio, boxRef) {
1753
3807
  const [scale, setScale] = useState(1);
1754
3808
  const [rw, rh] = (ratio ?? "16:9").split(/[:x/]/).map(Number);
1755
- const height = rw && rh ? Math.round(SLIDE_W * rh / rw) : 720;
3809
+ const width = rw && rh ? Math.round(SLIDE_H * rw / rh) : 1280;
1756
3810
  useEffect(() => {
1757
3811
  if (!ratio) return;
1758
3812
  const el = boxRef.current;
@@ -1760,16 +3814,17 @@ function useSlideFit(ratio, boxRef) {
1760
3814
  const fit = () => {
1761
3815
  const w = el.clientWidth;
1762
3816
  if (w <= 40) return;
1763
- setScale(Math.min(1, (w - 40) / SLIDE_W));
3817
+ setScale(Math.min(1, (w - 40) / width));
1764
3818
  };
1765
3819
  fit();
1766
3820
  const ro = new ResizeObserver(fit);
1767
3821
  ro.observe(el);
1768
3822
  return () => ro.disconnect();
1769
- }, [ratio, height]);
1770
- return { scale, width: SLIDE_W, height };
3823
+ }, [ratio, width]);
3824
+ return { scale, width, height: SLIDE_H };
1771
3825
  }
1772
3826
  function HtmlRenderer({ artifact: artifact2 }) {
3827
+ const t = useT();
1773
3828
  const iframeRef = useRef(null);
1774
3829
  const imgFileRef = useRef(null);
1775
3830
  const bgFileRef = useRef(null);
@@ -1869,6 +3924,7 @@ function HtmlRenderer({ artifact: artifact2 }) {
1869
3924
  };
1870
3925
  const setSlideStyle = (style) => sendIframeCommand({ artifactId: artifact2.id, type: "set_slide_style", style });
1871
3926
  const ratio = artifact2.meta?.ratio;
3927
+ const isDoc = !ratio && artifact2.meta?.kind === "doc";
1872
3928
  const slide = useSlideFit(ratio, stageRef);
1873
3929
  return /* @__PURE__ */ jsxs("div", { className: "cv-html-wrap", children: [
1874
3930
  /* @__PURE__ */ jsx("input", { ref: imgFileRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => {
@@ -1881,26 +3937,30 @@ function HtmlRenderer({ artifact: artifact2 }) {
1881
3937
  } }),
1882
3938
  /* @__PURE__ */ jsxs("div", { className: "cv-html-bar cv-chrome", children: [
1883
3939
  mode === "design" && /* @__PURE__ */ jsxs(Fragment, { children: [
1884
- !ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
1885
- /* @__PURE__ */ jsx("div", { className: "cv-html-seg", role: "group", "aria-label": "Preview width", children: DEVICES.map((d) => /* @__PURE__ */ jsx("button", { className: device === d.id ? "is-on" : "", onClick: () => setDevice(d.id), children: d.label }, d.id)) }),
3940
+ !ratio && !isDoc && /* @__PURE__ */ jsxs(Fragment, { children: [
3941
+ /* @__PURE__ */ jsx("div", { className: "cv-html-seg", role: "group", "aria-label": "Preview width", children: DEVICES.map((d) => /* @__PURE__ */ jsx("button", { className: device === d.id ? "is-on" : "", onClick: () => setDevice(d.id), children: DEVICE_KEYS[d.id] ? t(DEVICE_KEYS[d.id]) : d.label }, d.id)) }),
1886
3942
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" })
1887
3943
  ] }),
1888
- /* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: "Add" }),
1889
- (ratio ? BLOCKS.filter((b) => SLIDE_BLOCK_TAGS.has(b.tag)) : BLOCKS).map((b) => /* @__PURE__ */ jsx("button", { className: "cv-html-add", onClick: () => command("insert", { block: b.tag }), children: b.label }, b.tag)),
1890
- !ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
3944
+ isDoc && /* @__PURE__ */ jsxs("span", { className: "cv-html-doc-chip", title: t("kindDocument"), children: [
3945
+ "\u{1F4C4} ",
3946
+ t("kindDocument")
3947
+ ] }),
3948
+ /* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: t("add") }),
3949
+ (ratio || isDoc ? BLOCKS.filter((b) => SLIDE_BLOCK_TAGS.has(b.tag)) : BLOCKS).map((b) => /* @__PURE__ */ jsx("button", { className: "cv-html-add", onClick: () => command("insert", { block: b.tag }), children: BLOCK_KEYS[b.tag] ? t(BLOCK_KEYS[b.tag]) : b.label }, b.tag)),
3950
+ !ratio && !isDoc && /* @__PURE__ */ jsxs(Fragment, { children: [
1891
3951
  /* @__PURE__ */ jsxs(
1892
3952
  "select",
1893
3953
  {
1894
3954
  className: "cv-html-tpl",
1895
3955
  value: "",
1896
- title: "Start from a full page template (replaces the page)",
3956
+ title: t("pageTemplateTitle"),
1897
3957
  onChange: (e) => {
1898
3958
  const s = STARTERS[e.target.value];
1899
3959
  if (s) applyEvent({ type: "canvas.patch", id: artifact2.id, patch: { html: s.build() } });
1900
3960
  e.currentTarget.value = "";
1901
3961
  },
1902
3962
  children: [
1903
- /* @__PURE__ */ jsx("option", { value: "", children: "Page\u2026" }),
3963
+ /* @__PURE__ */ jsx("option", { value: "", children: t("pageMenu") }),
1904
3964
  Object.entries(STARTERS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1905
3965
  ]
1906
3966
  }
@@ -1910,14 +3970,14 @@ function HtmlRenderer({ artifact: artifact2 }) {
1910
3970
  {
1911
3971
  className: "cv-html-tpl",
1912
3972
  value: "",
1913
- title: "Insert a section template",
3973
+ title: t("sectionTemplateTitle"),
1914
3974
  onChange: (e) => {
1915
- const t = TEMPLATES[e.target.value];
1916
- if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
3975
+ const t2 = TEMPLATES[e.target.value];
3976
+ if (t2) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t2.html });
1917
3977
  e.currentTarget.value = "";
1918
3978
  },
1919
3979
  children: [
1920
- /* @__PURE__ */ jsx("option", { value: "", children: "Section\u2026" }),
3980
+ /* @__PURE__ */ jsx("option", { value: "", children: t("sectionMenu") }),
1921
3981
  Object.entries(TEMPLATES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1922
3982
  ]
1923
3983
  }
@@ -1927,34 +3987,51 @@ function HtmlRenderer({ artifact: artifact2 }) {
1927
3987
  {
1928
3988
  className: "cv-html-tpl",
1929
3989
  value: "",
1930
- title: "Jump to a heading",
3990
+ title: t("jumpToHeading"),
1931
3991
  onChange: (e) => {
1932
3992
  const idx = Number(e.target.value);
1933
3993
  if (!Number.isNaN(idx)) sendIframeCommand({ artifactId: artifact2.id, type: "scroll_to", index: idx });
1934
3994
  e.currentTarget.value = "";
1935
3995
  },
1936
3996
  children: [
1937
- /* @__PURE__ */ jsx("option", { value: "", children: "Outline\u2026" }),
3997
+ /* @__PURE__ */ jsx("option", { value: "", children: t("outlineMenu") }),
1938
3998
  outline.map((h, i) => /* @__PURE__ */ jsx("option", { value: i, children: "\xA0".repeat((h.level - 1) * 2) + h.text }, i))
1939
3999
  ]
1940
4000
  }
1941
4001
  ),
1942
- /* @__PURE__ */ jsx("button", { className: "cv-html-add", title: "Accessibility check", onClick: () => setA11y(checkA11y(artifact2.data.html)), children: "\u267F Check" })
4002
+ /* @__PURE__ */ jsx("button", { className: "cv-html-add", title: t("a11yTitle"), onClick: () => setA11y(checkA11y(artifact2.data.html)), children: "\u267F Check" })
1943
4003
  ] }),
4004
+ isDoc && outline.length > 0 && /* @__PURE__ */ jsxs(
4005
+ "select",
4006
+ {
4007
+ className: "cv-html-tpl",
4008
+ value: "",
4009
+ title: t("jumpToHeading"),
4010
+ onChange: (e) => {
4011
+ const idx = Number(e.target.value);
4012
+ if (!Number.isNaN(idx)) sendIframeCommand({ artifactId: artifact2.id, type: "scroll_to", index: idx });
4013
+ e.currentTarget.value = "";
4014
+ },
4015
+ children: [
4016
+ /* @__PURE__ */ jsx("option", { value: "", children: t("outlineMenu") }),
4017
+ outline.map((h, i) => /* @__PURE__ */ jsx("option", { value: i, children: h.text }, `d${i}`))
4018
+ ]
4019
+ }
4020
+ ),
1944
4021
  ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
1945
4022
  /* @__PURE__ */ jsxs(
1946
4023
  "select",
1947
4024
  {
1948
4025
  className: "cv-html-tpl",
1949
4026
  value: "",
1950
- title: "Insert a slide layout",
4027
+ title: t("insertSlideLayout"),
1951
4028
  onChange: (e) => {
1952
- const t = SLIDE_TEMPLATES[e.target.value];
1953
- if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
4029
+ const t2 = SLIDE_TEMPLATES[e.target.value];
4030
+ if (t2) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t2.html });
1954
4031
  e.currentTarget.value = "";
1955
4032
  },
1956
4033
  children: [
1957
- /* @__PURE__ */ jsx("option", { value: "", children: "Layout\u2026" }),
4034
+ /* @__PURE__ */ jsx("option", { value: "", children: t("layoutMenu") }),
1958
4035
  Object.entries(SLIDE_TEMPLATES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1959
4036
  ]
1960
4037
  }
@@ -1964,14 +4041,14 @@ function HtmlRenderer({ artifact: artifact2 }) {
1964
4041
  {
1965
4042
  className: "cv-html-tpl",
1966
4043
  value: "",
1967
- title: "Apply a slide theme",
4044
+ title: t("applySlideTheme"),
1968
4045
  onChange: (e) => {
1969
- const t = SLIDE_THEMES[e.target.value];
1970
- if (t) sendIframeCommand({ artifactId: artifact2.id, type: "set_slide_style", style: t.style });
4046
+ const t2 = SLIDE_THEMES[e.target.value];
4047
+ if (t2) sendIframeCommand({ artifactId: artifact2.id, type: "set_slide_style", style: t2.style });
1971
4048
  e.currentTarget.value = "";
1972
4049
  },
1973
4050
  children: [
1974
- /* @__PURE__ */ jsx("option", { value: "", children: "Theme\u2026" }),
4051
+ /* @__PURE__ */ jsx("option", { value: "", children: t("themeMenu") }),
1975
4052
  Object.entries(SLIDE_THEMES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1976
4053
  ]
1977
4054
  }
@@ -1981,14 +4058,14 @@ function HtmlRenderer({ artifact: artifact2 }) {
1981
4058
  {
1982
4059
  className: "cv-html-tpl",
1983
4060
  value: "",
1984
- title: "Insert a shape",
4061
+ title: t("insertShape"),
1985
4062
  onChange: (e) => {
1986
- const t = SLIDE_SHAPES[e.target.value];
1987
- if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
4063
+ const t2 = SLIDE_SHAPES[e.target.value];
4064
+ if (t2) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t2.html });
1988
4065
  e.currentTarget.value = "";
1989
4066
  },
1990
4067
  children: [
1991
- /* @__PURE__ */ jsx("option", { value: "", children: "Shape\u2026" }),
4068
+ /* @__PURE__ */ jsx("option", { value: "", children: t("shapeMenu") }),
1992
4069
  Object.entries(SLIDE_SHAPES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1993
4070
  ]
1994
4071
  }
@@ -1998,19 +4075,19 @@ function HtmlRenderer({ artifact: artifact2 }) {
1998
4075
  {
1999
4076
  className: "cv-html-tpl",
2000
4077
  value: "",
2001
- title: "Slide font",
4078
+ title: t("slideFont"),
2002
4079
  onChange: (e) => {
2003
4080
  const f = SLIDE_FONTS[e.target.value];
2004
4081
  if (f) setSlideStyle({ fontFamily: f.stack });
2005
4082
  e.currentTarget.value = "";
2006
4083
  },
2007
4084
  children: [
2008
- /* @__PURE__ */ jsx("option", { value: "", children: "Font\u2026" }),
4085
+ /* @__PURE__ */ jsx("option", { value: "", children: t("fontMenu") }),
2009
4086
  Object.entries(SLIDE_FONTS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
2010
4087
  ]
2011
4088
  }
2012
4089
  ),
2013
- /* @__PURE__ */ jsx("button", { className: "cv-html-add", title: "Slide background image", onClick: () => bgFileRef.current?.click(), children: "\u{1F5BC} BG" })
4090
+ /* @__PURE__ */ jsx("button", { className: "cv-html-add", title: t("slideBgImage"), onClick: () => bgFileRef.current?.click(), children: "\u{1F5BC} BG" })
2014
4091
  ] }),
2015
4092
  selected.length >= 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
2016
4093
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" }),
@@ -2040,33 +4117,33 @@ function HtmlRenderer({ artifact: artifact2 }) {
2040
4117
  )
2041
4118
  ] }),
2042
4119
  single && single.tag !== "img" && /* @__PURE__ */ jsxs(Fragment, { children: [
2043
- /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align left", onClick: () => command("style_persist", { prop: "textAlign", value: "left" }), children: "\u2B05" }),
2044
- /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align center", onClick: () => command("style_persist", { prop: "textAlign", value: "center" }), children: "\u2B0C" }),
2045
- /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align right", onClick: () => command("style_persist", { prop: "textAlign", value: "right" }), children: "\u27A1" }),
2046
- /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Bold", onClick: () => command("style_persist", { prop: "fontWeight", value: "800" }), children: "B" })
4120
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: t("alignLeft"), onClick: () => command("style_persist", { prop: "textAlign", value: "left" }), children: "\u2B05" }),
4121
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: t("alignCenter"), onClick: () => command("style_persist", { prop: "textAlign", value: "center" }), children: "\u2B0C" }),
4122
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: t("alignRight"), onClick: () => command("style_persist", { prop: "textAlign", value: "right" }), children: "\u27A1" }),
4123
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: t("bold"), onClick: () => command("style_persist", { prop: "fontWeight", value: "800" }), children: "B" })
2047
4124
  ] }),
2048
4125
  single && /* @__PURE__ */ jsxs(Fragment, { children: [
2049
- /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Duplicate", onClick: () => command("duplicate"), children: "\u29C9" }),
2050
- /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Move up", onClick: () => command("move_up"), children: "\u2191" }),
2051
- /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Move down", onClick: () => command("move_down"), children: "\u2193" }),
2052
- /* @__PURE__ */ jsx("button", { className: "cv-html-act cv-html-act--del", title: "Delete", onClick: () => command("delete"), children: "\u{1F5D1}" })
4126
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: t("duplicate"), onClick: () => command("duplicate"), children: "\u29C9" }),
4127
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: t("moveUp"), onClick: () => command("move_up"), children: "\u2191" }),
4128
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: t("moveDown"), onClick: () => command("move_down"), children: "\u2193" }),
4129
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act cv-html-act--del", title: t("delete"), onClick: () => command("delete"), children: "\u{1F5D1}" })
2053
4130
  ] })
2054
4131
  ] })
2055
4132
  ] }),
2056
4133
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__spacer" }),
2057
4134
  /* @__PURE__ */ jsxs("div", { className: "cv-html-seg", role: "group", "aria-label": "View mode", children: [
2058
- /* @__PURE__ */ jsx("button", { className: mode === "design" ? "is-on" : "", onClick: () => setMode("design"), children: "Design" }),
2059
- /* @__PURE__ */ jsx("button", { className: mode === "code" ? "is-on" : "", onClick: () => setMode("code"), children: "Code" })
4135
+ /* @__PURE__ */ jsx("button", { className: mode === "design" ? "is-on" : "", onClick: () => setMode("design"), children: t("design") }),
4136
+ /* @__PURE__ */ jsx("button", { className: mode === "code" ? "is-on" : "", onClick: () => setMode("code"), children: t("code") })
2060
4137
  ] })
2061
4138
  ] }),
2062
4139
  a11y !== null && /* @__PURE__ */ jsxs("div", { className: "cv-a11y", role: "status", children: [
2063
4140
  /* @__PURE__ */ jsx("button", { className: "cv-a11y__close", onClick: () => setA11y(null), "aria-label": "Dismiss", children: "\xD7" }),
2064
- a11y.length === 0 ? /* @__PURE__ */ jsx("span", { className: "cv-a11y__ok", children: "\u267F No accessibility issues found" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4141
+ a11y.length === 0 ? /* @__PURE__ */ jsx("span", { className: "cv-a11y__ok", children: t("a11yNone") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2065
4142
  /* @__PURE__ */ jsxs("b", { children: [
2066
4143
  "\u267F ",
2067
4144
  a11y.length,
2068
- " accessibility issue",
2069
- a11y.length > 1 ? "s" : ""
4145
+ " ",
4146
+ t("a11yIssues")
2070
4147
  ] }),
2071
4148
  /* @__PURE__ */ jsx("ul", { children: a11y.map((m, i) => /* @__PURE__ */ jsx("li", { children: m }, i)) })
2072
4149
  ] })
@@ -2096,31 +4173,48 @@ function HtmlRenderer({ artifact: artifact2 }) {
2096
4173
  sandbox: "allow-scripts allow-popups allow-modals",
2097
4174
  style: { width: DEVICES.find((d) => d.id === device).width }
2098
4175
  }
2099
- ) }) : /* @__PURE__ */ jsx(
2100
- "textarea",
2101
- {
2102
- className: "cv-html-code",
2103
- defaultValue: artifact2.data.html,
2104
- spellCheck: false,
2105
- onBlur: (e) => commitCode(e.target.value),
2106
- "aria-label": "HTML source"
2107
- },
2108
- artifact2.data.html
2109
- )
4176
+ ) }) : /* @__PURE__ */ jsx(CodePane, { html: artifact2.data.html, onCommit: commitCode })
2110
4177
  ] });
2111
4178
  }
4179
+ function CodePane({ html, onCommit }) {
4180
+ const [draft, setDraft] = useState(html);
4181
+ const dirty = useRef(false);
4182
+ useEffect(() => {
4183
+ if (!dirty.current) setDraft(html);
4184
+ }, [html]);
4185
+ return /* @__PURE__ */ jsx(
4186
+ "textarea",
4187
+ {
4188
+ className: "cv-html-code",
4189
+ value: draft,
4190
+ spellCheck: false,
4191
+ onChange: (e) => {
4192
+ dirty.current = true;
4193
+ setDraft(e.target.value);
4194
+ },
4195
+ onBlur: () => {
4196
+ if (!dirty.current) return;
4197
+ dirty.current = false;
4198
+ onCommit(draft);
4199
+ },
4200
+ "aria-label": "HTML source"
4201
+ }
4202
+ );
4203
+ }
2112
4204
 
2113
4205
  // src/components/renderers/index.ts
2114
- var ChartRenderer = lazy(() => import('./ChartRenderer-FX6ZN4NC.js').then((m) => ({ default: m.ChartRenderer })));
2115
- var DocumentRenderer = lazy(() => import('./DocumentRenderer-KJ6FTGKH.js').then((m) => ({ default: m.DocumentRenderer })));
2116
- var TableRenderer = lazy(() => import('./TableRenderer-Y4JFRSLU.js').then((m) => ({ default: m.TableRenderer })));
2117
- var SlidesRenderer = lazy(() => import('./SlidesRenderer-SK5VQDIL.js').then((m) => ({ default: m.SlidesRenderer })));
4206
+ var ChartRenderer = lazy(() => import('./ChartRenderer-ABRQ5YFN.js').then((m) => ({ default: m.ChartRenderer })));
4207
+ var DocumentRenderer = lazy(() => import('./DocumentRenderer-YTEMC3Z4.js').then((m) => ({ default: m.DocumentRenderer })));
4208
+ var TableRenderer = lazy(() => import('./TableRenderer-BJQE2HMO.js').then((m) => ({ default: m.TableRenderer })));
4209
+ var SlidesRenderer = lazy(() => import('./SlidesRenderer-JNPXNTNJ.js').then((m) => ({ default: m.SlidesRenderer })));
4210
+ var PdfRenderer = lazy(() => import('./PdfRenderer-GDA67MES.js').then((m) => ({ default: m.PdfRenderer })));
2118
4211
  var builtinRenderers = {
2119
4212
  html: HtmlRenderer,
2120
4213
  document: DocumentRenderer,
2121
4214
  chart: ChartRenderer,
2122
4215
  table: TableRenderer,
2123
- slides: SlidesRenderer
4216
+ slides: SlidesRenderer,
4217
+ pdf: PdfRenderer
2124
4218
  };
2125
4219
 
2126
4220
  // src/export/download.ts
@@ -2139,6 +4233,232 @@ function slugify(text) {
2139
4233
  return text.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "artifact";
2140
4234
  }
2141
4235
 
4236
+ // src/io/hwpxWrite.ts
4237
+ var CRC_TABLE = (() => {
4238
+ const table2 = new Uint32Array(256);
4239
+ for (let n = 0; n < 256; n++) {
4240
+ let c = n;
4241
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
4242
+ table2[n] = c >>> 0;
4243
+ }
4244
+ return table2;
4245
+ })();
4246
+ function crc32(bytes) {
4247
+ let c = 4294967295;
4248
+ for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
4249
+ return (c ^ 4294967295) >>> 0;
4250
+ }
4251
+ function storeZip(parts) {
4252
+ const dosTime = 0;
4253
+ const dosDate = 2026 - 1980 << 9 | 1 << 5 | 1;
4254
+ const enc = new TextEncoder();
4255
+ const chunks = [];
4256
+ const central = [];
4257
+ let offset = 0;
4258
+ for (const [name, data] of parts) {
4259
+ const nameBytes = enc.encode(name);
4260
+ const crc = crc32(data);
4261
+ const local = new Uint8Array(30 + nameBytes.length);
4262
+ const lv = new DataView(local.buffer);
4263
+ lv.setUint32(0, 67324752, true);
4264
+ lv.setUint16(4, 20, true);
4265
+ lv.setUint16(8, 0, true);
4266
+ lv.setUint16(10, dosTime, true);
4267
+ lv.setUint16(12, dosDate, true);
4268
+ lv.setUint32(14, crc, true);
4269
+ lv.setUint32(18, data.length, true);
4270
+ lv.setUint32(22, data.length, true);
4271
+ lv.setUint16(26, nameBytes.length, true);
4272
+ local.set(nameBytes, 30);
4273
+ chunks.push(local, data);
4274
+ const cdir = new Uint8Array(46 + nameBytes.length);
4275
+ const cv = new DataView(cdir.buffer);
4276
+ cv.setUint32(0, 33639248, true);
4277
+ cv.setUint16(4, 20, true);
4278
+ cv.setUint16(6, 20, true);
4279
+ cv.setUint16(10, 0, true);
4280
+ cv.setUint16(12, dosTime, true);
4281
+ cv.setUint16(14, dosDate, true);
4282
+ cv.setUint32(16, crc, true);
4283
+ cv.setUint32(20, data.length, true);
4284
+ cv.setUint32(24, data.length, true);
4285
+ cv.setUint16(28, nameBytes.length, true);
4286
+ cv.setUint32(42, offset, true);
4287
+ cdir.set(nameBytes, 46);
4288
+ central.push(cdir);
4289
+ offset += local.length + data.length;
4290
+ }
4291
+ const cdirSize = central.reduce((n, c) => n + c.length, 0);
4292
+ const eocd = new Uint8Array(22);
4293
+ const ev = new DataView(eocd.buffer);
4294
+ ev.setUint32(0, 101010256, true);
4295
+ ev.setUint16(8, parts.length, true);
4296
+ ev.setUint16(10, parts.length, true);
4297
+ ev.setUint32(12, cdirSize, true);
4298
+ ev.setUint32(16, offset, true);
4299
+ const all = [...chunks, ...central, eocd];
4300
+ const out = new Uint8Array(all.reduce((n, c) => n + c.length, 0));
4301
+ let pos = 0;
4302
+ for (const c of all) {
4303
+ out.set(c, pos);
4304
+ pos += c.length;
4305
+ }
4306
+ return out;
4307
+ }
4308
+ var XML_DECL = `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>`;
4309
+ var HWPML_NS = [
4310
+ `xmlns:ha="http://www.hancom.co.kr/hwpml/2011/app"`,
4311
+ `xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph"`,
4312
+ `xmlns:hp10="http://www.hancom.co.kr/hwpml/2016/paragraph"`,
4313
+ `xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section"`,
4314
+ `xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core"`,
4315
+ `xmlns:hh="http://www.hancom.co.kr/hwpml/2011/head"`,
4316
+ `xmlns:hhs="http://www.hancom.co.kr/hwpml/2011/history"`,
4317
+ `xmlns:hm="http://www.hancom.co.kr/hwpml/2011/master-page"`,
4318
+ `xmlns:hpf="http://www.hancom.co.kr/schema/2011/hpf"`,
4319
+ `xmlns:dc="http://purl.org/dc/elements/1.1/"`,
4320
+ `xmlns:opf="http://www.idpf.org/2007/opf/"`,
4321
+ `xmlns:ooxmlchart="http://www.hancom.co.kr/hwpml/2016/ooxmlchart"`,
4322
+ `xmlns:hwpunitchar="http://www.hancom.co.kr/hwpml/2016/HwpUnitChar"`,
4323
+ `xmlns:epub="http://www.idpf.org/2007/ops"`,
4324
+ `xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0"`
4325
+ ].join(" ");
4326
+ var VERSION_XML = XML_DECL + `<hv:HCFVersion xmlns:hv="http://www.hancom.co.kr/hwpml/2011/version" tagetApplication="WORDPROCESSOR" major="5" minor="0" micro="5" buildNumber="0" xmlVersion="1.4" application="langchain-canvas" appVersion="1.0"/>`;
4327
+ var CONTAINER_XML = XML_DECL + `<ocf:container xmlns:ocf="urn:oasis:names:tc:opendocument:xmlns:container" xmlns:hpf="http://www.hancom.co.kr/schema/2011/hpf"><ocf:rootfiles><ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/></ocf:rootfiles></ocf:container>`;
4328
+ var MANIFEST_XML = XML_DECL + `<odf:manifest xmlns:odf="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"/>`;
4329
+ var CONTENT_HPF = XML_DECL + `<opf:package ${HWPML_NS} version="" unique-identifier="" id=""><opf:metadata><opf:title/><opf:language>ko</opf:language></opf:metadata><opf:manifest><opf:item id="header" href="Contents/header.xml" media-type="application/xml"/><opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/><opf:item id="settings" href="settings.xml" media-type="application/xml"/></opf:manifest><opf:spine><opf:itemref idref="header"/><opf:itemref idref="section0"/></opf:spine></opf:package>`;
4330
+ var SETTINGS_XML = XML_DECL + `<ha:HWPApplicationSetting xmlns:ha="http://www.hancom.co.kr/hwpml/2011/app" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0"><ha:CaretPosition listIDRef="0" paraIDRef="0" pos="0"/></ha:HWPApplicationSetting>`;
4331
+ function fontfaces() {
4332
+ const langs = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
4333
+ const face = (lang) => `<hh:fontface lang="${lang}" fontCnt="1"><hh:font id="0" face="\uD568\uCD08\uB86C\uBC14\uD0D5" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="8" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font></hh:fontface>`;
4334
+ return `<hh:fontfaces itemCnt="7">${langs.map(face).join("")}</hh:fontfaces>`;
4335
+ }
4336
+ function borderFills() {
4337
+ const fill = (id) => `<hh:borderFill id="${id}" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0"><hh:slash type="NONE" Crooked="0" isCounter="0"/><hh:backSlash type="NONE" Crooked="0" isCounter="0"/><hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/><hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/><hh:topBorder type="NONE" width="0.1 mm" color="#000000"/><hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/><hh:diagonal type="SOLID" width="0.1 mm" color="#000000"/></hh:borderFill>`;
4338
+ return `<hh:borderFills itemCnt="2">${fill(1)}${fill(2)}</hh:borderFills>`;
4339
+ }
4340
+ var CHAR_PR = [
4341
+ { id: 0, height: 1e3, bold: false, italic: false },
4342
+ // body
4343
+ { id: 1, height: 1800, bold: true, italic: false },
4344
+ // heading 1
4345
+ { id: 2, height: 1500, bold: true, italic: false },
4346
+ // heading 2
4347
+ { id: 3, height: 1200, bold: true, italic: false },
4348
+ // heading 3
4349
+ { id: 4, height: 1e3, bold: true, italic: false },
4350
+ // inline bold
4351
+ { id: 5, height: 1e3, bold: false, italic: true }
4352
+ // inline italic
4353
+ ];
4354
+ function charProperties() {
4355
+ const one = (c) => `<hh:charPr id="${c.id}" height="${c.height}" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2"><hh:fontRef hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/><hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/><hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/><hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/><hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>` + (c.bold ? `<hh:bold/>` : "") + (c.italic ? `<hh:italic/>` : "") + `<hh:underline type="NONE" shape="SOLID" color="#000000"/><hh:strikeout shape="NONE" color="#000000"/><hh:outline type="NONE"/><hh:shadow type="NONE" color="#B2B2B2" offsetX="10" offsetY="10"/></hh:charPr>`;
4356
+ return `<hh:charProperties itemCnt="${CHAR_PR.length}">${CHAR_PR.map(one).join("")}</hh:charProperties>`;
4357
+ }
4358
+ function numberings() {
4359
+ const heads = Array.from(
4360
+ { length: 7 },
4361
+ (_, i) => `<hh:paraHead start="1" level="${i + 1}" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="DIGIT" charPrIDRef="4294967295" checkable="1">^${i + 1}.</hh:paraHead>`
4362
+ ).join("");
4363
+ return `<hh:numberings itemCnt="1"><hh:numbering id="1" start="0">${heads}</hh:numbering></hh:numberings>`;
4364
+ }
4365
+ function paraProperties() {
4366
+ return `<hh:paraProperties itemCnt="1"><hh:paraPr id="0" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="1" suppressLineNumbers="0" checked="0"><hh:align horizontal="JUSTIFY" vertical="BASELINE"/><hh:heading type="NONE" idRef="0" level="0"/><hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="KEEP_WORD" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/><hh:autoSpacing eAsianEng="0" eAsianNum="0"/><hh:margin><hc:intent value="0" unit="HWPUNIT"/><hc:left value="0" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="0" unit="HWPUNIT"/><hc:next value="0" unit="HWPUNIT"/></hh:margin><hh:lineSpacing type="PERCENT" value="160" unit="HWPUNIT"/><hh:border borderFillIDRef="2" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/></hh:paraPr></hh:paraProperties>`;
4367
+ }
4368
+ var STYLES = [
4369
+ { id: 0, name: "\uBC14\uD0D5\uAE00", engName: "Normal", charPr: 0, next: 0 },
4370
+ { id: 1, name: "\uAC1C\uC694 1", engName: "Outline 1", charPr: 1, next: 1 },
4371
+ { id: 2, name: "\uAC1C\uC694 2", engName: "Outline 2", charPr: 2, next: 2 },
4372
+ { id: 3, name: "\uAC1C\uC694 3", engName: "Outline 3", charPr: 3, next: 3 }
4373
+ ];
4374
+ function styles() {
4375
+ const one = (s) => `<hh:style id="${s.id}" type="PARA" name="${s.name}" engName="${s.engName}" paraPrIDRef="0" charPrIDRef="${s.charPr}" nextStyleIDRef="${s.next}" langID="1042" lockForm="0"/>`;
4376
+ return `<hh:styles itemCnt="${STYLES.length}">${STYLES.map(one).join("")}</hh:styles>`;
4377
+ }
4378
+ function headerXml() {
4379
+ return XML_DECL + `<hh:head ${HWPML_NS} version="1.4" secCnt="1"><hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/><hh:refList>` + fontfaces() + borderFills() + charProperties() + `<hh:tabProperties itemCnt="2"><hh:tabPr id="0" autoTabLeft="0" autoTabRight="0"/><hh:tabPr id="1" autoTabLeft="1" autoTabRight="0"/></hh:tabProperties>` + numberings() + paraProperties() + styles() + `</hh:refList><hh:compatibleDocument targetProgram="HWP201X"><hh:layoutCompatibility/></hh:compatibleDocument><hh:docOption><hh:linkinfo path="" pageInherit="0" footnoteInherit="0"/></hh:docOption><hh:trackchageConfig flags="56"/></hh:head>`;
4380
+ }
4381
+ var SEC_PR = `<hp:secPr id="" textDirection="HORIZONTAL" spaceColumns="1134" tabStop="8000" tabStopVal="4000" tabStopUnit="HWPUNIT" outlineShapeIDRef="1" memoShapeIDRef="0" textVerticalWidthHead="0"><hp:grid lineGrid="0" charGrid="0" wonggojiFormat="0"/><hp:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/><hp:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/><hp:lineNumberShape restartType="0" countBy="0" distance="0" startNumber="0"/><hp:pagePr landscape="WIDELY" width="59528" height="84188" gutterType="LEFT_ONLY"><hp:margin header="4252" footer="4252" gutter="0" left="8504" right="8504" top="5668" bottom="4252"/></hp:pagePr><hp:footNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="-1" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="283" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="EACH_COLUMN" beneathText="0"/></hp:footNotePr><hp:endNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="14692344" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="0" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="END_OF_DOCUMENT" beneathText="0"/></hp:endNotePr><hp:pageBorderFill type="BOTH" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER"><hp:offset left="1417" right="1417" top="1417" bottom="1417"/></hp:pageBorderFill><hp:pageBorderFill type="EVEN" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER"><hp:offset left="1417" right="1417" top="1417" bottom="1417"/></hp:pageBorderFill><hp:pageBorderFill type="ODD" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER"><hp:offset left="1417" right="1417" top="1417" bottom="1417"/></hp:pageBorderFill></hp:secPr><hp:ctrl><hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/></hp:ctrl>`;
4382
+ function escapeXml(value) {
4383
+ return String(value ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4384
+ }
4385
+ function inlineRuns(text, baseCharPr) {
4386
+ const runs = [];
4387
+ const pattern = /(\*\*[^*\n]+\*\*|\*[^*\n]+\*|`[^`\n]+`)/g;
4388
+ let last = 0;
4389
+ for (const m of text.matchAll(pattern)) {
4390
+ if (m.index > last) runs.push({ text: text.slice(last, m.index), charPr: baseCharPr });
4391
+ const token = m[0];
4392
+ if (token.startsWith("**")) runs.push({ text: token.slice(2, -2), charPr: 4 });
4393
+ else if (token.startsWith("*")) runs.push({ text: token.slice(1, -1), charPr: 5 });
4394
+ else runs.push({ text: token.slice(1, -1), charPr: baseCharPr });
4395
+ last = m.index + token.length;
4396
+ }
4397
+ if (last < text.length) runs.push({ text: text.slice(last), charPr: baseCharPr });
4398
+ return runs.length ? runs : [{ text: "", charPr: baseCharPr }];
4399
+ }
4400
+ function isTableSeparator(line) {
4401
+ return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/.test(line);
4402
+ }
4403
+ function tableCells(line) {
4404
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
4405
+ }
4406
+ function markdownToBlocks(content) {
4407
+ const blocks = [];
4408
+ for (const raw of content.split("\n")) {
4409
+ const line = raw.replace(/\s+$/, "");
4410
+ if (!line.trim()) continue;
4411
+ const heading = line.match(/^(#{1,3})\s+(.*)$/);
4412
+ if (heading) {
4413
+ const text = heading[2].replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
4414
+ blocks.push({ runs: [{ text, charPr: heading[1].length }], style: heading[1].length });
4415
+ continue;
4416
+ }
4417
+ const bullet = line.match(/^\s*[-*+]\s+(.*)$/);
4418
+ if (bullet) {
4419
+ blocks.push({ runs: [{ text: "\u2022 ", charPr: 0 }, ...inlineRuns(bullet[1], 0)], style: 0 });
4420
+ continue;
4421
+ }
4422
+ const numbered = line.match(/^\s*(\d+)[.)]\s+(.*)$/);
4423
+ if (numbered) {
4424
+ blocks.push({ runs: [{ text: `${numbered[1]}. `, charPr: 0 }, ...inlineRuns(numbered[2], 0)], style: 0 });
4425
+ continue;
4426
+ }
4427
+ if (line.trim().startsWith("|")) {
4428
+ if (isTableSeparator(line)) continue;
4429
+ blocks.push({ runs: [{ text: tableCells(line).join(" | "), charPr: 0 }], style: 0 });
4430
+ continue;
4431
+ }
4432
+ blocks.push({ runs: inlineRuns(line, 0), style: 0 });
4433
+ }
4434
+ return blocks.length ? blocks : [{ runs: [{ text: "", charPr: 0 }], style: 0 }];
4435
+ }
4436
+ function paragraphXml(block, index) {
4437
+ const runs = block.runs.map(
4438
+ (r, i) => `<hp:run charPrIDRef="${r.charPr}">${index === 0 && i === 0 ? SEC_PR : ""}<hp:t>${escapeXml(r.text)}</hp:t></hp:run>`
4439
+ ).join("");
4440
+ return `<hp:p id="${index}" paraPrIDRef="0" styleIDRef="${block.style}" pageBreak="0" columnBreak="0" merged="0">` + runs + `<hp:linesegarray><hp:lineseg textpos="0" vertpos="0" vertsize="1000" textheight="1000" baseline="850" spacing="600" horzpos="0" horzsize="42520" flags="393216"/></hp:linesegarray></hp:p>`;
4441
+ }
4442
+ function sectionXml(blocks) {
4443
+ return XML_DECL + `<hs:sec ${HWPML_NS}>` + blocks.map(paragraphXml).join("") + `</hs:sec>`;
4444
+ }
4445
+ async function documentToHwpx(data) {
4446
+ const enc = new TextEncoder();
4447
+ const blocks = markdownToBlocks(data.content ?? "");
4448
+ const parts = [
4449
+ // `mimetype` must be the container's first entry, stored.
4450
+ ["mimetype", enc.encode("application/hwp+zip")],
4451
+ ["version.xml", enc.encode(VERSION_XML)],
4452
+ ["META-INF/manifest.xml", enc.encode(MANIFEST_XML)],
4453
+ ["META-INF/container.xml", enc.encode(CONTAINER_XML)],
4454
+ ["Contents/content.hpf", enc.encode(CONTENT_HPF)],
4455
+ ["Contents/header.xml", enc.encode(headerXml())],
4456
+ ["Contents/section0.xml", enc.encode(sectionXml(blocks))],
4457
+ ["settings.xml", enc.encode(SETTINGS_XML)]
4458
+ ];
4459
+ return storeZip(parts);
4460
+ }
4461
+
2142
4462
  // src/export/exporters.ts
2143
4463
  var MIME = {
2144
4464
  md: "text/markdown",
@@ -2151,7 +4471,8 @@ var MIME = {
2151
4471
  var dataExporters = {
2152
4472
  document: [
2153
4473
  { label: "Markdown", extension: "md", mime: MIME.md, build: (a) => a.data.content },
2154
- { label: "Word", extension: "docx", mime: MIME.docx, build: (a) => documentToDocx(a.data) }
4474
+ { label: "Word", extension: "docx", mime: MIME.docx, build: (a) => documentToDocx(a.data) },
4475
+ { label: "\uD55C\uAE00 (HWPX)", extension: "hwpx", mime: "application/vnd.hancom.hwpx", build: (a) => documentToHwpx(a.data) }
2155
4476
  ],
2156
4477
  table: [
2157
4478
  { label: "CSV", extension: "csv", mime: MIME.csv, build: (a) => tableToCsv(a.data) },
@@ -2170,7 +4491,7 @@ function toStandaloneHtml(title, renderedHtml) {
2170
4491
  <head>
2171
4492
  <meta charset="utf-8" />
2172
4493
  <meta name="viewport" content="width=device-width, initial-scale=1" />
2173
- <title>${escapeHtml(title)}</title>
4494
+ <title>${escapeHtml2(title)}</title>
2174
4495
  <style>${EXPORT_CSS}</style>
2175
4496
  </head>
2176
4497
  <body>
@@ -2181,11 +4502,34 @@ ${renderedHtml}
2181
4502
  </html>`;
2182
4503
  }
2183
4504
  function tableToCsv(data) {
4505
+ if (data.sheet?.length) return fortuneToCsv(data.sheet[0]);
2184
4506
  const header = data.columns.map((c) => csvCell(c.label ?? c.key)).join(",");
2185
4507
  const body = data.rows.map((row) => data.columns.map((c) => csvCell(String(row[c.key] ?? ""))).join(",")).join("\n");
2186
4508
  return `${header}
2187
4509
  ${body}`;
2188
4510
  }
4511
+ function fortuneToCsv(sheet) {
4512
+ let maxR = -1;
4513
+ let maxC = -1;
4514
+ const grid = /* @__PURE__ */ new Map();
4515
+ for (const cell of sheet.celldata ?? []) {
4516
+ const v = cell?.v;
4517
+ if (v == null) continue;
4518
+ if (typeof v === "object" && v.mc && v.v == null && v.m == null) continue;
4519
+ const value = typeof v === "object" ? v.v ?? v.m ?? "" : v;
4520
+ if (value === "" || value == null) continue;
4521
+ grid.set(`${cell.r},${cell.c}`, String(value));
4522
+ if (cell.r > maxR) maxR = cell.r;
4523
+ if (cell.c > maxC) maxC = cell.c;
4524
+ }
4525
+ const lines = [];
4526
+ for (let r = 0; r <= maxR; r++) {
4527
+ const row = [];
4528
+ for (let c = 0; c <= maxC; c++) row.push(csvCell(grid.get(`${r},${c}`) ?? ""));
4529
+ lines.push(row.join(","));
4530
+ }
4531
+ return lines.join("\n");
4532
+ }
2189
4533
  async function tableToXlsx(data) {
2190
4534
  const { Workbook } = await loadOptional("exceljs", () => import('exceljs'));
2191
4535
  const workbook = new Workbook();
@@ -2249,8 +4593,10 @@ async function documentToDocx(data) {
2249
4593
  async function slidesToPptx(data, _title) {
2250
4594
  const PptxGenJS = (await loadOptional("pptxgenjs", () => import('pptxgenjs'))).default;
2251
4595
  const pptx = new PptxGenJS();
2252
- const W = 10;
2253
- const H = 5.625;
4596
+ const W = 13.333;
4597
+ const H = 7.5;
4598
+ pptx.defineLayout({ name: "CV_16x9", width: W, height: H });
4599
+ pptx.layout = "CV_16x9";
2254
4600
  for (const slide of data.slides) {
2255
4601
  const s = pptx.addSlide();
2256
4602
  if (slide.background && /^#[0-9a-f]{3,8}$/i.test(slide.background)) s.background = { color: slide.background.replace("#", "") };
@@ -2259,18 +4605,28 @@ async function slidesToPptx(data, _title) {
2259
4605
  const inset = (v) => pad + v / 100 * (1 - 2 * pad);
2260
4606
  for (const el of resolveElements(slide)) {
2261
4607
  const box = { x: inset(el.x) * W, y: inset(el.y) * H, w: el.w / 100 * (1 - 2 * pad) * W, h: el.h / 100 * (1 - 2 * pad) * H };
4608
+ const spin = el.rotate ? { rotate: (Math.round(el.rotate) % 360 + 360) % 360 } : {};
2262
4609
  if (el.type === "text") {
2263
4610
  const color = el.color ? el.color.replace("#", "") : tc;
2264
- s.addText(el.text ?? "", { ...box, fontSize: (el.fontSize ?? 24) * 0.75, bold: !!el.bold, align: el.align ?? "left", ...color ? { color } : {} });
4611
+ s.addText(el.text ?? "", { ...box, ...spin, fontSize: (el.fontSize ?? 24) * 0.75, bold: !!el.bold, align: el.align ?? "left", ...color ? { color } : {} });
2265
4612
  } else if (el.type === "shape") {
2266
4613
  const fill = (el.fill ?? "#5b5bd6").replace("#", "");
4614
+ const paint = el.fill ? { fill: { color: fill } } : { fill: { color: "FFFFFF", transparency: 100 }, line: { color: (tc || "1f2328").replace("#", ""), width: 1 } };
2267
4615
  if (el.shape === "line") {
2268
- s.addShape(pptx.ShapeType.line, { ...box, line: { color: fill, width: 2 } });
4616
+ s.addShape(pptx.ShapeType.line, { ...box, ...spin, line: { color: fill, width: 2 } });
4617
+ } else if (el.shape !== "ellipse" && el.radius) {
4618
+ s.addShape(pptx.ShapeType.roundRect, { ...box, ...spin, ...paint, rectRadius: el.radius * W / 1280 });
2269
4619
  } else {
2270
- s.addShape(el.shape === "ellipse" ? pptx.ShapeType.ellipse : pptx.ShapeType.rect, { ...box, fill: { color: fill } });
4620
+ s.addShape(el.shape === "ellipse" ? pptx.ShapeType.ellipse : pptx.ShapeType.rect, { ...box, ...spin, ...paint });
2271
4621
  }
2272
4622
  } else if (el.src) {
2273
- s.addImage({ data: el.src, ...box, sizing: { type: "contain", w: box.w, h: box.h } });
4623
+ s.addImage({
4624
+ data: el.src,
4625
+ ...box,
4626
+ ...spin,
4627
+ sizing: { type: el.fit === "cover" ? "cover" : "contain", w: box.w, h: box.h },
4628
+ ...el.radius ? { rounding: true } : {}
4629
+ });
2274
4630
  }
2275
4631
  }
2276
4632
  if (slide.notes) s.addNotes(slide.notes);
@@ -2283,29 +4639,34 @@ function slidesToPrintHtml(data, title) {
2283
4639
  const bg = slide.background ?? "#ffffff";
2284
4640
  const fg = slide.textColor ?? "#1f2328";
2285
4641
  const els = resolveElements(slide).map((el) => {
2286
- const box = `left:${el.x}%;top:${el.y}%;width:${el.w}%;height:${el.h}%`;
4642
+ const spin = el.rotate ? `;transform:rotate(${Number(el.rotate)}deg)` : "";
4643
+ const box = `left:${el.x}%;top:${el.y}%;width:${el.w}%;height:${el.h}%${spin}`;
2287
4644
  if (el.type === "text") {
2288
- const style = `${box};font-size:${(el.fontSize ?? 24) / 7.2}vw;font-weight:${el.bold ? 700 : 400};color:${escapeAttr(el.color ?? fg)};text-align:${escapeAttr(el.align ?? "left")};white-space:pre-wrap`;
2289
- return `<div class="el" style="${style}">${escapeXml(el.text ?? "")}</div>`;
4645
+ const style = `${box};font-size:${el.fontSize ?? 24}px;font-weight:${el.bold ? 700 : 400};color:${escapeAttr(el.color ?? fg)};text-align:${escapeAttr(el.align ?? "left")};white-space:pre-wrap`;
4646
+ return `<div class="el" style="${style}">${escapeXml2(el.text ?? "")}</div>`;
2290
4647
  }
2291
4648
  if (el.type === "shape") {
2292
- const fill = escapeAttr(el.fill ?? fg);
2293
- const radius = el.shape === "ellipse" ? "50%" : el.shape === "line" ? "2px" : "8px";
2294
- return `<div class="el" style="${box};background:${fill};border-radius:${radius}"></div>`;
4649
+ const radius = el.shape === "ellipse" ? "50%" : el.shape === "line" ? "2px" : `${Number(el.radius ?? 8)}px`;
4650
+ const paint = el.fill || el.shape === "line" ? `background:${escapeAttr(el.fill ?? fg)}` : `background:transparent;border:1.5px solid ${escapeAttr(fg)};box-sizing:border-box`;
4651
+ return `<div class="el" style="${box};${paint};border-radius:${radius}"></div>`;
2295
4652
  }
2296
4653
  const src = safeSrc(el.src);
2297
- return src ? `<img class="el" style="${box}" src="${escapeAttr(src)}"/>` : "";
4654
+ const imgStyle = `${box};object-fit:${el.fit === "cover" ? "cover" : "contain"}${el.radius ? `;border-radius:${Number(el.radius)}px` : ""}`;
4655
+ return src ? `<img class="el" style="${imgStyle}" src="${escapeAttr(src)}"/>` : "";
2298
4656
  }).join("");
2299
4657
  const pad = slide.padding ?? 0;
2300
4658
  const inner = pad ? `<div style="position:absolute;inset:${pad}%">${els}</div>` : els;
2301
4659
  return `<section class="slide" style="background:${escapeAttr(bg)}">${inner}</section>`;
2302
4660
  }).join("");
2303
- return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeXml(title)}</title><style>
4661
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeXml2(title)}</title><style>
2304
4662
  @page { size: 1280px 720px; margin: 0; }
2305
4663
  * { margin: 0; box-sizing: border-box; }
2306
4664
  body { font-family: Inter, Arial, sans-serif; }
2307
4665
  .slide { position: relative; width: 1280px; height: 720px; overflow: hidden; page-break-after: always; }
2308
- .el { position: absolute; overflow: hidden; line-height: 1.25; }
4666
+ .el { position: absolute; overflow: hidden; line-height: 1.2; }
4667
+ /* PowerPoint lets text paint past its box \u2014 only shapes/images clip. */
4668
+ div.el { overflow: visible; }
4669
+ img.el { overflow: hidden; }
2309
4670
  img.el { object-fit: contain; }
2310
4671
  </style></head><body>${pages}</body></html>`;
2311
4672
  }
@@ -2316,7 +4677,7 @@ function htmlSlideToPrintHtml(html, ratio) {
2316
4677
  const i = html.toLowerCase().lastIndexOf("</head>");
2317
4678
  return i === -1 ? style + html : html.slice(0, i) + style + html.slice(i);
2318
4679
  }
2319
- function escapeXml(value) {
4680
+ function escapeXml2(value) {
2320
4681
  return String(value ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
2321
4682
  }
2322
4683
  function escapeAttr(value) {
@@ -2329,7 +4690,7 @@ function safeSrc(src) {
2329
4690
  function csvCell(value) {
2330
4691
  return /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
2331
4692
  }
2332
- function escapeHtml(text) {
4693
+ function escapeHtml2(text) {
2333
4694
  return text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
2334
4695
  }
2335
4696
  var EXPORT_CSS = `
@@ -2373,7 +4734,10 @@ function printToPdf(html) {
2373
4734
  document.body.appendChild(iframe);
2374
4735
  }
2375
4736
  var PDF_TYPES = /* @__PURE__ */ new Set(["html", "document", "chart", "slides"]);
4737
+ var escapeHtml3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4738
+ var escapeAttr2 = (s) => escapeHtml3(s).replace(/"/g, "&quot;");
2376
4739
  function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
4740
+ const t = useT();
2377
4741
  const [open, setOpen] = useState(false);
2378
4742
  const stem = slugify(artifact2.title);
2379
4743
  const dataOptions = dataExporters[artifact2.type] ?? [];
@@ -2414,7 +4778,8 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
2414
4778
  return h == null ? null : toStandaloneHtml(artifact2.title, h);
2415
4779
  })();
2416
4780
  if (html == null) return;
2417
- const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
4781
+ const wrapper = `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml3(artifact2.title)}</title><style>html,body{margin:0;height:100%}iframe{display:block;width:100%;height:100%;border:0}</style></head><body><iframe sandbox="allow-scripts allow-popups allow-modals" srcdoc="${escapeAttr2(html)}"></iframe></body></html>`;
4782
+ const url = URL.createObjectURL(new Blob([wrapper], { type: "text/html" }));
2418
4783
  window.open(url, "_blank", "noopener");
2419
4784
  setTimeout(() => URL.revokeObjectURL(url), 1e4);
2420
4785
  setOpen(false);
@@ -2440,14 +4805,14 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
2440
4805
  onClick: () => setOpen((o) => !o),
2441
4806
  "aria-haspopup": "menu",
2442
4807
  "aria-expanded": open,
2443
- children: "Export \u25BE"
4808
+ children: t("export")
2444
4809
  }
2445
4810
  ),
2446
4811
  open && /* @__PURE__ */ jsxs(Fragment, { children: [
2447
4812
  /* @__PURE__ */ jsx("div", { className: "cv-export__scrim", onClick: () => setOpen(false) }),
2448
4813
  /* @__PURE__ */ jsxs("div", { className: "cv-export__menu", role: "menu", children: [
2449
- /* @__PURE__ */ jsx("button", { role: "menuitem", onClick: openInTab, children: "Open in new tab \u2197" }),
2450
- /* @__PURE__ */ jsx("button", { role: "menuitem", onClick: copyHtml, children: copied ? "Copied \u2713" : "Copy HTML" }),
4814
+ /* @__PURE__ */ jsx("button", { role: "menuitem", onClick: openInTab, children: t("openInTab") }),
4815
+ /* @__PURE__ */ jsx("button", { role: "menuitem", onClick: copyHtml, children: copied ? t("copied") : t("copyHtml") }),
2451
4816
  /* @__PURE__ */ jsxs("button", { role: "menuitem", onClick: exportHtml, children: [
2452
4817
  "HTML ",
2453
4818
  /* @__PURE__ */ jsx("span", { className: "cv-export__ext", children: ".html" })
@@ -2536,17 +4901,17 @@ var GRADIENTS = [
2536
4901
  function StylePanel({ selection }) {
2537
4902
  const send = useCanvasStore((s) => s.sendIframeCommand);
2538
4903
  const setSelections = useCanvasStore((s) => s.setSelections);
2539
- const styles = selection.styles ?? {};
2540
- const [color, setColor] = useState(toHex2(styles.color));
2541
- const [background, setBackground] = useState(toHex2(styles.backgroundColor));
2542
- const [fontSize, setFontSize] = useState(px(styles.fontSize, 16));
2543
- const [fontWeight, setFontWeight] = useState(String(styles.fontWeight ?? "400"));
2544
- const [textAlign, setTextAlign] = useState(styles.textAlign ?? "left");
2545
- const [lineHeight, setLineHeight] = useState(px(styles.lineHeight, 0));
2546
- const [letterSpacing, setLetterSpacing] = useState(px(styles.letterSpacing, 0));
2547
- const [padding, setPadding] = useState(px(styles.padding, 0));
2548
- const [radius, setRadius] = useState(px(styles.borderRadius, 0));
2549
- const [width, setWidth] = useState(px(styles.width, 0));
4904
+ const styles2 = selection.styles ?? {};
4905
+ const [color, setColor] = useState(toHex2(styles2.color));
4906
+ const [background, setBackground] = useState(toHex2(styles2.backgroundColor));
4907
+ const [fontSize, setFontSize] = useState(px(styles2.fontSize, 16));
4908
+ const [fontWeight, setFontWeight] = useState(String(styles2.fontWeight ?? "400"));
4909
+ const [textAlign, setTextAlign] = useState(styles2.textAlign ?? "left");
4910
+ const [lineHeight, setLineHeight] = useState(px(styles2.lineHeight, 0));
4911
+ const [letterSpacing, setLetterSpacing] = useState(px(styles2.letterSpacing, 0));
4912
+ const [padding, setPadding] = useState(px(styles2.padding, 0));
4913
+ const [radius, setRadius] = useState(px(styles2.borderRadius, 0));
4914
+ const [width, setWidth] = useState(px(styles2.width, 0));
2550
4915
  const bgFileRef = useRef(null);
2551
4916
  const dirty = useRef(false);
2552
4917
  const setStyle = (prop, value) => {
@@ -2788,10 +5153,17 @@ function useCanvasImport() {
2788
5153
  return { importFiles, canImport };
2789
5154
  }
2790
5155
  var ACCEPT = IMPORTABLE_EXTENSIONS.join(",");
2791
- function Canvas({ registry = builtinRenderers, emptyState, onEditElement, onUserEdit }) {
2792
- return /* @__PURE__ */ jsx(CanvasRegistryProvider, { registry, children: /* @__PURE__ */ jsx(CanvasPanel, { emptyState, onEditElement, onUserEdit }) });
5156
+ function Canvas({ registry = builtinRenderers, locale, emptyState, onEditElement, onUserEdit, onSave }) {
5157
+ return /* @__PURE__ */ jsx(CanvasRegistryProvider, { registry, children: /* @__PURE__ */ jsx(CanvasLocaleProvider, { locale, children: /* @__PURE__ */ jsx(CanvasPanel, { emptyState, onEditElement, onUserEdit, onSave }) }) });
2793
5158
  }
2794
- function CanvasPanel({ emptyState, onEditElement, onUserEdit }) {
5159
+ function CanvasPanel({
5160
+ emptyState,
5161
+ onEditElement,
5162
+ onUserEdit,
5163
+ onSave
5164
+ }) {
5165
+ const t = useT();
5166
+ const debouncedSave = useCanvasSave(onSave);
2795
5167
  const { artifacts, order, activeId } = useCanvasStore((s) => s.canvas);
2796
5168
  const history = useCanvasStore((s) => s.canvas.history);
2797
5169
  const setActive = useCanvasStore((s) => s.setActiveArtifact);
@@ -2801,9 +5173,16 @@ function CanvasPanel({ emptyState, onEditElement, onUserEdit }) {
2801
5173
  const { importFiles } = useCanvasImport();
2802
5174
  const [dropping, setDropping] = useState(false);
2803
5175
  useEffect(() => {
2804
- setOnUserEdit(onUserEdit ?? null);
5176
+ if (!onUserEdit && !debouncedSave) {
5177
+ setOnUserEdit(null);
5178
+ return;
5179
+ }
5180
+ setOnUserEdit((artifact2) => {
5181
+ onUserEdit?.(artifact2);
5182
+ debouncedSave?.(artifact2);
5183
+ });
2805
5184
  return () => setOnUserEdit(null);
2806
- }, [onUserEdit, setOnUserEdit]);
5185
+ }, [onUserEdit, debouncedSave, setOnUserEdit]);
2807
5186
  useEffect(() => {
2808
5187
  if (!selections.length) return;
2809
5188
  const onKey = (e) => {
@@ -2829,14 +5208,14 @@ function CanvasPanel({ emptyState, onEditElement, onUserEdit }) {
2829
5208
  if (e.dataTransfer.files.length) void importFiles(e.dataTransfer.files);
2830
5209
  }
2831
5210
  };
2832
- const dropOverlay = dropping ? /* @__PURE__ */ jsx("div", { className: "cv-canvas__drop", children: "Drop to open on the canvas" }) : null;
5211
+ const dropOverlay = dropping ? /* @__PURE__ */ jsx("div", { className: "cv-canvas__drop", children: t("dropToOpen") }) : null;
2833
5212
  if (!active) {
2834
5213
  return /* @__PURE__ */ jsxs("aside", { className: "cv-canvas cv-canvas--empty", ...dropProps, children: [
2835
5214
  emptyState ?? /* @__PURE__ */ jsx(EmptyState, { onOpenFiles: importFiles }),
2836
5215
  dropOverlay
2837
5216
  ] });
2838
5217
  }
2839
- const versions = history[active.id] ?? [active];
5218
+ const versions2 = history[active.id] ?? [active];
2840
5219
  const showSelection = Boolean(onEditElement) && selections.length > 0 && selections[0].artifactId === active.id;
2841
5220
  return /* @__PURE__ */ jsxs("aside", { className: "cv-canvas", ...dropProps, children: [
2842
5221
  dropOverlay,
@@ -2851,17 +5230,19 @@ function CanvasPanel({ emptyState, onEditElement, onUserEdit }) {
2851
5230
  },
2852
5231
  id
2853
5232
  )) }),
2854
- /* @__PURE__ */ jsx(ArtifactView, { artifact: active, versions }, active.id),
5233
+ /* @__PURE__ */ jsx(ArtifactView, { artifact: active, versions: versions2 }, active.id),
2855
5234
  showSelection && onEditElement && /* @__PURE__ */ jsxs(Fragment, { children: [
2856
5235
  active.type === "html" && selections.length === 1 && /* @__PURE__ */ jsx(StylePanel, { selection: selections[0] }, selections[0].cid),
2857
5236
  /* @__PURE__ */ jsx(SelectionBar, { selections, onEdit: onEditElement, onClear: () => setSelections([]) })
2858
5237
  ] })
2859
5238
  ] });
2860
5239
  }
2861
- function ArtifactView({ artifact: artifact2, versions }) {
5240
+ function ArtifactView({ artifact: artifact2, versions: versions2 }) {
5241
+ const t = useT();
2862
5242
  const [viewIndex, setViewIndex] = useState(null);
2863
5243
  const bodyRef = useRef(null);
2864
- const shown = viewIndex === null ? artifact2 : versions[viewIndex];
5244
+ const shown = viewIndex === null ? artifact2 : versions2[Math.min(viewIndex, versions2.length - 1)] ?? artifact2;
5245
+ const viewingHistory = viewIndex !== null && shown !== artifact2;
2865
5246
  const Renderer = useRenderer(shown.type);
2866
5247
  const getRenderedHtml = () => {
2867
5248
  const node = bodyRef.current;
@@ -2879,25 +5260,45 @@ function ArtifactView({ artifact: artifact2, versions }) {
2879
5260
  ] }),
2880
5261
  /* @__PURE__ */ jsxs("div", { className: "cv-header__actions", children: [
2881
5262
  /* @__PURE__ */ jsx(UndoRedo, {}),
2882
- versions.length > 1 && /* @__PURE__ */ jsx(
2883
- VersionRail,
5263
+ versions2.length > 1 && /* @__PURE__ */ jsx(
5264
+ VersionHistory,
2884
5265
  {
2885
- total: versions.length,
2886
- index: viewIndex ?? versions.length - 1,
2887
- onSelect: (i) => setViewIndex(i === versions.length - 1 ? null : i)
5266
+ versions: versions2,
5267
+ index: viewIndex ?? versions2.length - 1,
5268
+ onSelect: (i) => setViewIndex(i === versions2.length - 1 ? null : i)
2888
5269
  }
2889
5270
  ),
2890
5271
  /* @__PURE__ */ jsx(ExportMenu, { artifact: shown, getRenderedHtml })
2891
5272
  ] })
2892
5273
  ] }),
2893
- /* @__PURE__ */ jsx("div", { className: `cv-body${shown.type === "table" ? " cv-body--flush" : ""}`, ref: bodyRef, children: Renderer ? /* @__PURE__ */ jsx(RendererBoundary, { resetKey: `${shown.id}:${shown.version}`, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "cv-fallback", children: "Loading\u2026" }), children: /* @__PURE__ */ jsx(Renderer, { artifact: shown }) }) }) : /* @__PURE__ */ jsxs("div", { className: "cv-fallback", children: [
2894
- "No renderer registered for type \u201C",
2895
- shown.type,
2896
- "\u201D."
2897
- ] }) })
5274
+ viewingHistory && /* @__PURE__ */ jsxs("div", { className: "cv-history-banner", role: "status", children: [
5275
+ "v",
5276
+ Math.min((viewIndex ?? 0) + 1, versions2.length),
5277
+ " / ",
5278
+ versions2.length,
5279
+ " \xB7 ",
5280
+ t("historyNote"),
5281
+ " ",
5282
+ /* @__PURE__ */ jsx("button", { onClick: () => setViewIndex(null), children: t("backToLatest") })
5283
+ ] }),
5284
+ /* @__PURE__ */ jsx(
5285
+ "div",
5286
+ {
5287
+ className: `cv-body${shown.type === "table" || shown.type === "pdf" ? " cv-body--flush" : ""}${viewingHistory ? " cv-body--history" : ""}`,
5288
+ ref: bodyRef,
5289
+ "aria-readonly": viewingHistory || void 0,
5290
+ children: Renderer ? /* @__PURE__ */ jsx(RendererBoundary, { resetKey: `${shown.id}:${shown.version}`, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "cv-fallback", children: t("loading") }), children: /* @__PURE__ */ jsx(Renderer, { artifact: shown }) }) }) : /* @__PURE__ */ jsxs("div", { className: "cv-fallback", children: [
5291
+ t("noRenderer"),
5292
+ " \u201C",
5293
+ shown.type,
5294
+ "\u201D."
5295
+ ] })
5296
+ }
5297
+ )
2898
5298
  ] });
2899
5299
  }
2900
5300
  function UndoRedo() {
5301
+ const t = useT();
2901
5302
  const undo = useCanvasStore((s) => s.undo);
2902
5303
  const redo = useCanvasStore((s) => s.redo);
2903
5304
  const canUndo = useCanvasStore((s) => s.undoStack.length > 0);
@@ -2917,29 +5318,65 @@ function UndoRedo() {
2917
5318
  return () => window.removeEventListener("keydown", onKey);
2918
5319
  }, [undo, redo]);
2919
5320
  return /* @__PURE__ */ jsxs("div", { className: "cv-undo", role: "group", "aria-label": "Undo and redo", children: [
2920
- /* @__PURE__ */ jsx("button", { onClick: undo, disabled: !canUndo, title: "Undo (\u2318Z)", "aria-label": "Undo", children: "\u21B6" }),
2921
- /* @__PURE__ */ jsx("button", { onClick: redo, disabled: !canRedo, title: "Redo (\u2318\u21E7Z)", "aria-label": "Redo", children: "\u21B7" })
5321
+ /* @__PURE__ */ jsx("button", { onClick: undo, disabled: !canUndo, title: `${t("undo")} (\u2318Z)`, "aria-label": t("undo"), children: "\u21B6" }),
5322
+ /* @__PURE__ */ jsx("button", { onClick: redo, disabled: !canRedo, title: `${t("redo")} (\u2318\u21E7Z)`, "aria-label": t("redo"), children: "\u21B7" })
2922
5323
  ] });
2923
5324
  }
2924
- function VersionRail({ total, index, onSelect }) {
2925
- return /* @__PURE__ */ jsxs("div", { className: "cv-versions", role: "group", "aria-label": "Version history", children: [
2926
- /* @__PURE__ */ jsx("button", { className: "cv-versions__nav", disabled: index === 0, onClick: () => onSelect(index - 1), "aria-label": "Previous version", children: "\u2039" }),
2927
- /* @__PURE__ */ jsxs("span", { className: "cv-versions__label", children: [
2928
- "v",
2929
- index + 1,
2930
- " / ",
2931
- total
2932
- ] }),
5325
+ function VersionHistory({
5326
+ versions: versions2,
5327
+ index,
5328
+ onSelect
5329
+ }) {
5330
+ const t = useT();
5331
+ const [open, setOpen] = useState(false);
5332
+ const total = versions2.length;
5333
+ const pick = (i) => {
5334
+ setOpen(false);
5335
+ onSelect(i);
5336
+ };
5337
+ return /* @__PURE__ */ jsxs("div", { className: "cv-versions", role: "group", "aria-label": t("versionHistory"), children: [
5338
+ /* @__PURE__ */ jsx("button", { className: "cv-versions__nav", disabled: index === 0, onClick: () => pick(index - 1), "aria-label": t("prevVersion"), children: "\u2039" }),
5339
+ /* @__PURE__ */ jsxs(
5340
+ "button",
5341
+ {
5342
+ className: "cv-versions__label",
5343
+ "aria-expanded": open,
5344
+ "aria-label": "Open version history",
5345
+ onClick: () => setOpen((v) => !v),
5346
+ children: [
5347
+ "v",
5348
+ index + 1,
5349
+ " / ",
5350
+ total
5351
+ ]
5352
+ }
5353
+ ),
2933
5354
  /* @__PURE__ */ jsx(
2934
5355
  "button",
2935
5356
  {
2936
5357
  className: "cv-versions__nav",
2937
5358
  disabled: index === total - 1,
2938
- onClick: () => onSelect(index + 1),
2939
- "aria-label": "Next version",
5359
+ onClick: () => pick(index + 1),
5360
+ "aria-label": t("nextVersion"),
2940
5361
  children: "\u203A"
2941
5362
  }
2942
- )
5363
+ ),
5364
+ open && /* @__PURE__ */ jsx("ul", { className: "cv-versions__list", role: "listbox", "aria-label": "Versions", children: versions2.map((snapshot, i) => ({ snapshot, i })).reverse().map(({ snapshot, i }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
5365
+ "button",
5366
+ {
5367
+ role: "option",
5368
+ "aria-selected": i === index,
5369
+ className: i === index ? "is-current" : void 0,
5370
+ onClick: () => pick(i),
5371
+ children: [
5372
+ /* @__PURE__ */ jsxs("span", { className: "cv-versions__v", children: [
5373
+ "v",
5374
+ i + 1
5375
+ ] }),
5376
+ /* @__PURE__ */ jsx("span", { className: "cv-versions__desc", children: typeof snapshot.meta?.commitDescription === "string" ? snapshot.meta.commitDescription : "Snapshot" })
5377
+ ]
5378
+ }
5379
+ ) }, i)) })
2943
5380
  ] });
2944
5381
  }
2945
5382
  function StatusBadge({ status }) {
@@ -2972,11 +5409,12 @@ function EmptyState({ onOpenFiles }) {
2972
5409
  ] });
2973
5410
  }
2974
5411
  var TYPE_META = {
2975
- html: { icon: "\u{1F310}", label: "Web page" },
2976
- document: { icon: "\u{1F4C4}", label: "Word document" },
2977
- chart: { icon: "\u{1F4CA}", label: "Chart" },
2978
- table: { icon: "\u{1F522}", label: "Excel sheet" },
2979
- slides: { icon: "\u{1F4FD}\uFE0F", label: "PowerPoint deck" }
5412
+ html: { icon: "\u{1F310}", label: "kindWeb" },
5413
+ document: { icon: "\u{1F4C4}", label: "kindDocument" },
5414
+ chart: { icon: "\u{1F4CA}", label: "kindChart" },
5415
+ table: { icon: "\u{1F522}", label: "kindTable" },
5416
+ slides: { icon: "\u{1F4FD}\uFE0F", label: "kindSlides" },
5417
+ pdf: { icon: "\u{1F4D5}", label: "kindPdf" }
2980
5418
  };
2981
5419
  var KIND_META = {
2982
5420
  web: TYPE_META.html,
@@ -2987,14 +5425,16 @@ var KIND_META = {
2987
5425
  table: TYPE_META.table,
2988
5426
  sheet: TYPE_META.table,
2989
5427
  slide: TYPE_META.slides,
2990
- slides: TYPE_META.slides
5428
+ slides: TYPE_META.slides,
5429
+ pdf: TYPE_META.pdf
2991
5430
  };
2992
5431
  function resolveCardMeta(artifact2) {
2993
5432
  const kind = typeof artifact2.meta?.kind === "string" ? artifact2.meta.kind : void 0;
2994
5433
  const byKind = kind ? KIND_META[kind] : void 0;
2995
- return byKind ?? TYPE_META[artifact2.type] ?? { icon: "\u{1F4CE}", label: artifact2.type };
5434
+ return byKind ?? TYPE_META[artifact2.type] ?? { icon: "\u{1F4CE}" };
2996
5435
  }
2997
5436
  function ArtifactCard({ artifactId }) {
5437
+ const t = useT();
2998
5438
  const artifact2 = useCanvasStore((s) => s.canvas.artifacts[artifactId]);
2999
5439
  const setActive = useCanvasStore((s) => s.setActiveArtifact);
3000
5440
  if (!artifact2) return null;
@@ -3004,12 +5444,12 @@ function ArtifactCard({ artifactId }) {
3004
5444
  /* @__PURE__ */ jsxs("span", { className: "cv-card__meta", children: [
3005
5445
  /* @__PURE__ */ jsx("b", { children: artifact2.title }),
3006
5446
  /* @__PURE__ */ jsxs("span", { children: [
3007
- meta.label,
5447
+ meta.label ? t(meta.label) : artifact2.type,
3008
5448
  " \xB7 ",
3009
- artifact2.status === "streaming" ? "writing\u2026" : "open \u2192"
5449
+ artifact2.status === "streaming" ? t("writing") : t("open")
3010
5450
  ] })
3011
5451
  ] })
3012
5452
  ] });
3013
5453
  }
3014
5454
 
3015
- export { ArtifactCard, Canvas, CanvasRegistryProvider, ChartRenderer, DocumentRenderer, ExportMenu, HtmlRenderer, IMPORTABLE_EXTENSIONS, INSPECTOR_MARK, STYLE_PROPS, SelectionBar, SlidesRenderer, StylePanel, TableRenderer, builtinRenderers, canImport, dataExporters, downloadBlob, importFile, mergeRegistries, mockStream, parseCsv, parseSSE, printToPdf, scenarios, slidesToPrintHtml, slugify, streamChat, toStandaloneHtml, useCanvasImport, useCanvasReplay, useCanvasStream, useRenderer, withInspector };
5455
+ export { ArtifactCard, Canvas, CanvasRegistryProvider, ChartRenderer, DocumentRenderer, ExportMenu, HtmlRenderer, IMPORTABLE_EXTENSIONS, INSPECTOR_MARK, PdfRenderer, STYLE_PROPS, SelectionBar, SlidesRenderer, StylePanel, TableRenderer, builtinRenderers, canImport, dataExporters, downloadBlob, importFile, mergeRegistries, mockStream, parseCsv, parseSSE, printToPdf, scenarios, slidesToPrintHtml, slugify, streamChat, toStandaloneHtml, useCanvasImport, useCanvasReplay, useCanvasSave, useCanvasStream, useRenderer, withInspector };