agentic-documents 0.5.0 → 0.6.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/CHANGELOG.md +21 -0
- package/README.md +51 -8
- package/dist/{chunk-7R7ZPPRF.js → chunk-34C47PDC.js} +1 -1
- package/dist/{chunk-UPEGODUV.js → chunk-5IQX2W52.js} +1 -1
- package/dist/{chunk-S6MQDWFK.js → chunk-6UYJV2EJ.js} +19 -6
- package/dist/{chunk-E7B6T5WN.js → chunk-CN6UAW7B.js} +5 -3
- package/dist/chunk-DWVJIKVT.js +8 -0
- package/dist/{chunk-6QOYS63L.js → chunk-GQ5E6T7Z.js} +4 -2
- package/dist/{chunk-YNHZCIGO.js → chunk-JBAQEIOH.js} +5 -3
- package/dist/chunk-KC5KFB5R.js +354 -0
- package/dist/{chunk-FX755I7V.js → chunk-V6TBTOHW.js} +11 -8
- package/dist/cli.js +16 -14
- package/dist/client.d.ts +5 -5
- package/dist/{doc-mJPajXEQ.d.ts → doc-BXG9qG5X.d.ts} +4 -2
- package/dist/editor.css +37 -0
- package/dist/embed.d.ts +86 -0
- package/dist/embed.js +431 -0
- package/dist/formats.d.ts +6 -3
- package/dist/formats.js +14 -224
- package/dist/index.d.ts +12 -11
- package/dist/index.js +14 -12
- package/dist/live-worker.d.ts +2 -2
- package/dist/live-worker.js +7 -6
- package/dist/live.d.ts +2 -2
- package/dist/live.js +5 -4
- package/dist/{mcp-ESSEG46C.js → mcp-ZTUTC4CS.js} +5 -4
- package/dist/{export-Cwx276UP.d.ts → render-463xHb-x.d.ts} +3 -4
- package/dist/{review-Hgq8OgvD.d.ts → review-Ds-i5Vnc.d.ts} +2 -2
- package/dist/{service-CqKE9DKz.d.ts → service-Ck6oX-Kq.d.ts} +2 -2
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +65 -65
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +7 -6
- package/package.json +28 -11
- package/dist/chunk-L6W2AQUF.js +0 -258
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `agentic-documents/ui`: `mountEditor(element, { id, client, principal, room? })` and the `DocumentEditor` React component
|
|
8
|
+
render the editor in a host app, with live co-editing through agentic-realtime when a room is given. Its styles are
|
|
9
|
+
in `agentic-documents/ui.css`. `react` and `react-dom` are optional peer dependencies; the Tiptap packages and
|
|
10
|
+
`agentic-design` are now dependencies.
|
|
11
|
+
- The edit log has a new op, `live`. The editor writes one `live` entry when an agent principal types, so its text is
|
|
12
|
+
marked as agent work. Its counts are 0.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- The ODT export uses `agentic-formats`. The local ODT writer and the `fflate` dependency are gone. `exportOdt` and
|
|
17
|
+
`ODT_MIME` stay as thin wrappers. The ODT file now comes from the neutral text model, so its XML differs from before.
|
|
18
|
+
- The ODT export holds less than before, because the agentic-formats text model has no block for a quote, a rule, or a
|
|
19
|
+
code block, no list start number, one paragraph for each list item, and no WebP image. The text stays.
|
|
20
|
+
`ExportResult` has a `warnings` field that names each loss. It is empty for `md` and `html`. The field is required, so
|
|
21
|
+
code that builds an `ExportResult` by hand must add it. The CLI prints each warning on stderr. The HTTP API sends them
|
|
22
|
+
in the `x-export-warnings` header.
|
|
23
|
+
|
|
3
24
|
## 0.5.0
|
|
4
25
|
|
|
5
26
|
### Breaking
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
Write documents with people and agents in real time.
|
|
9
9
|
|
|
10
|
-
> Status: early, version 0.
|
|
10
|
+
> Status: early, version 0.6.0. The document model, the text operations, the CLI, the MCP server,
|
|
11
11
|
> the Worker API, live editing in a room, and a small web UI work. See [UI](#ui).
|
|
12
12
|
|
|
13
13
|
## Install
|
|
@@ -195,7 +195,7 @@ try {
|
|
|
195
195
|
}
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
- One method per route: `create`, `read`, `outline`, `edits`, `insert`, `replace`, `delete`, `append`,
|
|
198
|
+
- One method per route: `create`, `list`, `read`, `rename`, `remove`, `outline`, `edits`, `insert`, `replace`, `delete`, `append`,
|
|
199
199
|
`suggest`, `suggestions`, `accept`, `reject`, `comment`, `reply`, `resolve`, `reopen`, `threads`,
|
|
200
200
|
`export`, `room`, `me`, `health`, `state`, and `checkpoint`. The WebSocket route is not a method. Use
|
|
201
201
|
`LiveDoc` from `agentic-documents/live` for it.
|
|
@@ -205,6 +205,47 @@ try {
|
|
|
205
205
|
- The client never retries. A write is not safe to send twice.
|
|
206
206
|
- Pass `fetch` to use your own, for a Worker service binding or for tests.
|
|
207
207
|
|
|
208
|
+
## Embed the editor
|
|
209
|
+
|
|
210
|
+
`agentic-documents/ui` renders the editor inside your own app. You give it a document id, a client, the principal who
|
|
211
|
+
uses it, and an optional live room. With a room, people and agents edit together through agentic-realtime and see each
|
|
212
|
+
other. It needs `react` and `react-dom` 19 (peer dependencies).
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { createClient } from "agentic-documents/client";
|
|
216
|
+
import { mountEditor } from "agentic-documents/ui";
|
|
217
|
+
import "agentic-design/styles.css";
|
|
218
|
+
import "agentic-documents/ui.css";
|
|
219
|
+
|
|
220
|
+
const client = createClient({ baseUrl: "https://docs.example.com", token });
|
|
221
|
+
const principal = await client.me(); // { kind, name, role }
|
|
222
|
+
const editor = mountEditor(document.getElementById("editor")!, {
|
|
223
|
+
id: "plan",
|
|
224
|
+
client,
|
|
225
|
+
principal,
|
|
226
|
+
room: { url: "wss://docs.example.com/api/v1/docs/plan/ws", token },
|
|
227
|
+
});
|
|
228
|
+
// Later: editor.update({ ...options, id: "other" }), or await editor.unmount() to save this tab's changes and leave.
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- In a React app, render `<DocumentEditor id client principal room onError onLeave />` instead. `onLeave` gets the
|
|
232
|
+
promise of the last save.
|
|
233
|
+
- `client` needs only `state` and `checkpoint`. A client from `createClient` has them, or pass your own. Keep the
|
|
234
|
+
same client object between renders: a new one opens the document again.
|
|
235
|
+
- `principal` needs a `kind` (one of the principal kinds) and a `name`. A missing or unknown kind throws a
|
|
236
|
+
`TypeError`. It is never given a default. Only the `role` `editor` can change the text. Any other role, or no role,
|
|
237
|
+
opens it read only. The server still checks every write against the token. Use the principal that `client.me()`
|
|
238
|
+
returns for the same token, so the editor labels the text with the kind that the server gives the token.
|
|
239
|
+
- When an agent principal types, the editor writes one `live` entry to the edit log for its Yjs client. The editor
|
|
240
|
+
and the web UI then mark that text as agent work for everyone who opens the document.
|
|
241
|
+
- `room.url` is the WebSocket address of the document room. In a browser the token goes in a WebSocket subprotocol.
|
|
242
|
+
Pass `room.WebSocket` for a room that takes its token another way. Without a room, the editor saves through the
|
|
243
|
+
client and shows no other people.
|
|
244
|
+
- The editor saves 1.5 seconds after this tab changes the text, and again on `unmount`. The promise from `unmount`
|
|
245
|
+
rejects when that last save fails. A save never makes a document, so a removed document stays removed.
|
|
246
|
+
- The styles sit under the `.adoc` class and use the agentic-design tokens. Load `agentic-design/styles.css` first.
|
|
247
|
+
- File import and export are in `agentic-documents/formats` (see [File formats](#file-formats)).
|
|
248
|
+
|
|
208
249
|
## File formats
|
|
209
250
|
|
|
210
251
|
`agentic-documents/formats` imports and exports files, so an app that embeds the editor needs no mapping
|
|
@@ -215,7 +256,7 @@ code of its own. ODT and DOCX go through the neutral text model of
|
|
|
215
256
|
import { exportFile, importFile } from "agentic-documents/formats";
|
|
216
257
|
|
|
217
258
|
const { doc, title, warnings } = importFile(bytes, "docx", { kind: "human", name: "Ada" });
|
|
218
|
-
const odt = exportFile(doc, "odt", { title }); // { contentType, data
|
|
259
|
+
const odt = exportFile(doc, "odt", { title }); // { contentType, data (Uint8Array for odt), warnings }
|
|
219
260
|
```
|
|
220
261
|
|
|
221
262
|
- `importFile(input, format, author, { limits? })` reads `md` (text or UTF-8 bytes), `odt`, or `docx` (bytes) into a
|
|
@@ -250,10 +291,10 @@ const odt = exportFile(doc, "odt", { title }); // { contentType, data: Uint8Arra
|
|
|
250
291
|
one application and undoes the other, so the text and the edit log hold it once. An accept
|
|
251
292
|
beats a reject made at the same moment.
|
|
252
293
|
- Export to ODT (OpenDocument text): headings, paragraphs, bold, italic, strike, code, links, lists,
|
|
253
|
-
|
|
254
|
-
stored first. The CLI and the API send the file bytes.
|
|
255
|
-
|
|
256
|
-
in the
|
|
294
|
+
tables, and PNG, JPEG, and GIF images as embedded files. `agentic-formats` writes the file, with the
|
|
295
|
+
mimetype stored first. The CLI and the API send the file bytes. A quote, a code block, a rule, a list
|
|
296
|
+
start number, and a WebP image are not kept in full (see Limits). Each loss is in `warnings` (SDK and
|
|
297
|
+
MCP), on stderr (CLI), and in the `x-export-warnings` header (API).
|
|
257
298
|
- Export to Markdown and to self-contained HTML. The HTML loads nothing from other hosts. An image
|
|
258
299
|
stored in the document as a `data:` URI (PNG, JPEG, GIF, or WebP) shows. Any other image becomes
|
|
259
300
|
its alt text, because a remote image tells its host who opened the file. `remoteImages` turns
|
|
@@ -359,7 +400,9 @@ Limits of live editing:
|
|
|
359
400
|
of code text, a horizontal rule is left out, a numbered list starts at 1, and a task list becomes a list that starts
|
|
360
401
|
each item with `☐` or `☑`. On import, underline, superscript, and subscript are left out, and a footnote becomes
|
|
361
402
|
text in brackets, and code text keeps only its code mark. Each one is named in `warnings`.
|
|
362
|
-
- ODT export:
|
|
403
|
+
- ODT export: a quote becomes plain paragraphs, a code block becomes a paragraph of code text, a rule is
|
|
404
|
+
left out, a numbered list starts at 1, a list item keeps one paragraph, and a WebP image becomes its alt text.
|
|
405
|
+
Comments and suggestions are not written. A code block loses its language. A table
|
|
363
406
|
cell span is not written (the model has none yet). A task list becomes a bullet list that starts each
|
|
364
407
|
item with `☐` or `☑`. A remote image is not linked, because an office program would ask its host
|
|
365
408
|
for it when the file opens. It becomes its alt text.
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
EXPORT_FORMATS,
|
|
3
|
-
exportDoc
|
|
4
|
-
} from "./chunk-L6W2AQUF.js";
|
|
5
1
|
import {
|
|
6
2
|
Suggestions,
|
|
7
3
|
Threads
|
|
8
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-5IQX2W52.js";
|
|
5
|
+
import {
|
|
6
|
+
EXPORT_FORMATS,
|
|
7
|
+
exportFile,
|
|
8
|
+
exportText,
|
|
9
|
+
toBase64
|
|
10
|
+
} from "./chunk-KC5KFB5R.js";
|
|
9
11
|
import {
|
|
10
12
|
DocError
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-GQ5E6T7Z.js";
|
|
14
|
+
|
|
15
|
+
// src/export.ts
|
|
16
|
+
function exportDoc(doc, format, options = {}) {
|
|
17
|
+
if (format === "odt") {
|
|
18
|
+
const file = exportFile(doc, "odt");
|
|
19
|
+
return { format, contentType: file.contentType, encoding: "base64", data: toBase64(file.data), warnings: file.warnings };
|
|
20
|
+
}
|
|
21
|
+
const contentType = format === "html" ? "text/html; charset=utf-8" : "text/markdown; charset=utf-8";
|
|
22
|
+
return { format, contentType, encoding: "utf8", data: exportText(doc, format, options), warnings: [] };
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
// src/commands.ts
|
|
14
26
|
import { z } from "zod";
|
|
@@ -146,5 +158,6 @@ var commands = [
|
|
|
146
158
|
];
|
|
147
159
|
|
|
148
160
|
export {
|
|
161
|
+
exportDoc,
|
|
149
162
|
commands
|
|
150
163
|
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MAX_LIVE_BYTES
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JBAQEIOH.js";
|
|
4
4
|
import {
|
|
5
5
|
AgenticDoc,
|
|
6
6
|
DOC_ERROR_CODES,
|
|
7
|
-
DocError
|
|
7
|
+
DocError
|
|
8
|
+
} from "./chunk-GQ5E6T7Z.js";
|
|
9
|
+
import {
|
|
8
10
|
PRINCIPAL_KINDS
|
|
9
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-DWVJIKVT.js";
|
|
10
12
|
|
|
11
13
|
// src/live.ts
|
|
12
14
|
import * as Y from "yjs";
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PRINCIPAL_KINDS
|
|
3
|
+
} from "./chunk-DWVJIKVT.js";
|
|
4
|
+
|
|
1
5
|
// src/errors.ts
|
|
2
6
|
var DOC_ERROR_CODES = ["NO_MATCH", "AMBIGUOUS_MATCH", "STALE_ANCHOR", "NO_SECTION", "INVALID_AUTHOR", "INVALID_INPUT", "NO_THREAD", "NO_SUGGESTION", "ALREADY_DECIDED", "FORBIDDEN", "NO_DOCUMENT", "DOCUMENT_EXISTS", "LOCKED", "CONFLICT", "MISCONFIGURED"];
|
|
3
7
|
var DocError = class extends Error {
|
|
@@ -314,7 +318,6 @@ function settle(ydoc) {
|
|
|
314
318
|
var PREAMBLE = "preamble";
|
|
315
319
|
var FRAGMENT = "default";
|
|
316
320
|
var CONTEXT_CHARS = 20;
|
|
317
|
-
var PRINCIPAL_KINDS = ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
|
|
318
321
|
var isSection = (t) => "section" in t;
|
|
319
322
|
var isAnchor = (t) => "anchor" in t;
|
|
320
323
|
function checkAuthor(author) {
|
|
@@ -562,7 +565,6 @@ export {
|
|
|
562
565
|
blocksToMarkdown,
|
|
563
566
|
schema,
|
|
564
567
|
APPLIED,
|
|
565
|
-
PRINCIPAL_KINDS,
|
|
566
568
|
checkAuthor,
|
|
567
569
|
parseAuthor,
|
|
568
570
|
AgenticDoc
|
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
Suggestions,
|
|
3
3
|
Threads,
|
|
4
4
|
isPerson
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-5IQX2W52.js";
|
|
6
6
|
import {
|
|
7
7
|
AgenticDoc,
|
|
8
|
-
DocError
|
|
8
|
+
DocError
|
|
9
|
+
} from "./chunk-GQ5E6T7Z.js";
|
|
10
|
+
import {
|
|
9
11
|
PRINCIPAL_KINDS
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-DWVJIKVT.js";
|
|
11
13
|
|
|
12
14
|
// src/service.ts
|
|
13
15
|
import * as Y from "yjs";
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgenticDoc,
|
|
3
|
+
DocError,
|
|
4
|
+
checkAuthor
|
|
5
|
+
} from "./chunk-GQ5E6T7Z.js";
|
|
6
|
+
|
|
7
|
+
// src/formats.ts
|
|
8
|
+
import { DEFAULT_LIMITS, FormatError, readDocx, readOdt, writeDocx, writeOdt } from "agentic-formats";
|
|
9
|
+
|
|
10
|
+
// src/render.ts
|
|
11
|
+
var EXPORT_FORMATS = ["md", "html", "odt"];
|
|
12
|
+
var esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13
|
+
var url = (value) => /^(https?:|mailto:|[^:]*$)/i.test(String(value ?? "").trim()) ? esc(String(value).trim()) : "#";
|
|
14
|
+
var integer = (value, fallback) => Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : fallback;
|
|
15
|
+
var INLINE_IMAGE = /^data:image\/(png|jpeg|gif|webp);base64,[a-z0-9+/=]+$/i;
|
|
16
|
+
var textOf = (node) => node.text ?? (node.content ?? []).map(textOf).join("");
|
|
17
|
+
var MARK_TAGS = { bold: "strong", italic: "em", strike: "s", code: "code" };
|
|
18
|
+
function image(node, remote) {
|
|
19
|
+
const src = String(node.attrs?.src ?? "").trim();
|
|
20
|
+
const alt = esc(node.attrs?.alt);
|
|
21
|
+
if (INLINE_IMAGE.test(src)) return `<img src="${src}" alt="${alt}">`;
|
|
22
|
+
if (remote && url(src) !== "#") return `<img src="${url(src)}" alt="${alt}">`;
|
|
23
|
+
return `<span class="image">[image${alt ? `: ${alt}` : ""}]</span>`;
|
|
24
|
+
}
|
|
25
|
+
function inline(node, remote) {
|
|
26
|
+
if (node.type === "hardBreak") return "<br>";
|
|
27
|
+
if (node.type === "image") return image(node, remote);
|
|
28
|
+
return (node.marks ?? []).reduce((html, mark) => {
|
|
29
|
+
if (mark.type === "link") return `<a href="${url(mark.attrs?.href)}">${html}</a>`;
|
|
30
|
+
const tag = MARK_TAGS[mark.type];
|
|
31
|
+
return tag ? `<${tag}>${html}</${tag}>` : html;
|
|
32
|
+
}, esc(node.text));
|
|
33
|
+
}
|
|
34
|
+
function block(node, remote) {
|
|
35
|
+
const kids = (node.content ?? []).map((n) => block(n, remote)).join("");
|
|
36
|
+
const text = (node.content ?? []).map((n) => inline(n, remote)).join("");
|
|
37
|
+
switch (node.type) {
|
|
38
|
+
case "paragraph":
|
|
39
|
+
return `<p>${text}</p>
|
|
40
|
+
`;
|
|
41
|
+
case "heading": {
|
|
42
|
+
const level = Math.min(6, Math.max(1, integer(node.attrs?.level, 1)));
|
|
43
|
+
return `<h${level}>${text}</h${level}>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
case "blockquote":
|
|
47
|
+
return `<blockquote>
|
|
48
|
+
${kids}</blockquote>
|
|
49
|
+
`;
|
|
50
|
+
case "codeBlock": {
|
|
51
|
+
const lang = node.attrs?.language ? ` class="language-${esc(node.attrs.language)}"` : "";
|
|
52
|
+
return `<pre><code${lang}>${esc(textOf(node))}</code></pre>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
case "horizontalRule":
|
|
56
|
+
return "<hr>\n";
|
|
57
|
+
case "bulletList":
|
|
58
|
+
case "taskList":
|
|
59
|
+
return `<ul>
|
|
60
|
+
${kids}</ul>
|
|
61
|
+
`;
|
|
62
|
+
case "orderedList":
|
|
63
|
+
return `<ol start="${integer(node.attrs?.start, 1)}">
|
|
64
|
+
${kids}</ol>
|
|
65
|
+
`;
|
|
66
|
+
case "listItem":
|
|
67
|
+
return `<li>${kids}</li>
|
|
68
|
+
`;
|
|
69
|
+
case "taskItem":
|
|
70
|
+
return `<li><input type="checkbox" disabled${node.attrs?.checked ? " checked" : ""}> ${kids}</li>
|
|
71
|
+
`;
|
|
72
|
+
case "table":
|
|
73
|
+
return `<table>
|
|
74
|
+
${kids}</table>
|
|
75
|
+
`;
|
|
76
|
+
case "tableRow":
|
|
77
|
+
return `<tr>${kids}</tr>
|
|
78
|
+
`;
|
|
79
|
+
case "tableHeader":
|
|
80
|
+
return `<th>${kids}</th>`;
|
|
81
|
+
case "tableCell":
|
|
82
|
+
return `<td>${kids}</td>`;
|
|
83
|
+
default:
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function toHtml(blocks, remote) {
|
|
88
|
+
const title = textOf(blocks.find((b) => b.type === "heading") ?? { type: "doc" }) || "Document";
|
|
89
|
+
const css = "body{font-family:system-ui,sans-serif;max-width:46rem;margin:2rem auto;padding:0 1rem;line-height:1.5}table{border-collapse:collapse}td,th{border:1px solid #888;padding:.25rem .5rem}pre{overflow:auto}";
|
|
90
|
+
return `<!doctype html>
|
|
91
|
+
<html lang="en">
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="utf-8">
|
|
94
|
+
<title>${esc(title)}</title>
|
|
95
|
+
<style>${css}</style>
|
|
96
|
+
</head>
|
|
97
|
+
<body>
|
|
98
|
+
${blocks.map((b) => block(b, remote)).join("")}</body>
|
|
99
|
+
</html>
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
function exportText(doc, format, options = {}) {
|
|
103
|
+
return format === "html" ? toHtml(doc.toJSON().content ?? [], options.remoteImages === true) : doc.toMarkdown();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/package.ts
|
|
107
|
+
var DATA_IMAGE = /^data:(image\/(?:png|jpeg|gif|webp));base64,([a-z0-9+/=]+)$/i;
|
|
108
|
+
function toBase64(bytes) {
|
|
109
|
+
let text = "";
|
|
110
|
+
for (let i = 0; i < bytes.length; i += 32768) text += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
111
|
+
return btoa(text);
|
|
112
|
+
}
|
|
113
|
+
var fromBase64 = (value) => Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
|
|
114
|
+
|
|
115
|
+
// src/xml.ts
|
|
116
|
+
var safeHref = (value) => {
|
|
117
|
+
const href = String(value ?? "").trim();
|
|
118
|
+
return /^(https?:|mailto:|#|[^:]*$)/i.test(href) ? href : void 0;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/formats.ts
|
|
122
|
+
var IMPORT_FORMATS = ["md", "odt", "docx"];
|
|
123
|
+
var FILE_FORMATS = ["md", "html", "odt", "docx"];
|
|
124
|
+
var DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
125
|
+
var CONTENT_TYPES = {
|
|
126
|
+
md: "text/markdown; charset=utf-8",
|
|
127
|
+
html: "text/html; charset=utf-8",
|
|
128
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
129
|
+
docx: DOCX_MIME
|
|
130
|
+
};
|
|
131
|
+
var PM_MARKS = { bold: "bold", italic: "italic", strike: "strike", code: "code" };
|
|
132
|
+
var MODEL_IMAGE = /^image\/(png|jpeg|gif)$/;
|
|
133
|
+
var TASK = ["\u2610 ", "\u2611 "];
|
|
134
|
+
var warner = () => {
|
|
135
|
+
const seen = /* @__PURE__ */ new Set();
|
|
136
|
+
return { warn: (line) => void seen.add(line), list: () => [...seen] };
|
|
137
|
+
};
|
|
138
|
+
function toTextDoc(blocks, title) {
|
|
139
|
+
const { warn, list } = warner();
|
|
140
|
+
const inlines = (nodes = [], extra = []) => nodes.flatMap((node) => {
|
|
141
|
+
if (node.type === "hardBreak") return [{ type: "break" }];
|
|
142
|
+
if (node.type === "image") {
|
|
143
|
+
const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
|
|
144
|
+
const found = DATA_IMAGE.exec(String(node.attrs?.src ?? "").trim());
|
|
145
|
+
if (found && MODEL_IMAGE.test(found[1].toLowerCase())) {
|
|
146
|
+
try {
|
|
147
|
+
return [{ type: "image", data: fromBase64(found[2]), mime: found[1].toLowerCase(), ...alt ? { alt } : {} }];
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
warn("an image that is not an embedded PNG, JPEG, or GIF is kept as its alt text");
|
|
152
|
+
return [{ type: "text", text: `[image${alt ? `: ${alt}` : ""}]` }];
|
|
153
|
+
}
|
|
154
|
+
if (!node.text) return [];
|
|
155
|
+
const marks = [...extra, ...(node.marks ?? []).flatMap((m) => PM_MARKS[m.type] ? [PM_MARKS[m.type]] : [])];
|
|
156
|
+
const href = safeHref((node.marks ?? []).find((m) => m.type === "link")?.attrs?.href);
|
|
157
|
+
return [{ type: "text", text: node.text, ...marks.length ? { marks } : {}, ...href ? { href } : {} }];
|
|
158
|
+
});
|
|
159
|
+
const listOf = (node) => {
|
|
160
|
+
const items = (node.content ?? []).map((item) => {
|
|
161
|
+
const [first, ...rest] = item.content ?? [];
|
|
162
|
+
const content = inlines(first?.content);
|
|
163
|
+
if (item.type === "taskItem") content.unshift({ type: "text", text: TASK[item.attrs?.checked ? 1 : 0] });
|
|
164
|
+
let sublist;
|
|
165
|
+
for (const child of rest) {
|
|
166
|
+
if (!sublist && /^(bulletList|orderedList|taskList)$/.test(child.type)) sublist = listOf(child);
|
|
167
|
+
else {
|
|
168
|
+
warn("a list item with more than one paragraph is kept as one paragraph with line breaks");
|
|
169
|
+
for (const b of toBlocks(child)) content.push({ type: "break" }, ...textOfBlock(b));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { content, ...sublist ? { sublist } : {} };
|
|
173
|
+
});
|
|
174
|
+
if (node.type === "orderedList" && Number(node.attrs?.start ?? 1) !== 1) warn("a numbered list that does not start at 1 starts at 1");
|
|
175
|
+
return { type: "list", ordered: node.type === "orderedList", items };
|
|
176
|
+
};
|
|
177
|
+
const textOfBlock = (b) => b.type === "list" ? b.items.flatMap((i, n) => [...n ? [{ type: "break" }] : [], ...i.content]) : b.type === "table" ? b.rows.flat(2).flatMap(textOfBlock) : b.content;
|
|
178
|
+
const toBlocks = (node) => {
|
|
179
|
+
switch (node.type) {
|
|
180
|
+
case "paragraph":
|
|
181
|
+
return [{ type: "paragraph", content: inlines(node.content) }];
|
|
182
|
+
case "heading": {
|
|
183
|
+
const level = Math.min(6, Math.max(1, Math.trunc(Number(node.attrs?.level)) || 1));
|
|
184
|
+
return [{ type: "heading", level, content: inlines(node.content) }];
|
|
185
|
+
}
|
|
186
|
+
case "blockquote":
|
|
187
|
+
warn("a quote is kept as plain paragraphs");
|
|
188
|
+
return (node.content ?? []).flatMap(toBlocks);
|
|
189
|
+
case "codeBlock": {
|
|
190
|
+
warn("a code block is kept as a paragraph of code text");
|
|
191
|
+
const lines = (node.content ?? []).map((t) => t.text ?? "").join("").split("\n");
|
|
192
|
+
return [{ type: "paragraph", content: lines.flatMap((line, i) => [...i ? [{ type: "break" }] : [], ...line ? [{ type: "text", text: line, marks: ["code"] }] : []]) }];
|
|
193
|
+
}
|
|
194
|
+
case "horizontalRule":
|
|
195
|
+
warn("a horizontal rule is left out");
|
|
196
|
+
return [];
|
|
197
|
+
case "bulletList":
|
|
198
|
+
case "orderedList":
|
|
199
|
+
case "taskList":
|
|
200
|
+
return [listOf(node)];
|
|
201
|
+
case "table": {
|
|
202
|
+
const rows = node.content ?? [];
|
|
203
|
+
const header = rows.length > 0 && (rows[0].content ?? []).length > 0 && (rows[0].content ?? []).every((c) => c.type === "tableHeader");
|
|
204
|
+
return [{ type: "table", ...header ? { header: true } : {}, rows: rows.map((r) => (r.content ?? []).map((c) => (c.content ?? []).flatMap(toBlocks))) }];
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
warn(`a ${node.type} block is left out`);
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const model = { ...title ? { title } : {}, blocks: blocks.flatMap(toBlocks) };
|
|
212
|
+
return { model, warnings: list() };
|
|
213
|
+
}
|
|
214
|
+
function fromTextDoc(model) {
|
|
215
|
+
const { warn, list } = warner();
|
|
216
|
+
const text = (value, marks = []) => value ? [{ type: "text", text: value, ...marks.length ? { marks } : {} }] : [];
|
|
217
|
+
const inlines = (nodes) => nodes.flatMap((node) => {
|
|
218
|
+
switch (node.type) {
|
|
219
|
+
case "break":
|
|
220
|
+
return [{ type: "hardBreak" }];
|
|
221
|
+
case "image":
|
|
222
|
+
if (!MODEL_IMAGE.test(node.mime)) {
|
|
223
|
+
warn("an image that is not a PNG, JPEG, or GIF is kept as its alt text");
|
|
224
|
+
return text(`[image${node.alt ? `: ${node.alt}` : ""}]`);
|
|
225
|
+
}
|
|
226
|
+
return [{ type: "image", attrs: { src: `data:${node.mime};base64,${toBase64(node.data)}`, alt: node.alt ?? null, title: null } }];
|
|
227
|
+
case "footnote":
|
|
228
|
+
warn("a footnote is kept as text in brackets");
|
|
229
|
+
return [...text(" ["), ...inlines(node.content), ...text("]")];
|
|
230
|
+
case "text": {
|
|
231
|
+
const marks = [];
|
|
232
|
+
for (const mark of node.marks ?? []) {
|
|
233
|
+
if (mark === "bold" || mark === "italic" || mark === "strike" || mark === "code") marks.push({ type: mark });
|
|
234
|
+
else warn(`the ${mark} mark is left out; the text stays`);
|
|
235
|
+
}
|
|
236
|
+
const href = node.href === void 0 ? void 0 : safeHref(node.href);
|
|
237
|
+
if (node.href !== void 0 && !href) warn("a link that is not a web, mail, or relative address is left out; the text stays");
|
|
238
|
+
if (href) marks.push({ type: "link", attrs: { href } });
|
|
239
|
+
if (marks.length > 1 && marks.some((m) => m.type === "code")) {
|
|
240
|
+
warn("a mark or a link on code text is left out; the text stays as code");
|
|
241
|
+
return text(node.text, [{ type: "code" }]);
|
|
242
|
+
}
|
|
243
|
+
return text(node.text, marks);
|
|
244
|
+
}
|
|
245
|
+
default:
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const paragraph = (content) => ({ type: "paragraph", ...content.length ? { content: inlines(content) } : {} });
|
|
250
|
+
const toNodes = (block2) => {
|
|
251
|
+
switch (block2.type) {
|
|
252
|
+
case "heading":
|
|
253
|
+
return [{ type: "heading", attrs: { level: block2.level }, content: inlines(block2.content) }];
|
|
254
|
+
case "paragraph":
|
|
255
|
+
return [paragraph(block2.content)];
|
|
256
|
+
case "list":
|
|
257
|
+
return [
|
|
258
|
+
{
|
|
259
|
+
type: block2.ordered ? "orderedList" : "bulletList",
|
|
260
|
+
...block2.ordered ? { attrs: { start: 1, type: null } } : {},
|
|
261
|
+
content: block2.items.map((item) => ({ type: "listItem", content: [paragraph(item.content), ...item.sublist ? toNodes(item.sublist) : []] }))
|
|
262
|
+
}
|
|
263
|
+
];
|
|
264
|
+
case "table": {
|
|
265
|
+
const rows = block2.rows.filter((r) => r.length > 0);
|
|
266
|
+
if (rows.length === 0) return [];
|
|
267
|
+
return [
|
|
268
|
+
{
|
|
269
|
+
type: "table",
|
|
270
|
+
content: rows.map((row, i) => ({
|
|
271
|
+
type: "tableRow",
|
|
272
|
+
content: row.map((cell) => {
|
|
273
|
+
const content = cell.flatMap(toNodes);
|
|
274
|
+
return { type: block2.header && i === 0 ? "tableHeader" : "tableCell", attrs: { colspan: 1, rowspan: 1, colwidth: null }, content: content.length ? content : [{ type: "paragraph" }] };
|
|
275
|
+
})
|
|
276
|
+
}))
|
|
277
|
+
}
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const prune = (node) => node.content ? { ...node, content: node.content.filter((n) => n.type !== "text" || n.text).map(prune) } : node;
|
|
285
|
+
const blocks = model.blocks.flatMap(toNodes).map(prune);
|
|
286
|
+
return { blocks, ...model.title ? { title: model.title } : {}, warnings: list() };
|
|
287
|
+
}
|
|
288
|
+
var formatFailure = (error) => {
|
|
289
|
+
if (error instanceof FormatError) throw new DocError("INVALID_INPUT", `the file was refused (${error.code}): ${error.message}`);
|
|
290
|
+
throw error;
|
|
291
|
+
};
|
|
292
|
+
function importFile(input, format, author, options = {}) {
|
|
293
|
+
checkAuthor(author);
|
|
294
|
+
if (!IMPORT_FORMATS.includes(format)) throw new DocError("INVALID_INPUT", `import format must be one of ${IMPORT_FORMATS.join(", ")}`);
|
|
295
|
+
const maxBytes = options.limits?.maxInputBytes ?? DEFAULT_LIMITS.maxInputBytes;
|
|
296
|
+
if (format === "md") {
|
|
297
|
+
let markdown;
|
|
298
|
+
try {
|
|
299
|
+
markdown = typeof input === "string" ? input : new TextDecoder("utf-8", { fatal: true }).decode(input);
|
|
300
|
+
} catch {
|
|
301
|
+
throw new DocError("INVALID_INPUT", "a md file must be UTF-8 text");
|
|
302
|
+
}
|
|
303
|
+
if (new TextEncoder().encode(markdown).length > maxBytes) throw new DocError("INVALID_INPUT", `the file is larger than ${maxBytes} bytes`);
|
|
304
|
+
return { doc: AgenticDoc.fromMarkdown(markdown, author, options.doc), warnings: [] };
|
|
305
|
+
}
|
|
306
|
+
if (!(input instanceof Uint8Array)) throw new DocError("INVALID_INPUT", `a ${format} file must be bytes, not text`);
|
|
307
|
+
let read;
|
|
308
|
+
try {
|
|
309
|
+
read = format === "odt" ? readOdt(input, options.limits) : readDocx(input, options.limits);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return formatFailure(error);
|
|
312
|
+
}
|
|
313
|
+
const mapped = fromTextDoc(read.model);
|
|
314
|
+
return {
|
|
315
|
+
doc: AgenticDoc.fromBlocks(mapped.blocks, author, options.doc),
|
|
316
|
+
...mapped.title ? { title: mapped.title } : {},
|
|
317
|
+
warnings: [...read.warnings, ...mapped.warnings]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
var ODT_MIME = CONTENT_TYPES.odt;
|
|
321
|
+
function exportOdt(blocks) {
|
|
322
|
+
try {
|
|
323
|
+
return writeOdt(toTextDoc(blocks).model).bytes;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return formatFailure(error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function exportFile(doc, format, options = {}) {
|
|
329
|
+
if (!FILE_FORMATS.includes(format)) throw new DocError("INVALID_INPUT", `export format must be one of ${FILE_FORMATS.join(", ")}`);
|
|
330
|
+
if (format === "md" || format === "html") return { format, contentType: CONTENT_TYPES[format], data: exportText(doc, format, options), warnings: [] };
|
|
331
|
+
const { model, warnings } = toTextDoc(doc.toJSON().content ?? [], options.title);
|
|
332
|
+
let written;
|
|
333
|
+
try {
|
|
334
|
+
written = format === "odt" ? writeOdt(model) : writeDocx(model);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
return formatFailure(error);
|
|
337
|
+
}
|
|
338
|
+
return { format, contentType: CONTENT_TYPES[format], data: written.bytes, warnings: [...warnings, ...written.warnings] };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export {
|
|
342
|
+
EXPORT_FORMATS,
|
|
343
|
+
exportText,
|
|
344
|
+
toBase64,
|
|
345
|
+
IMPORT_FORMATS,
|
|
346
|
+
FILE_FORMATS,
|
|
347
|
+
DOCX_MIME,
|
|
348
|
+
toTextDoc,
|
|
349
|
+
fromTextDoc,
|
|
350
|
+
importFile,
|
|
351
|
+
ODT_MIME,
|
|
352
|
+
exportOdt,
|
|
353
|
+
exportFile
|
|
354
|
+
};
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
commands
|
|
3
|
+
} from "./chunk-6UYJV2EJ.js";
|
|
1
4
|
import {
|
|
2
5
|
DocsService,
|
|
3
6
|
MAX_LIVE_BYTES
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
commands
|
|
7
|
-
} from "./chunk-S6MQDWFK.js";
|
|
7
|
+
} from "./chunk-JBAQEIOH.js";
|
|
8
8
|
import {
|
|
9
9
|
DocError,
|
|
10
|
-
PRINCIPAL_KINDS,
|
|
11
10
|
parseAuthor
|
|
12
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-GQ5E6T7Z.js";
|
|
12
|
+
import {
|
|
13
|
+
PRINCIPAL_KINDS
|
|
14
|
+
} from "./chunk-DWVJIKVT.js";
|
|
13
15
|
|
|
14
16
|
// src/worker.ts
|
|
15
17
|
import { Hono } from "hono";
|
|
@@ -163,10 +165,11 @@ function createApp(source, options = {}) {
|
|
|
163
165
|
if (!command.readOnly && c.get("role") === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
164
166
|
const result = await command.run(await (typeof source === "function" ? source(c.env ?? {}) : source), input, c.get("author"));
|
|
165
167
|
if (command.name === "export") {
|
|
166
|
-
const { data, contentType, encoding, format } = result;
|
|
168
|
+
const { data, contentType, encoding, format, warnings } = result;
|
|
167
169
|
const images = input.remoteImages === true || input.remoteImages === "true" ? "data: https:" : "data:";
|
|
168
170
|
const disposition = format === "odt" ? { "content-disposition": `attachment; filename="${input.id}.odt"` } : {};
|
|
169
|
-
const
|
|
171
|
+
const lost = warnings.length ? { "x-export-warnings": JSON.stringify(warnings).replace(/[^\x20-\x7e]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`) } : {};
|
|
172
|
+
const headers = { ...disposition, ...lost, "content-type": contentType, "content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; img-src ${images}; sandbox` };
|
|
170
173
|
return encoding === "base64" ? c.body(Uint8Array.from(atob(data), (ch) => ch.charCodeAt(0)), 200, headers) : c.body(data, 200, headers);
|
|
171
174
|
}
|
|
172
175
|
if (typeof result === "string") return c.body(result, 200, { "content-type": "text/markdown; charset=utf-8" });
|