@phenix1024/editor 0.0.0-stage → 0.1.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.
@@ -0,0 +1,813 @@
1
+ import { Editor as e, Extension as t, Mark as n, Node as r } from "@tiptap/core";
2
+ import i from "@tiptap/starter-kit";
3
+ import a from "@tiptap/extension-underline";
4
+ import o from "@tiptap/extension-link";
5
+ import s from "@tiptap/extension-image";
6
+ import c from "@tiptap/extension-placeholder";
7
+ import { Table as l } from "@tiptap/extension-table";
8
+ import { TableRow as u } from "@tiptap/extension-table-row";
9
+ import { TableHeader as d } from "@tiptap/extension-table-header";
10
+ import { TableCell as f } from "@tiptap/extension-table-cell";
11
+ import p from "@tiptap/extension-heading";
12
+ import { Plugin as m, PluginKey as h } from "@tiptap/pm/state";
13
+ import { Decoration as g, DecorationSet as _ } from "@tiptap/pm/view";
14
+ import v from "jszip";
15
+ // src/editor/extensions.ts
16
+ var y = n.create({
17
+ name: "label",
18
+ inclusive: !1,
19
+ addAttributes() {
20
+ return { label: {
21
+ default: "highlight",
22
+ parseHTML: (e) => e.dataset.label ?? "highlight",
23
+ renderHTML: (e) => ({ "data-label": e.label })
24
+ } };
25
+ },
26
+ parseHTML() {
27
+ return [{ tag: "mark[data-label]" }, { tag: "mark" }];
28
+ },
29
+ renderHTML({ HTMLAttributes: e }) {
30
+ return [
31
+ "mark",
32
+ e,
33
+ 0
34
+ ];
35
+ }
36
+ });
37
+ function b() {
38
+ return {
39
+ fieldId: {
40
+ default: "",
41
+ parseHTML: (e) => e.getAttribute("data-ophx-field") ?? "",
42
+ renderHTML: (e) => ({ "data-ophx-field": e.fieldId ?? "" })
43
+ },
44
+ fieldLabel: {
45
+ default: null,
46
+ parseHTML: (e) => e.getAttribute("data-ophx-label"),
47
+ renderHTML: (e) => e.fieldLabel ? { "data-ophx-label": e.fieldLabel } : {}
48
+ },
49
+ required: {
50
+ default: !1,
51
+ parseHTML: (e) => e.getAttribute("data-ophx-required") === "true",
52
+ renderHTML: (e) => e.required ? { "data-ophx-required": "true" } : {}
53
+ }
54
+ };
55
+ }
56
+ var x = /^[A-Za-z0-9._-]+$/, S = r.create({
57
+ name: "ophxInlineField",
58
+ priority: 1e3,
59
+ group: "inline",
60
+ inline: !0,
61
+ content: "text*",
62
+ defining: !0,
63
+ addAttributes: b,
64
+ parseHTML() {
65
+ return [{
66
+ tag: "span.ophx-field[contenteditable=\"true\"][data-ophx-field]",
67
+ priority: 1e3
68
+ }];
69
+ },
70
+ renderHTML({ HTMLAttributes: e }) {
71
+ return [
72
+ "span",
73
+ {
74
+ ...e,
75
+ class: "ophx-field",
76
+ contenteditable: "true"
77
+ },
78
+ 0
79
+ ];
80
+ }
81
+ }), C = r.create({
82
+ name: "ophxBlockField",
83
+ priority: 1e3,
84
+ group: "block",
85
+ content: "text*",
86
+ defining: !0,
87
+ addAttributes() {
88
+ return {
89
+ ...b(),
90
+ tagName: {
91
+ default: "div",
92
+ parseHTML: (e) => e.tagName.toLowerCase(),
93
+ renderHTML: () => ({})
94
+ }
95
+ };
96
+ },
97
+ parseHTML() {
98
+ return [{
99
+ tag: "p.ophx-field[contenteditable=\"true\"][data-ophx-field]",
100
+ priority: 1e3
101
+ }, {
102
+ tag: "div.ophx-field[contenteditable=\"true\"][data-ophx-field]",
103
+ priority: 1e3
104
+ }];
105
+ },
106
+ renderHTML({ node: e, HTMLAttributes: t }) {
107
+ return [
108
+ e.attrs.tagName === "p" ? "p" : "div",
109
+ {
110
+ ...t,
111
+ class: "ophx-field",
112
+ contenteditable: "true"
113
+ },
114
+ 0
115
+ ];
116
+ }
117
+ }), w = n.create({
118
+ name: "ophxReadonlyInline",
119
+ inclusive: !1,
120
+ addAttributes() {
121
+ return { className: {
122
+ default: null,
123
+ parseHTML: (e) => e.getAttribute("class"),
124
+ renderHTML: (e) => e.className ? { class: e.className } : {}
125
+ } };
126
+ },
127
+ parseHTML() {
128
+ return [{
129
+ tag: "span[contenteditable=\"false\"]",
130
+ priority: 100
131
+ }];
132
+ },
133
+ renderHTML({ HTMLAttributes: e }) {
134
+ return [
135
+ "span",
136
+ {
137
+ ...e,
138
+ contenteditable: "false"
139
+ },
140
+ 0
141
+ ];
142
+ }
143
+ }), T = r.create({
144
+ name: "ophxReadonlyBlock",
145
+ priority: 1e3,
146
+ group: "block",
147
+ content: "block*",
148
+ defining: !0,
149
+ isolating: !0,
150
+ addAttributes() {
151
+ return {
152
+ id: {
153
+ default: null,
154
+ parseHTML: (e) => e.getAttribute("id"),
155
+ renderHTML: (e) => e.id ? { id: e.id } : {}
156
+ },
157
+ className: {
158
+ default: null,
159
+ parseHTML: (e) => e.getAttribute("class"),
160
+ renderHTML: (e) => e.className ? { class: e.className } : {}
161
+ },
162
+ title: {
163
+ default: null,
164
+ parseHTML: (e) => e.getAttribute("title"),
165
+ renderHTML: (e) => e.title ? { title: e.title } : {}
166
+ }
167
+ };
168
+ },
169
+ parseHTML() {
170
+ return [{
171
+ tag: "div[contenteditable=\"false\"]",
172
+ priority: 1e3
173
+ }];
174
+ },
175
+ renderHTML({ HTMLAttributes: e }) {
176
+ return [
177
+ "div",
178
+ {
179
+ ...e,
180
+ contenteditable: "false"
181
+ },
182
+ 0
183
+ ];
184
+ }
185
+ }), E = new h("ophxHeadingCollapse"), D = p.extend({
186
+ addAttributes() {
187
+ return {
188
+ ...this.parent?.(),
189
+ collapsed: {
190
+ default: !1,
191
+ parseHTML: (e) => e.dataset.collapsed === "true",
192
+ renderHTML: (e) => e.collapsed ? { "data-collapsed": "true" } : {}
193
+ }
194
+ };
195
+ },
196
+ addProseMirrorPlugins() {
197
+ return [new m({
198
+ key: E,
199
+ props: { decorations(e) {
200
+ let t = [], n = null;
201
+ return e.doc.forEach((e, r) => {
202
+ if (e.type.name === "heading") {
203
+ let i = Number(e.attrs.level);
204
+ n !== null && i <= n && (n = null), n !== null && t.push(g.node(r, r + e.nodeSize, { class: "ophx-collapsed-content" })), e.attrs.collapsed && (n = i);
205
+ } else n !== null && t.push(g.node(r, r + e.nodeSize, { class: "ophx-collapsed-content" }));
206
+ }), _.create(e.doc, t);
207
+ } }
208
+ })];
209
+ }
210
+ }), O = new h("ophxTextSearch"), k = t.create({
211
+ name: "ophxSearch",
212
+ addStorage() {
213
+ return { query: "" };
214
+ },
215
+ addCommands() {
216
+ return { setSearchQuery: (e) => ({ tr: t, dispatch: n }) => (this.storage.query = e, n?.(t.setMeta(O, e)), !0) };
217
+ },
218
+ addProseMirrorPlugins() {
219
+ let e = this;
220
+ return [new m({
221
+ key: O,
222
+ state: {
223
+ init: (t, n) => A(n.doc, e.storage.query),
224
+ apply: (t, n, r, i) => {
225
+ let a = t.getMeta(O);
226
+ return a !== void 0 && (e.storage.query = a), !t.docChanged && a === void 0 ? n : A(i.doc, e.storage.query);
227
+ }
228
+ },
229
+ props: { decorations(e) {
230
+ return O.getState(e);
231
+ } }
232
+ })];
233
+ }
234
+ });
235
+ function A(e, t) {
236
+ if (!t.trim()) return _.empty;
237
+ let n = [], r = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), i = new RegExp(r, "gi");
238
+ return e.descendants((e, t) => {
239
+ if (e.isText && e.text) {
240
+ i.lastIndex = 0;
241
+ for (let r of e.text.matchAll(i)) r.index !== void 0 && n.push(g.inline(t + r.index, t + r.index + r[0].length, { class: "ophx-search-match" }));
242
+ }
243
+ }), _.create(e, n);
244
+ }
245
+ var j = t.create({
246
+ name: "ophxWysiwymRules",
247
+ addProseMirrorPlugins() {
248
+ return [new m({ props: {
249
+ handleTextInput(e, t, n, r) {
250
+ return r === " " && /\s/.test(e.state.doc.textBetween(Math.max(0, t - 1), t));
251
+ },
252
+ handleKeyDown(e, t) {
253
+ if (t.key !== "Enter" || t.shiftKey) return !1;
254
+ let { $from: n } = e.state.selection;
255
+ if (!n.parent.isTextblock || n.parent.content.size !== 0 || n.depth === 0) return !1;
256
+ let r = n.index(n.depth - 1), i = n.node(n.depth - 1);
257
+ return [i.maybeChild(r - 1), i.maybeChild(r + 1)].some((e) => e?.isTextblock && e.content.size === 0);
258
+ }
259
+ } })];
260
+ }
261
+ }), M = t.create({
262
+ name: "ophxIndentation",
263
+ addGlobalAttributes() {
264
+ return [{
265
+ types: ["paragraph", "heading"],
266
+ attributes: { indent: {
267
+ default: 0,
268
+ parseHTML: (e) => Number(e.dataset.indent ?? 0),
269
+ renderHTML: (e) => e.indent ? { "data-indent": String(e.indent) } : {}
270
+ } }
271
+ }];
272
+ }
273
+ }), N = t.create({
274
+ name: "ophxEditability",
275
+ addGlobalAttributes() {
276
+ return [{
277
+ types: [
278
+ "paragraph",
279
+ "heading",
280
+ "blockquote",
281
+ "codeBlock",
282
+ "bulletList",
283
+ "orderedList",
284
+ "listItem",
285
+ "horizontalRule",
286
+ "image",
287
+ "table",
288
+ "tableRow",
289
+ "tableHeader",
290
+ "tableCell",
291
+ "bold",
292
+ "italic",
293
+ "underline",
294
+ "strike",
295
+ "link",
296
+ "label"
297
+ ],
298
+ attributes: { contenteditable: {
299
+ default: null,
300
+ parseHTML: (e) => {
301
+ let t = e.getAttribute("contenteditable");
302
+ return t === "true" || t === "false" ? t : null;
303
+ },
304
+ renderHTML: (e) => e.contenteditable ? { contenteditable: e.contenteditable } : {}
305
+ } }
306
+ }];
307
+ }
308
+ });
309
+ function P(e) {
310
+ return x.test(e);
311
+ }
312
+ // src/editor/ophx-editor.ts
313
+ var F = class {
314
+ editor;
315
+ constructor(t) {
316
+ this.editor = new e({
317
+ element: t.element,
318
+ content: t.content ?? "<p></p>",
319
+ editable: t.editable ?? !0,
320
+ extensions: [
321
+ i.configure({
322
+ heading: !1,
323
+ underline: !1,
324
+ link: !1
325
+ }),
326
+ D.configure({ levels: [
327
+ 1,
328
+ 2,
329
+ 3,
330
+ 4,
331
+ 5,
332
+ 6
333
+ ] }),
334
+ a,
335
+ o.configure({
336
+ openOnClick: !1,
337
+ autolink: !0,
338
+ linkOnPaste: !0
339
+ }),
340
+ s.configure({
341
+ allowBase64: !0,
342
+ inline: !1
343
+ }),
344
+ l.configure({ resizable: !0 }),
345
+ u,
346
+ d,
347
+ f,
348
+ S,
349
+ C,
350
+ T,
351
+ w,
352
+ y,
353
+ M,
354
+ N,
355
+ k,
356
+ j,
357
+ c.configure({ placeholder: t.placeholder ?? "Начните писать…" }),
358
+ ...t.extensions ?? []
359
+ ],
360
+ editorProps: { attributes: {
361
+ class: "ophx-editor-surface",
362
+ spellcheck: "true",
363
+ lang: "ru",
364
+ "aria-label": "Редактор документа"
365
+ } },
366
+ onUpdate: t.onUpdate
367
+ });
368
+ }
369
+ getHTML() {
370
+ return this.editor.getHTML();
371
+ }
372
+ getJSON() {
373
+ return this.editor.getJSON();
374
+ }
375
+ setContent(e) {
376
+ this.editor.commands.setContent(e, { emitUpdate: !1 });
377
+ }
378
+ setSearchQuery(e) {
379
+ this.editor.commands.setSearchQuery(e);
380
+ }
381
+ getFields() {
382
+ let e = Array.from(this.editor.view.dom.querySelectorAll("[data-ophx-field]")).filter((e) => {
383
+ let t = e.getAttribute("data-ophx-field") ?? "";
384
+ return e.classList.contains("ophx-field") && e.getAttribute("contenteditable") === "true" && P(t);
385
+ }), t = /* @__PURE__ */ new Map();
386
+ for (let n of e) {
387
+ let e = n.getAttribute("data-ophx-field");
388
+ t.set(e, (t.get(e) ?? 0) + 1);
389
+ }
390
+ return e.filter((e) => t.get(e.getAttribute("data-ophx-field")) === 1).map((e) => ({
391
+ id: e.getAttribute("data-ophx-field"),
392
+ label: e.getAttribute("data-ophx-label"),
393
+ required: e.getAttribute("data-ophx-required") === "true",
394
+ value: e.textContent ?? ""
395
+ }));
396
+ }
397
+ getMissingRequiredFields() {
398
+ return this.getFields().filter((e) => e.required && !e.value.trim());
399
+ }
400
+ destroy() {
401
+ this.editor.destroy();
402
+ }
403
+ }, I = [
404
+ {
405
+ action: "bold",
406
+ label: "B",
407
+ title: "Жирный (⌘/Ctrl+B)"
408
+ },
409
+ {
410
+ action: "italic",
411
+ label: "I",
412
+ title: "Курсив (⌘/Ctrl+I)"
413
+ },
414
+ {
415
+ action: "underline",
416
+ label: "U",
417
+ title: "Подчёркнутый (⌘/Ctrl+U)"
418
+ },
419
+ {
420
+ action: "strike",
421
+ label: "S̶",
422
+ title: "Зачёркнутый"
423
+ },
424
+ {
425
+ action: "bullet",
426
+ label: "• Список",
427
+ title: "Маркированный список"
428
+ },
429
+ {
430
+ action: "ordered",
431
+ label: "1. Список",
432
+ title: "Нумерованный список"
433
+ },
434
+ {
435
+ action: "outdent",
436
+ label: "← Отступ",
437
+ title: "Уменьшить отступ"
438
+ },
439
+ {
440
+ action: "indent",
441
+ label: "Отступ →",
442
+ title: "Увеличить отступ"
443
+ },
444
+ {
445
+ action: "link",
446
+ label: "Ссылка",
447
+ title: "Добавить ссылку"
448
+ },
449
+ {
450
+ action: "image",
451
+ label: "Изображение",
452
+ title: "Вставить изображение"
453
+ },
454
+ {
455
+ action: "table",
456
+ label: "Таблица",
457
+ title: "Вставить таблицу"
458
+ },
459
+ {
460
+ action: "collapse",
461
+ label: "Свернуть",
462
+ title: "Свернуть/развернуть раздел"
463
+ },
464
+ {
465
+ action: "label",
466
+ label: "Метка",
467
+ title: "Выделить текст меткой"
468
+ },
469
+ {
470
+ action: "toc",
471
+ label: "Содержание",
472
+ title: "Содержание документа"
473
+ },
474
+ {
475
+ action: "undo",
476
+ label: "↶",
477
+ title: "Отменить (⌘/Ctrl+Z)"
478
+ },
479
+ {
480
+ action: "redo",
481
+ label: "↷",
482
+ title: "Повторить (⌘/Ctrl+Shift+Z)"
483
+ }
484
+ ];
485
+ function L(e, t, n = {}) {
486
+ e.classList.add("ophx-toolbar"), e.replaceChildren();
487
+ let r = document.createElement("select");
488
+ r.className = "ophx-toolbar__style", r.setAttribute("aria-label", "Стиль текста"), r.innerHTML = "<option value=\"p\">Обычный текст</option><option value=\"1\">Заголовок 1</option><option value=\"2\">Заголовок 2</option><option value=\"3\">Заголовок 3</option>", r.addEventListener("change", () => {
489
+ let e = Number(r.value), n = t.chain().focus();
490
+ r.value === "p" ? n.setParagraph().run() : n.setHeading({ level: e }).run();
491
+ }), e.append(r);
492
+ let i = /* @__PURE__ */ new Map();
493
+ for (let n of I) {
494
+ let r = document.createElement("button");
495
+ r.type = "button", r.className = "ophx-toolbar__button", r.dataset.action = n.action, r.textContent = n.label, r.title = n.title, r.setAttribute("aria-label", n.title), r.addEventListener("mousedown", (e) => e.preventDefault()), r.addEventListener("click", () => R(n.action, t, o, s)), i.set(n.action, r), e.append(r);
496
+ }
497
+ let a = document.createElement("input");
498
+ a.type = "search", a.className = "ophx-toolbar__search", a.placeholder = "Поиск…", a.setAttribute("aria-label", "Поиск по тексту"), a.addEventListener("input", () => t.commands.setSearchQuery(a.value)), a.addEventListener("keydown", (e) => {
499
+ e.key === "Escape" && (a.value = "", t.commands.setSearchQuery(""), t.commands.focus());
500
+ }), e.append(a);
501
+ let o = document.createElement("input");
502
+ o.type = "file", o.accept = "image/*", o.hidden = !0, o.addEventListener("change", () => {
503
+ let e = o.files?.[0];
504
+ if (e) {
505
+ if (n.onInsertImage) n.onInsertImage(e);
506
+ else {
507
+ let n = new FileReader();
508
+ n.addEventListener("load", () => {
509
+ typeof n.result == "string" && t.chain().focus().setImage({
510
+ src: n.result,
511
+ alt: e.name
512
+ }).run();
513
+ }), n.readAsDataURL(e);
514
+ }
515
+ }
516
+ o.value = "";
517
+ }), e.append(o);
518
+ let s = document.createElement("nav");
519
+ s.className = "ophx-toc", s.setAttribute("aria-label", "Содержание документа"), s.hidden = !0, e.append(s);
520
+ let c = () => {
521
+ i.get("undo").disabled = !t.can().undo(), i.get("redo").disabled = !t.can().redo(), i.get("bold").classList.toggle("is-active", t.isActive("bold")), i.get("italic").classList.toggle("is-active", t.isActive("italic")), i.get("underline").classList.toggle("is-active", t.isActive("underline")), i.get("strike").classList.toggle("is-active", t.isActive("strike"));
522
+ let e = t.getAttributes("heading").level;
523
+ r.value = e ? String(e) : (t.isActive("paragraph"), "p"), s.replaceChildren(), t.state.doc.descendants((e, n) => {
524
+ if (e.type.name !== "heading") return;
525
+ let r = document.createElement("button");
526
+ r.type = "button", r.className = "ophx-toc__item", r.style.paddingInlineStart = `${Math.max(0, Number(e.attrs.level) - 1) * 12}px`, r.textContent = e.textContent || "Без названия", r.addEventListener("mousedown", (e) => e.preventDefault()), r.addEventListener("click", () => {
527
+ t.commands.setTextSelection(n + 1), t.commands.focus(), t.commands.scrollIntoView();
528
+ }), s.append(r);
529
+ });
530
+ };
531
+ return t.on("transaction", c), t.on("selectionUpdate", c), c(), () => {
532
+ t.off("transaction", c), t.off("selectionUpdate", c), e.replaceChildren(), e.classList.remove("ophx-toolbar");
533
+ };
534
+ }
535
+ function R(e, t, n, r) {
536
+ let i = t.chain().focus();
537
+ switch (e) {
538
+ case "bold":
539
+ i.toggleBold().run();
540
+ break;
541
+ case "italic":
542
+ i.toggleItalic().run();
543
+ break;
544
+ case "underline":
545
+ i.toggleUnderline().run();
546
+ break;
547
+ case "strike":
548
+ i.toggleStrike().run();
549
+ break;
550
+ case "bullet":
551
+ i.toggleBulletList().run();
552
+ break;
553
+ case "ordered":
554
+ i.toggleOrderedList().run();
555
+ break;
556
+ case "indent":
557
+ z(t, 1);
558
+ break;
559
+ case "outdent":
560
+ z(t, -1);
561
+ break;
562
+ case "link": {
563
+ let e = t.getAttributes("link").href, n = window.prompt("Адрес ссылки", e ?? "https://");
564
+ if (n === null) return;
565
+ n.trim() ? i.extendMarkRange("link").setLink({ href: n.trim() }).run() : i.extendMarkRange("link").unsetLink().run();
566
+ break;
567
+ }
568
+ case "image":
569
+ n.click();
570
+ break;
571
+ case "table":
572
+ i.insertTable({
573
+ rows: 3,
574
+ cols: 3,
575
+ withHeaderRow: !0
576
+ }).run();
577
+ break;
578
+ case "collapse": {
579
+ if (!t.isActive("heading")) return;
580
+ let e = !!t.getAttributes("heading").collapsed;
581
+ i.updateAttributes("heading", { collapsed: !e }).run();
582
+ break;
583
+ }
584
+ case "label": {
585
+ let e = window.prompt("Название метки", "важно");
586
+ e?.trim() && i.setMark("label", { label: e.trim() }).run();
587
+ break;
588
+ }
589
+ case "toc":
590
+ r.hidden = !r.hidden;
591
+ break;
592
+ case "undo":
593
+ i.undo().run();
594
+ break;
595
+ case "redo": i.redo().run();
596
+ }
597
+ }
598
+ function z(e, t) {
599
+ let { $from: n } = e.state.selection;
600
+ if (n.parent.type.name === "listItem") {
601
+ let n = e.chain().focus();
602
+ t > 0 ? n.sinkListItem("listItem").run() : n.liftListItem("listItem").run();
603
+ return;
604
+ }
605
+ let r = n.parent.type.name;
606
+ if (r !== "paragraph" && r !== "heading") return;
607
+ let i = Number(e.getAttributes(r).indent ?? 0);
608
+ e.chain().focus().updateAttributes(r, { indent: Math.max(0, Math.min(8, i + t)) }).run();
609
+ }
610
+ // src/editor/ophx-archive.ts
611
+ var B = {
612
+ avif: "image/avif",
613
+ bmp: "image/bmp",
614
+ gif: "image/gif",
615
+ ico: "image/x-icon",
616
+ jpeg: "image/jpeg",
617
+ jpg: "image/jpeg",
618
+ png: "image/png",
619
+ svg: "image/svg+xml",
620
+ tif: "image/tiff",
621
+ tiff: "image/tiff",
622
+ webp: "image/webp"
623
+ };
624
+ async function V(e, t = {}) {
625
+ let n = new v(), r = new DOMParser().parseFromString(e, "text/html");
626
+ G(r);
627
+ let i = K(t.filename ?? t.title ?? "Документ"), a = t.title ?? r.title ?? i, o = t.assets ?? {}, s = new Set(Object.keys(o).map(J)), c = 0;
628
+ for (let e of Array.from(r.querySelectorAll("img[src]"))) {
629
+ let t = e.getAttribute("src")?.trim();
630
+ if (!t || /^https?:\/\//i.test(t) || /^data:(?!image\/)/i.test(t)) continue;
631
+ if (/^data:image\//i.test(t) || /^blob:/i.test(t)) {
632
+ try {
633
+ let r = await fetch(t);
634
+ if (!r.ok) throw Error(`Не удалось прочитать изображение: ${t}`);
635
+ let i = await r.blob();
636
+ if (!i.type.startsWith("image/")) throw Error("Вложение с data/blob URL не является изображением");
637
+ let a = `image-${++c}.${Z(i.type, t)}`;
638
+ for (; s.has(a);) a = `image-${++c}.${Z(i.type, t)}`;
639
+ n.file(a, i), s.add(a), e.setAttribute("src", encodeURIComponent(a));
640
+ } catch (e) {
641
+ throw Error("Не удалось добавить изображение в архив ophx", { cause: e });
642
+ }
643
+ continue;
644
+ }
645
+ let r = J(t);
646
+ if (!q(r)) throw Error(`Путь изображения должен быть простым именем файла: ${t}`);
647
+ let i = o[r];
648
+ if (!i && !s.has(r)) throw Error(`Для локального изображения «${r}» не передан файл media`);
649
+ i && n.file(r, i), e.setAttribute("src", encodeURIComponent(r));
650
+ }
651
+ let l = W(t.styles ?? {});
652
+ for (let [e, t] of Object.entries(l)) n.file(e, t);
653
+ let u = Object.keys(l).map((e) => `<link rel="stylesheet" href="${$(encodeURIComponent(e))}">`).join(""), d = r.documentElement.lang || "ru", f = t.bodyContentEditable ? ` contenteditable="${t.bodyContentEditable}"` : "", p = `<!doctype html>\n<html lang="${$(d)}"><head><meta charset="utf-8"><title>${$(a)}</title>${u}</head><body${f}>${r.body.innerHTML}</body></html>`;
654
+ return n.file(`${i}.html`, p), n.generateAsync({
655
+ type: "blob",
656
+ mimeType: "application/zip",
657
+ compression: "DEFLATE",
658
+ compressionOptions: { level: 6 }
659
+ });
660
+ }
661
+ async function H(e) {
662
+ await X(e);
663
+ let t;
664
+ try {
665
+ t = await v.loadAsync(e);
666
+ } catch (e) {
667
+ throw Error("Не удалось прочитать ZIP-архив .ophx (шифрование и неподдерживаемые ZIP-режимы не поддерживаются)", { cause: e });
668
+ }
669
+ if (Object.values(t.files).some((e) => e.dir || /[\\/]/.test(e.name))) throw Error("В текущей версии .ophx все файлы должны находиться плоским списком в корне архива");
670
+ let n = Object.values(t.files).filter((e) => !e.dir), r = n.filter((e) => e.name.toLowerCase().endsWith(".html"));
671
+ if (r.length !== 1) throw r.length === 0 && n.some((e) => e.name.toLowerCase().endsWith(".xml")) ? Error("Обнаружен устаревший XML-вариант ophx; поддерживается только версия v2 с HTML5 внутри ZIP") : Error(`В архиве должен быть ровно один HTML-файл в корне; найдено: ${r.length}`);
672
+ let i = r[0], a = i.name.slice(0, -5), o = "name" in e && typeof e.name == "string" ? e.name : "";
673
+ if (/\.ophx$/i.test(o) && o.replace(/\.ophx$/i, "") !== a) throw Error(`Имя HTML-файла «${i.name}» должно совпадать с именем архива «${o}»`);
674
+ let s = await i.async("string");
675
+ if (!/^\s*<!doctype\s+html\b/i.test(s)) throw Error("Основной файл .ophx должен начинаться с <!doctype html>");
676
+ let c = new DOMParser().parseFromString(s, "text/html");
677
+ if (c.querySelector("head meta[charset]")?.getAttribute("charset")?.toLowerCase() !== "utf-8") throw Error("В <head> документа требуется <meta charset=\"utf-8\">");
678
+ let l = {};
679
+ for (let e of Array.from(c.querySelectorAll("head link[rel~=\"stylesheet\"][href]"))) {
680
+ let n = e.getAttribute("href")?.trim();
681
+ if (!n) continue;
682
+ if (Y(n)) throw Error(`CSS должен быть встроенным плоским файлом архива: ${n}`);
683
+ let r = J(n);
684
+ if (!q(r) || !r.toLowerCase().endsWith(".css")) throw Error(`Ссылка на стиль должна указывать на плоский .css-файл: ${n}`);
685
+ let i = t.file(r);
686
+ if (!i) throw Error(`Файл стиля «${r}» отсутствует в архиве`);
687
+ l[r] = await i.async("string");
688
+ }
689
+ for (let e of Array.from(c.querySelectorAll("img[src]"))) {
690
+ let n = e.getAttribute("src")?.trim();
691
+ if (!n || Y(n)) continue;
692
+ let r = J(n);
693
+ if (!q(r)) throw Error(`Ссылка на изображение должна быть простым именем файла: ${n}`);
694
+ let i = t.file(r);
695
+ if (!i) throw Error(`Файл изображения «${r}» отсутствует в архиве`);
696
+ let a = await i.async("base64"), o = r.split(".").pop()?.toLowerCase() ?? "", s = B[o] ?? (o ? `image/${o}` : "application/octet-stream");
697
+ e.setAttribute("src", `data:${s};base64,${a}`);
698
+ }
699
+ let u = c.body.getAttribute("contenteditable");
700
+ if (u === "false") for (let e of Array.from(c.body.children)) e.hasAttribute("contenteditable") || e.setAttribute("contenteditable", "false");
701
+ Q(c);
702
+ let d = Object.entries(l);
703
+ return {
704
+ html: c.body.innerHTML,
705
+ title: c.title || a,
706
+ filename: a,
707
+ styles: l,
708
+ css: d[0]?.[1] ?? "",
709
+ bodyContentEditable: u === "true" || u === "false" ? u : void 0
710
+ };
711
+ }
712
+ function U(e, t) {
713
+ let n = URL.createObjectURL(e), r = window.document.createElement("a");
714
+ r.href = n, r.download = t, r.click(), window.setTimeout(() => URL.revokeObjectURL(n), 0);
715
+ }
716
+ function W(e) {
717
+ let t = {};
718
+ for (let [n, r] of Object.entries(e)) {
719
+ let e = J(n);
720
+ if (!q(e) || !e.toLowerCase().endsWith(".css")) throw Error(`Имя файла стиля должно быть плоским и оканчиваться на .css: ${n}`);
721
+ if (Object.hasOwn(t, e)) throw Error(`Повторяется имя файла стиля: ${e}`);
722
+ t[e] = r;
723
+ }
724
+ return t;
725
+ }
726
+ function G(e) {
727
+ let t = Array.from(e.querySelectorAll("[data-ophx-field], .ophx-field")), n = /* @__PURE__ */ new Set();
728
+ for (let e of t) {
729
+ let t = e.getAttribute("data-ophx-field") ?? "", r = e.tagName.toLowerCase();
730
+ if (!e.classList.contains("ophx-field") || e.getAttribute("contenteditable") !== "true") throw Error("Каждое поле должно иметь класс ophx-field и contenteditable=\"true\"");
731
+ if (![
732
+ "span",
733
+ "div",
734
+ "p"
735
+ ].includes(r) || !/^[A-Za-z0-9._-]+$/.test(t)) throw Error(`Некорректная обёртка или идентификатор поля: ${t || "(пусто)"}`);
736
+ if (n.has(t)) throw Error(`Идентификатор поля должен быть уникальным: ${t}`);
737
+ if (e.parentElement?.closest("[data-ophx-field]")) throw Error(`Поля не могут быть вложенными: ${t}`);
738
+ n.add(t);
739
+ }
740
+ }
741
+ function K(e) {
742
+ let t = e.trim().replace(/\.(?:ophx|html)$/i, "");
743
+ if (!t) return "Документ";
744
+ if (!q(t) || /[\u0000-\u001f]/.test(t)) throw Error("Имя .ophx должно быть простым именем файла без каталогов");
745
+ return t;
746
+ }
747
+ function q(e) {
748
+ return !!e && e !== "." && e !== ".." && !/[\\/]/.test(e);
749
+ }
750
+ function J(e) {
751
+ try {
752
+ return decodeURIComponent(e);
753
+ } catch {
754
+ throw Error(`Некорректное percent-экранирование в имени ресурса: ${e}`);
755
+ }
756
+ }
757
+ function Y(e) {
758
+ return /^(?:data:|https?:\/\/)/i.test(e);
759
+ }
760
+ async function X(e) {
761
+ if (e.size < 2) throw Error("Файл не является ZIP-архивом .ophx (возможен устаревший формат)");
762
+ let t = new Uint8Array(await e.slice(0, 4).arrayBuffer());
763
+ if (t[0] !== 80 || t[1] !== 75) throw Error("Неподдерживаемый устаревший ophx: ожидается ZIP-архив версии v2");
764
+ }
765
+ function Z(e, t) {
766
+ let n = Object.entries(B).find(([, t]) => t === e)?.[0];
767
+ if (n) return n;
768
+ let r = t.match(/\.([a-z0-9]+)(?:[?#]|$)/i)?.[1]?.toLowerCase();
769
+ return r && /^[a-z0-9]{1,6}$/.test(r) ? r : "bin";
770
+ }
771
+ function Q(e) {
772
+ e.querySelectorAll("script, iframe, object, embed, style, link").forEach((e) => e.remove()), e.querySelectorAll("*").forEach((e) => {
773
+ for (let t of Array.from(e.attributes)) /^on/i.test(t.name) && e.removeAttribute(t.name), /^(href|src|xlink:href)$/i.test(t.name) && /^\s*(javascript|vbscript):/i.test(t.value) && e.removeAttribute(t.name), /^(src|xlink:href)$/i.test(t.name) && /^\s*data:(?!image\/)/i.test(t.value) && e.removeAttribute(t.name);
774
+ });
775
+ }
776
+ function $(e) {
777
+ return e.replace(/[&<>"']/g, (e) => ({
778
+ "&": "&amp;",
779
+ "<": "&lt;",
780
+ ">": "&gt;",
781
+ "\"": "&quot;",
782
+ "'": "&#39;"
783
+ })[e]);
784
+ }
785
+ // src/editor/template-fields.ts
786
+ function ee(e, t = "f") {
787
+ if (!/^[A-Za-z0-9._-]+$/.test(t)) throw Error("Префикс идентификаторов полей содержит недопустимые символы");
788
+ let n = new DOMParser().parseFromString(e, "text/html"), r = new Set(Array.from(n.querySelectorAll("[data-ophx-field]")).map((e) => e.getAttribute("data-ophx-field") ?? "").filter(Boolean)), i = n.createTreeWalker(n.body, 4), a = [];
789
+ for (; i.nextNode();) {
790
+ let e = i.currentNode, t = e.parentElement;
791
+ t && !t.closest("code, pre, script, style, [contenteditable=\"false\"], [data-ophx-field]") && /_{2,}/.test(e.data) && a.push(e);
792
+ }
793
+ let o = [], s = 1;
794
+ for (let e of a) {
795
+ let i = n.createDocumentFragment(), a = 0;
796
+ for (let c of e.data.matchAll(/_{2,}/g)) {
797
+ if (c.index === void 0) continue;
798
+ c.index > a && i.append(n.createTextNode(e.data.slice(a, c.index)));
799
+ let l = `${t}${s++}`;
800
+ for (; r.has(l);) l = `${t}${s++}`;
801
+ r.add(l), o.push(l);
802
+ let u = n.createElement("span");
803
+ u.className = "ophx-field", u.setAttribute("contenteditable", "true"), u.setAttribute("data-ophx-field", l), i.append(u), a = c.index + c[0].length;
804
+ }
805
+ a < e.data.length && i.append(n.createTextNode(e.data.slice(a))), e.replaceWith(i);
806
+ }
807
+ return {
808
+ html: n.body.innerHTML,
809
+ count: o.length,
810
+ ids: o
811
+ };
812
+ }
813
+ export { D as CollapsibleHeading, M as Indentation, y as Label, C as OphxBlockField, N as OphxEditability, F as OphxEditor, S as OphxInlineField, T as OphxReadonlyBlock, w as OphxReadonlyInline, k as TextSearch, j as WysiwymRules, ee as convertUnderscoresToFields, U as downloadBlob, V as exportOphx, H as importOphx, P as isValidOphxFieldId, L as mountToolbar };