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}