--- The nodetree package. -- -- Nodetree uses [LDoc](https://github.com/stevedonovan/ldoc) for the -- source code documentation. The supported tags are described on in -- the [wiki](https://github.com/stevedonovan/LDoc/wiki). -- -- Nodes in LuaTeX are connected. The nodetree view distinguishs -- between the `list` and `field` connections. -- -- * `list`: Nodes, which are double connected by `next` and -- `previous` fields. -- * `field`: Connections to nodes by other fields than `next` and -- `previous` fields, e. g. `head`, `pre`. -- @module nodetree -- luacheck: globals node tex luatexbase lfs callback os unicode status modules if not modules then modules = { } end modules ['nodetree'] = { version = '2.0', comment = 'nodetree', author = 'Josef Friedrich', copyright = 'Josef Friedrich', license = 'The LaTeX Project Public License Version 1.3c 2008-05-04' } --- A counter for the compiled TeX examples. Some TeX code snippets -- a written into file, wrapped with some TeX boilerplate code. -- This written files are compiled. local example_counter = 0 --- The default options local default_options = { callback = 'post_linebreak_filter', channel = 'term', color = 'colored', decimalplaces = 2, engine = 'luatex', -- Required for the callback registration unit = 'pt', verbosity = 1, } --- The current options -- They are changed very often. local options = {} for key, value in pairs(default_options) do options[key] = value end if arg[0] == 'lualatex' then options.engine = 'lualatex' end --- File descriptor local output_file --- The lua table named `tree_state` holds state values of the current -- tree item. -- -- `tree_state`: -- -- * `1` (level): -- * `list`: `continue` -- * `field`: `stop` -- * `2`: -- * `list`: `continue` -- * `field`: `stop` -- @table local tree_state = {} --- Format functions. -- -- Low level template functions. -- -- @section format local format = { --- -- @treturn string underscore = function(string) if options.channel == 'tex' then return string.gsub(string, '_', '\\_') else return string end end, --- -- @treturn string escape = function(string) if options.channel == 'tex' then return string.gsub(string, [[\]], [[\string\]]) else return string end end, -- @treturn number number = function(number) local mult = 10^(options.decimalplaces or 0) return math.floor(number * mult + 0.5) / mult end, --- -- @treturn string whitespace = function(count) local whitespace local output = '' if options.channel == 'tex' then whitespace = '\\hspace{0.5em}' else whitespace = ' ' end if not count then count = 1 end for _ = 1, count do output = output .. whitespace end return output end, --- -- @treturn string color_code = function(code) return string.char(27) .. '[' .. tostring(code) .. 'm' end, --- -- @treturn string color_tex = function(color, mode) if not mode then mode = '' end return 'NTE' .. color .. mode end, --- -- @treturn string node_begin = function() if options.channel == 'tex' then return '\\mbox{' else return '' end end, --- -- @treturn string node_end = function() if options.channel == 'tex' then return '}' else return '' end end, --- -- @treturn string new_line = function(count) local output = '' if not count then count = 1 end local new_line if options.channel == 'tex' then new_line = '\\par{}' else new_line = '\n' end for _ = 1, count do output = output .. new_line end return output end, --- -- @treturn string type_id = function(id) return '[' .. tostring(id) .. ']' end } --- Print the output to stdout or write it into a file (`output_file`). -- New text is appended. -- -- @tparam string text A text string. -- local function nodetree_print(text) if options.channel == 'log' or options.channel == 'tex' then output_file:write(text) else io.write(text) end end --- Template functions. -- @section template local template = { node_colors = { hlist = {'red', 'bright'}, vlist = {'green', 'bright'}, rule = {'blue', 'bright'}, ins = {'blue'}, mark = {'magenta'}, adjust = {'cyan'}, boundary = {'red', 'bright'}, disc = {'green', 'bright'}, whatsit = {'yellow', 'bright'}, local_par = {'blue', 'bright'}, dir = {'magenta', 'bright'}, math = {'cyan', 'bright'}, glue = {'magenta', 'bright'}, kern = {'green', 'bright'}, penalty = {'yellow', 'bright'}, unset = {'blue'}, style = {'magenta'}, choice = {'cyan'}, noad = {'red'}, radical = {'green'}, fraction = {'yellow'}, accent = {'blue'}, fence = {'magenta'}, math_char = {'cyan'}, sub_box = {'red', 'bright'}, sub_mlist = {'green', 'bright'}, math_text_char = {'yellow', 'bright'}, delim = {'blue', 'bright'}, margin_kern = {'magenta', 'bright'}, glyph = {'cyan', 'bright'}, align_record = {'red'}, pseudo_file = {'green'}, pseudo_line = {'yellow'}, page_insert = {'blue'}, split_insert = {'magenta'}, expr_stack = {'cyan'}, nested_list = {'red'}, span = {'green'}, attribute = {'yellow'}, glue_spec = {'magenta'}, attribute_list = {'cyan'}, temp = {'magenta'}, align_stack = {'red', 'bright'}, movement_stack = {'green', 'bright'}, if_stack = {'yellow', 'bright'}, unhyphenated = {'magenta', 'bright'}, hyphenated = {'cyan', 'bright'}, delta = {'red'}, passive = {'green'}, shape = {'yellow'}, }, --- -- [SGR (Select Graphic Rendition) Parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) -- -- __attributes__ -- -- | color |code| -- |------------|----| -- | reset | 0 | -- | clear | 0 | -- | bright | 1 | -- | dim | 2 | -- | underscore | 4 | -- | blink | 5 | -- | reverse | 7 | -- | hidden | 8 | -- -- __foreground__ -- -- | color |code| -- |------------|----| -- | black | 30 | -- | red | 31 | -- | green | 32 | -- | yellow | 33 | -- | blue | 34 | -- | magenta | 35 | -- | cyan | 36 | -- | white | 37 | -- -- __background__ -- -- | color |code| -- |------------|----| -- | onblack | 40 | -- | onred | 41 | -- | ongreen | 42 | -- | onyellow | 43 | -- | onblue | 44 | -- | onmagenta | 45 | -- | oncyan | 46 | -- | onwhite | 47 | -- -- @tparam string color A color name (`black`, `red`, `green`, -- `yellow`, `blue`, `magenta`, `cyan`, `white`). -- @tparam string mode `bright` or `dim`. -- @tparam boolean background Colorize the background not the text. -- -- @treturn string color = function(color, mode, background) if options.color ~= 'colored' then return '' end local output = '' local code if mode == 'bright' then output = format.color_code(1) elseif mode == 'dim' then output = format.color_code(2) end if not background then if color == 'reset' then code = 0 elseif color == 'red' then code = 31 elseif color == 'green' then code = 32 elseif color == 'yellow' then code = 33 elseif color == 'blue' then code = 34 elseif color == 'magenta' then code = 35 elseif color == 'cyan' then code = 36 else code = 37 end else if color == 'black' then code = 40 elseif color == 'red' then code = 41 elseif color == 'green' then code = 42 elseif color == 'yellow' then code = 43 elseif color == 'blue' then code = 44 elseif color == 'magenta' then code = 45 elseif color == 'cyan' then code = 46 elseif color == 'white' then code = 47 else code = 40 end end return output .. format.color_code(code) end, --- Format a single unicode character. -- -- @tparam string char A single input character. -- -- @treturn string char = function(char) char = string.format('%s', unicode.utf8.char(char)) char = '\'' .. char .. '\'' if options.channel == 'tex' then char = format.escape(char) end return char end, --- -- @treturn string line = function(length) local output if length == 'long' then output = '------------------------------------------' else output = '-----------------------' end return output .. format.new_line() end, --- -- @treturn string branch = function(connection_type, connection_state, last) local c = connection_type local s = connection_state local l = last if c == 'list' and s == 'stop' and l == false then return format.whitespace(2) elseif c == 'field' and s == 'stop' and l == false then return format.whitespace(2) elseif c == 'list' and s == 'continue' and l == false then return '│' .. format.whitespace() elseif c == 'field' and s == 'continue' and l == false then return '║' .. format.whitespace() elseif c == 'list' and s == 'continue' and l == true then return '├─' elseif c == 'field' and s == 'continue' and l == true then return '╠═' elseif c == 'list' and s == 'stop' and l == true then return '└─' elseif c == 'field' and s == 'stop' and l == true then return '╚═' end return '' end, } --- -- @treturn string function template.fill(number, order, field) local output if order ~= nil and order ~= 0 then if field == 'stretch' then output = '+' else output = '-' end return output .. string.format( '%g%s', number / 2^16, template.colored_string( 'fi' .. string.rep('l', order - 1), 'white', 'dim' ) ) else return template.length(number) end end --- Colorize a text string. -- -- @tparam string text A text string. -- @tparam string color A color name (`black`, `red`, `green`, -- `yellow`, `blue`, `magenta`, `cyan`, `white`). -- @tparam string mode `bright` or `dim`. -- @tparam boolean background Colorize the background not the text. -- -- @treturn string function template.colored_string(text, color, mode, background) if options.channel == 'tex' then if mode == 'dim' then mode = '' end return '\\textcolor{' .. format.color_tex(color, mode) .. '}{' .. text .. '}' else return template.color(color, mode, background) .. text .. template.color('reset') end end --- Format a scaled point input value into dimension string (`12pt`, -- `1cm`) -- -- @tparam number input -- -- @treturn string function template.length (input) input = tonumber(input) input = input / tex.sp('1' .. options.unit) return string.format( '%g%s', format.number(input), template.colored_string(options.unit, 'white', 'dim') ) end --- Convert a Lua table into a format string. -- -- @tparam table table A table to generate a inline view of. -- -- @treturn string function template.table_inline(table) local tex_escape = '' if options.channel == 'tex' then tex_escape = '\\' end if type(table) == 'table' then local output = tex_escape .. '{' local kv_list = '' for key, value in pairs(table) do if type(key) ~= 'numbers' then key = '\'' .. template.colored_string(key, 'cyan', 'dim') .. '\'' end kv_list = kv_list .. '[' .. key .. '] = ' .. template.table_inline(value) .. ', ' end output = output .. kv_list:gsub(', $', '') return output .. tex_escape .. '}' else return tostring(table) end end --- Format a key value pair (`key: value, `). -- -- @tparam string key A key -- @tparam string|number value A value -- @tparam string color A color name (`black`, `red`, `green`, -- `yellow`, `blue`, `magenta`, `cyan`, `white`). -- -- @treturn string function template.key_value(key, value, color) if type(color) ~= 'string' then color = 'yellow' end if options.channel == 'tex' then key = format.underscore(key) end local output = template.colored_string(key .. ':', color) if value then output = output .. ' ' .. value .. ', ' end return output end --- -- @treturn string function template.type(type, id) local output if options.channel == 'tex' then output = format.underscore(type) else output = type end output = string.upper(output) if options.verbosity > 1 then output = output .. format.type_id(id) end return template.colored_string( output .. format.whitespace(), template.node_colors[type][1], template.node_colors[type][2] ) end --- -- @treturn string function template.callback(callback_name, variables) nodetree_print( format.new_line(2) .. 'Callback: ' .. template.colored_string(format.underscore(callback_name), 'red', '', true) .. format.new_line() ) if variables then for name, value in pairs(variables) do if value ~= nil and value ~= '' then nodetree_print( '- ' .. format.underscore(name) .. ': ' .. tostring(value) .. format.new_line() ) end end end nodetree_print(template.line('long')) end --- -- @treturn string function template.branches(level, connection_type) local output = '' for i = 1, level - 1 do output = output .. template.branch('list', tree_state[i]['list'], false) output = output .. template.branch('field', tree_state[i]['field'], false) end -- Format the last branches if connection_type == 'list' then output = output .. template.branch('list', tree_state[level]['list'], true) else output = output .. template.branch('list', tree_state[level]['list'], false) output = output .. template.branch('field', tree_state[level]['field'], true) end return output end --- Extend the node library -- @section node_extended local node_extended = {} --- Get the ID of a node. -- -- We have to convert the node into a string and than have to extract -- the ID from this string using a regular expression. If you convert a -- node into a string it looks like: ` nil : -- hlist 2>`. -- -- @tparam node n A node. -- -- @treturn string function node_extended.node_id(n) return string.gsub(tostring(n), '^ 1 then output = output .. format.type_id(n.subtype) end return output else return tostring(n.subtype) end end --- Build the node tree. -- @section tree local tree = {} --- -- @tparam node head -- @tparam string field -- -- @treturn string function tree.format_field(head, field) local output -- Character "0" should be printed in a tree, because in TeX fonts the -- 0 slot usually has a symbol. if not head[field] or (head[field] == 0 and field ~= "char") then return '' end if options.verbosity < 2 and -- glyph field == 'font' or field == 'left' or field == 'right' or field == 'uchyph' or -- hlist field == 'dir' or field == 'glue_order' or field == 'glue_sign' or field == 'glue_set' or -- glue field == 'stretch_order' then return '' elseif options.verbosity < 3 and field == 'prev' or field == 'next' or field == 'id' then return '' end if field == 'prev' or field == 'next' then output = node_extended.node_id(head[field]) elseif field == 'subtype' then output = format.underscore(node_extended.subtype(head)) elseif field == 'width' or field == 'height' or field == 'depth' or field == 'kern' or field == 'shift' then output = template.length(head[field]) elseif field == 'char' then output = template.char(head[field]) elseif field == 'glue_set' then output = format.number(head[field]) elseif field == 'stretch' or field == 'shrink' then output = template.fill(head[field], head[field .. '_order'], field) else output = tostring(head[field]) end return template.key_value(field, output) end --- -- Attributes are key/value number pairs. They are printed as an inline -- list. The attribute `0` with the value `0` is skipped because this -- attribute is in every node by default. -- -- @tparam node head -- -- @treturn string function tree.format_attributes(head) if not head then return '' end local output = '' local attr = head.next while attr do if attr.number ~= 0 or (attr.number == 0 and attr.value ~= 0) then output = output .. tostring(attr.number) .. '=' .. tostring(attr.value) .. ' ' end attr = attr.next end return output end --- -- @tparam number level `level` is a integer beginning with 1. -- @tparam number connection_type The variable `connection_type` -- is a string, which can be either `list` or `field`. -- @tparam connection_state `connection_state` is a string, which can -- be either `continue` or `stop`. function tree.set_state(level, connection_type, connection_state) if not tree_state[level] then tree_state[level] = {} end tree_state[level][connection_type] = connection_state end --- -- @tparam table fields -- @tparam number level function tree.analyze_fields(fields, level) local max = 0 local connection_state for _ in pairs(fields) do max = max + 1 end local count = 0 for field_name, recursion_node in pairs(fields) do count = count + 1 if count == max then connection_state = 'stop' else connection_state = 'continue' end tree.set_state(level, 'field', connection_state) nodetree_print( format.node_begin() .. template.branches(level, 'field') .. template.key_value(field_name) .. format.node_end() .. format.new_line() ) tree.analyze_list(recursion_node, level + 1) end end --- -- @tparam node head -- @tparam number level function tree.analyze_node(head, level) local connection_state local output if head.next then connection_state = 'continue' else connection_state = 'stop' end tree.set_state(level, 'list', connection_state) output = template.branches(level, 'list') .. template.type(node.type(head.id), head.id) if options.verbosity > 1 then output = output .. template.key_value('no', node_extended.node_id(head)) end -- We store the attributes output to append it to the field list. local attributes -- We store fields which are nodes for later treatment. local fields = {} -- Inline fields, for example: char: 'm', width: 25pt, height: 13.33pt, local output_fields = '' for _, field_name in pairs(node.fields(head.id, head.subtype)) do if field_name == 'attr' then attributes = tree.format_attributes(head.attr) elseif field_name ~= 'next' and field_name ~= 'prev' and node.is_node(head[field_name]) then fields[field_name] = head[field_name] else output_fields = output_fields .. tree.format_field(head, field_name) end end if output_fields ~= '' then output = output .. output_fields end -- Append the attributes output if available if attributes ~= '' then output = output .. template.key_value('attr', attributes, 'blue') end output = output:gsub(', $', '') nodetree_print( format.node_begin() .. output .. format.node_end() .. format.new_line() ) local property = node.getproperty(head) if property then nodetree_print( format.node_begin() .. template.branches(level, 'field') .. ' ' .. template.colored_string('properties:', 'blue') .. ' ' .. template.table_inline(property) .. format.node_end() .. format.new_line() ) end tree.analyze_fields(fields, level) end --- -- @tparam node head -- @tparam number level function tree.analyze_list(head, level) while head do tree.analyze_node(head, level) head = head.next end end --- -- @tparam node head function tree.analyze_callback(head) tree.analyze_list(head, 1) nodetree_print(template.line('short') .. format.new_line()) end --- Callback wrapper. -- @section callbacks local callbacks = { --- -- @tparam string extrainfo contribute_filter = function(extrainfo) template.callback('contribute_filter', {extrainfo = extrainfo}) return true end, --- -- @tparam string extrainfo buildpage_filter = function(extrainfo) template.callback('buildpage_filter', {extrainfo = extrainfo}) return true end, --- -- @tparam string n -- @tparam string i build_page_insert = function(n, i) print('lol') template.callback('build_page_insert', {n = n, i = i}) return 0 end, --- -- @tparam node head -- @tparam string groupcode pre_linebreak_filter = function(head, groupcode) template.callback('pre_linebreak_filter', {groupcode = groupcode}) tree.analyze_callback(head) return true end, --- -- @tparam node head -- @tparam boolean is_display linebreak_filter = function(head, is_display) template.callback('linebreak_filter', {is_display = is_display}) tree.analyze_callback(head) return true end, --- -- @tparam node box -- @tparam string locationcode -- @tparam number prevdepth -- @tparam boolean mirrored append_to_vlist_filter = function(box, locationcode, prevdepth, mirrored) local variables = { locationcode = locationcode, prevdepth = prevdepth, mirrored = mirrored, } template.callback('append_to_vlist_filter', variables) tree.analyze_callback(box) return box end, --- -- @tparam node head -- @tparam string groupcode post_linebreak_filter = function(head, groupcode) template.callback('post_linebreak_filter', {groupcode = groupcode}) tree.analyze_callback(head) return true end, --- -- @tparam node head -- @tparam string groupcode -- @tparam number size -- @tparam string packtype -- @tparam string direction -- @tparam node attributelist hpack_filter = function(head, groupcode, size, packtype, direction, attributelist) local variables = { groupcode = groupcode, size = size, packtype = packtype, direction = direction, attributelist = attributelist, } template.callback('hpack_filter', variables) tree.analyze_callback(head) return true end, --- -- @tparam node head -- @tparam string groupcode -- @tparam number size -- @tparam string packtype -- @tparam number maxdepth -- @tparam string direction -- @tparam node attributelist vpack_filter = function(head, groupcode, size, packtype, maxdepth, direction, attributelist) local variables = { groupcode = groupcode, size = size, packtype = packtype, maxdepth = template.length(maxdepth), direction = direction, attributelist = attributelist, } template.callback('vpack_filter', variables) tree.analyze_callback(head) return true end, --- -- @tparam string incident -- @tparam number detail -- @tparam node head -- @tparam number first -- @tparam number last hpack_quality = function(incident, detail, head, first, last) local variables = { incident = incident, detail = detail, first = first, last = last, } template.callback('hpack_quality', variables) tree.analyze_callback(head) end, --- -- @tparam string incident -- @tparam number detail -- @tparam node head -- @tparam number first -- @tparam number last vpack_quality = function(incident, detail, head, first, last) local variables = { incident = incident, detail = detail, first = first, last = last, } template.callback('vpack_quality', variables) tree.analyze_callback(head) end, --- -- @tparam node head -- @tparam number width -- @tparam number height process_rule = function(head, width, height) local variables = { width = width, height = height, } template.callback('process_rule', variables) tree.analyze_callback(head) return true end, --- -- @tparam node head -- @tparam string groupcode -- @tparam number size -- @tparam string packtype -- @tparam number maxdepth -- @tparam string direction pre_output_filter = function(head, groupcode, size, packtype, maxdepth, direction) local variables = { groupcode = groupcode, size = size, packtype = packtype, maxdepth = maxdepth, direction = direction, } template.callback('pre_output_filter', variables) tree.analyze_callback(head) return true end, --- -- @tparam node head -- @tparam node tail hyphenate = function(head, tail) template.callback('hyphenate') nodetree_print('head:') tree.analyze_callback(head) nodetree_print('tail:') tree.analyze_callback(tail) end, --- -- @tparam node head -- @tparam node tail ligaturing = function(head, tail) template.callback('ligaturing') nodetree_print('head:') tree.analyze_callback(head) nodetree_print('tail:') tree.analyze_callback(tail) end, --- -- @tparam node head -- @tparam node tail kerning = function(head, tail) template.callback('kerning') nodetree_print('head:') tree.analyze_callback(head) nodetree_print('tail:') tree.analyze_callback(tail) end, --- -- @tparam node local_par -- @tparam string location insert_local_par = function(local_par, location) template.callback('insert_local_par', {location = location}) tree.analyze_callback(local_par) return true end, --- -- @tparam node head -- @tparam string display_type -- @tparam boolean need_penalties mlist_to_hlist = function(head, display_type, need_penalties) local variables = { display_type = display_type, need_penalties = need_penalties, } template.callback('mlist_to_hlist', variables) tree.analyze_callback(head) return node.mlist_to_hlist(head, display_type, need_penalties) end, } --- Set a single option key value pair. -- -- @tparam string key The key of the option pair. -- @tparam number|string value The value of the option pair. local function set_option(key, value) if not options then options = {} end if key == 'verbosity' or key == 'decimalplaces' then options[key] = tonumber(value) else options[key] = value end end --- Set multiple key value pairs using a table. -- -- @tparam table opts Options local function set_options(opts) if not options then options = {} end for key, value in pairs(opts) do set_option(key, value) end end --- Check if the given callback name exists. -- -- Throw an error if it doen’t. -- -- @tparam string callback_name The name of a callback to check. -- -- @treturn string The unchanged input of the function. local function check_callback_name(callback_name) local info = callback.list() if info[callback_name] == nil then tex.error( 'Package "nodetree": Unkown callback name or callback alias: "' .. callback_name .. '"' ) end return callback_name end --- Get the real callback name from an alias string. -- -- @tparam string alias The alias of a callback name or the callback -- name itself. -- -- @treturn string The real callback name. local function get_callback_name(alias) local callback_name -- Listed as in the LuaTeX reference manual. if alias == 'contribute' or alias == 'contributefilter' then callback_name = 'contribute_filter' -- Formerly called buildpage, now there is a build_page_insert. elseif alias == 'buildfilter' or alias == 'buildpagefilter' then callback_name = 'buildpage_filter' -- Untested: I don’t know how to invoke this filter. elseif alias == 'buildinsert' or alias == 'buildpageinsert' then callback_name = 'build_page_insert' elseif alias == 'preline' or alias == 'prelinebreakfilter' then callback_name = 'pre_linebreak_filter' elseif alias == 'line' or alias == 'linebreakfilter' then callback_name = 'linebreak_filter' elseif alias == 'append' or alias == 'appendtovlistfilter' then callback_name = 'append_to_vlist_filter' -- postlinebreak is not documented. elseif alias == 'postline' or alias == 'postlinebreak' or alias == 'postlinebreakfilter' then callback_name = 'post_linebreak_filter' elseif alias == 'hpack' or alias == 'hpackfilter' then callback_name = 'hpack_filter' elseif alias == 'vpack' or alias == 'vpackfilter' then callback_name = 'vpack_filter' elseif alias == 'hpackq' or alias == 'hpackquality' then callback_name = 'hpack_quality' elseif alias == 'vpackq' or alias == 'vpackquality' then callback_name = 'vpack_quality' elseif alias == 'process' or alias == 'processrule' then callback_name = 'process_rule' elseif alias == 'preout' or alias == 'preoutputfilter' then callback_name = 'pre_output_filter' elseif alias == 'hyph' or alias == 'hyphenate' then callback_name = 'hyphenate' elseif alias == 'liga' or alias == 'ligaturing' then callback_name = 'ligaturing' elseif alias == 'kern' or alias == 'kerning' then callback_name = 'kerning' elseif alias == 'insert' or alias == 'insertlocalpar' then callback_name = 'insert_local_par' elseif alias == 'mhlist' or alias == 'mlisttohlist' then callback_name = 'mlist_to_hlist' else callback_name = alias end return check_callback_name(callback_name) end --- Register a callback. -- -- @tparam string cb The name of a callback. local function register_callback(cb) if options.engine == 'lualatex' then luatexbase.add_to_callback(cb, callbacks[cb], 'nodetree') else callback.register(cb, callbacks[cb]) end end --- Unregister a callback. -- -- @tparam string cb The name of a callback. local function unregister_callback(cb) if options.engine == 'lualatex' then luatexbase.remove_from_callback(cb, 'nodetree') else register_callback(cb, nil) end end --- Exported functions. -- @section export local export = { set_option = set_option, set_options = set_options, --- register_callbacks = function() if options.channel == 'log' or options.channel == 'tex' then -- nt = nodetree -- jobname.nttex -- jobname.ntlog local file_name = tex.jobname .. '.nt' .. options.channel io.open(file_name, 'w'):close() -- Clear former content output_file = io.open(file_name, 'a') end for alias in string.gmatch(options.callback, '([^,]+)') do register_callback(get_callback_name(alias)) end end, --- unregister_callbacks = function() for alias in string.gmatch(options.callback, '([^,]+)') do unregister_callback(get_callback_name(alias)) end end, --- Compile a TeX snippet. -- -- Write some TeX snippets into a temporary LaTeX file, compile this -- file using `latexmk` and read the generated `*.nttex` file and -- return its content. -- -- @tparam string tex_markup -- -- @treturn string compile_include = function(tex_markup) -- Generate a subfolder for all tempory files: _nodetree-jobname. local parent_path = lfs.currentdir() .. '/' .. '_nodetree-' .. tex.jobname lfs.mkdir(parent_path) -- Generate the temporary LuaTeX or LuaLaTeX file. example_counter = example_counter + 1 local filename_tex = example_counter .. '.tex' local absolute_path_tex = parent_path .. '/' .. filename_tex output_file = io.open(absolute_path_tex, 'w') local format_option = function (key, value) return '\\NodetreeSetOption[' .. key .. ']{' .. value .. '}' .. '\n' end -- Process the options local options = format_option('channel', 'tex') .. format_option('verbosity', options.verbosity) .. format_option('unit', options.unit) .. format_option('decimalplaces', options.decimalplaces) .. '\\NodetreeUnregisterCallback{post_linebreak_filter}' .. '\n' .. '\\NodetreeRegisterCallback{' .. options.callback .. '}' local prefix = '%!TEX program = lualatex\n' .. '\\documentclass{article}\n' .. '\\usepackage{nodetree}\n' .. options .. '\n' .. '\\begin{document}\n' local suffix = '\n\\end{document}' output_file:write(prefix .. tex_markup .. suffix) output_file:close() -- Compile the temporary LuaTeX or LuaLaTeX file. os.spawn({ 'latexmk', '-cd', '-pdflua', absolute_path_tex }) local include_file = assert(io.open(parent_path .. '/' .. example_counter .. '.nttex', 'rb')) local include_content = include_file:read("*all") include_file:close() include_content = include_content:gsub('[\r\n]', '') tex.print(include_content) end, --- Check for `--shell-escape` -- check_shell_escape = function() local info = status.list() if info.shell_escape == 0 then tex.error('Package "nodetree-embed": You have to use the --shell-escape option') end end, --- Print a node tree. --- -- @tparam node head The head node of a node list. -- @tparam table opts Options as a table. print = function(head, opts) if opts and type(opts) == 'table' then set_options(opts) end nodetree_print(format.new_line()) tree.analyze_list(head, 1) end, --- Format a scaled point value into a formated string. -- -- @tparam number sp A scaled point value -- -- @treturn string format_dim = function(sp) return template.length(sp) end, --- Get a default option that is not changed. -- @tparam string key The key of the option. -- -- @treturn string|number|boolean get_default_option = function(key) return default_options[key] end } --- Use export.print -- @tparam node head export.analyze = export.print return export