// Renders MinerU's content_list_v2.json (already page-grouped: one array per // PDF page) into per-page Markdown. This is a faithful re-render from MinerU's // structured extraction, not a split of full.md — full.md is a flat, furniture- // stripped concatenation that cannot be reliably realigned to page boundaries. // // Page furniture (page_header / page_footer / page_number / page_footnote) is // dropped to match full.md's clean body. Unknown item types are dropped. interface Span { type?: string; content?: string; } interface ContentItem { type?: string; content?: any; } function renderSpans(spans: Span[] | undefined): string { if (!Array.isArray(spans)) return ""; return spans .map((s) => (s?.type === "equation_inline" ? `$${s.content ?? ""}$` : (typeof s?.content === "string" ? s.content : ""))) .join(""); } function renderItem(item: ContentItem): string | null { const c = item?.content ?? {}; switch (item?.type) { case "title": { const level = Math.min(Math.max(Number(c.level) || 1, 1), 6); const text = renderSpans(c.title_content).trim(); return text ? `${"#".repeat(level)} ${text}` : null; } case "paragraph": { return renderSpans(c.paragraph_content).trim() || null; } case "equation_interline": { const math = String(c.math_content ?? "").trim(); return math ? `$$\n${math}\n$$` : null; } case "table": { const caption = renderSpans(c.table_caption).trim(); const html = String(c.html ?? "").trim(); if (!html && !caption) return null; return [caption, html].filter(Boolean).join("\n\n"); } case "image": { // Images are not extracted from MinerU's zip yet (deferred by design), so // this reference points at an images/.jpg entry that is NOT written // to disk — the link is intentionally dangling for now. // FUTURE: extract the zip's images/ entries next to the page files and // rewrite this path to a relative on-disk location (e.g. ./images/.jpg) // to make the Markdown self-contained. Source field: content.image_source.path. const path = String(c.image_source?.path ?? ""); const caption = renderSpans(c.image_caption).trim(); return `![${caption}](${path})`; } case "list": { const ordered = c.attribute === "ordered"; const items: any[] = Array.isArray(c.list_items) ? c.list_items : []; const lines = items .map((li, i) => { const text = renderSpans(li?.item_content).trim(); if (!text) return null; return ordered ? `${i + 1}. ${text}` : `- ${text}`; }) .filter((l): l is string => !!l); return lines.length ? lines.join("\n") : null; } case "algorithm": { const caption = renderSpans(c.algorithm_caption).trim(); const body = renderSpans(c.algorithm_content).trim(); if (!body) return null; return [caption, "```\n" + body + "\n```"].filter(Boolean).join("\n\n"); } default: // page_header / page_footer / page_number / page_footnote and any unknown // type are dropped. return null; } } export interface RenderedPage { name: string; markdown: string; } export interface RenderedPages { pages: RenderedPage[]; index: string; pageCount: number; } export function renderContentListPages(v2: unknown): RenderedPages { const groups: unknown[] = Array.isArray(v2) ? v2 : []; const pad = Math.max(String(groups.length).length, 2); const pages: RenderedPage[] = groups.map((group, idx) => { const items: ContentItem[] = Array.isArray(group) ? group : []; const blocks = items.map(renderItem).filter((b): b is string => !!b); return { name: `page-${String(idx + 1).padStart(pad, "0")}.md`, markdown: blocks.join("\n\n") + "\n", }; }); const index = ["# Index", "", ...pages.map((p, i) => `${i + 1}. [Page ${i + 1}](./${p.name})`), ""].join("\n"); return { pages, index, pageCount: pages.length }; }