refactor(conversion): centralize format contracts in native-doc.ts; document lossiness matrix
This commit is contained in:
parent
90b7f2cf76
commit
4ce0a5ebfb
6 changed files with 48 additions and 12 deletions
|
|
@ -17,6 +17,7 @@ import type {
|
|||
} from "markdown-it";
|
||||
import { wikilinkPlugin, type WikilinkTarget } from "../renderer/wikilinks.js";
|
||||
import {
|
||||
CALLOUT_TITLE_CLASS,
|
||||
CURRENT_DOC_SCHEMA_VERSION,
|
||||
type FolioChapterDoc,
|
||||
type FolioMark,
|
||||
|
|
@ -151,7 +152,7 @@ function calloutPlugin(md: MarkdownItInstance) {
|
|||
|
||||
if (title) {
|
||||
const titleOpen = new state.Token("paragraph_open", "p", 1);
|
||||
titleOpen.attrs = [["class", "callout-title"]];
|
||||
titleOpen.attrs = [["class", CALLOUT_TITLE_CLASS]];
|
||||
const titleInline = new state.Token("inline", "", 0);
|
||||
titleInline.content = title;
|
||||
const titleText = new state.Token("text", "", 0);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,21 @@ export interface FolioChapterDoc {
|
|||
// Bump this whenever the native document shape needs a one-way migration.
|
||||
export const CURRENT_DOC_SCHEMA_VERSION = 1;
|
||||
|
||||
// ---- Cross-converter contracts -------------------------------------------
|
||||
// These string constants are part of the document format itself. They are
|
||||
// consumed by every module that reads or writes native docs (markdown-to-native,
|
||||
// native-to-markdown, renderer/print-format), so they live here — next to the
|
||||
// schema they belong to — instead of being re-typed as literals in each
|
||||
// converter. Changing any of them changes what is persisted on disk.
|
||||
//
|
||||
// Callout title paragraphs carry this class so serializers can tell the title
|
||||
// apart from body paragraphs (set on parse, stripped on serialize/render).
|
||||
export const CALLOUT_TITLE_CLASS = "callout-title";
|
||||
|
||||
// Internal chapter-link scheme: folio:chapter/<id>. Serializers turn it back
|
||||
// into [[wikilinks]]; the wikilink plugin turns [[text]] into it on parse.
|
||||
export const CHAPTER_LINK_PREFIX = "folio:chapter/";
|
||||
|
||||
// Each entry migrates the document FROM its key version TO key+1.
|
||||
// Entries are additive and must be non-destructive.
|
||||
const MIGRATIONS: Record<number, (doc: FolioChapterDoc) => FolioChapterDoc> = {};
|
||||
|
|
|
|||
|
|
@ -8,14 +8,25 @@
|
|||
//
|
||||
// The output is intentionally "good enough" Markdown that round-trips through
|
||||
// markdown-to-native.ts; it is NOT a pixel-perfect reconstruction of the
|
||||
// original source (e.g. raw control characters inside ==highlight== are kept
|
||||
// literal, and folio:chapter/<id> links become [[text]] wikilinks).
|
||||
// original source. Known, accepted losses on serialize:
|
||||
// - hard breaks degrade to soft breaks (re-parse yields a plain space)
|
||||
// - table cells holding multiple blocks are flattened: block boundaries
|
||||
// vanish because cells render as one inline run
|
||||
// - chapter links serialize as [[display text]]; re-parsing resolves that
|
||||
// text against chapter titles, so a link whose label differs from its
|
||||
// target's title may point elsewhere after a round-trip
|
||||
// Everything else (marks incl. colored highlights/underline, code fences with
|
||||
// language, task-list state, callout type+title, image alt/title) survives.
|
||||
|
||||
import type {
|
||||
FolioChapterDoc,
|
||||
FolioMark,
|
||||
FolioNode,
|
||||
} from "./native-doc.js";
|
||||
import {
|
||||
CALLOUT_TITLE_CLASS,
|
||||
CHAPTER_LINK_PREFIX,
|
||||
} from "./native-doc.js";
|
||||
|
||||
// Marks applied outermost-first so nested formatting renders as valid Markdown.
|
||||
const MARK_ORDER = [
|
||||
|
|
@ -52,7 +63,7 @@ function wrap(mark: FolioMark, text: string): string {
|
|||
return `<span style="color:${attrs.color}">${text}</span>`;
|
||||
case "link": {
|
||||
const href = String(attrs.href ?? "");
|
||||
if (href.startsWith("folio:chapter/")) return `[[${text}]]`;
|
||||
if (href.startsWith(CHAPTER_LINK_PREFIX)) return `[[${text}]]`;
|
||||
const title = attrs.title ? ` "${attrs.title}"` : "";
|
||||
return `[${text}](${href}${title})`;
|
||||
}
|
||||
|
|
@ -142,7 +153,7 @@ function blockToMd(node: FolioNode, depth = 0): string {
|
|||
const rest: FolioNode[] = [];
|
||||
let title = "";
|
||||
for (const c of node.content ?? []) {
|
||||
if (c.type === "paragraph" && c.attrs?.class === "callout-title") {
|
||||
if (c.type === "paragraph" && c.attrs?.class === CALLOUT_TITLE_CLASS) {
|
||||
title = inlineToMd(c.content);
|
||||
} else {
|
||||
rest.push(c);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ import {
|
|||
} from "docx";
|
||||
import { loadBook, type ChapterEntry } from "./project.js";
|
||||
import { listChaptersInOrder, loadChapterDoc } from "./chapters.js";
|
||||
import {
|
||||
CALLOUT_TITLE_CLASS,
|
||||
CHAPTER_LINK_PREFIX,
|
||||
} from "./native-doc.js";
|
||||
import type { FolioMark, FolioNode } from "./native-doc.js";
|
||||
|
||||
const MONO_FONT = "Consolas";
|
||||
|
|
@ -165,7 +169,7 @@ function textNodeRuns(node: FolioNode, ctx: InlineCtx): (TextRun | ExternalHyper
|
|||
const href = String(linkMark.attrs?.href ?? "");
|
||||
// Wikilinks point back into the book; the export has no in-app chapter URLs,
|
||||
// so they are rendered as plain label text.
|
||||
if (href.startsWith("folio:chapter/") || href === "#" || href.startsWith("folio:")) return [run];
|
||||
if (href.startsWith(CHAPTER_LINK_PREFIX) || href === "#" || href.startsWith("folio:")) return [run];
|
||||
if (href) return [new ExternalHyperlink({ link: href, children: [run] })];
|
||||
return [run];
|
||||
}
|
||||
|
|
@ -254,7 +258,7 @@ function renderNodes(nodes: FolioNode[], bookPath: string): BlockEl[] {
|
|||
break;
|
||||
}
|
||||
case "paragraph": {
|
||||
const isTitle = String(n.attrs?.class ?? "") === "callout-title";
|
||||
const isTitle = String(n.attrs?.class ?? "") === CALLOUT_TITLE_CLASS;
|
||||
out.push(
|
||||
new Paragraph({
|
||||
children: renderInline(n.content, isTitle ? { ...defaultCtx, bold: true } : defaultCtx),
|
||||
|
|
@ -360,7 +364,7 @@ function renderQuoted(content: FolioNode[], isCallout: boolean, bookPath: string
|
|||
const out: BlockEl[] = [];
|
||||
for (const block of content) {
|
||||
if (block.type === "paragraph") {
|
||||
const isTitle = String(block.attrs?.class ?? "") === "callout-title";
|
||||
const isTitle = String(block.attrs?.class ?? "") === CALLOUT_TITLE_CLASS;
|
||||
out.push(
|
||||
new Paragraph({
|
||||
children: renderInline((block.content ?? []) as FolioNode[], isTitle ? { ...defaultCtx, bold: true } : defaultCtx),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
// <section class="print-chapter"> blocks, delimited by the chapterBreak nodes
|
||||
// the Full Book assembly inserts between chapters.
|
||||
import type { FolioMark, FolioNode } from "../main/native-doc.js";
|
||||
import {
|
||||
CALLOUT_TITLE_CLASS,
|
||||
CHAPTER_LINK_PREFIX,
|
||||
} from "../main/native-doc.js";
|
||||
|
||||
// Native-document print rendering for the PDF-exported manuscript (#printBook).
|
||||
//
|
||||
|
|
@ -56,7 +60,7 @@ function applyMarksHtml(text: string, marks: FolioMark[] = []): string {
|
|||
const link = marks.find((m) => m.type === "link");
|
||||
if (link?.attrs?.href) {
|
||||
const href = String(link.attrs.href);
|
||||
const internal = href.startsWith("folio:chapter/");
|
||||
const internal = href.startsWith(CHAPTER_LINK_PREFIX);
|
||||
const cls = internal ? ' class="wikilink internal"' : "";
|
||||
out = `<a href="${esc(href)}"${cls}>${out}</a>`;
|
||||
}
|
||||
|
|
@ -84,8 +88,8 @@ function blockToHtml(node: FolioNode): string {
|
|||
return `<h${level}${ALIGN(node)}>${inlineToHtml(node.content)}</h${level}>`;
|
||||
}
|
||||
case "paragraph": {
|
||||
const isTitle = String(node.attrs?.class ?? "") === "callout-title";
|
||||
const cls = isTitle ? ' class="callout-title"' : "";
|
||||
const isTitle = String(node.attrs?.class ?? "") === CALLOUT_TITLE_CLASS;
|
||||
const cls = isTitle ? ` class="${CALLOUT_TITLE_CLASS}"` : "";
|
||||
return `<p${cls}${ALIGN(node)}>${inlineToHtml(node.content)}</p>`;
|
||||
}
|
||||
case "bulletList":
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { MarkdownIt, StateInline } from "markdown-it";
|
||||
import { CHAPTER_LINK_PREFIX } from "../main/native-doc.js";
|
||||
|
||||
export interface WikilinkTarget {
|
||||
id: string;
|
||||
|
|
@ -65,7 +66,7 @@ export function wikilinkPlugin(md: MarkdownIt, opts: WikilinkOpts): void {
|
|||
const open = state.push("link_open", "a", 1);
|
||||
if (id !== null) {
|
||||
open.attrs = [
|
||||
["href", `folio:chapter/${encodeURIComponent(id)}`],
|
||||
["href", `${CHAPTER_LINK_PREFIX}${encodeURIComponent(id)}`],
|
||||
["class", "wikilink internal"],
|
||||
];
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue