Josef-Friedrich / nodetree

LuaTeX package to visualize node lists in a tree view.
https://www.ctan.org/pkg/nodetree
LaTeX Project Public License v1.3c
14 stars 2 forks source link

Nodetree doesn't print node properties if its coming from "metatable" #7

Closed poetaman closed 3 years ago

poetaman commented 3 years ago

Hi Josef,

Nodetree doesn't print the node properties table if properties reside in metatable. Properties will reside in a metatable if nodes were copied using an operation like box copy: (\copy) . LuaTeX manual states this: "If the second argument of set_properties_mode is true then a metatable approach is chosen: the copy gets its own table with the original table as metatable. If you use the generic font loader the mode is enabled that way." As a minimal test example (to observe this issue), please run the following code twice (there are NOTEs to comment/uncomment some lines of code for 2nd run). I have posted this as a question on TeX.SE here

% >> lualatex glyphprops.tex
\documentclass{article}
\usepackage{fontspec}
\setmainfont[Renderer=HarfBuzz,Script=Latin]{Linux Libertine O}
% ctan version of package nodetree gives lua error on HarfBuzz, use Node instead if you don't have latest github version
%\setmainfont[Renderer=Node,Script=Latin]{Linux Libertine O}

\usepackage[callback={}]{nodetree}

\begin{document}

    \NodetreeRegisterCallback{vpack_filter}

    \setbox0=\vbox{{\hsize=2in\relax Hello world\endgraf}}

    % NOTE: Comment following for second run
    \box0

    % NOTE: Uncomment following for second run
    %\copy0

    \newpage\null

    \NodetreeUnregisterCallback{vpack_filter}

\end{document}

Excerpt from 1st run (properties for nodes from original box, this is fine):

    │   │       ├─GLYPH subtype: 256, char: 'H', width: 7.3pt, height: 6.47pt, depth: 0.02pt
    │   │       │   properties: {['glyph_info'] = H}
    │   │       ├─GLYPH subtype: 256, char: 'e', width: 4.47pt, height: 4.37pt, depth: 0.1pt
    │   │       │   properties: {['glyph_info'] = e}
    │   │       ├─GLYPH subtype: 256, char: 'l', width: 2.64pt, height: 6.98pt, depth: 0.02pt
    │   │       │   properties: {['glyph_info'] = l}
    │   │       ├─GLYPH subtype: 256, char: 'l', width: 2.64pt, height: 6.98pt, depth: 0.02pt
    │   │       │   properties: {['glyph_info'] = l}
    │   │       ├─GLYPH subtype: 256, char: 'o', width: 5.04pt, height: 4.39pt, depth: 0.1pt
    │   │       │   properties: {['glyph_info'] = o}
    │   │       ├─GLUE subtype: spaceskip, width: 2.5pt, stretch: 1.25pt, shrink: 0.83pt
    │   │       ├─GLYPH subtype: 256, char: 'w', width: 7.47pt, height: 4.31pt, depth: 0.12pt
    │   │       │   properties: {['glyph_info'] = w}
    │   │       ├─KERN kern: -0.08pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'o', width: 5.04pt, height: 4.39pt, depth: 0.1pt
    │   │       │   properties: {['glyph_info'] = o}
    │   │       ├─GLYPH subtype: 256, char: 'r', width: 3.72pt, height: 4.42pt, depth: 0.02pt
    │   │       │   properties: {['glyph_info'] = r}
    │   │       ├─GLYPH subtype: 256, char: 'l', width: 2.64pt, height: 6.98pt, depth: 0.02pt
    │   │       │   properties: {['glyph_info'] = l}
    │   │       ├─GLYPH subtype: 256, char: 'd', width: 5.06pt, height: 6.98pt, depth: 0.13pt
    │   │       │   properties: {['glyph_info'] = d}

Excerpt from 2st run (properties for nodes from copied box, this isn't most useful as properties are not listed):

    │   │       ├─GLYPH subtype: 256, char: 'H', width: 7.3pt, height: 6.47pt, depth: 0.02pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'e', width: 4.47pt, height: 4.37pt, depth: 0.1pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'l', width: 2.64pt, height: 6.98pt, depth: 0.02pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'l', width: 2.64pt, height: 6.98pt, depth: 0.02pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'o', width: 5.04pt, height: 4.39pt, depth: 0.1pt
    │   │       │   properties: {}
    │   │       ├─GLUE subtype: spaceskip, width: 2.5pt, stretch: 1.25pt, shrink: 0.83pt
    │   │       ├─GLYPH subtype: 256, char: 'w', width: 7.47pt, height: 4.31pt, depth: 0.12pt
    │   │       │   properties: {}
    │   │       ├─KERN kern: -0.08pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'o', width: 5.04pt, height: 4.39pt, depth: 0.1pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'r', width: 3.72pt, height: 4.42pt, depth: 0.02pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'l', width: 2.64pt, height: 6.98pt, depth: 0.02pt
    │   │       │   properties: {}
    │   │       ├─GLYPH subtype: 256, char: 'd', width: 5.06pt, height: 6.98pt, depth: 0.13pt
    │   │       │   properties: {}
Josef-Friedrich commented 3 years ago

Thank you for your excellent bug report! I read your post on Stackoverflow and all answers very carefully. I’m afraid, but I will need some extra tutoring, how to access the properties of copied nodes using metatables __index ...

poetaman commented 3 years ago

If you were to access each property one by one (by name), then you don't need to anything special. In your code you are trying to iterate over whole properties table (and rightly so) using pairs(), thats where it doesn't really do anything if values are actually in metatable associated with properties table (instead of being in properties table itself). Replacing the definition of function template.table_inline(table) in your nodetree.lua with the following code will resolve the problem. The only change made to your current definition of template.table_inline(table) is the addition of expression table = getAllData(table) before you iterate over the key, value pairs of table. As you can see in the definition of function getAllData, all it does is merge the values of key, value pairs of table with its metatable.


function template.table_inline(table)
  local tex_escape = ''
  if options.channel == 'tex' then
    tex_escape = '\\'
  end
  if type(table) == 'table' then
    table = getAllData(table)
    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

--- Merge key-value pairs from metatable into table.
-- 
-- Source: https://stackoverflow.com/a/5639667
-- Works if __index returns table, which it should as per luatex manual
-- 
-- @tparam t table
-- @tparam prevData table
--
-- @treturn table. Merged table
function getAllData(t, prevData)
  -- if prevData == nil, start empty, otherwise start with prevData
  local data = prevData or {}

  -- copy all the attributes from t
  for k,v in pairs(t) do
    data[k] = data[k] or v
  end

  -- get t's metatable, or exit if not existing
  local mt = getmetatable(t)
  if type(mt)~='table' then return data end

  -- get the __index from mt, or exit if not table
  local index = mt.__index
  if type(index)~='table' then return data end

  -- include the data from index into data, recursively, and return
  return getAllData(index, data)
end
Josef-Friedrich commented 3 years ago

Thank you very much! Would you be so kind and review the latest master branch.

poetaman commented 3 years ago

@Josef-Friedrich You are welcome :) Thanks for the fix. I confirm that the issue reported in this bug is now fixed with your last commit.