T wo T r:commands.lua
From LuaTeXWiki
--- Copyright (c) 2021 by Toadstone Enterprises. --- ISC-type license, see License.txt for details. ----------------------------------------------------------------- local util = require("utils") local read_group = util.read_group local is_command_terminator = util.is_command_terminator local is_whitespace = util.is_whitespace local node_type = util.node_type local reader = require("reader") local push_reader = reader.push_reader local pop_reader = reader.pop_reader local read_value = reader.read_value local unread_value = reader.unread_value local replace_text = reader.replace_text local pos = reader.pos local format = require("format") local update_state = format.update_state local update_locals = format.update_locals local push_tbl = format.push_tbl local pop_tbl = format.pop_tbl local top_tbl = format.top_tbl local push_do_command_stop = format.push_do_command_stop local footnote_tbl = format.footnote_tbl local emph_text_font = format.emph_text_font local bold_text_font = format.bold_text_font local title_font = format.title_font local superscript_font = format.superscript_font local pages = require("pages") local process_par = pages.process_par local build_par = pages.build_par local build_pages = pages.build_pages local main = main or require("main") local main_loop = main.main_loop ----------------------------------------------------------------- --- We will keep our various commands in a table, so that we --- can retrieve them by name. We use a metatable to throw --- a consistent error if a non-existant command is looked --- for. The idea was that this would throw the error at that --- time, instead of later on when we tried to use it. local command_table = {} local mt = {__index = function (_, command) error("Bad Command:" .. command) end } setmetatable(command_table, mt) local register_command = function(name, func) command_table[name] = func end ----------------------------------------------------------------- local get_command = function() -- A command in initiated by a "\", and terminated by a -- "[", "{", or whitespace. (But the initiating "\" has -- already been read past.) Return the command as a string. -- We also 'eat' any extra following whitespace, and then -- back up by one character, so the command can see the -- terminator; although we may lose the distinction of -- which whitespace terminated the command. -- This backing up will also allow the main_loop to see -- the "{", if there was one, and so doing the do_command_start. local command = "" local value = read_value() if is_command_terminator(value) then -- A one character command, consisting solely of a command terminator. command = unicode.utf8.char(value) else while (value and (not is_command_terminator(value))) do command = command .. unicode.utf8.char(value) value = read_value() end end while is_whitespace(value) do -- eat the white value = read_value() end -- so command can see the 'terminator' -- Note that if the command was terminated by whitespace, -- and then followed by more whitespace, we will not be -- distinguishing between the various types of whitespace. -- Does this matter? unread_value() return command end --- WARNING: An Emph in a Footnote will result in a change of font size! --- There should be some notion of a font family where given the current --- font, an appropriate Emph font can be chosen. At least consider size. --- Emph displays most of the standard ways to do a command. --- We define a function do_emph, which will then be registered. local do_emph = function() -- We start off with some initialization. In this case, are we -- emphasizing the following group, or are we (presumably) already -- within a group? local value = read_value() unread_value{} if value == unicode.utf8.byte("{") then -- We emphasize the following group -- We now finish the initialization in this branch of the -- if statement. -- We push the current table, to that we can restore it -- at the end, namely in the do_command_stop. push_tbl(copy_table(top_tbl())) -- We have finished the initialization, so we define a -- do_command_start function to be executed upon encountering -- the "{" local do_command_start = function() -- Switch over to the Emph font. local tbl = top_tbl() tbl.font = emph_text_font end -- We then put this function into the proper place in State. State.do_command_start = do_command_start -- We also define a do_command_stop function, to be executed -- upon encountering the terminating "}". local do_command_stop = function() -- Pop the table, to return to the -- previous situation. pop_tbl() end -- We then put this function into the proper place in State. push_do_command_stop(do_command_stop) else -- Emphasize all the following text (presumably within a group) -- This change of font will get reset when the group is exited, -- since State.tbl has been copied and pushed upon entrance into -- the group. local tbl = top_tbl() tbl.font = emph_text_font end end register_command("Emph", do_emph) local do_bold = function() local value = read_value() unread_value{} if value == unicode.utf8.byte("{") then push_tbl(copy_table(top_tbl())) local do_command_start = function() local tbl = top_tbl() tbl.font = bold_text_font end State.do_command_start = do_command_start local do_command_stop = function() pop_tbl() end push_do_command_stop(do_command_stop) else local tbl = top_tbl() tbl.font = bold_text_font end end register_command("Bold", do_bold) local do_input = function() -- Input a file. local filename local do_command_start = function() -- so that the following read_group will see the opening "{" unread_value() -- Note that with the argument true to read_group -- Input{filename} -- Input { filename } -- Input{file name} -- wil all read "filename" -- Do we want this? The second seems useful, the -- third does not. filename = read_group(true) -- So that main_loop will see the closing "}" -- and call do_command_stop. unread_value() end State.do_command_start = do_command_start local do_command_stop = function() -- We could have done all this above. -- Now read in the file push_reader("file:" .. filename) -- Recursively call main_loop to typeset the -- material we have read in. At the end of the -- files contents, main_loop will terminate, -- and execution will resume with the call to -- pop_reader below. -- Why do I need to qualify this? main.main_loop() -- Back to the original reader. pop_reader() -- And we now build the pages. We could probably -- just leave the material there, but I am -- envisioning each of these input files to -- represent a chapter, to be typeset as their -- own set of pages. build_pages() end push_do_command_stop(do_command_stop) end register_command("Input", do_input) local do_fix = function() -- If one is transcribing a document, one may wish to correct -- typos. Three args - {<old>}{<new>}{<reason>} -- Perhaps not too useful in general, but a nice demonstration -- piece. -- Read the three groups, and insert the new text. local start = pos() local old = read_group(false) local new = read_group(false) local reason = read_group(false) local stop = pos() replace_text(new, start, stop-1) end register_command("Fix", do_fix) local do_nb_space = function() -- Non breaking space. Note that we may have eaten any -- extra following white space, so the char at pos() may not -- be the original space. local cur_pos = pos() replace_text(unicode.utf8.char(0x00A0), cur_pos -2, cur_pos -1) end register_command(" ", do_nb_space) --- QUESTION: Why, unlike in Footnote, do we not need to save the --- current head and tail? Why do we set them to nil when we return --- in do_command_stop? local do_title = function() local value = read_value() if value == unicode.utf8.byte("[") then -- We use the contents of this optional argument to set the -- text of the header. unread_value() local args = read_group(false) -- Set the text of the header. State.header_text = args else -- So that the opening "{" will be seen. unread_value() end local old_baselineskip = tex.baselineskip push_tbl(copy_table(top_tbl())) local do_command_start = function() -- Set the new font and baselineskip tex.baselineskip = make_glue_spec(tex.sp("25pt")) -- Add a title_tbl in format.lua? local tbl = top_tbl() tbl.font = title_font tbl.parindent = tex.sp("0pt") tbl.leftskip = filll() tbl.rightskip = filll() end State.do_command_start = do_command_start local do_command_stop = function() -- We have now read the title, so force -- building a paragraph local head, tail, _, _ = update_locals() local par = build_par(head, tail) process_par(par) -- Insert the (vertical) space after the title. local title_sep = make_glue("userskip", tex.sp("0.25in")) process_par(title_sep) -- Reset various variables update_state(nil, nil, false, false) tex.baselineskip = old_baselineskip pop_tbl() -- New chapter started State.first_paragraph_of_chapter = true State.first_page_of_chapter = true State.notes = {} State.max_notes = 0 end push_do_command_stop(do_command_stop) end register_command("Title", do_title) local do_footnote = function() -- See the QUESTION: before do_title. local old_head, old_tail, _, _ = update_locals() local old_baselineskip = tex.baselineskip State.max_notes = State.max_notes + 1 local do_command_start = function() -- We want to mark the first preceding glyph -- node with the Footnote marker. local to_be_marked = old_tail -- Search backward while (node_type(to_be_marked) ~= "glyph") do to_be_marked = to_be_marked.prev end -- Naked glyphs cannot go into a vlist: local marker = node.hpack(make_glyph(unicode.utf8.byte(tostring(State.max_notes)), superscript_font, 1, 3, 3)) -- Some glue to raise the mark local marker_glue = make_glue(0, tex.sp("5pt")) link_nodes(marker, marker_glue) -- vpack, so that the glue is on the bottom, -- not the left. local packed = node.vpack(marker) -- Link things back up. if (to_be_marked == old_tail) then link_nodes(to_be_marked, packed) old_tail = packed else link_nodes(to_be_marked, packed, to_be_marked.next) end -- Add attribute to to_be_marked, so that we can -- see it while building the page, and get the -- note. node.set_attribute(to_be_marked, 444, State.max_notes) -- Now, set up the first glyph of the footnote -- to also receive the footnote mark. Here we need -- to search forward using the reader. local pos = pos() local value = read_value() -- We make the (possibly false) assumption that -- the footnote starts with a glyph. There should -- be some way to identify glyphs. What if the -- footnote started with an Emph? replace_text("\\Mark{" .. unicode.utf8.char(value) .. "}", pos, pos) -- Switch to footnote mode, and call main_loop -- to typeset the footnote. State.mode = "footnote" update_state(nil, nil, false, false) push_tbl(footnote_tbl) main.main_loop() end State.do_command_start = do_command_start local do_command_stop = function() -- We now build the (final) paragraph of the -- footnote and process it (put it in -- State.notes[max_notes]). Note that if the -- footnote contains multiple paragraphs, the -- earlier ones have already been processed. -- Note also that, unlike with Input, where -- main_loop will terminate on eol, and so -- build_par, process_par, and build_pages, -- we have to do some of this now. local head, tail, _, _ = update_locals() tex.baselineskip = make_glue_spec(tex.sp("12pt")) local par = build_par(head, tail) process_par(par) tex.baselineskip = old_baselineskip -- and restore the previous mode, head, and tail. pop_tbl() State.mode = "main_text" update_state(old_head, old_tail, false, false) end push_do_command_stop(do_command_stop) end register_command("Footnote", do_footnote) local do_mark = function() -- Even though we do nothing in do_command_start, we -- must define it, so that upon seeing the "{" main_loop -- will know that we are starting a command, and not -- entering a new group. local do_command_start = function() end State.do_command_start = do_command_start local do_command_stop = function() local head, tail, _, _ = update_locals() -- We should have just read the single chararcter -- at the start of the footnote. Therefore head -- and tail should be the same. Check this. assert((head == tail), "Head and tail are not the same.") -- add the mark as in Footnote. local marker = node.hpack(make_glyph(unicode.utf8.byte(tostring(State.max_notes)), superscript_font, 1, 3, 3)) local marker_glue = make_glue(0, tex.sp("5pt")) link_nodes(marker, marker_glue) local packed = node.vpack(marker) link_nodes(packed, head) update_state(packed, tail, false, false) end push_do_command_stop(do_command_stop) end register_command("Mark", do_mark) local initialize_command = function(command) return command_table[command]() end local commands = {get_command = get_command, initialize_command = initialize_command, } return commands