Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

Print.pudu

Pudu128 lines6.1 KB

GitHub ↗
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 }1415/// Footer used when a table names none: the page number over the page count.16export 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>"1718/// A printable document for every table of contents whose metadata sets `pdf`. Each lists its19/// pages in reading order after an optional cover (`pdfCoverPage`) and contents page20/// (`pdfTocPage`); `pdfFileName` names the output beside the table.21export 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}7071/// The `lang` attribute of a document, when metadata names a language.72fn lang(meta: &Array[(Str, Docgen.Meta)]) -> Str {73  let written = Meta.textOr(meta, "_lang", "")74  if written.isEmpty() { "" } else { " lang=\"" + Paths.escape(written) + "\"" }75}7677/// Page HTML moved into a printed document: ids gain a prefix, links to pages in the document78/// become section links, and other relative addresses are rebased to the document.79export 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}102103/// The index of the next id, href, or src attribute, or -1.104fn 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}112113/// An address moved from a page to a printed document.114fn address(value: Str, page: Str, document: Str, numbers: &Map[Str, Int], prefix: Str) -> Str {115  let decoded = value.replace("&amp;", "&")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