mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
feat(export): serialize figures to Typst with image extraction
This commit is contained in:
1 parent
6dc1aeb99b
commit
29af847179
1 file changed
+49
-7
+49
-7
@@ -1,5 +1,6 @@
|
||||
import type { JSONContent } from "@tiptap/core";
|
||||
import type { Book, TrimSize } from "../model/book";
|
||||
import type { ImageInput } from "../ipc";
|
||||
|
||||
const TRIM: Record<TrimSize, { w: string; h: string }> = {
|
||||
"6x9": { w: "6in", h: "9in" },
|
||||
@@ -47,28 +48,38 @@ function listItem(item: JSONContent): string {
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function block(node: JSONContent): string {
|
||||
function figure(node: JSONContent, paths: Map<string, string>): string {
|
||||
const path = paths.get(node.attrs?.src);
|
||||
if (!path) return "";
|
||||
const caption = node.attrs?.caption ? `, caption: [${esc(node.attrs.caption)}]` : "";
|
||||
const placement = node.attrs?.placement === "float-top" ? ", placement: top" : "";
|
||||
return `#figure(image(${str(path)}, width: 100%)${caption}${placement})`;
|
||||
}
|
||||
|
||||
function block(node: JSONContent, paths: Map<string, string>): string {
|
||||
switch (node.type) {
|
||||
case "paragraph":
|
||||
return guardLineStart(inlines(node.content));
|
||||
case "heading":
|
||||
return `#heading(level: ${node.attrs?.level ?? 2})[${inlines(node.content)}]`;
|
||||
case "blockquote":
|
||||
return `#blockquote[${(node.content ?? []).map(block).join("\n\n")}]`;
|
||||
return `#blockquote[${(node.content ?? []).map((n) => block(n, paths)).join("\n\n")}]`;
|
||||
case "bulletList":
|
||||
return (node.content ?? []).map((li) => `- ${listItem(li)}`).join("\n");
|
||||
case "orderedList":
|
||||
return (node.content ?? []).map((li) => `+ ${listItem(li)}`).join("\n");
|
||||
case "horizontalRule":
|
||||
return "#scenebreak";
|
||||
case "figure":
|
||||
return figure(node, paths);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function chapterBody(content: JSONContent): string {
|
||||
function chapterBody(content: JSONContent, paths: Map<string, string>): string {
|
||||
return (content.content ?? [])
|
||||
.map(block)
|
||||
.map((n) => block(n, paths))
|
||||
.filter((s) => s.length > 0)
|
||||
.join("\n\n");
|
||||
}
|
||||
@@ -91,7 +102,7 @@ function preamble(book: Book): string {
|
||||
|
||||
#let scenebreak = align(center)[#v(0.5em) #line(length: 13%, stroke: 0.5pt + rgb("#d6cfbd")) #v(0.5em)]
|
||||
|
||||
#let blockquote(body) = pad(left: 1.2em, rest: 0pt)[#set text(style: "italic", fill: rgb("#6b6458")); #body]
|
||||
#let blockquote(body) = pad(left: 1.2em)[#set text(style: "italic", fill: rgb("#6b6458")); #body]
|
||||
|
||||
#let chapteropener(num, title) = {
|
||||
pagebreak(weak: true, to: "odd")
|
||||
@@ -105,12 +116,43 @@ function preamble(book: Book): string {
|
||||
}`;
|
||||
}
|
||||
|
||||
export function bookToTypst(book: Book): string {
|
||||
function imageExtension(dataUrl: string): string {
|
||||
const match = /^data:image\/([a-z0-9.+-]+)/i.exec(dataUrl);
|
||||
const kind = (match?.[1] ?? "png").toLowerCase();
|
||||
return kind === "jpeg" ? "jpg" : kind;
|
||||
}
|
||||
|
||||
export function extractImages(book: Book): { images: ImageInput[]; paths: Map<string, string> } {
|
||||
const paths = new Map<string, string>();
|
||||
const images: ImageInput[] = [];
|
||||
|
||||
const visit = (node: JSONContent) => {
|
||||
if (node.type === "figure") {
|
||||
const src: string | undefined = node.attrs?.src;
|
||||
if (src && src.startsWith("data:") && !paths.has(src)) {
|
||||
const path = `assets/figure-${images.length + 1}.${imageExtension(src)}`;
|
||||
paths.set(src, path);
|
||||
images.push({ path, data: src.slice(src.indexOf(",") + 1) });
|
||||
}
|
||||
}
|
||||
(node.content ?? []).forEach(visit);
|
||||
};
|
||||
|
||||
book.chapters.forEach((chapter) => visit(chapter.content));
|
||||
return { images, paths };
|
||||
}
|
||||
|
||||
export function bookToTypst(book: Book, paths: Map<string, string> = new Map()): string {
|
||||
const body = book.chapters
|
||||
.map((chapter, i) => {
|
||||
const opener = `#chapteropener(${str(String(i + 1))}, [${esc(chapter.title || "Untitled")}])`;
|
||||
return `${opener}\n\n${chapterBody(chapter.content)}`;
|
||||
return `${opener}\n\n${chapterBody(chapter.content, paths)}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
return `${preamble(book)}\n\n${body}\n`;
|
||||
}
|
||||
|
||||
export function bookToPdfInputs(book: Book): { source: string; images: ImageInput[] } {
|
||||
const { images, paths } = extractImages(book);
|
||||
return { source: bookToTypst(book, paths), images };
|
||||
}
|
||||
Reference in new issue
Block a user