///|
// The conversion of a document (asciidoctor-pdf's `convert_document`): the
// theme and page setup, the title page or title, the TOC, the body, the
// running content and the PDF's outline, labels and viewer settings.
///|
/// Convert a document loaded with `backend=pdf` (see `register`) to PDF,
/// in its theme's fonts (read through its VFS: run it inside
/// `@io.run_with_files`), or the catalog `register` was given. Load it with
/// `sourcemap` for `conversion_warnings` to say where the unsupported
/// content is.
pub fn convert(doc : @core.Node) -> Bytes raise {
activate_session(doc)
convert_document(doc, document_catalog(doc))
}
///|
/// asciidoctor-pdf's `Document#attr_unspecified?`: the attribute is not
/// locked, the document did not set or unset it, and the API's attributes
/// do not name it (soft `name@`, `!name` and `name!` forms included).
fn attr_unspecified(doc : @core.Node, name : String) -> Bool {
if doc.attribute_locked(name) || doc.attribute_modified(name) {
return false
}
for entry in doc.options().attributes {
let (key, value) = entry
let named = if key == name {
true
} else if value is Str("@") {
key == "!\{name}" || key == "\{name}!"
} else if key.has_suffix("@") {
let k = key[:key.length() - 1].to_owned()
k == name || k == "!\{name}" || k == "\{name}!"
} else {
false
}
if named {
return false
}
}
true
}
///|
/// The front matter of the document: how many pages have no running
/// content, and how many are not numbered from the body on
/// (`running_content_start_at`, `page_numbering_start_at`).
fn front_matter(
title_as_page : Bool,
has_title_page : Bool,
toc_at_top : Bool,
body_start : Int,
has_front_cover? : Bool = false,
) -> (Int, Int) {
// pages (0-based) before the body
let body_offset = body_start
if !title_as_page {
let running = match tv("running_content_start_at") {
Int(n) => body_offset + @cmp.maximum(n.to_int() - 1, 0)
_ => body_offset
}
let numbering = match tv("page_numbering_start_at") {
Int(n) => body_offset + @cmp.maximum(n.to_int() - 1, 0)
Str("cover") if has_front_cover => 0
_ => body_offset
}
return (running, numbering)
}
let zero = if has_front_cover { 1 } else { 0 }
let first = if has_title_page { zero + 1 } else { zero }
let resolve = (key : String) => {
match tv(key) {
Int(n) => ("body", body_offset + @cmp.maximum(n.to_int() - 1, 0))
v => {
let at = value_str(v).unwrap_or("body")
let numbering = key == "page_numbering_start_at"
match at {
"cover" if numbering && has_front_cover => ("cover", 0)
"cover" if numbering => ("title", body_offset)
"title" => (if has_title_page { "title" } else { "toc" }, body_offset)
"toc" => (if toc_at_top { "toc" } else { "body" }, body_offset)
"after-toc" => ("body", body_offset)
other => (other, body_offset)
}
}
}
}
let (running_at, running_body) = resolve("running_content_start_at")
let (numbering_at, numbering_body) = resolve("page_numbering_start_at")
let offset = (at : String, body : Int) => {
match at {
"title" => zero
"toc" => first
_ => body
}
}
(offset(running_at, running_body), offset(numbering_at, numbering_body))
}
///|
/// Convert a document loaded with `backend=pdf` (see `register`) to PDF,
/// set in the fonts of `catalog`.
pub fn convert_document(doc : @core.Node, catalog : FontCatalog) -> Bytes raise {
// the document's own state (its theme, index, footnotes, images), which
// another document loaded or converted since must not have touched
activate_session(doc)
let state = session()
diagnostics.clear()
diagnostics.append(state.pending_diagnostics)
state.converting = true
defer {
state.converting = false
}
trace.clear()
trace_depth.val = 0
let theme = document_theme(doc)
current_theme.val = theme
font_scope.val = None
page_setup.val = resolve_page_setup(doc)
report_document_features(doc, catalog)
// `outline`, `outline-title` and `pagenums` are on unless specified
for name in ["outline", "outline-title", "pagenums"] {
if attr_unspecified(doc, name) {
ignore(doc.set_attr(name, value=Str("")))
}
}
let flow = Flow::new(catalog)
let c = Converter::new(doc, flow)
c.text_align = match doc.attr("text-align") {
Some("left" | "right" | "center" | "justify" as align) =>
alignment_of(align)
_ => t_align("base_text_align", Left)
}
let is_book = doc.doctype() == "book"
// a book (or `title-page`) has a title page instead of a title heading
let title_as_page = is_book || doc.has_attr("title-page")
let has_title_page = title_as_page &&
doc.has_header() &&
!doc.notitle() &&
!(tv("title_page") is Bool(false))
// `media=prepress` (`init_pdf`): recto pages take the inner margin on
// the left and the outer on the right, verso pages the reverse; the first
// page keeps the theme's margins (an article without a title page takes
// the recto margins); a book's chapters start on recto pages
if doc.attr("media") == Some("prepress") {
c.ppbook = is_book
let setup = page_setup.val
let outer = t_num("page_margin_outer")
let inner = t_num("page_margin_inner")
let recto = (
inner.unwrap_or(setup.margin_left),
outer.unwrap_or(setup.margin_right),
)
let verso = (
outer.unwrap_or(setup.margin_left),
inner.unwrap_or(setup.margin_right),
)
let cover = if has_title_page ||
doc.has_attr("front-cover-image") ||
t_set("cover_front_image") {
(setup.margin_left, setup.margin_right)
} else {
recto
}
side_margins.val = Some((recto, verso))
flow.page_margins = Some(index => {
if index == 0 {
cover
} else if index % 2 == 0 {
recto
} else {
verso
}
})
} else {
side_margins.val = None
}
flow.start_new_page()
// the front cover, on a page of its own (`ink_cover_page`)
let has_front_cover = c.ink_cover_page("front")
c.has_front_cover = has_front_cover
// a book's title page goes on a recto page after a front cover
// (`start_title_page`)
if c.ppbook && has_front_cover && has_title_page {
flow.start_new_page()
}
// an article's abstract section becomes an abstract block, out of the
// TOC and the outline (the "demote abstract section" hack)
match doc.sections().get(0) {
Some(first) if doc.doctype() == "article" &&
first.sectname == Some("abstract") => {
first.set_context(Open)
first.style = Some("abstract")
}
_ => ()
}
let title_page_index = flow.page
if has_title_page {
c.ink_title_page()
flow.start_new_page()
}
if !title_as_page && doc.has_header() && !doc.notitle() {
match doc.doctitle() {
Some(title) =>
c.ink_heading(
title,
1,
align=t_str("heading_h1_text_align")
.map(alignment_of)
.unwrap_or(Center),
)
None => ()
}
}
// an article's body starts where its title is, before the TOC
let article_body_start = flow.page
let levels = resolve_toclevels(doc)
c.toc_levels = levels
// the TOC at the top, unless the macro or the preamble places it
let placement = doc.attr("toc-placement").unwrap_or("auto")
let toc_at_top = has_toc(doc) &&
placement != "macro" &&
placement != "preamble"
// the body, the TOC included, is inside the section indent
// (`indent_section`)
let (indent_left, indent_right) = section_indent().unwrap_or((0.0, 0.0))
flow.left += indent_left
flow.right -= indent_right
if toc_at_top {
// a prepress book's TOC starts on a recto page
if c.ppbook && flow.on_verso() {
flow.start_new_page()
}
flow.add_anchor(
"toc",
flow.dest_x(),
if flow.at_page_top() {
0.0
} else {
flow.y
},
)
c.toc_extent = Some(
c.allocate_toc(
levels,
title_as_page && t_str("toc_break_after") != Some("auto"),
),
)
}
// a prepress book's body starts on a recto page, unless its first block
// is `nonfacing`
let first_block = match doc.blocks.get(0) {
Some(b) if b.context == Preamble => b.blocks.get(0)
other => other
}
if c.ppbook &&
flow.on_verso() &&
!(first_block is Some(b) && b.has_option("nonfacing")) {
flow.start_new_page()
}
// pages before the body are front matter: numbered in roman, and without
// running content (by default)
let body_start = if title_as_page { flow.page } else { article_body_start }
let (skip_running, skip_numbering) = front_matter(
title_as_page,
has_title_page,
toc_at_top,
body_start,
has_front_cover~,
)
session().index.start_page_number = skip_numbering + 1
// the top of the document, for `<<_top>>`-style references: the top of
// the page the body starts on (`add_dest_for_top`)
let top = top_anchor(doc)
flow.add_anchor(top, 0.0, 0.0)
let body = () => {
// a man page starts with its name and purpose, as a section the
// outline (built from the document's sections) does not list
if doc.doctype() == "manpage" && doc.has_attr("manpurpose") {
let listed = c.outline.length()
c.convert_section(generate_manname_section(doc))
while c.outline.length() > listed {
ignore(c.outline.pop())
}
}
c.traverse(doc)
c.outdent_section(() => c.ink_footnotes(doc))
}
// an article's body in `page_columns` columns `page_column_gap` apart
// (the font size by default, Prawn's column box spacer)
let page_columns = t_num_or("page_columns", 1.0).to_int()
if !is_book && page_columns >= 2 {
flow.column_box(
page_columns,
t_num_or("page_column_gap", base_font_size()),
body,
)
} else {
body()
}
// a TOC the macro placed in the body of a book starts the running content
// or the page numbering when they start at `toc` or `after-toc`
let (skip_running, skip_numbering) = match c.toc_extent {
Some(extent) if title_as_page && !toc_at_top => {
let at = (key : String) => {
match tv(key) {
Str("toc") => Some(extent.from_page)
Str("after-toc") => {
// in a prepress book, the recto page after the TOC
let after = extent.to_page + 1
Some(if c.ppbook && after % 2 == 1 { after + 1 } else { after })
}
_ => None
}
}
(
at("running_content_start_at").unwrap_or(skip_running),
at("page_numbering_start_at").unwrap_or(skip_numbering),
)
}
_ => (skip_running, skip_numbering)
}
match c.toc_extent {
Some(extent) => {
let last = flow.page
ignore(c.ink_toc(levels, extent.from_page, extent.from_y, skip_numbering))
flow.go_to_page(last)
}
None => ()
}
flow.left -= indent_left
flow.right += indent_right
// a page started but never written to is dropped
let pages = flow.model.pages
if pages.length() > 1 &&
pages[pages.length() - 1].items.iter().all(fn(i) { i is Anchor(_) }) {
ignore(pages.pop())
}
// the page backgrounds (`init_page`), under everything
c.ink_page_backgrounds(
if has_title_page {
Some(title_page_index)
} else {
None
},
front_cover=has_front_cover,
)
if flow.model.pages.length() > body_start {
if !doc.has_attr("noheader") && t_num_or("header_height", 0.0) != 0.0 {
c.ink_running_content(
"header",
skip_running,
skip_numbering,
body_start + 1,
)
}
if !doc.has_attr("nofooter") && t_num_or("footer_height", 0.0) != 0.0 {
c.ink_running_content(
"footer",
skip_running,
skip_numbering,
body_start + 1,
)
}
}
// the back cover, on a page after the last; it is not labeled itself
// (the labels are added before it), so it takes the last page's label
let labeled_pages = flow.model.pages.length()
c.end_page_h = flow.model.pages[labeled_pages - 1].height_pt
ignore(c.ink_cover_page("back"))
keep_last_anchors(flow.model)
for entry in @pdf.unembeddable_images(flow.model) {
report("UNSUPPORTED image \{entry.0} (\{entry.1}x) at ")
}
let initial_zoom : @pdf.InitialZoom? = match t_str("page_initial_zoom") {
Some("Fit") => Some(Fit)
Some("FitV") => Some(FitV)
// `dest_fit_horizontally page_height, state.pages[0]`: the window's top
// at the height of the page current when the open action is set (the
// last content page, before the back cover), measured from the first
// page's bottom edge
Some("FitH") =>
Some(FitHAt(top=flow.model.pages[0].height_pt - c.end_page_h))
_ => None
}
let page_mode : @pdf.PageMode = match
doc.attr("pdf-page-mode").or_some(t_str("page_mode")) {
Some("none") => UseNone
Some("thumbs") => UseThumbs
Some(
"fullscreen"
| "fullscreen none"
| "fullscreen outline"
| "fullscreen thumbs"
) => FullScreen
_ => UseOutlines
}
@pdf.render_pdf(
flow.model,
options=@pdf.RenderOptions::new(
// the faces the flow measured with, drawn and embedded as they are
fonts=catalog.registry,
outline=c.document_outline(),
info=pdf_info(doc),
initial_zoom?,
page_mode~,
display_doc_title=true,
page_boxes=true,
truncate_widths=true,
// Prawn draws a character the font lacks as its .notdef glyph and
// keeps it in the text layer
notdef_text=true,
// ttfunk reads the descriptor's ascent, descent and cap height from
// the OS/2 typographic metrics
typo_metrics=true,
page_labels=Array::makei(flow.model.pages.length(), i => {
page_label(@cmp.minimum(i, labeled_pages - 1) + 1, skip_numbering)
}),
),
)
}