@dou-ui/editor 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/catalog.js +18 -0
- package/dist/catalog.js.map +1 -0
- package/dist/code/code-editor-block.js +6 -0
- package/dist/code/code-editor-block.js.map +1 -0
- package/dist/code/code-editor-block.vue_vue_type_script_setup_true_lang.js +116 -0
- package/dist/code/code-editor-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/code/index.js +69 -0
- package/dist/code/index.js.map +1 -0
- package/dist/code/languages.js +51 -0
- package/dist/code/languages.js.map +1 -0
- package/dist/code/view.js +255 -0
- package/dist/code/view.js.map +1 -0
- package/dist/collaboration/binding.js +381 -0
- package/dist/collaboration/binding.js.map +1 -0
- package/dist/collaboration/cursors.js +150 -0
- package/dist/collaboration/cursors.js.map +1 -0
- package/dist/collaboration/index.js +108 -0
- package/dist/collaboration/index.js.map +1 -0
- package/dist/collaboration/order.js +88 -0
- package/dist/collaboration/order.js.map +1 -0
- package/dist/collaboration/table.js +234 -0
- package/dist/collaboration/table.js.map +1 -0
- package/dist/collaboration/text.js +91 -0
- package/dist/collaboration/text.js.map +1 -0
- package/dist/components/block-handle.js +6 -0
- package/dist/components/block-handle.js.map +1 -0
- package/dist/components/block-handle.vue_vue_type_script_setup_true_lang.js +474 -0
- package/dist/components/block-handle.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/bubble-toolbar.js +6 -0
- package/dist/components/bubble-toolbar.js.map +1 -0
- package/dist/components/bubble-toolbar.vue_vue_type_script_setup_true_lang.js +381 -0
- package/dist/components/bubble-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/comment-button.js +6 -0
- package/dist/components/comment-button.js.map +1 -0
- package/dist/components/comment-button.vue_vue_type_script_setup_true_lang.js +83 -0
- package/dist/components/comment-button.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/comment-card.js +6 -0
- package/dist/components/comment-card.js.map +1 -0
- package/dist/components/comment-card.vue_vue_type_script_setup_true_lang.js +104 -0
- package/dist/components/comment-card.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/menu-escape.js +25 -0
- package/dist/components/menu-escape.js.map +1 -0
- package/dist/components/search-bar.js +6 -0
- package/dist/components/search-bar.js.map +1 -0
- package/dist/components/search-bar.vue_vue_type_script_setup_true_lang.js +192 -0
- package/dist/components/search-bar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/slash-menu.js +6 -0
- package/dist/components/slash-menu.js.map +1 -0
- package/dist/components/slash-menu.vue_vue_type_script_setup_true_lang.js +164 -0
- package/dist/components/slash-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/core/align.js +94 -0
- package/dist/core/align.js.map +1 -0
- package/dist/core/block-selection.js +155 -0
- package/dist/core/block-selection.js.map +1 -0
- package/dist/core/blocks.js +132 -0
- package/dist/core/blocks.js.map +1 -0
- package/dist/core/changes.js +85 -0
- package/dist/core/changes.js.map +1 -0
- package/dist/core/colors.js +16 -0
- package/dist/core/colors.js.map +1 -0
- package/dist/core/commands.js +379 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/comments.js +186 -0
- package/dist/core/comments.js.map +1 -0
- package/dist/core/compare.js +106 -0
- package/dist/core/compare.js.map +1 -0
- package/dist/core/convert.js +28 -0
- package/dist/core/convert.js.map +1 -0
- package/dist/core/diff.js +141 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/editor.js +138 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/extension.js +99 -0
- package/dist/core/extension.js.map +1 -0
- package/dist/core/file-drop.js +56 -0
- package/dist/core/file-drop.js.map +1 -0
- package/dist/core/highlight.js +120 -0
- package/dist/core/highlight.js.map +1 -0
- package/dist/core/input-rules.js +61 -0
- package/dist/core/input-rules.js.map +1 -0
- package/dist/core/keymap.js +38 -0
- package/dist/core/keymap.js.map +1 -0
- package/dist/core/model.js +196 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/output.js +263 -0
- package/dist/core/output.js.map +1 -0
- package/dist/core/plugins.js +102 -0
- package/dist/core/plugins.js.map +1 -0
- package/dist/core/prepare.js +102 -0
- package/dist/core/prepare.js.map +1 -0
- package/dist/core/schema.js +158 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.js +197 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/selection.js +16 -0
- package/dist/core/selection.js.map +1 -0
- package/dist/core/slash.js +83 -0
- package/dist/core/slash.js.map +1 -0
- package/dist/core/transaction.js +311 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/define-block.js +177 -0
- package/dist/define-block.js.map +1 -0
- package/dist/document.js +157 -0
- package/dist/document.js.map +1 -0
- package/dist/dom.js +48 -0
- package/dist/dom.js.map +1 -0
- package/dist/editor.js +33 -0
- package/dist/editor.js.map +1 -0
- package/dist/editor.vue_vue_type_script_setup_true_lang.js +564 -0
- package/dist/editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/editor2.js +6 -0
- package/dist/editor2.js.map +1 -0
- package/dist/extensions/align/align-menu.js +6 -0
- package/dist/extensions/align/align-menu.js.map +1 -0
- package/dist/extensions/align/align-menu.vue_vue_type_script_setup_true_lang.js +82 -0
- package/dist/extensions/align/align-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/align/align-sub.js +6 -0
- package/dist/extensions/align/align-sub.js.map +1 -0
- package/dist/extensions/align/align-sub.vue_vue_type_script_setup_true_lang.js +57 -0
- package/dist/extensions/align/align-sub.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/align/commands.js +40 -0
- package/dist/extensions/align/commands.js.map +1 -0
- package/dist/extensions/align/index.js +27 -0
- package/dist/extensions/align/index.js.map +1 -0
- package/dist/extensions/attachment/attachment-block.js +6 -0
- package/dist/extensions/attachment/attachment-block.js.map +1 -0
- package/dist/extensions/attachment/attachment-block.vue_vue_type_script_setup_true_lang.js +119 -0
- package/dist/extensions/attachment/attachment-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/attachment/files.js +66 -0
- package/dist/extensions/attachment/files.js.map +1 -0
- package/dist/extensions/attachment/index.js +132 -0
- package/dist/extensions/attachment/index.js.map +1 -0
- package/dist/extensions/blocks.js +170 -0
- package/dist/extensions/blocks.js.map +1 -0
- package/dist/extensions/callout/callout-block.js +6 -0
- package/dist/extensions/callout/callout-block.js.map +1 -0
- package/dist/extensions/callout/callout-block.vue_vue_type_script_setup_true_lang.js +71 -0
- package/dist/extensions/callout/callout-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/callout/html.js +61 -0
- package/dist/extensions/callout/html.js.map +1 -0
- package/dist/extensions/callout/index.js +52 -0
- package/dist/extensions/callout/index.js.map +1 -0
- package/dist/extensions/callout/tones.js +12 -0
- package/dist/extensions/callout/tones.js.map +1 -0
- package/dist/extensions/code-block/code-block-view.js +6 -0
- package/dist/extensions/code-block/code-block-view.js.map +1 -0
- package/dist/extensions/code-block/code-block-view.vue_vue_type_script_setup_true_lang.js +87 -0
- package/dist/extensions/code-block/code-block-view.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/code-block/index.js +98 -0
- package/dist/extensions/code-block/index.js.map +1 -0
- package/dist/extensions/code-block/languages.js +116 -0
- package/dist/extensions/code-block/languages.js.map +1 -0
- package/dist/extensions/colors/color-menu.js +6 -0
- package/dist/extensions/colors/color-menu.js.map +1 -0
- package/dist/extensions/colors/color-menu.vue_vue_type_script_setup_true_lang.js +113 -0
- package/dist/extensions/colors/color-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/colors/index.js +49 -0
- package/dist/extensions/colors/index.js.map +1 -0
- package/dist/extensions/colors/palette.js +26 -0
- package/dist/extensions/colors/palette.js.map +1 -0
- package/dist/extensions/editor-kit.js +35 -0
- package/dist/extensions/editor-kit.js.map +1 -0
- package/dist/extensions/embed/embed-block.js +6 -0
- package/dist/extensions/embed/embed-block.js.map +1 -0
- package/dist/extensions/embed/embed-block.vue_vue_type_script_setup_true_lang.js +210 -0
- package/dist/extensions/embed/embed-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/embed/index.js +118 -0
- package/dist/extensions/embed/index.js.map +1 -0
- package/dist/extensions/embed/targets.js +79 -0
- package/dist/extensions/embed/targets.js.map +1 -0
- package/dist/extensions/formatting.js +153 -0
- package/dist/extensions/formatting.js.map +1 -0
- package/dist/extensions/image/image-block.js +6 -0
- package/dist/extensions/image/image-block.js.map +1 -0
- package/dist/extensions/image/image-block.vue_vue_type_script_setup_true_lang.js +206 -0
- package/dist/extensions/image/image-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/image/index.js +125 -0
- package/dist/extensions/image/index.js.map +1 -0
- package/dist/extensions/image/uploads.js +96 -0
- package/dist/extensions/image/uploads.js.map +1 -0
- package/dist/extensions/list-html.js +58 -0
- package/dist/extensions/list-html.js.map +1 -0
- package/dist/extensions/lists.js +182 -0
- package/dist/extensions/lists.js.map +1 -0
- package/dist/extensions/mention/index.js +56 -0
- package/dist/extensions/mention/index.js.map +1 -0
- package/dist/extensions/mention/mention-menu.js +6 -0
- package/dist/extensions/mention/mention-menu.js.map +1 -0
- package/dist/extensions/mention/mention-menu.vue_vue_type_script_setup_true_lang.js +132 -0
- package/dist/extensions/mention/mention-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/mention/state.js +113 -0
- package/dist/extensions/mention/state.js.map +1 -0
- package/dist/extensions/table/commands.js +356 -0
- package/dist/extensions/table/commands.js.map +1 -0
- package/dist/extensions/table/html.js +134 -0
- package/dist/extensions/table/html.js.map +1 -0
- package/dist/extensions/table/index.js +56 -0
- package/dist/extensions/table/index.js.map +1 -0
- package/dist/extensions/table/plugin.js +81 -0
- package/dist/extensions/table/plugin.js.map +1 -0
- package/dist/extensions/table/schema.js +258 -0
- package/dist/extensions/table/schema.js.map +1 -0
- package/dist/extensions/table/table-controls.js +6 -0
- package/dist/extensions/table/table-controls.js.map +1 -0
- package/dist/extensions/table/table-controls.vue_vue_type_script_setup_true_lang.js +698 -0
- package/dist/extensions/table/table-controls.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/table/view.js +19 -0
- package/dist/extensions/table/view.js.map +1 -0
- package/dist/extensions/toggle/index.js +87 -0
- package/dist/extensions/toggle/index.js.map +1 -0
- package/dist/extensions/toggle/state.js +120 -0
- package/dist/extensions/toggle/state.js.map +1 -0
- package/dist/extensions/toggle/toggle-block.js +6 -0
- package/dist/extensions/toggle/toggle-block.js.map +1 -0
- package/dist/extensions/toggle/toggle-block.vue_vue_type_script_setup_true_lang.js +42 -0
- package/dist/extensions/toggle/toggle-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/locale.js +522 -0
- package/dist/locale.js.map +1 -0
- package/dist/markdown/index.js +24 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +8 -0
- package/dist/types/catalog.d.ts +8 -0
- package/dist/types/code/code-editor-block.vue.d.ts +17 -0
- package/dist/types/code/index.d.ts +18 -0
- package/dist/types/code/languages.d.ts +7 -0
- package/dist/types/code/types.d.ts +37 -0
- package/dist/types/code/view.d.ts +12 -0
- package/dist/types/collaboration/binding.d.ts +32 -0
- package/dist/types/collaboration/cursors.d.ts +26 -0
- package/dist/types/collaboration/index.d.ts +47 -0
- package/dist/types/collaboration/order.d.ts +20 -0
- package/dist/types/collaboration/table.d.ts +25 -0
- package/dist/types/collaboration/text.d.ts +9 -0
- package/dist/types/components/block-handle.vue.d.ts +3 -0
- package/dist/types/components/bubble-toolbar.vue.d.ts +7 -0
- package/dist/types/components/comment-button.vue.d.ts +3 -0
- package/dist/types/components/comment-card.vue.d.ts +3 -0
- package/dist/types/components/menu-escape.d.ts +7 -0
- package/dist/types/components/search-bar.vue.d.ts +22 -0
- package/dist/types/components/slash-menu.vue.d.ts +11 -0
- package/dist/types/core/align.d.ts +16 -0
- package/dist/types/core/block-selection.d.ts +22 -0
- package/dist/types/core/blocks.d.ts +52 -0
- package/dist/types/core/changes.d.ts +14 -0
- package/dist/types/core/colors.d.ts +4 -0
- package/dist/types/core/commands.d.ts +61 -0
- package/dist/types/core/comments.d.ts +44 -0
- package/dist/types/core/compare.d.ts +10 -0
- package/dist/types/core/convert.d.ts +12 -0
- package/dist/types/core/diff.d.ts +49 -0
- package/dist/types/core/editor.d.ts +64 -0
- package/dist/types/core/extension.d.ts +167 -0
- package/dist/types/core/file-drop.d.ts +19 -0
- package/dist/types/core/highlight.d.ts +32 -0
- package/dist/types/core/input-rules.d.ts +21 -0
- package/dist/types/core/keymap.d.ts +8 -0
- package/dist/types/core/model.d.ts +158 -0
- package/dist/types/core/output.d.ts +64 -0
- package/dist/types/core/plugins.d.ts +24 -0
- package/dist/types/core/prepare.d.ts +23 -0
- package/dist/types/core/schema.d.ts +38 -0
- package/dist/types/core/search.d.ts +83 -0
- package/dist/types/core/selection.d.ts +8 -0
- package/dist/types/core/slash.d.ts +21 -0
- package/dist/types/core/transaction.d.ts +97 -0
- package/dist/types/define-block.d.ts +66 -0
- package/dist/types/document.d.ts +46 -0
- package/dist/types/dom.d.ts +17 -0
- package/dist/types/editor.d.ts +252 -0
- package/dist/types/editor.vue.d.ts +210 -0
- package/dist/types/extensions/align/align-menu.vue.d.ts +3 -0
- package/dist/types/extensions/align/align-sub.vue.d.ts +7 -0
- package/dist/types/extensions/align/commands.d.ts +13 -0
- package/dist/types/extensions/align/index.d.ts +7 -0
- package/dist/types/extensions/attachment/attachment-block.vue.d.ts +17 -0
- package/dist/types/extensions/attachment/files.d.ts +6 -0
- package/dist/types/extensions/attachment/index.d.ts +20 -0
- package/dist/types/extensions/blocks.d.ts +8 -0
- package/dist/types/extensions/callout/callout-block.vue.d.ts +14 -0
- package/dist/types/extensions/callout/html.d.ts +4 -0
- package/dist/types/extensions/callout/index.d.ts +3 -0
- package/dist/types/extensions/callout/tones.d.ts +8 -0
- package/dist/types/extensions/code-block/code-block-view.vue.d.ts +16 -0
- package/dist/types/extensions/code-block/index.d.ts +19 -0
- package/dist/types/extensions/code-block/languages.d.ts +9 -0
- package/dist/types/extensions/colors/color-menu.vue.d.ts +3 -0
- package/dist/types/extensions/colors/index.d.ts +7 -0
- package/dist/types/extensions/colors/palette.d.ts +7 -0
- package/dist/types/extensions/editor-kit.d.ts +37 -0
- package/dist/types/extensions/embed/embed-block.vue.d.ts +16 -0
- package/dist/types/extensions/embed/index.d.ts +18 -0
- package/dist/types/extensions/embed/targets.d.ts +32 -0
- package/dist/types/extensions/formatting.d.ts +8 -0
- package/dist/types/extensions/image/image-block.vue.d.ts +16 -0
- package/dist/types/extensions/image/index.d.ts +16 -0
- package/dist/types/extensions/image/uploads.d.ts +35 -0
- package/dist/types/extensions/list-html.d.ts +11 -0
- package/dist/types/extensions/lists.d.ts +6 -0
- package/dist/types/extensions/mention/index.d.ts +11 -0
- package/dist/types/extensions/mention/mention-menu.vue.d.ts +7 -0
- package/dist/types/extensions/mention/state.d.ts +28 -0
- package/dist/types/extensions/table/commands.d.ts +69 -0
- package/dist/types/extensions/table/html.d.ts +4 -0
- package/dist/types/extensions/table/index.d.ts +8 -0
- package/dist/types/extensions/table/plugin.d.ts +4 -0
- package/dist/types/extensions/table/schema.d.ts +23 -0
- package/dist/types/extensions/table/table-controls.vue.d.ts +3 -0
- package/dist/types/extensions/table/view.d.ts +13 -0
- package/dist/types/extensions/toggle/index.d.ts +7 -0
- package/dist/types/extensions/toggle/state.d.ts +17 -0
- package/dist/types/extensions/toggle/toggle-block.vue.d.ts +14 -0
- package/dist/types/index.d.ts +266 -0
- package/dist/types/locale.d.ts +195 -0
- package/dist/types/markdown/index.d.ts +14 -0
- package/dist/types/use-comments.d.ts +17 -0
- package/dist/use-comments.js +141 -0
- package/dist/use-comments.js.map +1 -0
- package/package.json +98 -4
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { isColor } from "./colors.js";
|
|
2
|
+
import { isAlign } from "./align.js";
|
|
3
|
+
import { isSafeHref } from "./schema.js";
|
|
4
|
+
// src/core/output.ts
|
|
5
|
+
var tagsOf = (tags, value) => typeof tags === "function" ? tags(value) : tags;
|
|
6
|
+
function outputsOf(kit) {
|
|
7
|
+
const outputs = {
|
|
8
|
+
blocks: {},
|
|
9
|
+
inlines: {},
|
|
10
|
+
marks: {},
|
|
11
|
+
fields: Object.values(kit.schema.marks).filter((type) => type.spec.field).map((type) => [type.spec.field, type.name]),
|
|
12
|
+
aligned: new Set(Object.values(kit.schema.nodes).filter((type) => type.spec.attrs?.align).map((type) => type.name))
|
|
13
|
+
};
|
|
14
|
+
for (let index = kit.extensions.length - 1; index >= 0; index--) {
|
|
15
|
+
const extension = kit.extensions[index];
|
|
16
|
+
Object.assign(outputs.blocks, extension.output?.blocks);
|
|
17
|
+
Object.assign(outputs.inlines, extension.output?.inlines);
|
|
18
|
+
Object.assign(outputs.marks, extension.output?.marks);
|
|
19
|
+
}
|
|
20
|
+
return outputs;
|
|
21
|
+
}
|
|
22
|
+
/** Fill in what a partial block leaves out, so writers always get a whole block. */
|
|
23
|
+
var toInline = (content) => typeof content === "string" ? content ? [{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: content
|
|
26
|
+
}] : [] : content;
|
|
27
|
+
function complete(block) {
|
|
28
|
+
const result = {
|
|
29
|
+
id: block.id ?? "",
|
|
30
|
+
type: block.type ?? "paragraph",
|
|
31
|
+
props: block.props ?? {},
|
|
32
|
+
content: toInline(block.content ?? []),
|
|
33
|
+
children: (block.children ?? []).map(complete)
|
|
34
|
+
};
|
|
35
|
+
if (block.rows) result.rows = block.rows.map((row) => ({
|
|
36
|
+
id: row.id ?? "",
|
|
37
|
+
cells: row.cells.map((cell) => typeof cell === "string" ? { content: toInline(cell) } : {
|
|
38
|
+
...cell,
|
|
39
|
+
content: toInline(cell.content ?? [])
|
|
40
|
+
})
|
|
41
|
+
}));
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
var escapeHTML = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
45
|
+
var plainText = (content) => content.map((item) => item.type === "text" ? item.text : item.type === "break" ? "\n" : "").join("");
|
|
46
|
+
function inlineHTML(content, outputs) {
|
|
47
|
+
let result = "";
|
|
48
|
+
for (const item of content) if (item.type === "break") result += "<br>";
|
|
49
|
+
else if (item.type === "inline") result += outputs.inlines[item.name]?.html?.(item.props) ?? "";
|
|
50
|
+
else {
|
|
51
|
+
let text = escapeHTML(item.text);
|
|
52
|
+
const marks = item.marks ?? [];
|
|
53
|
+
for (let index = marks.length - 1; index >= 0; index--) {
|
|
54
|
+
const tags = tagsOf(outputs.marks[marks[index]]?.html, "");
|
|
55
|
+
if (tags) text = tags[0] + text + tags[1];
|
|
56
|
+
}
|
|
57
|
+
for (let index = outputs.fields.length - 1; index >= 0; index--) {
|
|
58
|
+
const [field, mark] = outputs.fields[index];
|
|
59
|
+
const value = item[field];
|
|
60
|
+
const tags = isColor(value) && tagsOf(outputs.marks[mark]?.html, value);
|
|
61
|
+
if (tags) text = tags[0] + text + tags[1];
|
|
62
|
+
}
|
|
63
|
+
if (item.link && isSafeHref(item.link)) text = `<a href="${escapeHTML(item.link)}">${text}</a>`;
|
|
64
|
+
result += text;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
var closingTag = (group) => `</${group.split(/\s/)[0]}>`;
|
|
69
|
+
/** An aligned block's first element gets `style="text-align: …"`. */
|
|
70
|
+
function withAlign(html, align) {
|
|
71
|
+
if (!isAlign(align) || align === "left") return html;
|
|
72
|
+
return html.replace(/^<([a-z][a-z0-9]*)/i, `<$1 style="text-align: ${align}"`);
|
|
73
|
+
}
|
|
74
|
+
/** Top-level blocks go on lines of their own, so the HTML stays readable. */
|
|
75
|
+
function blocksHTML(blocks, outputs, top = false) {
|
|
76
|
+
const newline = top ? "\n" : "";
|
|
77
|
+
let result = "";
|
|
78
|
+
let group;
|
|
79
|
+
let index = 0;
|
|
80
|
+
blocks.forEach((block, position) => {
|
|
81
|
+
const output = outputs.blocks[block.type];
|
|
82
|
+
index = position > 0 && blocks[position - 1].type === block.type ? index + 1 : 1;
|
|
83
|
+
if (output?.group !== group) {
|
|
84
|
+
if (group) result += newline + closingTag(group);
|
|
85
|
+
group = output?.group;
|
|
86
|
+
if (group) result += (result ? newline : "") + `<${group}>`;
|
|
87
|
+
}
|
|
88
|
+
if (result) result += newline;
|
|
89
|
+
const context = {
|
|
90
|
+
content: inlineHTML(block.content, outputs),
|
|
91
|
+
text: plainText(block.content),
|
|
92
|
+
children: blocksHTML(block.children, outputs),
|
|
93
|
+
index,
|
|
94
|
+
inline: (content) => inlineHTML(content, outputs)
|
|
95
|
+
};
|
|
96
|
+
const html = output?.html ? output.html(block, context) : `<p>${context.content}</p>${context.children}`;
|
|
97
|
+
result += outputs.aligned.has(block.type) ? withAlign(html, block.props.align) : html;
|
|
98
|
+
});
|
|
99
|
+
if (group) result += newline + closingTag(group);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Write blocks as HTML, e.g. to show a document outside the editor or to
|
|
104
|
+
* send it by email. Each extension decides how its blocks look; the
|
|
105
|
+
* result has no editor classes.
|
|
106
|
+
*/
|
|
107
|
+
function writeHTML(blocks, kit) {
|
|
108
|
+
return blocksHTML(blocks.map(complete), outputsOf(kit), true);
|
|
109
|
+
}
|
|
110
|
+
/** Characters that mean something anywhere in Markdown text. */
|
|
111
|
+
var SPECIAL = /[\\`*_[\]<>~|]|&(?=#?\w+;)/g;
|
|
112
|
+
/** What would start a block at the beginning of a line. */
|
|
113
|
+
var LINE_START = /^(\s*)(#{1,6}(?=\s|$)|[-+](?=\s|$)|-+(?=\s*$)|=+(?=\s*$)|(\d+)([.)])(?=\s|$))/;
|
|
114
|
+
function escapeLine(text) {
|
|
115
|
+
return text.replace(LINE_START, (_all, space, mark, digits, dot) => digits ? `${space}${digits}\\${dot}` : `${space}\\${mark}`);
|
|
116
|
+
}
|
|
117
|
+
var escapeMarkdown = (text) => text.replace(SPECIAL, "\\$&");
|
|
118
|
+
var PUNCTUATION = /[\p{P}\p{S}]/u;
|
|
119
|
+
var SPACE = /\s/u;
|
|
120
|
+
/**
|
|
121
|
+
* Whether a delimiter such as `**` can open (or close) emphasis here: the
|
|
122
|
+
* text inside must not start (end) with a space, and when it starts (ends)
|
|
123
|
+
* with punctuation, the outside must be a space, punctuation or the edge.
|
|
124
|
+
* Otherwise the HTML tag is written, which Markdown reads as well.
|
|
125
|
+
*/
|
|
126
|
+
function flanks(inner, outer) {
|
|
127
|
+
if (!inner || SPACE.test(inner)) return false;
|
|
128
|
+
if (!PUNCTUATION.test(inner)) return true;
|
|
129
|
+
return !outer || SPACE.test(outer) || PUNCTUATION.test(outer);
|
|
130
|
+
}
|
|
131
|
+
function codeSpan(text) {
|
|
132
|
+
const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
|
|
133
|
+
const fence = "`".repeat(longest + 1);
|
|
134
|
+
const pad = /^`|`$/.test(text) || text.startsWith(" ") && text.endsWith(" ") && text.trim() ? " " : "";
|
|
135
|
+
return fence + pad + text + pad + fence;
|
|
136
|
+
}
|
|
137
|
+
/** A key for each formatting a run has, outermost first: the link, then the marks. */
|
|
138
|
+
function keysOf(item, outputs) {
|
|
139
|
+
if (item.type !== "text") return [];
|
|
140
|
+
const keys = [];
|
|
141
|
+
if (item.link && isSafeHref(item.link)) keys.push(`link:${item.link}`);
|
|
142
|
+
for (const [field, mark] of outputs.fields) {
|
|
143
|
+
const value = item[field];
|
|
144
|
+
if (isColor(value) && hasOutput(mark, outputs)) keys.push(`${mark}=${value}`);
|
|
145
|
+
}
|
|
146
|
+
for (const mark of item.marks ?? []) if (mark !== "code" && hasOutput(mark, outputs)) keys.push(mark);
|
|
147
|
+
return keys;
|
|
148
|
+
}
|
|
149
|
+
var hasOutput = (mark, outputs) => !!(outputs.marks[mark]?.markdown ?? outputs.marks[mark]?.html);
|
|
150
|
+
function linkDestination(href) {
|
|
151
|
+
return /[\s()<>]/.test(href) ? `<${href.replace(/[<>]/g, encodeURIComponent)}>` : href;
|
|
152
|
+
}
|
|
153
|
+
var textOf = (item) => item?.type === "text" ? item.text : item?.type === "inline" ? "@" : item ? "\n" : "";
|
|
154
|
+
function inlineMarkdown(content, outputs) {
|
|
155
|
+
let result = "";
|
|
156
|
+
const stack = [];
|
|
157
|
+
/** The text a mark will cover from `start`, and the character after it. */
|
|
158
|
+
function span(start, key) {
|
|
159
|
+
let text = "";
|
|
160
|
+
let end = start;
|
|
161
|
+
while (end < content.length && keysOf(content[end], outputs).includes(key)) {
|
|
162
|
+
text += textOf(content[end]);
|
|
163
|
+
end++;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
text: text.trim(),
|
|
167
|
+
after: textOf(content[end])[0]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function open(key, start) {
|
|
171
|
+
if (key.startsWith("link:")) {
|
|
172
|
+
stack.push({
|
|
173
|
+
key,
|
|
174
|
+
close: `](${linkDestination(key.slice(5))})`
|
|
175
|
+
});
|
|
176
|
+
result += "[";
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const [name, value = ""] = key.split("=");
|
|
180
|
+
const tags = {
|
|
181
|
+
markdown: tagsOf(outputs.marks[name].markdown, value),
|
|
182
|
+
html: tagsOf(outputs.marks[name].html, value)
|
|
183
|
+
};
|
|
184
|
+
const { text, after } = span(start, key);
|
|
185
|
+
const delimiter = tags.markdown;
|
|
186
|
+
const [opening, closing] = delimiter && flanks(text[0], result.at(-1)) && flanks(text.at(-1), after ?? void 0) ? delimiter : tags.html ?? delimiter;
|
|
187
|
+
result += opening;
|
|
188
|
+
stack.push({
|
|
189
|
+
key,
|
|
190
|
+
close: closing
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
content.forEach((item, index) => {
|
|
194
|
+
const keys = keysOf(item, outputs);
|
|
195
|
+
let keep = 0;
|
|
196
|
+
while (keep < stack.length && keys.includes(stack[keep].key)) keep++;
|
|
197
|
+
let text = item.type === "text" ? item.text : "";
|
|
198
|
+
const lead = keys.length > keep ? /^\s+/.exec(text)?.[0] ?? "" : "";
|
|
199
|
+
while (stack.length > keep) result += stack.pop().close;
|
|
200
|
+
result += lead;
|
|
201
|
+
text = text.slice(lead.length);
|
|
202
|
+
for (const key of keys) if (!stack.some((entry) => entry.key === key)) open(key, index);
|
|
203
|
+
if (item.type === "break") {
|
|
204
|
+
result += "\\\n";
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (item.type === "inline") {
|
|
208
|
+
result += outputs.inlines[item.name]?.markdown?.(item.props) ?? "";
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const nextKeys = keysOf(content[index + 1] ?? { type: "break" }, outputs);
|
|
212
|
+
const trail = stack.some((entry) => !nextKeys.includes(entry.key)) ? /\s+$/.exec(text)?.[0] ?? "" : "";
|
|
213
|
+
if (trail) text = text.slice(0, -trail.length);
|
|
214
|
+
let written = item.marks?.includes("code") ? codeSpan(text) : escapeMarkdown(text);
|
|
215
|
+
if (!item.marks?.includes("code")) written = written.split("\n").map(escapeLine).join("\n");
|
|
216
|
+
result += written;
|
|
217
|
+
if (trail) {
|
|
218
|
+
const stay = stack.findIndex((entry) => !nextKeys.includes(entry.key));
|
|
219
|
+
while (stack.length > stay) result += stack.pop().close;
|
|
220
|
+
result += trail;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
while (stack.length) result += stack.pop().close;
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
/** Put `prefix` before every line, and `rest` (default: as many spaces) before the ones after the first. */
|
|
227
|
+
function indentLines(text, prefix, rest = " ".repeat(prefix.length)) {
|
|
228
|
+
return text.split("\n").map((line, index) => index === 0 ? prefix + line : line ? rest + line : rest.trimEnd()).join("\n");
|
|
229
|
+
}
|
|
230
|
+
function blocksMarkdown(blocks, outputs) {
|
|
231
|
+
let result = "";
|
|
232
|
+
let index = 0;
|
|
233
|
+
blocks.forEach((block, position) => {
|
|
234
|
+
const output = outputs.blocks[block.type];
|
|
235
|
+
const previous = blocks[position - 1];
|
|
236
|
+
index = previous?.type === block.type ? index + 1 : 1;
|
|
237
|
+
const context = {
|
|
238
|
+
content: inlineMarkdown(block.content, outputs),
|
|
239
|
+
html: inlineHTML(block.content, outputs),
|
|
240
|
+
text: plainText(block.content),
|
|
241
|
+
children: blocksMarkdown(block.children, outputs),
|
|
242
|
+
index,
|
|
243
|
+
inline: (content) => inlineMarkdown(content, outputs),
|
|
244
|
+
inlineHTML: (content) => inlineHTML(content, outputs)
|
|
245
|
+
};
|
|
246
|
+
const written = output?.markdown ? output.markdown(block, context) : [context.content, context.children].filter(Boolean).join("\n\n");
|
|
247
|
+
if (!written) return;
|
|
248
|
+
if (result) {
|
|
249
|
+
const tight = previous && output?.group && outputs.blocks[previous.type]?.group === output.group;
|
|
250
|
+
result += tight ? "\n" : "\n\n";
|
|
251
|
+
}
|
|
252
|
+
result += written;
|
|
253
|
+
});
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
/** Write blocks as Markdown (CommonMark with GitHub's extensions). */
|
|
257
|
+
function writeMarkdown(blocks, kit) {
|
|
258
|
+
const markdown = blocksMarkdown(blocks.map(complete), outputsOf(kit));
|
|
259
|
+
return markdown ? `${markdown}\n` : "";
|
|
260
|
+
}
|
|
261
|
+
export { escapeHTML, escapeMarkdown, indentLines, writeHTML, writeMarkdown };
|
|
262
|
+
|
|
263
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","names":[],"sources":["../../src/core/output.ts"],"sourcesContent":["import type { ResolvedEditorExtensions } from './extension'\nimport type { Block, InlineContent, MarkField, PartialBlock } from './model'\nimport { isAlign } from './align'\nimport { isColor } from './colors'\nimport { isSafeHref } from './schema'\n\n/** What a block's writer gets besides the block. */\nexport interface BlockOutputContext {\n /** The block's text, already written in the output format. */\n content: string\n /** The block's text without formatting (e.g. for a code block). */\n text: string\n /** The child blocks, already written. */\n children: string\n /** The block's position, from 1, among the siblings of the same type right before it. */\n index: number\n /** Write other inline content (e.g. a table cell's) in the same format. */\n inline: (content: InlineContent[]) => string\n}\n\nexport interface MarkdownBlockContext extends BlockOutputContext {\n /** The block's text as HTML, for Markdown that holds HTML (e.g. `<summary>`). */\n html: string\n /** Write other inline content as HTML. */\n inlineHTML: (content: InlineContent[]) => string\n}\n\n/** How a block type is written as HTML and Markdown. */\nexport interface BlockOutput {\n html?: (block: Block, context: BlockOutputContext) => string\n /**\n * Consecutive blocks with the same `group` sit in one element, written\n * as its opening tag (e.g. `ul`, `ol`); in Markdown they are kept\n * together without blank lines.\n */\n group?: string\n markdown?: (block: Block, context: MarkdownBlockContext) => string\n}\n\n/** How an inline node (e.g. a mention) is written. */\nexport interface InlineOutput {\n html?: (props: Record<string, unknown>) => string\n markdown?: (props: Record<string, unknown>) => string\n}\n\n/**\n * How a mark is written: the text between an opening and a closing string.\n * A mark with a value (a color) gives a function of the value.\n */\nexport type MarkTags = [open: string, close: string]\n\nexport interface MarkOutput {\n html?: MarkTags | ((value: string) => MarkTags)\n markdown?: MarkTags | ((value: string) => MarkTags)\n}\n\n/** What an extension contributes to `blocksToHTML` and `blocksToMarkdown`. */\nexport interface EditorExtensionOutput {\n blocks?: Record<string, BlockOutput>\n inlines?: Record<string, InlineOutput>\n marks?: Record<string, MarkOutput>\n}\n\ninterface Outputs {\n blocks: Record<string, BlockOutput>\n inlines: Record<string, InlineOutput>\n marks: Record<string, MarkOutput>\n /** Marks stored as a field of the text run, by field: `color` → `textColor`. */\n fields: [field: MarkField, mark: string][]\n /** The block types that can be aligned. */\n aligned: Set<string>\n}\n\nconst tagsOf = (tags: MarkOutput['html'] | undefined, value: string): MarkTags | undefined =>\n typeof tags === 'function' ? tags(value) : tags\n\nfunction outputsOf(kit: ResolvedEditorExtensions): Outputs {\n const fields = Object.values(kit.schema.marks)\n .filter((type) => type.spec.field)\n .map((type) => [type.spec.field as MarkField, type.name] as [MarkField, string])\n const aligned = new Set(\n Object.values(kit.schema.nodes)\n .filter((type) => type.spec.attrs?.align)\n .map((type) => type.name),\n )\n const outputs: Outputs = { blocks: {}, inlines: {}, marks: {}, fields, aligned }\n // extensions that come first win, as with their keys\n for (let index = kit.extensions.length - 1; index >= 0; index--) {\n const extension = kit.extensions[index]!\n Object.assign(outputs.blocks, extension.output?.blocks)\n Object.assign(outputs.inlines, extension.output?.inlines)\n Object.assign(outputs.marks, extension.output?.marks)\n }\n return outputs\n}\n\n/** Fill in what a partial block leaves out, so writers always get a whole block. */\nconst toInline = (content: InlineContent[] | string): InlineContent[] =>\n typeof content === 'string' ? (content ? [{ type: 'text', text: content }] : []) : content\n\nfunction complete(block: PartialBlock): Block {\n const result: Block = {\n id: block.id ?? '',\n type: block.type ?? 'paragraph',\n props: block.props ?? {},\n content: toInline(block.content ?? []),\n children: (block.children ?? []).map(complete),\n }\n if (block.rows)\n result.rows = block.rows.map((row) => ({\n id: row.id ?? '',\n cells: row.cells.map((cell) =>\n typeof cell === 'string'\n ? { content: toInline(cell) }\n : { ...cell, content: toInline(cell.content ?? []) },\n ),\n }))\n return result\n}\n\n/* HTML */\n\nexport const escapeHTML = (text: string) =>\n text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"')\n\nconst plainText = (content: InlineContent[]) =>\n content\n .map((item) => (item.type === 'text' ? item.text : item.type === 'break' ? '\\n' : ''))\n .join('')\n\nfunction inlineHTML(content: InlineContent[], outputs: Outputs): string {\n let result = ''\n for (const item of content) {\n if (item.type === 'break') result += '<br>'\n else if (item.type === 'inline') result += outputs.inlines[item.name]?.html?.(item.props) ?? ''\n else {\n let text = escapeHTML(item.text)\n const marks = item.marks ?? []\n for (let index = marks.length - 1; index >= 0; index--) {\n const tags = tagsOf(outputs.marks[marks[index]!]?.html, '')\n if (tags) text = tags[0] + text + tags[1]\n }\n // highlight inside the color, both inside a link\n for (let index = outputs.fields.length - 1; index >= 0; index--) {\n const [field, mark] = outputs.fields[index]!\n const value = item[field]\n const tags = isColor(value) && tagsOf(outputs.marks[mark]?.html, value)\n if (tags) text = tags[0] + text + tags[1]\n }\n if (item.link && isSafeHref(item.link))\n text = `<a href=\"${escapeHTML(item.link)}\">${text}</a>`\n result += text\n }\n }\n return result\n}\n\nconst closingTag = (group: string) => `</${group.split(/\\s/)[0]}>`\n\n/** An aligned block's first element gets `style=\"text-align: …\"`. */\nfunction withAlign(html: string, align: unknown): string {\n if (!isAlign(align) || align === 'left') return html\n return html.replace(/^<([a-z][a-z0-9]*)/i, `<$1 style=\"text-align: ${align}\"`)\n}\n\n/** Top-level blocks go on lines of their own, so the HTML stays readable. */\nfunction blocksHTML(blocks: Block[], outputs: Outputs, top = false): string {\n const newline = top ? '\\n' : ''\n let result = ''\n let group: string | undefined\n let index = 0\n blocks.forEach((block, position) => {\n const output = outputs.blocks[block.type]\n index = position > 0 && blocks[position - 1]!.type === block.type ? index + 1 : 1\n if (output?.group !== group) {\n if (group) result += newline + closingTag(group)\n group = output?.group\n if (group) result += (result ? newline : '') + `<${group}>`\n }\n if (result) result += newline\n const context: BlockOutputContext = {\n content: inlineHTML(block.content, outputs),\n text: plainText(block.content),\n children: blocksHTML(block.children, outputs),\n index,\n inline: (content) => inlineHTML(content, outputs),\n }\n const html = output?.html\n ? output.html(block, context)\n : `<p>${context.content}</p>${context.children}`\n // only blocks that can be aligned, as the editor would keep them\n result += outputs.aligned.has(block.type) ? withAlign(html, block.props.align) : html\n })\n if (group) result += newline + closingTag(group)\n return result\n}\n\n/**\n * Write blocks as HTML, e.g. to show a document outside the editor or to\n * send it by email. Each extension decides how its blocks look; the\n * result has no editor classes.\n */\nexport function writeHTML(blocks: PartialBlock[], kit: ResolvedEditorExtensions): string {\n return blocksHTML(blocks.map(complete), outputsOf(kit), true)\n}\n\n/* Markdown */\n\n/** Characters that mean something anywhere in Markdown text. */\nconst SPECIAL = /[\\\\`*_[\\]<>~|]|&(?=#?\\w+;)/g\n\n/** What would start a block at the beginning of a line. */\nconst LINE_START = /^(\\s*)(#{1,6}(?=\\s|$)|[-+](?=\\s|$)|-+(?=\\s*$)|=+(?=\\s*$)|(\\d+)([.)])(?=\\s|$))/\n\nfunction escapeLine(text: string): string {\n return text.replace(\n LINE_START,\n (_all, space: string, mark: string, digits?: string, dot?: string) =>\n digits ? `${space}${digits}\\\\${dot}` : `${space}\\\\${mark}`,\n )\n}\n\nexport const escapeMarkdown = (text: string) => text.replace(SPECIAL, '\\\\$&')\n\nconst PUNCTUATION = /[\\p{P}\\p{S}]/u\nconst SPACE = /\\s/u\n\n/**\n * Whether a delimiter such as `**` can open (or close) emphasis here: the\n * text inside must not start (end) with a space, and when it starts (ends)\n * with punctuation, the outside must be a space, punctuation or the edge.\n * Otherwise the HTML tag is written, which Markdown reads as well.\n */\nfunction flanks(inner: string | undefined, outer: string | undefined) {\n if (!inner || SPACE.test(inner)) return false\n if (!PUNCTUATION.test(inner)) return true\n return !outer || SPACE.test(outer) || PUNCTUATION.test(outer)\n}\n\nfunction codeSpan(text: string): string {\n const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length))\n const fence = '`'.repeat(longest + 1)\n const pad =\n /^`|`$/.test(text) || (text.startsWith(' ') && text.endsWith(' ') && text.trim()) ? ' ' : ''\n return fence + pad + text + pad + fence\n}\n\n/** A key for each formatting a run has, outermost first: the link, then the marks. */\nfunction keysOf(item: InlineContent, outputs: Outputs): string[] {\n if (item.type !== 'text') return []\n const keys: string[] = []\n if (item.link && isSafeHref(item.link)) keys.push(`link:${item.link}`)\n for (const [field, mark] of outputs.fields) {\n const value = item[field]\n if (isColor(value) && hasOutput(mark, outputs)) keys.push(`${mark}=${value}`)\n }\n for (const mark of item.marks ?? [])\n if (mark !== 'code' && hasOutput(mark, outputs)) keys.push(mark)\n return keys\n}\n\nconst hasOutput = (mark: string, outputs: Outputs) =>\n !!(outputs.marks[mark]?.markdown ?? outputs.marks[mark]?.html)\n\nfunction linkDestination(href: string) {\n return /[\\s()<>]/.test(href) ? `<${href.replace(/[<>]/g, encodeURIComponent)}>` : href\n}\n\nconst textOf = (item: InlineContent | undefined) =>\n item?.type === 'text' ? item.text : item?.type === 'inline' ? '@' : item ? '\\n' : ''\n\nfunction inlineMarkdown(content: InlineContent[], outputs: Outputs): string {\n let result = ''\n const stack: { key: string; close: string }[] = []\n\n /** The text a mark will cover from `start`, and the character after it. */\n function span(start: number, key: string) {\n let text = ''\n let end = start\n while (end < content.length && keysOf(content[end]!, outputs).includes(key)) {\n text += textOf(content[end])\n end++\n }\n return { text: text.trim(), after: textOf(content[end])[0] }\n }\n\n function open(key: string, start: number) {\n if (key.startsWith('link:')) {\n stack.push({ key, close: `](${linkDestination(key.slice(5))})` })\n result += '['\n return\n }\n const [name, value = ''] = key.split('=') as [string, string?]\n const tags = {\n markdown: tagsOf(outputs.marks[name]!.markdown, value),\n html: tagsOf(outputs.marks[name]!.html, value),\n }\n const { text, after } = span(start, key)\n const delimiter = tags.markdown\n const usable =\n delimiter && flanks(text[0], result.at(-1)) && flanks(text.at(-1), after ?? undefined)\n const [opening, closing] = usable ? delimiter : (tags.html ?? delimiter!)\n result += opening\n stack.push({ key, close: closing })\n }\n\n content.forEach((item, index) => {\n const keys = keysOf(item, outputs)\n let keep = 0\n while (keep < stack.length && keys.includes(stack[keep]!.key)) keep++\n // spaces at the edges of formatted text go outside the delimiters\n let text = item.type === 'text' ? item.text : ''\n const lead = keys.length > keep ? (/^\\s+/.exec(text)?.[0] ?? '') : ''\n while (stack.length > keep) result += stack.pop()!.close\n result += lead\n text = text.slice(lead.length)\n for (const key of keys) if (!stack.some((entry) => entry.key === key)) open(key, index)\n\n if (item.type === 'break') {\n result += '\\\\\\n'\n return\n }\n if (item.type === 'inline') {\n result += outputs.inlines[item.name]?.markdown?.(item.props) ?? ''\n return\n }\n const nextKeys = keysOf(content[index + 1] ?? { type: 'break' }, outputs)\n const closes = stack.some((entry) => !nextKeys.includes(entry.key))\n const trail = closes ? (/\\s+$/.exec(text)?.[0] ?? '') : ''\n if (trail) text = text.slice(0, -trail.length)\n let written = item.marks?.includes('code') ? codeSpan(text) : escapeMarkdown(text)\n if (!item.marks?.includes('code')) written = written.split('\\n').map(escapeLine).join('\\n')\n result += written\n if (trail) {\n // close what ends here before the spaces; the next run closes nothing more\n const stay = stack.findIndex((entry) => !nextKeys.includes(entry.key))\n while (stack.length > stay) result += stack.pop()!.close\n result += trail\n }\n })\n while (stack.length) result += stack.pop()!.close\n return result\n}\n\n/** Put `prefix` before every line, and `rest` (default: as many spaces) before the ones after the first. */\nexport function indentLines(\n text: string,\n prefix: string,\n rest = ' '.repeat(prefix.length),\n): string {\n return text\n .split('\\n')\n .map((line, index) => (index === 0 ? prefix + line : line ? rest + line : rest.trimEnd()))\n .join('\\n')\n}\n\nfunction blocksMarkdown(blocks: Block[], outputs: Outputs): string {\n let result = ''\n let index = 0\n blocks.forEach((block, position) => {\n const output = outputs.blocks[block.type]\n const previous = blocks[position - 1]\n index = previous?.type === block.type ? index + 1 : 1\n const context: MarkdownBlockContext = {\n content: inlineMarkdown(block.content, outputs),\n html: inlineHTML(block.content, outputs),\n text: plainText(block.content),\n children: blocksMarkdown(block.children, outputs),\n index,\n inline: (content) => inlineMarkdown(content, outputs),\n inlineHTML: (content) => inlineHTML(content, outputs),\n }\n const written = output?.markdown\n ? output.markdown(block, context)\n : [context.content, context.children].filter(Boolean).join('\\n\\n')\n if (!written) return\n if (result) {\n const tight =\n previous && output?.group && outputs.blocks[previous.type]?.group === output.group\n result += tight ? '\\n' : '\\n\\n'\n }\n result += written\n })\n return result\n}\n\n/** Write blocks as Markdown (CommonMark with GitHub's extensions). */\nexport function writeMarkdown(blocks: PartialBlock[], kit: ResolvedEditorExtensions): string {\n const markdown = blocksMarkdown(blocks.map(complete), outputsOf(kit))\n return markdown ? `${markdown}\\n` : ''\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createId } from "./model.js";
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
4
|
+
// src/core/plugins.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a transaction came from another collaborator. Collaborative
|
|
7
|
+
* bindings mark theirs; the Yjs binding uses the `y-sync$` key.
|
|
8
|
+
*/
|
|
9
|
+
function isRemote(tr) {
|
|
10
|
+
return !!tr.getMeta("y-sync$")?.isChangeOrigin || tr.getMeta("remote") === true;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Every block gets a unique id. Splitting a block copies its attributes,
|
|
14
|
+
* so the copy gets a fresh id here; the first occurrence keeps the old one.
|
|
15
|
+
* Changes from collaborators are left alone, so two editors never fight
|
|
16
|
+
* over an id.
|
|
17
|
+
*/
|
|
18
|
+
function idPlugin() {
|
|
19
|
+
return new Plugin({
|
|
20
|
+
key: new PluginKey("dou-editor-ids"),
|
|
21
|
+
appendTransaction(transactions, _old, state) {
|
|
22
|
+
if (!transactions.some((tr) => tr.docChanged) || transactions.every(isRemote)) return null;
|
|
23
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24
|
+
let tr = null;
|
|
25
|
+
state.doc.descendants((node, pos) => {
|
|
26
|
+
if (node.type.name !== "block") return node.type.name === "blockGroup";
|
|
27
|
+
const id = node.attrs.id;
|
|
28
|
+
if (!id || seen.has(id)) {
|
|
29
|
+
tr ??= state.tr;
|
|
30
|
+
const fresh = createId();
|
|
31
|
+
tr.setNodeAttribute(pos, "id", fresh);
|
|
32
|
+
seen.add(fresh);
|
|
33
|
+
} else seen.add(id);
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
if (tr) tr.setMeta("addToHistory", false);
|
|
37
|
+
return tr;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Numbered items count up within each run of consecutive numbered siblings. */
|
|
42
|
+
function numberDecorations(doc) {
|
|
43
|
+
const decorations = [];
|
|
44
|
+
const visit = (container, start) => {
|
|
45
|
+
let count = 0;
|
|
46
|
+
container.forEach((block, offset) => {
|
|
47
|
+
const pos = start + offset;
|
|
48
|
+
const content = block.firstChild;
|
|
49
|
+
if (content?.type.name === "numberedItem") {
|
|
50
|
+
count++;
|
|
51
|
+
decorations.push(Decoration.node(pos + 1, pos + 1 + content.nodeSize, { "data-index": String(count) }));
|
|
52
|
+
} else count = 0;
|
|
53
|
+
if (block.childCount > 1) visit(block.child(1), pos + 1 + content.nodeSize + 1);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
visit(doc, 0);
|
|
57
|
+
return DecorationSet.create(doc, decorations);
|
|
58
|
+
}
|
|
59
|
+
function numberingPlugin() {
|
|
60
|
+
return new Plugin({
|
|
61
|
+
key: new PluginKey("dou-editor-numbering"),
|
|
62
|
+
state: {
|
|
63
|
+
init: (_config, state) => numberDecorations(state.doc),
|
|
64
|
+
apply: (tr, value) => tr.docChanged ? numberDecorations(tr.doc) : value
|
|
65
|
+
},
|
|
66
|
+
props: { decorations(state) {
|
|
67
|
+
return this.getState(state);
|
|
68
|
+
} }
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** A hint inside the empty block under the cursor, and in an empty document. */
|
|
72
|
+
function placeholderPlugin(options) {
|
|
73
|
+
return new Plugin({
|
|
74
|
+
key: new PluginKey("dou-editor-placeholder"),
|
|
75
|
+
props: { decorations(state) {
|
|
76
|
+
const { doc, selection } = state;
|
|
77
|
+
const decorations = [];
|
|
78
|
+
const onlyBlock = doc.childCount === 1 && doc.firstChild.childCount === 1 && doc.firstChild.firstChild;
|
|
79
|
+
if (onlyBlock && onlyBlock.type.name === "paragraph" && onlyBlock.content.size === 0) {
|
|
80
|
+
decorations.push(Decoration.node(1, 1 + onlyBlock.nodeSize, {
|
|
81
|
+
class: "is-empty",
|
|
82
|
+
"data-placeholder": options.empty()
|
|
83
|
+
}));
|
|
84
|
+
return DecorationSet.create(doc, decorations);
|
|
85
|
+
}
|
|
86
|
+
const { $from } = selection;
|
|
87
|
+
const parent = $from.parent;
|
|
88
|
+
if (!selection.empty || !parent.isTextblock || parent.content.size > 0) return null;
|
|
89
|
+
if (parent.type.spec.code) return null;
|
|
90
|
+
const text = parent.type.name === "heading" ? options.heading() : options.text();
|
|
91
|
+
const start = $from.before();
|
|
92
|
+
decorations.push(Decoration.node(start, start + parent.nodeSize, {
|
|
93
|
+
class: "is-empty",
|
|
94
|
+
"data-placeholder": text
|
|
95
|
+
}));
|
|
96
|
+
return DecorationSet.create(doc, decorations);
|
|
97
|
+
} }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export { idPlugin, isRemote, numberingPlugin, placeholderPlugin };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=plugins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugins.js","names":[],"sources":["../../src/core/plugins.ts"],"sourcesContent":["import type { Node as PMNode } from 'prosemirror-model'\nimport { Plugin, PluginKey, type Transaction } from 'prosemirror-state'\nimport { Decoration, DecorationSet } from 'prosemirror-view'\nimport { createId } from './model'\n\n/**\n * Whether a transaction came from another collaborator. Collaborative\n * bindings mark theirs; the Yjs binding uses the `y-sync$` key.\n */\nexport function isRemote(tr: Transaction): boolean {\n const meta = tr.getMeta('y-sync$') as { isChangeOrigin?: boolean } | undefined\n return !!meta?.isChangeOrigin || tr.getMeta('remote') === true\n}\n\n/**\n * Every block gets a unique id. Splitting a block copies its attributes,\n * so the copy gets a fresh id here; the first occurrence keeps the old one.\n * Changes from collaborators are left alone, so two editors never fight\n * over an id.\n */\nexport function idPlugin(): Plugin {\n return new Plugin({\n key: new PluginKey('dou-editor-ids'),\n appendTransaction(transactions, _old, state) {\n if (!transactions.some((tr) => tr.docChanged) || transactions.every(isRemote)) return null\n const seen = new Set<string>()\n let tr: Transaction | null = null\n state.doc.descendants((node, pos) => {\n if (node.type.name !== 'block') return node.type.name === 'blockGroup'\n const id = node.attrs.id as string | null\n if (!id || seen.has(id)) {\n tr ??= state.tr\n const fresh = createId()\n tr.setNodeAttribute(pos, 'id', fresh)\n seen.add(fresh)\n } else seen.add(id)\n return true\n })\n if (tr) (tr as Transaction).setMeta('addToHistory', false)\n return tr\n },\n })\n}\n\n/** Numbered items count up within each run of consecutive numbered siblings. */\nfunction numberDecorations(doc: PMNode): DecorationSet {\n const decorations: Decoration[] = []\n const visit = (container: PMNode, start: number) => {\n let count = 0\n container.forEach((block, offset) => {\n const pos = start + offset\n const content = block.firstChild\n if (content?.type.name === 'numberedItem') {\n count++\n decorations.push(\n Decoration.node(pos + 1, pos + 1 + content.nodeSize, { 'data-index': String(count) }),\n )\n } else count = 0\n if (block.childCount > 1) visit(block.child(1), pos + 1 + content!.nodeSize + 1)\n })\n }\n visit(doc, 0)\n return DecorationSet.create(doc, decorations)\n}\n\nexport function numberingPlugin(): Plugin {\n return new Plugin<DecorationSet>({\n key: new PluginKey('dou-editor-numbering'),\n state: {\n init: (_config, state) => numberDecorations(state.doc),\n apply: (tr, value) => (tr.docChanged ? numberDecorations(tr.doc) : value),\n },\n props: {\n decorations(state) {\n return this.getState(state)\n },\n },\n })\n}\n\nexport interface PlaceholderOptions {\n /** The hint in the empty block the cursor is in. */\n text: () => string\n /** The hint in an empty heading. */\n heading: () => string\n /** The hint when the whole document is one empty paragraph. */\n empty: () => string\n}\n\n/** A hint inside the empty block under the cursor, and in an empty document. */\nexport function placeholderPlugin(options: PlaceholderOptions): Plugin {\n return new Plugin({\n key: new PluginKey('dou-editor-placeholder'),\n props: {\n decorations(state) {\n const { doc, selection } = state\n const decorations: Decoration[] = []\n const onlyBlock =\n doc.childCount === 1 && doc.firstChild!.childCount === 1 && doc.firstChild!.firstChild!\n if (onlyBlock && onlyBlock.type.name === 'paragraph' && onlyBlock.content.size === 0) {\n decorations.push(\n Decoration.node(1, 1 + onlyBlock.nodeSize, {\n class: 'is-empty',\n 'data-placeholder': options.empty(),\n }),\n )\n return DecorationSet.create(doc, decorations)\n }\n const { $from } = selection\n const parent = $from.parent\n if (!selection.empty || !parent.isTextblock || parent.content.size > 0) return null\n if (parent.type.spec.code) return null\n const text = parent.type.name === 'heading' ? options.heading() : options.text()\n const start = $from.before()\n decorations.push(\n Decoration.node(start, start + parent.nodeSize, {\n class: 'is-empty',\n 'data-placeholder': text,\n }),\n )\n return DecorationSet.create(doc, decorations)\n },\n },\n })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createId, docToBlocks } from "./model.js";
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { DOMParser as DOMParser$1 } from "prosemirror-model";
|
|
4
|
+
// src/core/prepare.ts
|
|
5
|
+
/**
|
|
6
|
+
* The editor's own markup for a block: a content element of `type` holding
|
|
7
|
+
* `content`, followed by a group holding `children`. Extensions use it in
|
|
8
|
+
* `prepareHTML` to turn containers such as `<details>` into blocks.
|
|
9
|
+
*/
|
|
10
|
+
function blockElement(doc, type, content, children = [], data = {}) {
|
|
11
|
+
const block = doc.createElement("div");
|
|
12
|
+
block.dataset.blockId = "";
|
|
13
|
+
const element = doc.createElement("div");
|
|
14
|
+
element.dataset.contentType = type;
|
|
15
|
+
Object.assign(element.dataset, data);
|
|
16
|
+
element.append(...content);
|
|
17
|
+
block.append(element);
|
|
18
|
+
if (children.some((node) => node.nodeType === 1 || node.textContent?.trim())) {
|
|
19
|
+
const group = doc.createElement("div");
|
|
20
|
+
group.className = "dou-editor__group";
|
|
21
|
+
group.append(...children);
|
|
22
|
+
block.append(group);
|
|
23
|
+
}
|
|
24
|
+
return block;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Split a container's children into its own text (the first paragraph, or
|
|
28
|
+
* the inline nodes before the first block element) and the blocks after it.
|
|
29
|
+
*/
|
|
30
|
+
function splitContainer(element) {
|
|
31
|
+
const nodes = Array.from(element.childNodes);
|
|
32
|
+
while (nodes.length && isBlank(nodes[0])) nodes.shift();
|
|
33
|
+
const first = nodes[0];
|
|
34
|
+
if (first instanceof Element && first.tagName === "P") return {
|
|
35
|
+
content: Array.from(first.childNodes),
|
|
36
|
+
children: nodes.slice(1)
|
|
37
|
+
};
|
|
38
|
+
const content = [];
|
|
39
|
+
while (nodes.length && !isBlockElement(nodes[0])) content.push(nodes.shift());
|
|
40
|
+
return {
|
|
41
|
+
content,
|
|
42
|
+
children: nodes
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
46
|
+
"ADDRESS",
|
|
47
|
+
"ARTICLE",
|
|
48
|
+
"ASIDE",
|
|
49
|
+
"BLOCKQUOTE",
|
|
50
|
+
"DETAILS",
|
|
51
|
+
"DIV",
|
|
52
|
+
"DL",
|
|
53
|
+
"FIGURE",
|
|
54
|
+
"FOOTER",
|
|
55
|
+
"H1",
|
|
56
|
+
"H2",
|
|
57
|
+
"H3",
|
|
58
|
+
"H4",
|
|
59
|
+
"H5",
|
|
60
|
+
"H6",
|
|
61
|
+
"HEADER",
|
|
62
|
+
"HR",
|
|
63
|
+
"OL",
|
|
64
|
+
"P",
|
|
65
|
+
"PRE",
|
|
66
|
+
"SECTION",
|
|
67
|
+
"TABLE",
|
|
68
|
+
"UL"
|
|
69
|
+
]);
|
|
70
|
+
var isBlank = (node) => node.nodeType === 3 && !node.textContent?.trim();
|
|
71
|
+
var isBlockElement = (node) => node instanceof Element && BLOCK_TAGS.has(node.tagName);
|
|
72
|
+
/** Run every extension's `prepareHTML` over the markup. */
|
|
73
|
+
function prepareElement(root, kit) {
|
|
74
|
+
for (const extension of kit.extensions) extension.prepareHTML?.(root);
|
|
75
|
+
}
|
|
76
|
+
/** Read HTML as blocks, with the block types, inline nodes and marks of `kit`. */
|
|
77
|
+
function parseHTML(html, kit) {
|
|
78
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
79
|
+
prepareElement(doc.body, kit);
|
|
80
|
+
const blocks = docToBlocks(DOMParser$1.fromSchema(kit.schema).parse(doc.body));
|
|
81
|
+
const fill = (list) => list.map((block) => ({
|
|
82
|
+
...block,
|
|
83
|
+
id: block.id || createId(),
|
|
84
|
+
children: fill(block.children)
|
|
85
|
+
}));
|
|
86
|
+
return fill(blocks);
|
|
87
|
+
}
|
|
88
|
+
/** Pasted HTML goes through the extensions' `prepareHTML` too. */
|
|
89
|
+
function preparePastePlugin(kit) {
|
|
90
|
+
return new Plugin({
|
|
91
|
+
key: new PluginKey("dou-editor-paste"),
|
|
92
|
+
props: { transformPastedHTML(html) {
|
|
93
|
+
if (!kit.extensions.some((extension) => extension.prepareHTML)) return html;
|
|
94
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
95
|
+
prepareElement(doc.body, kit);
|
|
96
|
+
return doc.body.innerHTML;
|
|
97
|
+
} }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export { blockElement, parseHTML, prepareElement, preparePastePlugin, splitContainer };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=prepare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prepare.js","names":["PMDOMParser"],"sources":["../../src/core/prepare.ts"],"sourcesContent":["import { DOMParser as PMDOMParser } from 'prosemirror-model'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport type { ResolvedEditorExtensions } from './extension'\nimport { createId, docToBlocks, type Block } from './model'\n\n/**\n * The editor's own markup for a block: a content element of `type` holding\n * `content`, followed by a group holding `children`. Extensions use it in\n * `prepareHTML` to turn containers such as `<details>` into blocks.\n */\nexport function blockElement(\n doc: Document,\n type: string,\n content: Node[],\n children: Node[] = [],\n data: Record<string, string> = {},\n): HTMLElement {\n const block = doc.createElement('div')\n block.dataset.blockId = ''\n const element = doc.createElement('div')\n element.dataset.contentType = type\n Object.assign(element.dataset, data)\n element.append(...content)\n block.append(element)\n if (children.some((node) => node.nodeType === 1 || node.textContent?.trim())) {\n const group = doc.createElement('div')\n group.className = 'dou-editor__group'\n group.append(...children)\n block.append(group)\n }\n return block\n}\n\n/**\n * Split a container's children into its own text (the first paragraph, or\n * the inline nodes before the first block element) and the blocks after it.\n */\nexport function splitContainer(element: Element): { content: Node[]; children: Node[] } {\n const nodes = Array.from(element.childNodes)\n while (nodes.length && isBlank(nodes[0]!)) nodes.shift()\n const first = nodes[0]\n if (first instanceof Element && first.tagName === 'P')\n return { content: Array.from(first.childNodes), children: nodes.slice(1) }\n const content: Node[] = []\n while (nodes.length && !isBlockElement(nodes[0]!)) content.push(nodes.shift()!)\n return { content, children: nodes }\n}\n\nconst BLOCK_TAGS = new Set([\n 'ADDRESS',\n 'ARTICLE',\n 'ASIDE',\n 'BLOCKQUOTE',\n 'DETAILS',\n 'DIV',\n 'DL',\n 'FIGURE',\n 'FOOTER',\n 'H1',\n 'H2',\n 'H3',\n 'H4',\n 'H5',\n 'H6',\n 'HEADER',\n 'HR',\n 'OL',\n 'P',\n 'PRE',\n 'SECTION',\n 'TABLE',\n 'UL',\n])\n\nconst isBlank = (node: Node) => node.nodeType === 3 && !node.textContent?.trim()\n\nconst isBlockElement = (node: Node) => node instanceof Element && BLOCK_TAGS.has(node.tagName)\n\n/** Run every extension's `prepareHTML` over the markup. */\nexport function prepareElement(root: HTMLElement, kit: ResolvedEditorExtensions) {\n for (const extension of kit.extensions) extension.prepareHTML?.(root)\n}\n\n/** Read HTML as blocks, with the block types, inline nodes and marks of `kit`. */\nexport function parseHTML(html: string, kit: ResolvedEditorExtensions): Block[] {\n const doc = new DOMParser().parseFromString(html, 'text/html')\n prepareElement(doc.body, kit)\n const blocks = docToBlocks(PMDOMParser.fromSchema(kit.schema).parse(doc.body))\n // read blocks are shared and read-only: copy the ones without an id\n const fill = (list: Block[]): Block[] =>\n list.map((block) => ({ ...block, id: block.id || createId(), children: fill(block.children) }))\n return fill(blocks)\n}\n\n/** Pasted HTML goes through the extensions' `prepareHTML` too. */\nexport function preparePastePlugin(kit: ResolvedEditorExtensions): Plugin {\n return new Plugin({\n key: new PluginKey('dou-editor-paste'),\n props: {\n transformPastedHTML(html) {\n if (!kit.extensions.some((extension) => extension.prepareHTML)) return html\n const doc = new DOMParser().parseFromString(html, 'text/html')\n prepareElement(doc.body, kit)\n return doc.body.innerHTML\n },\n },\n })\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,aACd,KACA,MACA,SACA,WAAmB,CAAC,GACpB,OAA+B,CAAC,GACnB;CACb,MAAM,QAAQ,IAAI,cAAc,KAAK;CACrC,MAAM,QAAQ,UAAU;CACxB,MAAM,UAAU,IAAI,cAAc,KAAK;CACvC,QAAQ,QAAQ,cAAc;CAC9B,OAAO,OAAO,QAAQ,SAAS,IAAI;CACnC,QAAQ,OAAO,GAAG,OAAO;CACzB,MAAM,OAAO,OAAO;CACpB,IAAI,SAAS,MAAM,SAAS,KAAK,aAAa,KAAK,KAAK,aAAa,KAAK,CAAC,GAAG;EAC5E,MAAM,QAAQ,IAAI,cAAc,KAAK;EACrC,MAAM,YAAY;EAClB,MAAM,OAAO,GAAG,QAAQ;EACxB,MAAM,OAAO,KAAK;CACpB;CACA,OAAO;AACT;;;;;AAMA,SAAgB,eAAe,SAAyD;CACtF,MAAM,QAAQ,MAAM,KAAK,QAAQ,UAAU;CAC3C,OAAO,MAAM,UAAU,QAAQ,MAAM,EAAG,GAAG,MAAM,MAAM;CACvD,MAAM,QAAQ,MAAM;CACpB,IAAI,iBAAiB,WAAW,MAAM,YAAY,KAChD,OAAO;EAAE,SAAS,MAAM,KAAK,MAAM,UAAU;EAAG,UAAU,MAAM,MAAM,CAAC;CAAE;CAC3E,MAAM,UAAkB,CAAC;CACzB,OAAO,MAAM,UAAU,CAAC,eAAe,MAAM,EAAG,GAAG,QAAQ,KAAK,MAAM,MAAM,CAAE;CAC9E,OAAO;EAAE;EAAS,UAAU;CAAM;AACpC;AAEA,IAAM,6BAAa,IAAI,IAAI;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,IAAM,WAAW,SAAe,KAAK,aAAa,KAAK,CAAC,KAAK,aAAa,KAAK;AAE/E,IAAM,kBAAkB,SAAe,gBAAgB,WAAW,WAAW,IAAI,KAAK,OAAO;;AAG7F,SAAgB,eAAe,MAAmB,KAA+B;CAC/E,KAAK,MAAM,aAAa,IAAI,YAAY,UAAU,cAAc,IAAI;AACtE;;AAGA,SAAgB,UAAU,MAAc,KAAwC;CAC9E,MAAM,MAAM,IAAI,UAAU,CAAC,CAAC,gBAAgB,MAAM,WAAW;CAC7D,eAAe,IAAI,MAAM,GAAG;CAC5B,MAAM,SAAS,YAAYA,YAAY,WAAW,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC;CAE7E,MAAM,QAAQ,SACZ,KAAK,KAAK,WAAW;EAAE,GAAG;EAAO,IAAI,MAAM,MAAM,SAAS;EAAG,UAAU,KAAK,MAAM,QAAQ;CAAE,EAAE;CAChG,OAAO,KAAK,MAAM;AACpB;;AAGA,SAAgB,mBAAmB,KAAuC;CACxE,OAAO,IAAI,OAAO;EAChB,KAAK,IAAI,UAAU,kBAAkB;EACrC,OAAO,EACL,oBAAoB,MAAM;GACxB,IAAI,CAAC,IAAI,WAAW,MAAM,cAAc,UAAU,WAAW,GAAG,OAAO;GACvE,MAAM,MAAM,IAAI,UAAU,CAAC,CAAC,gBAAgB,MAAM,WAAW;GAC7D,eAAe,IAAI,MAAM,GAAG;GAC5B,OAAO,IAAI,KAAK;EAClB,EACF;CACF,CAAC;AACH"}
|