diff --git a/src/export/epub.ts b/src/export/epub.ts
index 02f4081..3615dac 100644
--- a/src/export/epub.ts
+++ b/src/export/epub.ts
@@ -25,7 +25,7 @@ function slug(text: string): string {
function imageExtension(dataUrl: string): string {
const match = /^data:image\/([a-z0-9.+-]+)/i.exec(dataUrl);
- const kind = (match?.[1] ?? "png").toLowerCase();
+ const kind = (match?.[1] ?? "png").toLowerCase().split("+")[0];
return kind === "jpeg" ? "jpg" : kind;
}
@@ -42,8 +42,11 @@ function inline(node: JSONContent): string {
if (node.type === "text") {
let text = esc(node.text ?? "");
const marks = node.marks ?? [];
+ if (marks.some((m) => m.type === "code")) text = `${text}`;
if (marks.some((m) => m.type === "bold")) text = `${text}`;
if (marks.some((m) => m.type === "italic")) text = `${text}`;
+ if (marks.some((m) => m.type === "strike")) text = `${text}`;
+ if (marks.some((m) => m.type === "underline")) text = `${text}`;
const href = marks.find((m) => m.type === "link")?.attrs?.href;
if (href) text = `${text}`;
return text;
@@ -56,11 +59,11 @@ function inlines(content: JSONContent[] = []): string {
return content.map(inline).join("");
}
-function listItem(item: JSONContent): string {
+function listItem(item: JSONContent, paths: Map): string {
const inner = (item.content ?? [])
- .filter((c) => c.type === "paragraph")
- .map((p) => inlines(p.content))
- .join(" ");
+ .map((child) => block(child, paths))
+ .filter((s) => s.length > 0)
+ .join("");
return `${inner}`;
}
@@ -92,9 +95,9 @@ function block(node: JSONContent, paths: Map): string {
case "blockquote":
return `${(node.content ?? []).map((n) => block(n, paths)).join("")}
`;
case "bulletList":
- return `${(node.content ?? []).map(listItem).join("")}
`;
+ return `${(node.content ?? []).map((li) => listItem(li, paths)).join("")}
`;
case "orderedList":
- return `${(node.content ?? []).map(listItem).join("")}
`;
+ return `${(node.content ?? []).map((li) => listItem(li, paths)).join("")}
`;
case "horizontalRule":
return `
`;
case "figure":
@@ -406,6 +409,11 @@ h2, h3 {
margin: 0;
}
+code {
+ font-family: "SF Mono", Menlo, Consolas, monospace;
+ font-size: 0.9em;
+}
+
blockquote {
margin: 1.2em 1.6em;
font-style: italic;
diff --git a/src/export/typst.ts b/src/export/typst.ts
index 0482024..7a89916 100644
--- a/src/export/typst.ts
+++ b/src/export/typst.ts
@@ -10,27 +10,32 @@ const TRIM: Record = {
};
const INLINE_SPECIAL = /[\\#$*_`<>@~[\]]/g;
+const LINE_SEPARATORS = /[\n\r\v\f\u2028\u2029]+/g;
function esc(text: string): string {
- return text.replace(INLINE_SPECIAL, (m) => "\\" + m);
+ return text.replace(LINE_SEPARATORS, " ").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 guardLineStart(text: string): string {
+ if (/^[=\-+/]/.test(text)) return "\\" + text;
+ if (/^\d+\./.test(text)) return text.replace(".", "\\.");
+ return text;
}
function str(value: string): string {
return JSON.stringify(value);
}
-function inline(node: JSONContent): string {
+function inline(node: JSONContent, atLineStart: boolean): string {
if (node.type === "text") {
- let text = esc(node.text ?? "");
const marks = node.marks ?? [];
+ if (marks.some((m) => m.type === "code")) return `#raw(${str(node.text ?? "")})`;
+ let text = esc(node.text ?? "");
+ if (atLineStart) text = guardLineStart(text);
if (marks.some((m) => m.type === "italic")) text = `#emph[${text}]`;
if (marks.some((m) => m.type === "bold")) text = `#strong[${text}]`;
+ if (marks.some((m) => m.type === "strike")) text = `#strike[${text}]`;
+ if (marks.some((m) => m.type === "underline")) text = `#underline[${text}]`;
const href = marks.find((m) => m.type === "link")?.attrs?.href;
if (href) text = `#link(${str(href)})[${text}]`;
return text;
@@ -39,15 +44,15 @@ function inline(node: JSONContent): string {
return "";
}
-function inlines(content: JSONContent[] = []): string {
- return content.map(inline).join("");
+function inlines(content: JSONContent[] = [], atLineStart = false): string {
+ return content.map((node, i) => inline(node, atLineStart && i === 0)).join("");
}
-function listItem(item: JSONContent): string {
- return (item.content ?? [])
- .filter((c) => c.type === "paragraph")
- .map((p) => inlines(p.content))
- .join(" ");
+function listItem(item: JSONContent, paths: Map): string {
+ const parts = (item.content ?? [])
+ .map((child) => block(child, paths))
+ .filter((s) => s.length > 0);
+ return `[${parts.join("\n\n")}]`;
}
function figure(node: JSONContent, paths: Map): string {
@@ -59,7 +64,7 @@ function figure(node: JSONContent, paths: Map): string {
}
const placed = placement as FigurePlacement;
const width = (isResizablePlacement(placed) ? node.attrs?.width : null) ?? FIGURE_WIDTH[placed] ?? 100;
- const caption = node.attrs?.caption ? `, caption: [${esc(node.attrs.caption)}]` : "";
+ const caption = node.attrs?.caption ? `, caption: [${guardLineStart(esc(node.attrs.caption))}]` : "";
const float = placement === "float-top" ? ", placement: top" : "";
return `#figure(image(${str(path)}, width: ${width}%)${caption}${float})`;
}
@@ -67,18 +72,18 @@ function figure(node: JSONContent, paths: Map): string {
function block(node: JSONContent, paths: Map): string {
switch (node.type) {
case "paragraph": {
- const body = guardLineStart(inlines(node.content));
+ const body = inlines(node.content, true);
if (!body.trim()) return "~";
return node.attrs?.indent ? `#h(1.3em)${body}` : body;
}
case "heading":
- return `#heading(level: ${node.attrs?.level ?? 2})[${inlines(node.content)}]`;
+ return `#heading(level: ${node.attrs?.level ?? 2})[${inlines(node.content, true)}]`;
case "blockquote":
return `#blockquote[${(node.content ?? []).map((n) => block(n, paths)).join("\n\n")}]`;
case "bulletList":
- return (node.content ?? []).map((li) => `- ${listItem(li)}`).join("\n");
+ return `#list(${(node.content ?? []).map((li) => listItem(li, paths)).join(", ")})`;
case "orderedList":
- return (node.content ?? []).map((li) => `+ ${listItem(li)}`).join("\n");
+ return `#enum(${(node.content ?? []).map((li) => listItem(li, paths)).join(", ")})`;
case "horizontalRule":
return "#scenebreak";
case "figure":
@@ -188,7 +193,7 @@ function preamble(book: Book): string {
function imageExtension(dataUrl: string): string {
const match = /^data:image\/([a-z0-9.+-]+)/i.exec(dataUrl);
- const kind = (match?.[1] ?? "png").toLowerCase();
+ const kind = (match?.[1] ?? "png").toLowerCase().split("+")[0];
return kind === "jpeg" ? "jpg" : kind;
}