T wo T r:pages.lua
From LuaTeXWiki
--- Copyright (c) 2021 by Toadstone Enterprises. --- ISC-type license, see License.txt for details. ----------------------------------------------------------------- local util = require("utils") local node_type = util.node_type local link_nodes = util.link_nodes local make_glue = util.make_glue local make_rule = util.make_rule local format = require("format") local text_height = format.text_height local top_margin = format.top_margin local left_margin = format.left_margin local header_height = format.header_height local header_sep = format.header_sep local footer_height = format.footer_height local footer_sep = format.footer_sep local page_number = format.page_number local header_tbl = format.header_tbl local footer_tbl = format.footer_tbl local push_tbl = format.push_tbl local pop_tbl = format.pop_tbl local top_tbl = format.top_tbl local reader = require("reader") local push_reader = reader.push_reader local pop_reader = reader.pop_reader local main = main or require("main") local main_loop = main.main_loop ----------------------------------------------------------------- local par_height = function(par) -- But can really be used on any list of lines including -- multiple paragraphs. local height = 0 while par do if (node_type(par) == "hlist") then height = height + par.height + par.depth elseif (node_type(par) == "glue") then height = height + par.width else -- A penalty has zero height, but what else might we find? assert((node_type(par) == "penalty"), "Found a: " .. node_type(par)) end par = par.next end return height end local build_par = function(head, tail) -- We have a linked list of nodes, starting with head, and ending -- with tail. We insert the initial parindent and final penalty -- and parfillskip. local tbl = top_tbl() local n if State.first_paragraph_of_chapter then -- This is the first paragraph after a title. Do not indent. n = make_glue(0, 0) State.first_paragraph_of_chapter = false else --indent. n = make_glue(0, tbl.parindent or tex.parindent) end link_nodes(n, head) head = n local penalty = node.new("penalty") penalty.penalty = 10000 local parfillskip = make_glue("parfillskip", 0, tbl.parfillskipstretch or 2^16, 0, tbl.parfillskipstretch_order or 2, 0) link_nodes(tail, penalty, parfillskip) -- hyphenate, kern, and ligature lang.hyphenate(head) -- With otf fonts, use this rather than kern and ligature. nodes.simple_font_handler(head) -- and make the paragraph. local par = tex.linebreak(head, tbl) return par end local process_par = function(par) -- Move par to the appropriate place, based on State.mode -- If we want to build pages as we go, instead of in batches, -- we can call the page builder from in here. local a_par_is_already_there if (State.mode == "main_text") then a_par_is_already_there = State.main elseif (State.mode == "header") then a_par_is_already_there = State.header elseif (State.mode == "footer") then a_par_is_already_there = State.footer elseif (State.mode == "footnote") then a_par_is_already_there = State.notes[State.max_notes] else assert(false, "Invalid mode seen in process_par") end if a_par_is_already_there then -- at least one paragraph has already been processed, so we link -- the new par to the old one. local tail = node.tail(a_par_is_already_there) if ((node_type(tail) == "hlist") and (node_type(par) == "hlist")) then -- add tex.baselineskip local glue_needed = tex.baselineskip.width - (par.height + tail.depth) local n = make_glue("baselineskip", glue_needed) link_nodes(tail, n, par) else -- tail or par is glue? assert(((node_type(tail) == "glue") or (node_type(par) == "glue")), "Not glue!") -- We assume that this glue is enough. link_nodes(tail, par) end else -- This is the first par, so we just put it where it belongs. if (State.mode == "main_text") then State.main = par elseif (State.mode == "header") then State.header = par elseif (State.mode == "footer") then State.footer = par elseif (State.mode == "footnote") then State.notes[State.max_notes] = par else assert(false, "Invalid mode seen in process_par") end end end local pop_line = function() -- Pop the top line from State.main. -- This is used by build_page to incrementally build a page. if State.main then local line = State.main State.main = State.main.next if State.main then State.main.prev = nil end line.next = nil return line else return nil end end local return_line = function(line) -- Return a (previously popped) line to State.main line.next = State.main if State.main then State.main.prev = line end State.main = line end ----------------------------------------------------------------- --- Now, on to build_page. local make_header = function() -- Make the header -- The text of our header. -- When we call main_loop, this will be the reader used. if State.header_text then push_reader(State.header_text) else push_reader("Header") end -- This is the tbl that will be used by main_loop. push_tbl(header_tbl) local old_mode = State.mode State.mode = "header" -- Now we do the work. -- When pages and main became mutually dependant, I had to -- prefix main_loop with the package name. Otherwise: -- attempt to call a nil value (upvalue 'main_loop') -- Similarly in make_footer. -- Why is this? main.main_loop() State.mode = old_mode -- Restore the old reader and tbl. pop_reader() pop_tbl() -- We assume that the header is only one line local line = State.header State.header = nil assert((line.next == nil), "Header is not just one line.") -- We center the header (vertically) in its header_height box local needed_height = header_height - line.height local top_glue = make_glue(0, needed_height / 2) local bot_glue = make_glue(0, needed_height / 2) -- Add a rule local header_rule = make_rule("normal", text_width, tex.sp("1pt")) header_rule_box = node.hpack(header_rule) -- Must hpack. Why? -- centered (vertically) in its header_sep height box local top_sep_glue = make_glue(0, (header_sep - tex.sp("1pt") / 2)) local bot_sep_glue = make_glue(0, (header_sep - tex.sp("1pt") / 2)) -- link it al together link_nodes(top_glue, line, bot_glue, top_sep_glue, header_rule_box, bot_sep_glue) -- and package it up in a vbox local header = node.vpack(top_glue) return header end local make_footer = function() -- the text of our footer push_reader(tostring(page_number)) push_tbl(footer_tbl) local old_mode = State.mode State.mode = "footer" main.main_loop() State.mode = old_mode pop_reader() pop_tbl() local line = State.footer State.footer = nil assert((line.next == nil), "Footer is not just one line.") local needed_height = footer_height - line.height local foot_glue = make_glue(0, needed_height) local sep_glue = make_glue(0, footer_sep) link_nodes(sep_glue, foot_glue, line) local footer = node.vpack(foot_glue) return footer end local build_page_step_one = function(line) -- 1) get the main text from main_text, a line at a time until -- we either run out of lines or text_height. local head = line local tail = line local note = nil local notes = nil local current_height = 0 while ((current_height < text_height) and line) do if node_type(line) == "hlist" then -- See if this line has an associated footnote. local v, _ = node.find_attribute(line.head, 444) if v then -- Get the note, and account for its height. note = State.notes[v] local extra = par_height(note) if notes then -- The separation between notes. extra = extra + tex.sp("4pt") else -- The separation before the first note. extra = extra + tex.sp("8pt") end -- Does the line still fit on the page? if (current_height + line.height + line.depth + extra) < text_height then -- Add the line to the end of the text. link_nodes(tail, line) tail = line current_height = (current_height + line.height + line.depth + par_height(note)) if notes then -- Add the note to the notes. local n = make_glue(0, tex.sp("4pt")) link_nodes(node.tail(notes), n, note) else -- This is the first note. notes = note end else -- The line plus the note do not fit on the page. return_line(line, main_text) return current_height, head, tail, notes end elseif (current_height + line.height + line.depth) < text_height then -- We have a line without a note, that fits on the page. link_nodes(tail, line) tail = line current_height = (current_height + line.height + line.depth) else -- The line does not fit on the page. return_line(line, main_text) return current_height, head, tail, notes end elseif node_type(line) == "glue" then -- line is glue, not an hlist. if (current_height + line.width) < text_height then -- line fits on the page. link_nodes(tail, line) tail = line current_height = current_height + line.width else -- line does not fit on the page. return_line(line, main_text) return current_height, head, tail, notes end else -- line is neither an hlist nor glue. assert((node_type(line) == "penalty"), "Threw away a non-penalty: " .. node_type(line)) end -- Get the next line, and go to the top of the while loop line = pop_line() end return current_height, head, tail, notes end local build_page_step_four = function(text_box, notes, header_box, footer_box) -- 4) put it all together local top_margin_glue = make_glue(0, top_margin) if State.first_page_of_chapter then -- No header if notes then local n = make_glue(0, tex.sp("8pt")) link_nodes(top_margin_glue, text_box, n, notes) else link_nodes(top_margin_glue, text_box) end State.first_page_of_chapter = false else -- Include a header if notes then local n = make_glue(0, tex.sp("8pt")) link_nodes(top_margin_glue, header_box, text_box, n, notes, footer_box) else link_nodes(top_margin_glue, header_box, text_box, footer_box) end end -- 4a) Finish the assembly of the page, and ship it out. local main_box = node.vpack(top_margin_glue) local left_margin_glue = make_glue(0, left_margin) link_nodes(left_margin_glue, main_box) local page_box = node.hpack(left_margin_glue) tex.setbox(666, page_box) tex.shipout(666) -- Increment the page counters, both tex's and ours. tex.count[0] = tex.count[0] + 1 page_number = page_number + 1 end local build_page = function() -- Build a page by: -- 1) get the main text from main_text, a line at a time until -- we either run out of lines or text_height. -- 2) pad out the text to fill text_height -- 3) make our header and footer -- 4) put it all together local line = pop_line() local current_height = 0 local head, tail local notes -- We are at the top of the page, so we throw away anything -- that is not an hlist while node_type(line) ~= "hlist" do print("Throwing away a: " .. node_type(line)) line = pop_line(main_text) end -- 1) get the main text from State.text, a line at a time until -- we either run out of lines or text_height. current_height, head, tail, notes = build_page_step_one(line) -- We have fit all the text that will fit on the page. -- 2) pad out the text to fill text_height if (current_height < text_height) then local needed_height = text_height - current_height local n = make_glue(0, needed_height) link_nodes(tail, n) end local text_box = node.vpack(head) -- 3) make our header and footer local header_box = make_header() local footer_box = make_footer() -- 4) put it all together build_page_step_four(text_box, notes, header_box, footer_box) end local build_pages = function() -- While there is text to fill a page, we build pages. while State.main do build_page() end end return {build_par = build_par, process_par = process_par, build_pages = build_pages}