@dooc-dev/dooc 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +26 -2
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +244 -0
  4. package/dist/code.global.js +285 -0
  5. package/dist/compiler/bundle-urls.d.ts +3 -0
  6. package/dist/compiler/bundle-urls.js +294 -0
  7. package/dist/compiler/bundle.d.ts +2 -0
  8. package/dist/compiler/bundle.js +100 -0
  9. package/dist/compiler/check-prose.d.ts +11 -0
  10. package/dist/compiler/check-prose.js +138 -0
  11. package/dist/compiler/check-types.d.ts +44 -0
  12. package/dist/compiler/check-types.js +1 -0
  13. package/dist/compiler/check.d.ts +5 -0
  14. package/dist/compiler/check.js +450 -0
  15. package/dist/compiler/code-blocks.d.ts +17 -0
  16. package/dist/compiler/code-blocks.js +198 -0
  17. package/dist/compiler/code-languages.d.ts +10 -0
  18. package/dist/compiler/code-languages.js +109 -0
  19. package/dist/compiler/compile.d.ts +95 -0
  20. package/dist/compiler/compile.js +410 -0
  21. package/dist/compiler/config.d.ts +7 -0
  22. package/dist/compiler/config.js +52 -0
  23. package/dist/compiler/dialect.d.ts +24 -0
  24. package/dist/compiler/dialect.js +198 -0
  25. package/dist/compiler/drawio.d.ts +26 -0
  26. package/dist/compiler/drawio.js +176 -0
  27. package/dist/compiler/figures.d.ts +14 -0
  28. package/dist/compiler/figures.js +221 -0
  29. package/dist/compiler/file-cache.d.ts +14 -0
  30. package/dist/compiler/file-cache.js +61 -0
  31. package/dist/compiler/files.d.ts +29 -0
  32. package/dist/compiler/files.js +431 -0
  33. package/dist/compiler/fonts.d.ts +10 -0
  34. package/dist/compiler/fonts.js +54 -0
  35. package/dist/compiler/format.d.ts +1 -0
  36. package/dist/compiler/format.js +42 -0
  37. package/dist/compiler/from-mdast.d.ts +16 -0
  38. package/dist/compiler/from-mdast.js +142 -0
  39. package/dist/compiler/icons.d.ts +14 -0
  40. package/dist/compiler/icons.js +85 -0
  41. package/dist/compiler/language-codes.d.ts +7 -0
  42. package/dist/compiler/language-codes.js +17 -0
  43. package/dist/compiler/locales.d.ts +7 -0
  44. package/dist/compiler/locales.js +67 -0
  45. package/dist/compiler/manifest.d.ts +6 -0
  46. package/dist/compiler/manifest.js +30 -0
  47. package/dist/compiler/ops.d.ts +44 -0
  48. package/dist/compiler/ops.js +362 -0
  49. package/dist/compiler/public-file.d.ts +2 -0
  50. package/dist/compiler/public-file.js +93 -0
  51. package/dist/compiler/serialize.d.ts +3 -0
  52. package/dist/compiler/serialize.js +16 -0
  53. package/dist/compiler/terms.d.ts +9 -0
  54. package/dist/compiler/terms.js +44 -0
  55. package/dist/compiler/textfile-remote.d.ts +12 -0
  56. package/dist/compiler/textfile-remote.js +124 -0
  57. package/dist/compiler/textfile.d.ts +9 -0
  58. package/dist/compiler/textfile.js +113 -0
  59. package/dist/compiler/theme-variants.d.ts +4 -0
  60. package/dist/compiler/theme-variants.js +22 -0
  61. package/dist/compiler/zip.d.ts +6 -0
  62. package/dist/compiler/zip.js +48 -0
  63. package/dist/core/blocks.d.ts +99 -0
  64. package/dist/core/blocks.js +29 -0
  65. package/dist/core/code-file.d.ts +12 -0
  66. package/dist/core/code-file.js +68 -0
  67. package/dist/core/content-metadata.d.ts +48 -0
  68. package/dist/core/content-metadata.js +264 -0
  69. package/dist/core/content-types.d.ts +45 -0
  70. package/dist/core/content-types.js +1 -0
  71. package/dist/core/figure-frame.d.ts +12 -0
  72. package/dist/core/figure-frame.js +108 -0
  73. package/dist/core/file-embed.d.ts +13 -0
  74. package/dist/core/file-embed.js +43 -0
  75. package/dist/core/files.d.ts +15 -0
  76. package/dist/core/files.js +59 -0
  77. package/dist/core/flow-base-css.generated.d.ts +1 -0
  78. package/dist/core/flow-base-css.generated.js +4 -0
  79. package/dist/core/flow-css.d.ts +9 -0
  80. package/dist/core/flow-css.js +54 -0
  81. package/dist/core/graph-common.d.ts +32 -0
  82. package/dist/core/graph-common.js +113 -0
  83. package/dist/core/graph.d.ts +117 -0
  84. package/dist/core/graph.js +480 -0
  85. package/dist/core/html.d.ts +29 -0
  86. package/dist/core/html.js +444 -0
  87. package/dist/core/icons.d.ts +1 -0
  88. package/dist/core/icons.js +1 -0
  89. package/dist/core/inline.d.ts +27 -0
  90. package/dist/core/inline.js +42 -0
  91. package/dist/core/language.d.ts +41 -0
  92. package/dist/core/language.js +44 -0
  93. package/dist/core/languages/ja.d.ts +2 -0
  94. package/dist/core/languages/ja.js +5 -0
  95. package/dist/core/markdown.d.ts +27 -0
  96. package/dist/core/markdown.js +154 -0
  97. package/dist/core/previewers.d.ts +37 -0
  98. package/dist/core/previewers.js +103 -0
  99. package/dist/core/routing.d.ts +20 -0
  100. package/dist/core/routing.js +64 -0
  101. package/dist/core/schema-comments.d.ts +14 -0
  102. package/dist/core/schema-comments.js +52 -0
  103. package/dist/core/schema-graph-flow.d.ts +9 -0
  104. package/dist/core/schema-graph-flow.js +379 -0
  105. package/dist/core/schema-graph-mount.d.ts +9 -0
  106. package/dist/core/schema-graph-mount.js +105 -0
  107. package/dist/core/schema-graph.d.ts +254 -0
  108. package/dist/core/schema-graph.js +981 -0
  109. package/dist/core/table-ui.d.ts +2 -0
  110. package/dist/core/table-ui.js +180 -0
  111. package/dist/core/taxonomy.d.ts +12 -0
  112. package/dist/core/taxonomy.js +49 -0
  113. package/dist/core/term-reference.d.ts +5 -0
  114. package/dist/core/term-reference.js +19 -0
  115. package/dist/core/terms-ui.d.ts +6 -0
  116. package/dist/core/terms-ui.js +22 -0
  117. package/dist/core/textfile.d.ts +10 -0
  118. package/dist/core/textfile.js +39 -0
  119. package/dist/core/theme-variants.d.ts +3 -0
  120. package/dist/core/theme-variants.js +37 -0
  121. package/dist/core/toc-ui.d.ts +2 -0
  122. package/dist/core/toc-ui.js +161 -0
  123. package/dist/core/video-ui.d.ts +2 -0
  124. package/dist/core/video-ui.js +253 -0
  125. package/dist/core/workspace.d.ts +15 -0
  126. package/dist/core/workspace.js +28 -0
  127. package/dist/edit.global.js +305 -0
  128. package/dist/flow.global.js +4959 -0
  129. package/dist/index.d.ts +17 -0
  130. package/dist/index.js +31 -0
  131. package/dist/mcp.d.ts +24 -0
  132. package/dist/mcp.js +467 -0
  133. package/dist/plugins/actions.d.ts +45 -0
  134. package/dist/plugins/actions.js +125 -0
  135. package/dist/plugins/blocks.d.ts +92 -0
  136. package/dist/plugins/blocks.js +99 -0
  137. package/dist/plugins/index.d.ts +4 -0
  138. package/dist/plugins/index.js +1 -0
  139. package/dist/plugins/oauth.d.ts +14 -0
  140. package/dist/plugins/oauth.js +164 -0
  141. package/dist/plugins/runtime.d.ts +24 -0
  142. package/dist/plugins/runtime.js +339 -0
  143. package/dist/plugins/types.d.ts +56 -0
  144. package/dist/plugins/types.js +1 -0
  145. package/dist/plugins/view.d.ts +29 -0
  146. package/dist/plugins/view.js +60 -0
  147. package/dist/ui.global.js +77 -0
  148. package/dist/viewer/assets.d.ts +2 -0
  149. package/dist/viewer/assets.js +3 -0
  150. package/dist/viewer/build.d.ts +12 -0
  151. package/dist/viewer/build.js +55 -0
  152. package/dist/viewer/bundle-network.d.ts +2 -0
  153. package/dist/viewer/bundle-network.js +88 -0
  154. package/dist/viewer/code-file.d.ts +20 -0
  155. package/dist/viewer/code-file.js +42 -0
  156. package/dist/viewer/editor-tools.d.ts +38 -0
  157. package/dist/viewer/editor-tools.js +211 -0
  158. package/dist/viewer/file-references.d.ts +14 -0
  159. package/dist/viewer/file-references.js +38 -0
  160. package/dist/viewer/font-picker.d.ts +4 -0
  161. package/dist/viewer/font-picker.js +145 -0
  162. package/dist/viewer/font-settings.d.ts +11 -0
  163. package/dist/viewer/font-settings.js +50 -0
  164. package/dist/viewer/fonts/LICENSE.txt +119 -0
  165. package/dist/viewer/fonts/udev-gothic-regular.woff2 +0 -0
  166. package/dist/viewer/fonts.d.ts +2 -0
  167. package/dist/viewer/fonts.js +13 -0
  168. package/dist/viewer/git-diff.d.ts +3 -0
  169. package/dist/viewer/git-diff.js +33 -0
  170. package/dist/viewer/git-types.d.ts +73 -0
  171. package/dist/viewer/git-types.js +1 -0
  172. package/dist/viewer/git.d.ts +5 -0
  173. package/dist/viewer/git.js +337 -0
  174. package/dist/viewer/hmr.d.ts +53 -0
  175. package/dist/viewer/hmr.js +335 -0
  176. package/dist/viewer/i18n.d.ts +6 -0
  177. package/dist/viewer/i18n.js +65 -0
  178. package/dist/viewer/ja-lint.d.ts +3 -0
  179. package/dist/viewer/ja-lint.js +44 -0
  180. package/dist/viewer/language-picker.d.ts +2 -0
  181. package/dist/viewer/language-picker.js +233 -0
  182. package/dist/viewer/languages.d.ts +11 -0
  183. package/dist/viewer/languages.js +20 -0
  184. package/dist/viewer/locales/eng.json +294 -0
  185. package/dist/viewer/locales/jpn.json +294 -0
  186. package/dist/viewer/locales/zho.json +294 -0
  187. package/dist/viewer/nav-focus.d.ts +2 -0
  188. package/dist/viewer/nav-focus.js +64 -0
  189. package/dist/viewer/plugin-blocks.d.ts +3 -0
  190. package/dist/viewer/plugin-blocks.js +115 -0
  191. package/dist/viewer/plugin-config.d.ts +35 -0
  192. package/dist/viewer/plugin-config.js +57 -0
  193. package/dist/viewer/plugin-view.d.ts +3 -0
  194. package/dist/viewer/plugin-view.js +28 -0
  195. package/dist/viewer/plugins.d.ts +8 -0
  196. package/dist/viewer/plugins.js +214 -0
  197. package/dist/viewer/resize.d.ts +4 -0
  198. package/dist/viewer/resize.js +238 -0
  199. package/dist/viewer/resource-library.d.ts +29 -0
  200. package/dist/viewer/resource-library.js +104 -0
  201. package/dist/viewer/search.d.ts +16 -0
  202. package/dist/viewer/search.js +358 -0
  203. package/dist/viewer/serve.d.ts +206 -0
  204. package/dist/viewer/serve.js +3233 -0
  205. package/dist/viewer/site-icon.d.ts +8 -0
  206. package/dist/viewer/site-icon.js +56 -0
  207. package/dist/viewer/textfile.d.ts +9 -0
  208. package/dist/viewer/textfile.js +50 -0
  209. package/dist/viewer/theme-picker.d.ts +2 -0
  210. package/dist/viewer/theme-picker.js +135 -0
  211. package/dist/viewer/workspace.d.ts +4 -0
  212. package/dist/viewer/workspace.js +47 -0
  213. package/package.json +127 -5
