mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
feat(export): add book-to-Typst source generator
This commit is contained in:
1 parent
e21db2be5d
commit
f939674794
2 files changed
+127
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import type { JSONContent } from "@tiptap/core";
|
||||
import type { Book, TrimSize } from "../model/book";
|
||||
|
||||
const TRIM: Record<TrimSize, { w: string; h: string }> = {
|
||||
"6x9": { w: "6in", h: "9in" },
|
||||
"5.5x8.5": { w: "5.5in", h: "8.5in" },
|
||||
"5x8": { w: "5in", h: "8in" },
|
||||
a5: { w: "148mm", h: "210mm" },
|
||||
};
|
||||
|
||||
const INLINE_SPECIAL = /[\\#$*_`<>@~[\]]/g;
|
||||
|
||||
function esc(text: string): string {
|
||||
return text.replace(INLINE_SPECIAL, (m) => "\\" + m);
|
||||
}
|
||||
|
||||
function guardLineStart(line: string): string {
|
||||
if (/^[=\-+/]/.test(line)) return "\\" + line;
|
||||
if (/^\d+\./.test(line)) return line.replace(".", "\\.");
|
||||
return line;
|
||||
}
|
||||
|
||||
function str(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function inline(node: JSONContent): string {
|
||||
if (node.type === "text") {
|
||||
let text = esc(node.text ?? "");
|
||||
const marks = (node.marks ?? []).map((m) => m.type);
|
||||
if (marks.includes("italic")) text = `#emph[${text}]`;
|
||||
if (marks.includes("bold")) text = `#strong[${text}]`;
|
||||
return text;
|
||||
}
|
||||
if (node.type === "hardBreak") return "#linebreak()";
|
||||
return "";
|
||||
}
|
||||
|
||||
function inlines(content: JSONContent[] = []): string {
|
||||
return content.map(inline).join("");
|
||||
}
|
||||
|
||||
function listItem(item: JSONContent): string {
|
||||
return (item.content ?? [])
|
||||
.filter((c) => c.type === "paragraph")
|
||||
.map((p) => inlines(p.content))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function block(node: JSONContent): 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")}]`;
|
||||
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";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function chapterBody(content: JSONContent): string {
|
||||
return (content.content ?? [])
|
||||
.map(block)
|
||||
.filter((s) => s.length > 0)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function preamble(book: Book): string {
|
||||
const trim = TRIM[book.settings.trim];
|
||||
const meta = book.metadata;
|
||||
return `#set document(title: ${str(meta.title || "Untitled")}, author: ${str(meta.author)})
|
||||
#set page(
|
||||
width: ${trim.w},
|
||||
height: ${trim.h},
|
||||
margin: (inside: 0.875in, outside: 0.625in, top: 0.8in, bottom: 0.8in),
|
||||
binding: left,
|
||||
numbering: "1",
|
||||
)
|
||||
#set text(font: "Literata", size: 11pt, lang: ${str(meta.language || "en")}, hyphenate: true)
|
||||
#set par(justify: true, leading: 0.72em, spacing: 0.72em, first-line-indent: (amount: 1.3em, all: false))
|
||||
#show heading: set text(font: "Literata", weight: "medium")
|
||||
#show heading: set block(above: 1.4em, below: 0.8em)
|
||||
|
||||
#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 chapteropener(num, title) = {
|
||||
pagebreak(weak: true, to: "odd")
|
||||
v(2.1in)
|
||||
align(center)[
|
||||
#text(font: "Hanken Grotesk", size: 8.5pt, weight: "semibold", tracking: 0.28em)[#upper("Chapter " + num)]
|
||||
#v(0.7em)
|
||||
#text(font: "Literata", size: 22pt, weight: "medium")[#title]
|
||||
]
|
||||
v(1.5em)
|
||||
}`;
|
||||
}
|
||||
|
||||
export function bookToTypst(book: Book): 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)}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
return `${preamble(book)}\n\n${body}\n`;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface ImageInput {
|
||||
path: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export async function compilePdf(source: string, images: ImageInput[] = []): Promise<Uint8Array> {
|
||||
const buffer = await invoke<ArrayBuffer>("compile_pdf", { source, images });
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
Reference in new issue
Block a user