yamlover 0.3.4 → 0.3.6
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/bin/yamlover.js +199 -112
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
- package/dist/client/assets/decoded-CFkcuQ1f.js +1 -0
- package/dist/client/assets/djvu-B3wuFWwI.js +1 -0
- package/dist/client/assets/docx-putHYXzO.js +230 -0
- package/dist/client/assets/heic-oIxhIbIW.js +66 -0
- package/dist/client/assets/imagemap-CUKUIcGe.js +1 -0
- package/dist/client/assets/index-CQ3TGVSx.js +617 -0
- package/dist/client/assets/index-wTjOzmSY.css +1 -0
- package/dist/client/assets/map-CZS083CS.js +1 -0
- package/dist/client/assets/paged-CjxSuuik.js +1 -0
- package/dist/client/assets/panzoom-C7OuIIda.js +4 -0
- package/dist/client/assets/panzoom-CIGW-MKW.css +1 -0
- package/dist/client/assets/pdf-Dz3gUx0B.js +12 -0
- package/dist/client/assets/pdf-m5e3jsV4.css +1 -0
- package/dist/client/assets/pdf.worker.min-qwK7q_zL.mjs +28 -0
- package/dist/client/assets/psd-C8yNwHFw.js +11 -0
- package/dist/client/assets/spreadsheet-DBv7aqoN.js +38 -0
- package/dist/client/assets/tiff-73RKtIux.js +1 -0
- package/{index.html → dist/client/index.html} +2 -1
- package/package.json +18 -21
- package/src/client/App.tsx +0 -424
- package/src/client/NodeView.tsx +0 -422
- package/src/client/TaskStrip.tsx +0 -34
- package/src/client/Tree.tsx +0 -97
- package/src/client/api.ts +0 -213
- package/src/client/icons.ts +0 -91
- package/src/client/links.tsx +0 -108
- package/src/client/live.ts +0 -42
- package/src/client/main.tsx +0 -10
- package/src/client/paste-html.ts +0 -228
- package/src/client/paste-links.ts +0 -42
- package/src/client/paths.ts +0 -139
- package/src/client/render.tsx +0 -339
- package/src/client/renderers/annotate.tsx +0 -641
- package/src/client/renderers/asciidoc.tsx +0 -36
- package/src/client/renderers/chapter.tsx +0 -142
- package/src/client/renderers/csv.tsx +0 -234
- package/src/client/renderers/decoded.tsx +0 -72
- package/src/client/renderers/djvu.tsx +0 -271
- package/src/client/renderers/djvuWorker.ts +0 -99
- package/src/client/renderers/doc.tsx +0 -40
- package/src/client/renderers/docx.tsx +0 -49
- package/src/client/renderers/epub.tsx +0 -147
- package/src/client/renderers/explorer.tsx +0 -294
- package/src/client/renderers/fb2.tsx +0 -149
- package/src/client/renderers/headings.ts +0 -69
- package/src/client/renderers/heic.tsx +0 -23
- package/src/client/renderers/imagemap.tsx +0 -170
- package/src/client/renderers/kml.ts +0 -46
- package/src/client/renderers/latex.tsx +0 -37
- package/src/client/renderers/map.tsx +0 -205
- package/src/client/renderers/marklower.tsx +0 -120
- package/src/client/renderers/markup.tsx +0 -64
- package/src/client/renderers/media.tsx +0 -19
- package/src/client/renderers/paged.ts +0 -137
- package/src/client/renderers/panzoom.ts +0 -101
- package/src/client/renderers/pdf.tsx +0 -316
- package/src/client/renderers/plaintext.tsx +0 -120
- package/src/client/renderers/plantuml.tsx +0 -83
- package/src/client/renderers/psd.tsx +0 -25
- package/src/client/renderers/registry.tsx +0 -411
- package/src/client/renderers/rtf.tsx +0 -210
- package/src/client/renderers/spreadsheet.tsx +0 -105
- package/src/client/renderers/tag.tsx +0 -113
- package/src/client/renderers/text.tsx +0 -42
- package/src/client/renderers/tiff.tsx +0 -33
- package/src/client/styles.css +0 -1212
- package/src/client/vendor/README.md +0 -30
- package/src/client/vite-env.d.ts +0 -31
- package/src/server/api.ts +0 -147
- package/src/server/embed.ts +0 -187
- package/src/server/engine-api.ts +0 -1563
- package/src/server/gitignore.ts +0 -81
- package/src/server/node-kind.ts +0 -61
- package/src/server/tasks.ts +0 -83
- package/src/server/yamlover.ts +0 -1133
- /package/{src/client/vendor/djvu.js → dist/client/assets/djvu-Ci0lFKxE.js} +0 -0
package/src/client/paste-html.ts
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
// HTML-clipboard paste: a selection copied from a web page (Wikipedia, docs, …) arrives as
|
|
2
|
-
// text/html alongside the plain text. When it carries STRUCTURE the plain flavor loses —
|
|
3
|
-
// images, headings — it becomes a RICH paste: headings nest subchapters (by level), images
|
|
4
|
-
// become separate chunks (downloaded in the browser — e.g. Wikimedia sends
|
|
5
|
-
// `access-control-allow-origin: *`), text blocks become marklower prose. Formatted text with
|
|
6
|
-
// no images/headings stays a plain text paste. Used by NodeView's paste listener.
|
|
7
|
-
|
|
8
|
-
/** The client-side draft: images still by URL (downloaded later by resolveImages). */
|
|
9
|
-
export interface RichDraft {
|
|
10
|
-
title?: string;
|
|
11
|
-
chunks: Array<{ text: string } | { image: { url: string; alt: string } }>;
|
|
12
|
-
children: RichDraft[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** The wire payload for POST /api/paste {rich}: images resolved to inline file bytes. */
|
|
16
|
-
export interface RichNode {
|
|
17
|
-
title?: string;
|
|
18
|
-
chunks: Array<{ text: string } | { file: { name: string; contentBase64: string } }>;
|
|
19
|
-
children: RichNode[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type Block =
|
|
23
|
-
| { kind: "heading"; level: number; text: string }
|
|
24
|
-
| { kind: "image"; url: string; alt: string }
|
|
25
|
-
| { kind: "text"; text: string };
|
|
26
|
-
|
|
27
|
-
const SKIP = new Set(["script", "style", "noscript", "template", "iframe", "svg", "head", "title"]);
|
|
28
|
-
const BLOCKS = new Set(["p", "div", "section", "article", "main", "aside", "header", "footer", "figure", "figcaption", "ul", "ol", "table", "thead", "tbody", "tr", "dl", "dt", "dd", "nav", "form", "body", "html"]);
|
|
29
|
-
|
|
30
|
-
/** Parse an HTML clipboard fragment into a chapter draft — or null when it has no images and
|
|
31
|
-
* no headings (plain formatted text: the normal text paste serves it better). */
|
|
32
|
-
export function htmlToRich(html: string): RichDraft | null {
|
|
33
|
-
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
34
|
-
const blocks = mergeBullets(blocksOf(doc.body));
|
|
35
|
-
if (!blocks.some((b) => b.kind !== "text")) return null;
|
|
36
|
-
|
|
37
|
-
// headings nest by level: deeper headings open children of the nearest shallower one
|
|
38
|
-
const root: RichDraft = { chunks: [], children: [] };
|
|
39
|
-
const stack: Array<{ node: RichDraft; level: number }> = [{ node: root, level: 0 }];
|
|
40
|
-
for (const b of blocks) {
|
|
41
|
-
if (b.kind === "heading") {
|
|
42
|
-
while (stack.length > 1 && stack[stack.length - 1].level >= b.level) stack.pop();
|
|
43
|
-
const child: RichDraft = { title: b.text || "Untitled", chunks: [], children: [] };
|
|
44
|
-
stack[stack.length - 1].node.children.push(child);
|
|
45
|
-
stack.push({ node: child, level: b.level });
|
|
46
|
-
} else {
|
|
47
|
-
const top = stack[stack.length - 1].node;
|
|
48
|
-
top.chunks.push(b.kind === "image" ? { image: { url: b.url, alt: b.alt } } : { text: b.text });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return root;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Walk the fragment in document order, flushing inline text at block boundaries. */
|
|
55
|
-
function blocksOf(body: HTMLElement): Block[] {
|
|
56
|
-
const out: Block[] = [];
|
|
57
|
-
let buf = "";
|
|
58
|
-
const flush = () => {
|
|
59
|
-
const t = buf.replace(/[ \t]+/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{2,}/g, "\n").trim();
|
|
60
|
-
buf = "";
|
|
61
|
-
if (t) out.push({ kind: "text", text: t });
|
|
62
|
-
};
|
|
63
|
-
const walk = (n: Node): void => {
|
|
64
|
-
if (n.nodeType === Node.TEXT_NODE) {
|
|
65
|
-
buf += n.textContent ?? "";
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (!(n instanceof Element)) return;
|
|
69
|
-
const tag = n.tagName.toLowerCase();
|
|
70
|
-
if (SKIP.has(tag)) return;
|
|
71
|
-
if (/^h[1-6]$/.test(tag)) {
|
|
72
|
-
flush();
|
|
73
|
-
// Wikipedia headings carry an "[edit]" section link — noise in a title
|
|
74
|
-
out.push({ kind: "heading", level: Number(tag[1]), text: (n.textContent ?? "").replace(/\[edit\]/gi, "").trim() });
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (tag === "img") {
|
|
78
|
-
flush();
|
|
79
|
-
const url = imageUrl(n);
|
|
80
|
-
if (url) out.push({ kind: "image", url, alt: n.getAttribute("alt") ?? "" });
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (tag === "li") {
|
|
84
|
-
// one bullet per line; images inside the item still surface as their own chunks
|
|
85
|
-
flush();
|
|
86
|
-
n.childNodes.forEach(walk);
|
|
87
|
-
const t = buf.replace(/\s+/g, " ").trim();
|
|
88
|
-
buf = "";
|
|
89
|
-
if (t) out.push({ kind: "text", text: "- " + t });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (tag === "pre") {
|
|
93
|
-
flush();
|
|
94
|
-
const t = (n.textContent ?? "").replace(/\n+$/, "");
|
|
95
|
-
if (t.trim()) out.push({ kind: "text", text: "```\n" + t + "\n```" });
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (tag === "blockquote") {
|
|
99
|
-
flush();
|
|
100
|
-
n.childNodes.forEach(walk);
|
|
101
|
-
const t = buf.trim();
|
|
102
|
-
buf = "";
|
|
103
|
-
if (t) out.push({ kind: "text", text: t.split("\n").map((l) => "> " + l).join("\n") });
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (tag === "br") {
|
|
107
|
-
buf += "\n";
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (tag === "td" || tag === "th") {
|
|
111
|
-
n.childNodes.forEach(walk);
|
|
112
|
-
buf += " ";
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (BLOCKS.has(tag)) {
|
|
116
|
-
flush();
|
|
117
|
-
n.childNodes.forEach(walk);
|
|
118
|
-
flush();
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
// an inline element: render to marklower — unless an image hides inside (then descend, so
|
|
122
|
-
// the image becomes its own chunk rather than vanishing into the text)
|
|
123
|
-
if (!n.querySelector("img")) {
|
|
124
|
-
buf += inlineMd(n);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
n.childNodes.forEach(walk);
|
|
128
|
-
};
|
|
129
|
-
walk(body);
|
|
130
|
-
flush();
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Inline content → marklower: links, emphasis, code; everything else passes through as text. */
|
|
135
|
-
function inlineMd(n: Node): string {
|
|
136
|
-
if (n.nodeType === Node.TEXT_NODE) return n.textContent ?? "";
|
|
137
|
-
if (!(n instanceof Element)) return "";
|
|
138
|
-
const tag = n.tagName.toLowerCase();
|
|
139
|
-
if (SKIP.has(tag)) return "";
|
|
140
|
-
if (tag === "br") return "\n";
|
|
141
|
-
const inner = Array.from(n.childNodes).map(inlineMd).join("");
|
|
142
|
-
if (tag === "a") {
|
|
143
|
-
const href = n.getAttribute("href") ?? "";
|
|
144
|
-
const t = inner.trim();
|
|
145
|
-
return /^https?:\/\//.test(href) && t ? `[${t}](${href})` : inner;
|
|
146
|
-
}
|
|
147
|
-
if (tag === "strong" || tag === "b") return inner.trim() ? `**${inner.trim()}**` : "";
|
|
148
|
-
if (tag === "em" || tag === "i") return inner.trim() ? `*${inner.trim()}*` : "";
|
|
149
|
-
if (tag === "code") return inner.trim() ? "`" + inner.trim() + "`" : "";
|
|
150
|
-
return inner;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** A usable image URL: absolute http(s) or data:; protocol-relative gains https:; lazy-load
|
|
154
|
-
* attributes win over a missing/placeholder src; anything relative is dropped (a clipboard
|
|
155
|
-
* fragment has no base to resolve it against). */
|
|
156
|
-
function imageUrl(img: Element): string | null {
|
|
157
|
-
const raw = img.getAttribute("src") || img.getAttribute("data-src") || "";
|
|
158
|
-
if (raw.startsWith("//")) return "https:" + raw;
|
|
159
|
-
if (/^https?:\/\//.test(raw) || raw.startsWith("data:image/")) return raw;
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Consecutive single-bullet blocks (one per <li>) merge into one list chunk. */
|
|
164
|
-
function mergeBullets(blocks: Block[]): Block[] {
|
|
165
|
-
const out: Block[] = [];
|
|
166
|
-
for (const b of blocks) {
|
|
167
|
-
const prev = out[out.length - 1];
|
|
168
|
-
if (b.kind === "text" && b.text.startsWith("- ") && prev?.kind === "text" && prev.text.startsWith("- ")) {
|
|
169
|
-
prev.text += "\n" + b.text;
|
|
170
|
-
} else out.push(b);
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const MIME_EXT: Record<string, string> = {
|
|
176
|
-
"image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp",
|
|
177
|
-
"image/svg+xml": "svg", "image/bmp": "bmp", "image/tiff": "tiff", "image/avif": "avif",
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
/** Download every image of a draft (order kept) into inline file chunks; a failed fetch
|
|
181
|
-
* degrades to a marklower image link, so the reference survives even when the bytes don't. */
|
|
182
|
-
export async function resolveImages(draft: RichDraft): Promise<RichNode> {
|
|
183
|
-
return {
|
|
184
|
-
title: draft.title,
|
|
185
|
-
chunks: await Promise.all(
|
|
186
|
-
draft.chunks.map(async (c) => {
|
|
187
|
-
if ("text" in c) return { text: c.text };
|
|
188
|
-
try {
|
|
189
|
-
const res = await fetch(c.image.url);
|
|
190
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
191
|
-
const blob = await res.blob();
|
|
192
|
-
return { file: { name: imageName(c.image.url, blob.type), contentBase64: await blobBase64(blob) } };
|
|
193
|
-
} catch {
|
|
194
|
-
return { text: `` };
|
|
195
|
-
}
|
|
196
|
-
}),
|
|
197
|
-
),
|
|
198
|
-
children: await Promise.all(draft.children.map(resolveImages)),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** A filename for a downloaded image: the URL path's basename, extension from the MIME type
|
|
203
|
-
* when the URL has none (data: URLs, extensionless CDNs). */
|
|
204
|
-
function imageName(url: string, mime: string): string {
|
|
205
|
-
let base = "";
|
|
206
|
-
try {
|
|
207
|
-
base = decodeURIComponent(new URL(url).pathname.split("/").pop() ?? "");
|
|
208
|
-
} catch {
|
|
209
|
-
/* data: or malformed — synthesize below */
|
|
210
|
-
}
|
|
211
|
-
if (!base || url.startsWith("data:")) base = "image";
|
|
212
|
-
if (!/\.[A-Za-z0-9]{2,5}$/.test(base)) base += "." + (MIME_EXT[mime] || "bin");
|
|
213
|
-
return base;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function blobBase64(blob: Blob): Promise<string> {
|
|
217
|
-
return new Promise((resolve, reject) => {
|
|
218
|
-
const r = new FileReader();
|
|
219
|
-
r.onload = () => resolve(String(r.result).split(",")[1] || "");
|
|
220
|
-
r.onerror = () => reject(new Error("could not read image"));
|
|
221
|
-
r.readAsDataURL(blob);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** How many images a draft holds (for the progress toast). */
|
|
226
|
-
export function countImages(draft: RichDraft): number {
|
|
227
|
-
return draft.chunks.filter((c) => "image" in c).length + draft.children.reduce((n, k) => n + countImages(k), 0);
|
|
228
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Pasted-LINK handlers: a paste that is EXACTLY one well-known URL means the linked CONTENT is
|
|
2
|
-
// wanted, not the URL text. Each recognizer turns the link into something the normal paste flows
|
|
3
|
-
// accept — a file to upload (arXiv) or a text to chunk (a tweet). A link inside longer text is
|
|
4
|
-
// left alone (the prose is the paste). Used by NodeView's paste listener.
|
|
5
|
-
|
|
6
|
-
/** The arXiv paper behind a pasted link: a lone `arxiv.org/{abs|pdf|html}/<id>` URL — fetch its
|
|
7
|
-
* PDF (arXiv sends `access-control-allow-origin: *`) and run the normal file-paste flow. New
|
|
8
|
-
* (`2605.00615v2`) and old (`math/0211159`) id styles. */
|
|
9
|
-
export function arxivPdf(text: string): { url: string; name: string } | null {
|
|
10
|
-
const m = /^(?:https?:\/\/)?(?:www\.)?arxiv\.org\/(?:abs|pdf|html)\/(.+?)(?:\.pdf)?(?:[?#][^\s]*)?$/i.exec(text.trim());
|
|
11
|
-
if (!m) return null;
|
|
12
|
-
const id = m[1];
|
|
13
|
-
if (!/^(?:\d{4}\.\d{4,5}|[a-z-]+(?:\.[A-Z]{2})?\/\d{7})(?:v\d+)?$/.test(id)) return null;
|
|
14
|
-
return { url: `https://arxiv.org/pdf/${id}`, name: `arxiv-${id.replace(/\//g, "-")}.pdf` };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** The tweet behind a pasted `x.com`/`twitter.com` status link, canonicalized; null if the text
|
|
18
|
-
* is not exactly one such link. */
|
|
19
|
-
export function tweetUrl(text: string): string | null {
|
|
20
|
-
const m = /^(?:https?:\/\/)?(?:www\.|mobile\.)?(?:x\.com|twitter\.com)\/(\w{1,15})\/status(?:es)?\/(\d+)(?:[/?#][^\s]*)?$/i.exec(text.trim());
|
|
21
|
-
return m ? `https://twitter.com/${m[1]}/status/${m[2]}` : null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Fetch a tweet's FULL content via X's public oEmbed endpoint (`publish.x.com/oembed` — the
|
|
25
|
-
* embedded-tweet API: no auth, CORS-open) and compose it as pasteable text — the whole message,
|
|
26
|
-
* then an author/date attribution line, then the link. */
|
|
27
|
-
export async function fetchTweetText(statusUrl: string): Promise<string> {
|
|
28
|
-
const res = await fetch(`https://publish.x.com/oembed?url=${encodeURIComponent(statusUrl)}&omit_script=true&dnt=true`);
|
|
29
|
-
if (!res.ok) throw new Error(`oEmbed HTTP ${res.status}`);
|
|
30
|
-
const o = (await res.json()) as { html?: string; author_name?: string; author_url?: string; url?: string };
|
|
31
|
-
// the payload's `html` is a <blockquote><p>tweet…</p>— Author (@handle) <a>date</a></blockquote>
|
|
32
|
-
const doc = new DOMParser().parseFromString((o.html ?? "").replace(/<br\s*\/?>/gi, "\n"), "text/html");
|
|
33
|
-
const quote = doc.querySelector("blockquote");
|
|
34
|
-
const body = quote?.querySelector("p")?.textContent?.trim();
|
|
35
|
-
if (!body) throw new Error("no tweet text in the oEmbed payload");
|
|
36
|
-
const handle = (o.author_url ?? "").split("/").filter(Boolean).pop();
|
|
37
|
-
const dateLinks = quote ? Array.from(quote.querySelectorAll(":scope > a")) : [];
|
|
38
|
-
const date = dateLinks[dateLinks.length - 1]?.textContent?.trim();
|
|
39
|
-
const who = [o.author_name, handle && `@${handle}`].filter(Boolean).join(" ");
|
|
40
|
-
const attribution = [who && `— ${who}`, date].filter(Boolean).join(", ");
|
|
41
|
-
return [body, "", [attribution, o.url ?? statusUrl].filter(Boolean).join("\n")].join("\n");
|
|
42
|
-
}
|
package/src/client/paths.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// JSON-space path helpers. The CANONICAL client path is COLON-form (SEPARATOR.md M4):
|
|
2
|
-
// `:key[0]:sub`, root `:` — what the API speaks and the UI displays. The BROWSER URL
|
|
3
|
-
// stays SLASH-transported (`/key[0]/sub` — ruling: "the URL should be slashed, of
|
|
4
|
-
// course"), converted at this boundary only.
|
|
5
|
-
//
|
|
6
|
-
// A key may itself contain `:`, `/`, `[`, or `]` (e.g. `@vitejs/plugin-react`), so each
|
|
7
|
-
// key is percent-encoded (encodeURIComponent) — the structural separators then
|
|
8
|
-
// unambiguously tokenize, and the URL spelling is address-bar-safe as is.
|
|
9
|
-
|
|
10
|
-
export type Seg = string | number;
|
|
11
|
-
|
|
12
|
-
/** Canonical client path: `:key[0]:sub` (keys percent-encoded), root `:`. */
|
|
13
|
-
export function segsToStr(segs: Seg[]): string {
|
|
14
|
-
return segs.map((s) => (typeof s === "number" ? `[${s}]` : `:${encodeURIComponent(s)}`)).join("") || ":";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const PATH_TOKEN = /\[\d+\]|[^:\[\]]+/g; // canonical (colon) form
|
|
18
|
-
const URL_TOKEN = /\[\d+\]|[^/\[\]]+/g; // URL (slash) transport form
|
|
19
|
-
|
|
20
|
-
export function strToSegs(str: string): Seg[] {
|
|
21
|
-
const out: Seg[] = [];
|
|
22
|
-
for (const tok of str.match(PATH_TOKEN) || []) {
|
|
23
|
-
out.push(/^\[\d+\]$/.test(tok) ? Number(tok.slice(1, -1)) : safeDecode(tok));
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Canonical key for cross-SCOPE comparison: collapse the colon scope ladder so a
|
|
29
|
-
* project ref (`::yamlover:…`), a document ref (`:yamlover:…`), and a server-echoed
|
|
30
|
-
* `:`-form path all compare equal. The ladder colons are not tokens (PATH_TOKEN), so
|
|
31
|
-
* re-emitting drops them; keys are normalized through one decode→encode pass. */
|
|
32
|
-
export function canonPath(p: string): string {
|
|
33
|
-
return segsToStr(strToSegs(p));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function safeDecode(s: string): string {
|
|
37
|
-
try {
|
|
38
|
-
return decodeURIComponent(s);
|
|
39
|
-
} catch {
|
|
40
|
-
return s;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The current JSON path taken from the browser URL (slash transport), in canonical
|
|
45
|
-
* COLON form. The pathname is already per-key-encoded, so it is tokenized *before*
|
|
46
|
-
* decoding — segments ride through encoded into the canonical string. */
|
|
47
|
-
export function pathFromUrl(): string {
|
|
48
|
-
const segs: Seg[] = [];
|
|
49
|
-
for (const tok of window.location.pathname.match(URL_TOKEN) || []) {
|
|
50
|
-
segs.push(/^\[\d+\]$/.test(tok) ? Number(tok.slice(1, -1)) : safeDecode(tok));
|
|
51
|
-
}
|
|
52
|
-
return segsToStr(segs);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** The slash-transport URL spelling of a canonical path (`:a[0]:b` → `/a[0]/b`). */
|
|
56
|
-
export function urlOfPath(path: string): string {
|
|
57
|
-
const segs = strToSegs(path);
|
|
58
|
-
return segs.map((s) => (typeof s === "number" ? `[${s}]` : `/${encodeURIComponent(s)}`)).join("") || "/";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** A human-readable form of a canonical path: each key decoded (so a percent-encoded
|
|
62
|
-
* segment like `%D0%9F…` shows as its actual characters), colon-separated, indices as
|
|
63
|
-
* `[i]`. For display only — tooltips, labels — never for URLs or navigation. */
|
|
64
|
-
export function displayPath(path: string): string {
|
|
65
|
-
const segs = strToSegs(path);
|
|
66
|
-
if (!segs.length) return ":";
|
|
67
|
-
return segs.map((s) => (typeof s === "number" ? `[${s}]` : `:${s}`)).join("");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** A human-readable form of a path-LIKE key whose structure must survive verbatim —
|
|
71
|
-
* a relations key (`..`, `:eve`, `::a:b`), where {@link displayPath} would mangle the
|
|
72
|
-
* leading `::` or a bare `..`. Each key token is decoded in place; display only. */
|
|
73
|
-
export function displayKey(key: string): string {
|
|
74
|
-
return key.replace(/[^:/\[\]]+/g, (tok) => safeDecode(tok));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Whether canonical path `a` is a (strict) ancestor of `p`. Root `:` is an ancestor
|
|
78
|
-
* of everything; otherwise `p` must continue past `a` at a `:` or `[`. */
|
|
79
|
-
export function isAncestorPath(a: string, p: string): boolean {
|
|
80
|
-
if (a === p) return false;
|
|
81
|
-
if (a === ":") return true;
|
|
82
|
-
return p.startsWith(a + ":") || p.startsWith(a + "[");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** The current representation taken from the URL's `?format=` (or `fallback`). */
|
|
86
|
-
export function formatFromUrl(fallback: string): string {
|
|
87
|
-
return new URLSearchParams(window.location.search).get("format") || fallback;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** The current page from the URL's `?page=` (1-based). 1 when absent, ≤1, or not an integer —
|
|
91
|
-
* used by paged viewers (PDF/DjVu) to restore where the reader left off. */
|
|
92
|
-
export function pageFromUrl(): number {
|
|
93
|
-
const n = Number(new URLSearchParams(window.location.search).get("page"));
|
|
94
|
-
return Number.isInteger(n) && n > 1 ? n : 1;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Record the current page in `?page=` via replaceState — preserving every other param, and
|
|
98
|
-
* DROPPING the param at page 1 (the implicit default). This does NOT remount the renderer (only
|
|
99
|
-
* path/format/refreshSignal do), so a paged viewer can update it freely while scrolling. */
|
|
100
|
-
export function writePageToUrl(n: number): void {
|
|
101
|
-
const params = new URLSearchParams(window.location.search);
|
|
102
|
-
if (n > 1) params.set("page", String(n));
|
|
103
|
-
else params.delete("page");
|
|
104
|
-
const qs = params.toString();
|
|
105
|
-
const url = window.location.pathname + (qs ? "?" + qs : "");
|
|
106
|
-
if (url !== window.location.pathname + window.location.search) window.history.replaceState({}, "", url);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Write the JSON path (canonical colon form, converted to the slash-transport URL)
|
|
110
|
-
* plus `?format=` into the URL. Path navigation pushes a history entry; switching
|
|
111
|
-
* format replaces. Any other query params already present are kept (e.g. a renderer's
|
|
112
|
-
* own options such as the CSV `sep`/`header`), so only `format` is overwritten here. */
|
|
113
|
-
export function writeUrl(path: string, format: string, replace = false): void {
|
|
114
|
-
const params = new URLSearchParams(window.location.search);
|
|
115
|
-
params.set("format", format);
|
|
116
|
-
// `?page=` is node-specific: a format switch (replace) keeps it, but navigating to another node
|
|
117
|
-
// (push) must not carry the old node's page over — drop it there.
|
|
118
|
-
if (!replace) params.delete("page");
|
|
119
|
-
const url = `${urlOfPath(path || ":")}?${params.toString()}`;
|
|
120
|
-
if (url === window.location.pathname + window.location.search) return;
|
|
121
|
-
if (replace) window.history.replaceState({}, "", url);
|
|
122
|
-
else window.history.pushState({}, "", url);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Breadcrumb crumbs for a path. The head is the CLI ROOT (`rootLabel`); it is
|
|
126
|
-
* omitted when blank, so a default (cwd) root shows just the in-tree segments.
|
|
127
|
-
* e.g. (":a:b", "examples") → [examples, a, b] (each decoded, linking to its path). */
|
|
128
|
-
export function crumbs(p: string, rootLabel: string): { label: string; path: string }[] {
|
|
129
|
-
const segs = strToSegs(p);
|
|
130
|
-
const out: { label: string; path: string }[] = [];
|
|
131
|
-
if (rootLabel) out.push({ label: rootLabel, path: ":" });
|
|
132
|
-
segs.forEach((s, i) => {
|
|
133
|
-
out.push({
|
|
134
|
-
label: typeof s === "number" ? `[${s}]` : s,
|
|
135
|
-
path: segsToStr(segs.slice(0, i + 1)),
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
return out;
|
|
139
|
-
}
|