
Print.pudu
Pudu128 lines6.1 KB
1/** @Docgen.Site.Print — printable documents assembled from tables of contents */2module PuduLangDocgen.Site.Print34import Std.Map as Map5import PuduLangDocgen.Build.Site as Site6import PuduLangDocgen as Docgen7import PuduLangDocgen.Meta as Meta8import PuduLangDocgen.Navigation.Resolve as Resolve9import PuduLangDocgen.Paths as Paths10import PuduLangDocgen.Site.Format as Format1112/** @Docgen.Site.Printable — one document to print and the settings of its table */13export type Printable = { html: Str, path: Str, pdf: Str, header: Str, footer: Str, background: Bool }141516export const FOOTER: Str = "<div style=\"width: 100%; font-size: 12px;\"><div style=\"float: right; padding: 0 2em\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div></div>"1718192021export fn documents(pages: &Array[Docgen.Page], books: &Array[Site.Book]) -> Array[Printable] {22 var byPath: Map[Str, Docgen.Page] = mapOf([])23 for page in *pages { byPath = Map.insert(&byPath, page.path, page) }24 var result: Array[Printable] = []25 for book in *books {26 if !Meta.flag(&book.meta, "pdf") { continue }27 let pdf = Site.pdfOf(&book)28 let path = pdf.replace(".pdf", "") + ".print.html"29 var included: Array[Docgen.Page] = []30 for item in Resolve.readingOrder(&book.items) {31 match Map.get(&byPath, Paths.pathOf(item.href)) {32 case Some(page) => { if !included.contains(page) { included = included.push(page) } }33 case None => {}34 }35 }36 var numbers: Map[Str, Int] = mapOf([])37 var index = 038 for page in included {39 numbers = Map.insert(&numbers, page.path, index)40 index = index + 141 }42 var pieces: Array[Str] = []43 let cover = Meta.textOr(&book.meta, "pdfCoverPage", "")44 match Map.get(&byPath, cover) {45 case Some(page) => { pieces = pieces.push("<section class=\"print-cover\">" + rebase(page.body, page.path, path, &numbers, "cover-") + "</section>") }46 case None => {}47 }48 if Meta.flag(&book.meta, "pdfTocPage") {49 let entries = included.map(fn(page: Docgen.Page) -> Str {50 "<li><a href=\"#page-" + show(Map.getOr(&numbers, page.path, 0)) + "\">" + Paths.escape(page.title) + "</a><span class=\"spacer\"></span><span class=\"page-number\"></span></li>"51 })52 pieces = pieces.push("<section class=\"pdftoc print-section\"><h1>Table of Contents</h1><ul>" + entries.join("") + "</ul></section>")53 }54 for page in included {55 let number = show(Map.getOr(&numbers, page.path, 0))56 pieces = pieces.push("<section class=\"print-section\" id=\"page-" + number + "\">" + rebase(page.body, page.path, path, &numbers, "p" + number + "-") + "</section>")57 }58 let title = Meta.textOr(&book.meta, "_appTitle", Meta.textOr(&book.meta, "_appName", "Documentation"))59 let background = Meta.flag(&book.meta, "pdfPrintBackground")60 let html = [61 "<!doctype html>", "<html" + lang(&book.meta) + ">", "<head>", "<meta charset=\"utf-8\">", "<title>" + Paths.escape(title) + "</title>",62 "<link rel=\"stylesheet\" href=\"" + Paths.rootOf(path) + "_assets/site.css\">",63 if background { "<style>* \{ print-color-adjust: exact; -webkit-print-color-adjust: exact; \}</style>" } else { "" },64 "</head>", "<body class=\"print\">", "<main class=\"content\">", pieces.join("\n"), "</main>", "</body>", "</html>"65 ].join("\n") + "\n"66 result = result.push(Printable{html: Format.html(html), path: path, pdf: pdf, header: Meta.textOr(&book.meta, "pdfHeaderTemplate", ""), footer: Meta.textOr(&book.meta, "pdfFooterTemplate", FOOTER), background: background})67 }68 result69}707172fn lang(meta: &Array[(Str, Docgen.Meta)]) -> Str {73 let written = Meta.textOr(meta, "_lang", "")74 if written.isEmpty() { "" } else { " lang=\"" + Paths.escape(written) + "\"" }75}76777879export fn rebase(body: Str, page: Str, document: Str, numbers: &Map[Str, Int], prefix: Str) -> Str {80 var pieces: Array[Str] = []81 var rest = body82 while !rest.isEmpty() {83 let at = next(rest)84 if at < 0 {85 pieces = pieces.push(rest)86 break87 }88 let marker = if rest.drop(at).startsWith(" id=\"") { " id=\"" } else if rest.drop(at).startsWith(" href=\"") { " href=\"" } else { " src=\"" }89 let start = at + marker.length()90 let end = rest.drop(start).indexOf("\"")91 if end < 0 {92 pieces = pieces.push(rest)93 break94 }95 let value = rest.drop(start).take(end)96 let changed = if marker == " id=\"" { prefix + value } else { address(value, page, document, numbers, prefix) }97 pieces = pieces.push(rest.take(start) + changed + "\"")98 rest = rest.drop(start + end + 1)99 }100 pieces.join("")101}102103104fn next(text: Str) -> Int {105 var best = -1106 for marker in [" id=\"", " href=\"", " src=\""] {107 let found = text.indexOf(marker)108 if found >= 0 && (best < 0 || found < best) { best = found }109 }110 best111}112113114fn address(value: Str, page: Str, document: Str, numbers: &Map[Str, Int], prefix: Str) -> Str {115 let decoded = value.replace("&", "&")116 if decoded.startsWith("#") { return "#" + prefix + decoded.drop(1) }117 if Paths.remote(decoded) || decoded.startsWith("data:") || !Paths.href(decoded) { return value }118 let target = match Paths.join(Paths.directoryOf(page), Paths.pathOf(decoded)) {119 case Ok(found) => found120 case Err(_) => { return value }121 }122 let fragment = if decoded.contains("#") { decoded.split("#")[1] } else { "" }123 match Map.get(numbers, target) {124 case Some(number) => "#" + (if fragment.isEmpty() { "page-" + show(number) } else { "p" + show(number) + "-" + fragment })125 case None => Paths.escape(Paths.between(document, target) + Paths.suffixOf(decoded))126 }127}128