@@ -0,0 +1,64 @@
1
+ /** In narrow trees, show the current level beneath its compact ancestor chain. */
2
+ export const focusNavTree = (root) => {
3
+ if (!root)
4
+ return;
5
+ const current = root.querySelector(".nav-row > a.cur")?.closest("[data-doc-id]");
6
+ const path = [];
7
+ for (let item = current; item; item = item.parentElement?.closest("[data-doc-id]")) {
8
+ path.unshift(item);
9
+ }
10
+ const width = root.clientWidth;
11
+ if (!width)
12
+ return;
13
+ const indent = 48 + Math.max(0, path.length - 2) * 29;
14
+ const ancestors = new Set(path.length > 2 && width < indent + 150 ? path.slice(0, -1) : []);
15
+ const items = [...root.querySelectorAll(".nav-item[data-doc-id]")];
16
+ const changed = items.some((item) => item.classList.contains("nav-ancestor") !== ancestors.has(item));
17
+ const collapsed = ".nav-list:has(> .nav-ancestor) > .nav-item:not(.nav-ancestor)";
18
+ const animate = changed &&
19
+ root.dataset.focusedDoc !== undefined &&
20
+ !matchMedia("(prefers-reduced-motion: reduce)").matches;
21
+ const siblings = [...root.querySelectorAll(".nav-list > .nav-item")];
22
+ const before = animate
23
+ ? siblings.map((item) => ({
24
+ item,
25
+ hidden: item.matches(collapsed),
26
+ height: item.getBoundingClientRect().height,
27
+ opacity: getComputedStyle(item).opacity,
28
+ }))
29
+ : [];
30
+ for (const item of items) {
31
+ item.classList.toggle("nav-ancestor", ancestors.has(item));
32
+ const list = item.querySelector(":scope > .nav-list");
33
+ if (!list)
34
+ continue;
35
+ const level = Number(item.parentElement?.style.getPropertyValue("--nav-depth") || 1);
36
+ list.style.setProperty("--nav-depth", String(ancestors.has(item) ? 2 : level + 1));
37
+ }
38
+ for (const item of siblings)
39
+ item.inert = item.matches(collapsed);
40
+ // Animate measured heights so siblings and Resources move together, including on reversal.
41
+ for (const previous of before) {
42
+ const { item } = previous;
43
+ const hidden = item.matches(collapsed);
44
+ if (hidden === previous.hidden)
45
+ continue;
46
+ for (const animation of item.getAnimations()) {
47
+ if (animation.id === "nav-collapse")
48
+ animation.cancel();
49
+ }
50
+ item.animate({
51
+ height: [`${previous.height}px`, `${item.getBoundingClientRect().height}px`],
52
+ opacity: [previous.opacity, hidden ? "0" : "1"],
53
+ visibility: "visible",
54
+ overflow: "hidden",
55
+ }, { id: "nav-collapse", duration: 260, easing: "cubic-bezier(.2, .8, .2, 1)" });
56
+ }
57
+ const id = current?.dataset.docId ?? "";
58
+ if (root.dataset.focusedDoc !== id) {
59
+ root.dataset.focusedDoc = id;
60
+ for (const item of ancestors)
61
+ item.classList.add("open");
62
+ current?.querySelector(":scope > .nav-row")?.scrollIntoView({ block: "nearest" });
63
+ }
64
+ };
@@ -0,0 +1,3 @@
1
+ import { type PluginBlock } from "../plugins/blocks.js";
2
+ export declare const pluginMarkdown: (text: string) => string;
3
+ export declare const pluginBlocksHtml: (blocks: PluginBlock[]) => string;
@@ -0,0 +1,115 @@
1
+ import { escapeHtml } from "../core/html.js";
2
+ import { parseMdast } from "../core/markdown.js";
3
+ import { pluginLink, } from "../plugins/blocks.js";
4
+ const link = (url, html) => pluginLink(url)
5
+ ? `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${html}</a>`
6
+ : html;
7
+ const markdownNode = (node) => {
8
+ const children = "children" in node
9
+ ? node.children.map((child) => markdownNode(child)).join("")
10
+ : "";
11
+ switch (node.type) {
12
+ case "root":
13
+ return children;
14
+ case "text":
15
+ case "html":
16
+ return escapeHtml(node.value);
17
+ case "paragraph":
18
+ return `<p>${children}</p>`;
19
+ case "heading":
20
+ return `<strong class="plugin-markdown-heading">${children}</strong>`;
21
+ case "strong":
22
+ return `<strong>${children}</strong>`;
23
+ case "emphasis":
24
+ return `<em>${children}</em>`;
25
+ case "delete":
26
+ return `<del>${children}</del>`;
27
+ case "inlineCode":
28
+ return `<code>${escapeHtml(node.value)}</code>`;
29
+ case "code":
30
+ return `<pre><code>${escapeHtml(node.value)}</code></pre>`;
31
+ case "link":
32
+ return link(node.url, children);
33
+ case "image":
34
+ return escapeHtml(node.alt ?? "");
35
+ case "list":
36
+ return node.ordered ? `<ol>${children}</ol>` : `<ul>${children}</ul>`;
37
+ case "listItem": {
38
+ const body = node.children
39
+ .map((child) => child.type === "paragraph"
40
+ ? child.children.map(markdownNode).join("")
41
+ : markdownNode(child))
42
+ .join("");
43
+ return node.checked === null || node.checked === undefined
44
+ ? `<li>${body}</li>`
45
+ : `<li class="plugin-task"><span aria-label="${node.checked ? "Completed" : "Incomplete"}">${node.checked ? "☑" : "☐"}</span><div>${body}</div></li>`;
46
+ }
47
+ case "blockquote":
48
+ return `<blockquote>${children}</blockquote>`;
49
+ case "break":
50
+ return "<br>";
51
+ case "thematicBreak":
52
+ return "<hr>";
53
+ default:
54
+ return children;
55
+ }
56
+ };
57
+ export const pluginMarkdown = (text) => markdownNode(parseMdast(text));
58
+ const binding = (edit) => edit ? ` data-block-edit="${escapeHtml(JSON.stringify(edit))}"` : "";
59
+ const inline = (value) => {
60
+ if (typeof value === "string")
61
+ return `<span>${escapeHtml(value)}</span>`;
62
+ let html = escapeHtml(value.text);
63
+ if (value.code)
64
+ html = `<code>${html}</code>`;
65
+ if (value.strong)
66
+ html = `<strong>${html}</strong>`;
67
+ if (value.url)
68
+ html = link(value.url, html);
69
+ if (value.tone)
70
+ html = `<span class="plugin-badge" data-tone="${value.tone}">${html}</span>`;
71
+ if (value.copy !== undefined)
72
+ html = `<span class="plugin-copy-value">${html}<button type="button" class="plugin-copy" data-block-copy="${escapeHtml(value.copy)}" aria-label="Copy" title="Copy" data-i18n-aria-label="Copy" data-i18n-title="Copy"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="2"/><path d="M13 4V3H3v10h1"/></svg></button></span>`;
73
+ return html;
74
+ };
75
+ const inlines = (items) => items.map(inline).join(" ");
76
+ const actions = (ids, composer = false) => ids
77
+ .map((id) => `<span data-block-action="${escapeHtml(id)}"${composer ? ' data-block-composer=""' : ""}></span>`)
78
+ .join("");
79
+ export const pluginBlocksHtml = (blocks) => blocks
80
+ .map((block) => {
81
+ switch (block.type) {
82
+ case "heading":
83
+ return `<h3 class="plugin-block-heading"${binding(block.edit)}>${escapeHtml(block.text)}</h3>`;
84
+ case "text":
85
+ return `<div class="plugin-block-text"${binding(block.edit)}>${block.markdown ? pluginMarkdown(block.text) : escapeHtml(block.text) || "—"}</div>`;
86
+ case "context":
87
+ return `<div class="plugin-block-context">${inlines(block.items)}</div>`;
88
+ case "fields":
89
+ return `<dl class="plugin-block-fields">${block.items.map((item) => `<div><dt>${escapeHtml(item.label)}</dt><dd${binding(item.edit)}>${inlines(item.items) || "—"}</dd></div>`).join("")}</dl>`;
90
+ case "columns":
91
+ return `<div class="plugin-block-columns"><div>${pluginBlocksHtml(block.main)}</div><aside>${pluginBlocksHtml(block.aside)}</aside></div>`;
92
+ case "section":
93
+ return `<details class="plugin-block-section"${block.collapsed ? "" : " open"}><summary>${escapeHtml(block.title)}</summary>${block.note ? `<p class="plugin-block-note">${escapeHtml(block.note)}</p>` : ""}${pluginBlocksHtml(block.blocks)}</details>`;
94
+ case "table":
95
+ return `<div class="plugin-block-table" tabindex="0" role="region" aria-label="Table" data-i18n-aria-label="Table"><table><thead><tr>${block.headers.map((h) => `<th scope="col">${escapeHtml(h)}</th>`).join("")}${block.rows.some((r) => r.actions?.length) ? '<th scope="col" aria-label="Actions" data-i18n-aria-label="Actions"></th>' : ""}</tr></thead><tbody>${block.rows.map((row) => `<tr>${row.cells.map((c) => `<td${binding(c.edit)}>${inlines(c.items) || "—"}</td>`).join("")}${block.rows.some((r) => r.actions?.length) ? `<td>${actions(row.actions ?? [])}</td>` : ""}</tr>`).join("")}</tbody></table></div>`;
96
+ case "code":
97
+ return `<div${binding(block.edit)}><pre class="plugin-block-code${block.diff ? " plugin-block-diff" : ""}"><code>${block.diff
98
+ ? block.text
99
+ .split("\n")
100
+ .map((line) => `<span data-tone="${line.startsWith("+") ? "success" : line.startsWith("-") ? "danger" : "neutral"}">${escapeHtml(line)}</span>`)
101
+ .join("\n")
102
+ : escapeHtml(block.text)}</code></pre></div>`;
103
+ case "message":
104
+ return `<div class="plugin-block-message"><div class="plugin-message-byline"><span class="plugin-avatar" aria-hidden="true">${escapeHtml(block.author.slice(0, 2).toUpperCase())}</span><strong>${escapeHtml(block.author)}</strong>${block.time ? `<span>${escapeHtml(block.time)}</span>` : ""}<span class="plugin-message-actions">${actions(block.actions ?? [])}</span></div><div class="plugin-message-body"${binding(block.edit)}>${pluginMarkdown(block.text) || "—"}</div>${block.reactions?.length ? `<div class="plugin-reactions">${block.reactions.map((r) => (r.action ? `<button type="button" data-block-action="${escapeHtml(r.action)}" data-block-values="${escapeHtml(JSON.stringify(r.values ?? {}))}" aria-pressed="${!!r.active}" title="${escapeHtml(r.label)}">${escapeHtml(r.label)} ${r.count}</button>` : `<span>${escapeHtml(r.label)} ${r.count}</span>`)).join("")}</div>` : ""}</div>`;
105
+ case "actions":
106
+ return `<div class="plugin-block-actions">${actions(block.ids, block.composer)}</div>`;
107
+ case "progress":
108
+ return `<div class="plugin-block-progress"><progress max="100" value="${block.value}" aria-label="${escapeHtml(block.label)}"></progress><span>${escapeHtml(block.label)} ${block.value}%</span></div>`;
109
+ case "divider":
110
+ return "<hr>";
111
+ default:
112
+ throw new Error("Invalid plugin block");
113
+ }
114
+ })
115
+ .join("");
@@ -0,0 +1,35 @@
1
+ import { type WorkspaceSource } from "../core/workspace.js";
2
+ import type { DataPlugin } from "../plugins/types.js";
3
+ import type { GitOrigin } from "./git-types.js";
4
+ export interface PluginMock {
5
+ origin: string;
6
+ /** Complete local mock authorization automatically when displaying a preview. */
7
+ autoConnect?: boolean;
8
+ handle: (request: Request) => Response | Promise<Response>;
9
+ }
10
+ export interface ViewerPlugins {
11
+ workspace?: WorkspaceSource;
12
+ gitOrigin?: GitOrigin;
13
+ allowedDomains?: string[];
14
+ setup?: (viewer: ViewerRuntime) => void | Promise<void>;
15
+ plugins: readonly DataPlugin[];
16
+ mocks: Readonly<Record<string, PluginMock>>;
17
+ }
18
+ export interface ViewerPluginRegistry {
19
+ /** Replacing an ID clears that plugin's connections and pending authorization attempts. */
20
+ register(plugin: DataPlugin, options?: {
21
+ mock?: PluginMock;
22
+ }): void;
23
+ unregister(id: string): void;
24
+ }
25
+ export interface ViewerRuntime extends ViewerPluginRegistry {
26
+ /** Replaces the host-owned workspace location; undefined restores local discovery. */
27
+ setWorkspace(workspace: WorkspaceSource | undefined): void;
28
+ /** The host owns origin operations and authentication; undefined disconnects it. */
29
+ setGitOrigin(origin: GitOrigin | undefined): void;
30
+ /** Replaces the network policy and refreshes open viewers; an empty list revokes all hosts. */
31
+ setAllowedDomains(domains: readonly string[]): void;
32
+ }
33
+ /** Viewer modules are explicitly injected; document roots never select executable plugins. */
34
+ export declare const loadViewerPlugins: (specifier?: string) => Promise<ViewerPlugins>;
35
+ export declare const validateViewerPlugins: (input: unknown) => ViewerPlugins;
@@ -0,0 +1,57 @@
1
+ import { createRequire } from "node:module";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { parsePreviewSettings } from "../core/previewers.js";
5
+ import { parseWorkspaceSource } from "../core/workspace.js";
6
+ import { httpsUrl, validatePlugins } from "../plugins/oauth.js";
7
+ import { validateGitOrigin } from "./git.js";
8
+ /** Viewer modules are explicitly injected; document roots never select executable plugins. */
9
+ export const loadViewerPlugins = async (specifier) => {
10
+ if (!specifier)
11
+ return { plugins: [], mocks: {} };
12
+ const path = isAbsolute(specifier) || specifier.startsWith(".")
13
+ ? resolve(specifier)
14
+ : createRequire(resolve("dooc-viewer.cjs")).resolve(specifier);
15
+ const { default: config } = await import(pathToFileURL(path).href);
16
+ return validateViewerPlugins(config);
17
+ };
18
+ export const validateViewerPlugins = (input) => {
19
+ const config = input;
20
+ if (!config || typeof config !== "object" || Array.isArray(config))
21
+ throw new Error("Viewer plugin module must export a configuration object");
22
+ if (config.setup !== undefined && typeof config.setup !== "function")
23
+ throw new Error("Viewer setup must be a function");
24
+ const plugins = validatePlugins(config.plugins);
25
+ if ("mounts" in config)
26
+ throw new Error("Use ![](service-url) instead of viewer mounts");
27
+ const mocks = config.mocks ?? {};
28
+ if (typeof mocks !== "object" || Array.isArray(mocks))
29
+ throw new Error("Viewer mocks must be an object");
30
+ const origins = new Set();
31
+ for (const [id, mock] of Object.entries(mocks)) {
32
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(id) ||
33
+ !mock ||
34
+ typeof mock.handle !== "function" ||
35
+ (mock.autoConnect !== undefined && typeof mock.autoConnect !== "boolean") ||
36
+ typeof mock.origin !== "string" ||
37
+ httpsUrl(mock.origin).origin !== mock.origin ||
38
+ !new URL(mock.origin).hostname.endsWith(".invalid") ||
39
+ origins.has(mock.origin))
40
+ throw new Error("Viewer mocks require distinct HTTPS .invalid origins and handlers");
41
+ origins.add(mock.origin);
42
+ }
43
+ return {
44
+ plugins,
45
+ mocks,
46
+ ...(config.gitOrigin === undefined
47
+ ? {}
48
+ : { gitOrigin: validateGitOrigin(config.gitOrigin) }),
49
+ ...(config.allowedDomains === undefined
50
+ ? {}
51
+ : { allowedDomains: parsePreviewSettings({ domains: config.allowedDomains }).domains }),
52
+ ...(config.setup === undefined ? {} : { setup: config.setup }),
53
+ ...(config.workspace === undefined
54
+ ? {}
55
+ : { workspace: parseWorkspaceSource(config.workspace) }),
56
+ };
57
+ };
@@ -0,0 +1,3 @@
1
+ import { type PluginView } from "../plugins/view.js";
2
+ /** Authored HTML cannot run scripts; auto height permits only a nonce-protected reporter. */
3
+ export declare const pluginViewHtml: (input: PluginView) => string;
@@ -0,0 +1,28 @@
1
+ import { escapeHtml } from "../core/html.js";
2
+ import { validatePluginView } from "../plugins/view.js";
3
+ import { pluginBlocksHtml } from "./plugin-blocks.js";
4
+ /** Authored HTML cannot run scripts; auto height permits only a nonce-protected reporter. */
5
+ export const pluginViewHtml = (input) => {
6
+ const view = validatePluginView(input);
7
+ if (view.format === "blocks")
8
+ return `<div class="plugin-blocks">${pluginBlocksHtml(view.blocks)}</div>`;
9
+ if (view.format === "html") {
10
+ const auto = view.height === "auto";
11
+ const nonce = auto ? crypto.randomUUID() : "";
12
+ const script = auto
13
+ ? `<script nonce="${nonce}">(()=>{const report=()=>parent.postMessage({doocPluginHeight:Math.ceil(document.body.getBoundingClientRect().height)},"*");new ResizeObserver(report).observe(document.body);report();})()</script>`
14
+ : "";
15
+ const source = `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';${auto ? ` script-src 'nonce-${nonce}';` : ""} base-uri 'none'; form-action 'none'"><meta name="referrer" content="no-referrer"><style>html,body{margin:0}body{display:flow-root}</style></head><body>${view.html}${script}</body></html>`;
16
+ return `<iframe title="Plugin content" sandbox="${auto ? "allow-scripts" : ""}"${auto ? ' data-plugin-auto-height=""' : ""} referrerpolicy="no-referrer" srcdoc="${escapeHtml(source)}" style="width:100%;height:${auto ? 120 : (view.height ?? 240)}px;border:0"></iframe>`;
17
+ }
18
+ switch (view.preset) {
19
+ case "card":
20
+ return `<article class="plugin-card"><strong>${escapeHtml(view.title)}</strong><p>${escapeHtml(view.text)}</p></article>`;
21
+ case "list":
22
+ return `<ul>${view.items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
23
+ case "table":
24
+ return `<table><thead><tr>${view.headers.map((header) => `<th scope="col">${escapeHtml(header)}</th>`).join("")}</tr></thead><tbody>${view.rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell === null ? "" : String(cell))}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
25
+ case "code":
26
+ return `<pre><code>${escapeHtml(JSON.stringify(view.value, null, 2))}</code></pre>`;
27
+ }
28
+ };
@@ -0,0 +1,8 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { DataPlugin } from "../plugins/types.js";
3
+ export type { GitWorkspace, StorageWorkspace, WorkspaceSource } from "../core/workspace.js";
4
+ import { type ViewerPluginRegistry, type ViewerPlugins } from "./plugin-config.js";
5
+ export type { GitContext, GitOrigin, GitOriginAction, GitOriginState, GitPullRequest, } from "./git-types.js";
6
+ export type { PluginMock, ViewerPluginRegistry, ViewerPlugins, ViewerRuntime, } from "./plugin-config.js";
7
+ /** Local viewer only: the cookie identifies a browser session, never a document-supplied user. */
8
+ export declare const createPluginHandler: (plugins: readonly DataPlugin[], presentation?: Pick<ViewerPlugins, "mocks">) => ((req: IncomingMessage, res: ServerResponse) => Promise<boolean>) & ViewerPluginRegistry;
@@ -0,0 +1,214 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { isPluginError, PluginError } from "../plugins/oauth.js";
3
+ import { createPluginRuntime } from "../plugins/runtime.js";
4
+ import { isLocalToolRequest } from "./editor-tools.js";
5
+ import { validateViewerPlugins, } from "./plugin-config.js";
6
+ const readBody = (req) => new Promise((resolve, reject) => {
7
+ let body = "";
8
+ let size = 0;
9
+ req.setEncoding("utf8");
10
+ req.on("data", (chunk) => {
11
+ size += Buffer.byteLength(chunk);
12
+ if (size > 65_536) {
13
+ body = "";
14
+ reject(new PluginError(413, "Plugin request is too large"));
15
+ }
16
+ else
17
+ body += chunk;
18
+ });
19
+ req.on("error", reject);
20
+ req.on("end", () => resolve(body));
21
+ });
22
+ const readInput = async (req) => {
23
+ const body = await readBody(req);
24
+ try {
25
+ const input = JSON.parse(body);
26
+ if (!input || typeof input !== "object" || Array.isArray(input))
27
+ throw new Error();
28
+ return input;
29
+ }
30
+ catch {
31
+ throw new PluginError(400, "Invalid plugin request");
32
+ }
33
+ };
34
+ /** Local viewer only: the cookie identifies a browser session, never a document-supplied user. */
35
+ export const createPluginHandler = (plugins, presentation = { mocks: {} }) => {
36
+ let config = validateViewerPlugins({ plugins, ...presentation });
37
+ let mocks = new Map(Object.entries(config.mocks));
38
+ let origins = new Map([...mocks].map(([id, mock]) => [mock.origin, { id, ...mock }]));
39
+ const transport = async (input, init) => {
40
+ if (!origins.size)
41
+ return fetch(input, init);
42
+ const request = new Request(input, init);
43
+ const mock = origins.get(new URL(request.url).origin);
44
+ return mock ? mock.handle(request) : fetch(input, init);
45
+ };
46
+ const runtime = createPluginRuntime(config.plugins, transport);
47
+ const cookie = `dooc-plugins-${randomBytes(8).toString("hex")}`;
48
+ let revision = 0;
49
+ const streams = new Set();
50
+ const notify = (res) => {
51
+ res.write(`data: ${cookie}:${revision}\n\n`);
52
+ };
53
+ const update = (next) => {
54
+ config = next;
55
+ mocks = new Map(Object.entries(config.mocks));
56
+ origins = new Map([...mocks].map(([id, mock]) => [mock.origin, { id, ...mock }]));
57
+ revision++;
58
+ for (const stream of streams)
59
+ notify(stream);
60
+ };
61
+ const registry = {
62
+ register(plugin, options = {}) {
63
+ const addition = validateViewerPlugins({
64
+ plugins: [plugin],
65
+ mocks: options.mock ? { [plugin.id]: options.mock } : {},
66
+ });
67
+ const next = validateViewerPlugins({
68
+ plugins: [...config.plugins.filter((item) => item.id !== plugin.id), plugin],
69
+ mocks: {
70
+ ...Object.fromEntries(Object.entries(config.mocks).filter(([id]) => id !== plugin.id)),
71
+ ...addition.mocks,
72
+ },
73
+ });
74
+ runtime.register(plugin);
75
+ update(next);
76
+ },
77
+ unregister(id) {
78
+ if (!config.plugins.some((plugin) => plugin.id === id))
79
+ return;
80
+ runtime.unregister(id);
81
+ update({
82
+ plugins: config.plugins.filter((plugin) => plugin.id !== id),
83
+ mocks: Object.fromEntries(Object.entries(config.mocks).filter(([key]) => key !== id)),
84
+ });
85
+ },
86
+ };
87
+ const handle = async (req, res) => {
88
+ const url = new URL(req.url ?? "/", "http://localhost");
89
+ if (url.pathname !== "/api/plugins" && !url.pathname.startsWith("/api/plugins/"))
90
+ return false;
91
+ res.setHeader("cache-control", "no-store");
92
+ res.setHeader("referrer-policy", "no-referrer");
93
+ res.setHeader("x-content-type-options", "nosniff");
94
+ res.setHeader("content-security-policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'");
95
+ const send = (status, body) => {
96
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
97
+ res.end(JSON.stringify(body));
98
+ };
99
+ try {
100
+ const host = req.headers.host ?? "";
101
+ if (!["localhost", "127.0.0.1", "[::1]"].some((name) => host === `${name}:${req.socket.localPort}`))
102
+ throw new PluginError(403, "Plugins require the local viewer");
103
+ if (req.method === "GET" && url.pathname === "/api/plugins/events") {
104
+ res.writeHead(200, {
105
+ "content-type": "text/event-stream",
106
+ connection: "keep-alive",
107
+ });
108
+ streams.add(res);
109
+ res.on("close", () => streams.delete(res));
110
+ notify(res);
111
+ return true;
112
+ }
113
+ const mockRoute = /^\/api\/plugins\/mock\/([a-z][a-z0-9-]{0,63})(\/.*)$/.exec(url.pathname);
114
+ if (mockRoute) {
115
+ const mock = mocks.get(mockRoute[1] ?? "");
116
+ const target = mock && `${mock.origin}${mockRoute[2]}`;
117
+ if (!mock ||
118
+ !config.plugins.some((plugin) => plugin.oauth.authorizationUrl === target))
119
+ throw new PluginError(404, "Mock authorization is not configured");
120
+ if (req.method !== "GET" &&
121
+ (req.method !== "POST" ||
122
+ req.headers.origin !== `http://${host}` ||
123
+ req.headers["content-type"]?.split(";")[0] !==
124
+ "application/x-www-form-urlencoded"))
125
+ throw new PluginError(403, "Mock consent requires a same-origin request");
126
+ const response = await mock.handle(new Request(`${target}${url.search}`, {
127
+ method: req.method,
128
+ ...(req.method === "POST"
129
+ ? {
130
+ body: await readBody(req),
131
+ headers: { "content-type": "application/x-www-form-urlencoded" },
132
+ }
133
+ : {}),
134
+ }));
135
+ const location = response.headers.get("location");
136
+ if (location && !location.startsWith(`http://${host}/api/plugins/callback/`))
137
+ throw new PluginError(400, "Invalid mock callback");
138
+ // Preserve the Origin header on local consent form submissions.
139
+ res.setHeader("referrer-policy", "same-origin");
140
+ res.setHeader("content-security-policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'");
141
+ res.writeHead(response.status, {
142
+ "content-type": response.headers.get("content-type") ?? "text/plain; charset=utf-8",
143
+ ...(location ? { location } : {}),
144
+ });
145
+ res.end(await response.text());
146
+ return true;
147
+ }
148
+ let session = req.headers.cookie
149
+ ?.split(";")
150
+ .map((part) => part.trim())
151
+ .find((part) => part.startsWith(`${cookie}=`))
152
+ ?.slice(cookie.length + 1) ?? "";
153
+ const callback = /^\/api\/plugins\/callback\/([a-z][a-z0-9-]{0,63})$/.exec(url.pathname);
154
+ if (req.method === "GET" && callback?.[1]) {
155
+ await runtime.callback(session, callback[1], `http://${host}${url.pathname}`, url.searchParams);
156
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
157
+ res.end('<!doctype html><html lang="en"><meta charset="utf-8"><title>Plugin connected</title><p>Connected. Close this tab and choose Load data in dooc.</p></html>');
158
+ return true;
159
+ }
160
+ if (url.pathname !== "/api/plugins")
161
+ throw new PluginError(404, "Plugin route not found");
162
+ if (req.method !== "POST" ||
163
+ !isLocalToolRequest(host, req.headers.origin, req.headers["content-type"]))
164
+ throw new PluginError(403, "Plugins require a same-origin local request");
165
+ const input = await readInput(req);
166
+ if (input.action === "resolve") {
167
+ const plugin = runtime.resolve(input.url);
168
+ send(200, {
169
+ plugin,
170
+ ...(plugin && mocks.get(plugin)?.autoConnect ? { autoConnect: true } : {}),
171
+ });
172
+ return true;
173
+ }
174
+ if (typeof input.plugin !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(input.plugin))
175
+ throw new PluginError(400, "Invalid plugin ID");
176
+ if (input.action === "connect") {
177
+ if (!runtime.hasSession(session)) {
178
+ session = runtime.createSession();
179
+ res.setHeader("set-cookie", `${cookie}=${session}; HttpOnly; SameSite=Lax; Path=/api/plugins; Max-Age=28800`);
180
+ }
181
+ const authorization = new URL(runtime.connect(session, input.plugin, `http://${host}/api/plugins/callback/${input.plugin}`));
182
+ const mock = origins.get(authorization.origin);
183
+ send(200, {
184
+ url: mock
185
+ ? `http://${host}/api/plugins/mock/${mock.id}${authorization.pathname}${authorization.search}`
186
+ : authorization.href,
187
+ });
188
+ }
189
+ else if (input.action === "disconnect") {
190
+ if (runtime.hasSession(session))
191
+ runtime.disconnect(session, input.plugin);
192
+ send(200, { disconnected: true });
193
+ }
194
+ else if (input.action === "read") {
195
+ send(200, await runtime.read(session, input.plugin, input.url));
196
+ }
197
+ else if (input.action === "actions") {
198
+ send(200, await runtime.actions(session, input.plugin, input.url));
199
+ }
200
+ else if (input.action === "act") {
201
+ send(200, await runtime.act(session, input.plugin, input.url, input.operation, input.requestId));
202
+ }
203
+ else
204
+ throw new PluginError(400, "Invalid plugin action");
205
+ }
206
+ catch (error) {
207
+ send(isPluginError(error) ? error.status : 502, {
208
+ error: isPluginError(error) ? error.message : "Plugin request failed",
209
+ });
210
+ }
211
+ return true;
212
+ };
213
+ return Object.assign(handle, registry);
214
+ };
@@ -0,0 +1,4 @@
1
+ /** Shared by editor, preview, and published pages; keep this function self-contained. */
2
+ export declare const mountPanelResize: () => void;
3
+ /** The navigation is replaced on refresh; keep listeners and height on its stable sidebar. */
4
+ export declare const mountToolsResize: () => (() => void);