texlive[68244] Master/texmf-dist: nodetree (11sep23)

commits+karl at tug.org commits+karl at tug.org
Mon Sep 11 23:18:51 CEST 2023


Revision: 68244
          http://tug.org/svn/texlive?view=revision&revision=68244
Author:   karl
Date:     2023-09-11 23:18:50 +0200 (Mon, 11 Sep 2023)
Log Message:
-----------
nodetree (11sep23)

Modified Paths:
--------------
    trunk/Master/texmf-dist/doc/luatex/nodetree/README.md
    trunk/Master/texmf-dist/doc/luatex/nodetree/nodetree.pdf
    trunk/Master/texmf-dist/source/luatex/nodetree/nodetree.dtx
    trunk/Master/texmf-dist/source/luatex/nodetree/nodetree.ins
    trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree-embed.sty
    trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.lua
    trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.sty
    trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.tex

Added Paths:
-----------
    trunk/Master/texmf-dist/doc/luatex/nodetree/nodetree-doc.tex

Modified: trunk/Master/texmf-dist/doc/luatex/nodetree/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/luatex/nodetree/README.md	2023-09-11 21:18:30 UTC (rev 68243)
+++ trunk/Master/texmf-dist/doc/luatex/nodetree/README.md	2023-09-11 21:18:50 UTC (rev 68244)
@@ -18,7 +18,7 @@
 
 # License
 
-Copyright (C) 2016-2022 by Josef Friedrich <josef at friedrich.rocks>
+Copyright (C) 2016-2023 by Josef Friedrich <josef at friedrich.rocks>
 ------------------------------------------------------------------------
 This work may be distributed and/or modified under the conditions of
 the LaTeX Project Public License, either version 1.3 of this license
@@ -150,8 +150,8 @@
 ### Update the copyright year:
 
 ```
-sed -i 's/(C) 2016-2022/(C) 2016-2021/g' nodetree.ins
-sed -i 's/(C) 2016-2022/(C) 2016-2021/g' nodetree.dtx
+sed -i 's/(C) 2016-2023/(C) 2016-2021/g' nodetree.ins
+sed -i 's/(C) 2016-2023/(C) 2016-2021/g' nodetree.dtx
 ```
 
 ### Command line tasks:

Added: trunk/Master/texmf-dist/doc/luatex/nodetree/nodetree-doc.tex
===================================================================
--- trunk/Master/texmf-dist/doc/luatex/nodetree/nodetree-doc.tex	                        (rev 0)
+++ trunk/Master/texmf-dist/doc/luatex/nodetree/nodetree-doc.tex	2023-09-11 21:18:50 UTC (rev 68244)
@@ -0,0 +1,1444 @@
+%!TEX program = lualatex
+\documentclass{ltxdoc}
+\usepackage{paralist,fontspec,microtype}
+\usepackage[
+  colorlinks=true,
+  linkcolor=red,
+  filecolor=red,
+  urlcolor=red,
+]{hyperref}
+\usepackage{nodetree-embed}
+\mdfsetup{
+  innerleftmargin=0.2em,
+  innerrightmargin=0.2em,
+}
+\EnableCrossrefs
+\CodelineIndex
+\RecordChanges
+
+% Improve ltxdoc's `|...|` feature by allowing breaks.
+\usepackage{fvextra}
+\fvset{breaklines=true,breakbefore=.-_}
+\AtBeginDocument{\DefineShortVerb{\|}}
+
+% We use 'pmboxdraw' to draw box elements in minted environments.
+
+% https://tex.stackexchange.com/questions/281368/print-box-drawing-characters-with-pdflatex/355403#355403
+\usepackage{pmboxdraw}
+\begingroup
+  \def\DeclareUnicodeCharacter#1{%
+    \begingroup
+      \lccode`\~="#1\relax
+    \lowercase{\endgroup
+      \global\catcode`~=\active
+      \gdef~%
+    }%
+  }%
+  \input{pmboxdrawenc.dfu}%
+\endgroup
+
+% https://tex.stackexchange.com/questions/108127/block-element-characters-pmboxdraw-are-shown-too-wide-in-verbatim-and-verbatim
+\pmboxdrawsetup{
+  Block/box={\texttt{0}},
+}
+
+\usepackage{minted}
+\usemintedstyle{colorful}
+\BeforeBeginEnvironment{minted}{\begin{mdframed}[backgroundcolor=gray!3]}
+\AfterEndEnvironment{minted}{\end{mdframed}}
+\setminted{
+  breaklines=true,
+  fontsize=\footnotesize,
+}
+\setmintedinline{
+  fontsize=auto
+}
+
+\def\TmpLuaCodeInline#1{\texttt{\scantokens{\catcode`\_=12\relax#1}}}
+
+\def\TmpSecRef#1{(\rightarrow\ \ref{#1})}
+
+\def\TmpPageSecRef#1{
+  Page
+  \pageref{#1},
+  Section
+  \ref{#1}
+}
+
+\newcommand{\TmpExample}[2]{
+\begin{NodetreeEmbedView}[fontsize=#2]
+\input{examples/#1.nttex}
+\end{NodetreeEmbedView}
+}
+
+\newcommand{\TmpVerbExample}[2]{
+\VerbatimInput[firstline=4]{examples/#1.tex}
+\TmpExample{#1}{#2}
+}
+
+\newcommand\TmpMacroName[1]{%
+  \texorpdfstring{\cs{#1}}{\textbackslash #1}%
+}
+
+\DefineVerbatimEnvironment{code}{Verbatim}
+{
+  frame=single,
+  fontsize=\footnotesize,
+}
+
+\newcommand{\TmpLuaFunction}[1]{
+  \marginpar{%
+    \raggedleft%
+    \MacroFont%
+    \texttt{%
+      \scantokens{\catcode`\_=12\relax#1}%
+    }%
+  }%
+}
+
+\begin{document}
+
+\providecommand*{\url}{\texttt}
+\GetFileInfo{nodetree.dtx}
+\title{The \textsf{nodetree} package}
+\author{%
+  Josef Friedrich\\%
+  \url{josef at friedrich.rocks}\\%
+  \href{https://github.com/Josef-Friedrich/nodetree}{github.com/Josef-Friedrich/nodetree}\\%
+  with contributions by Werner Lemberg
+}
+\date{v2.3.0 from 2023/09/10}
+
+\maketitle
+
+\begin{NodetreeEmbedEnv}
+nodetree
+\end{NodetreeEmbedEnv}
+
+\newpage
+
+\tableofcontents
+
+\newpage
+
+%-----------------------------------------------------------------------
+% Abstract
+%-----------------------------------------------------------------------
+
+\section{Abstract}
+
+|nodetree| is a Lua\TeX{} development package for both plain \TeX{}
+and \LaTeX{} that visualizes the structure of node lists while
+compiling with the \TeX{} engine. It uses a visual representation of
+node lists similar to the UNIX |tree| command’s output for folder
+trees. The processed document isn’t changed.
+
+The tree view can be emitted to the console, to a log file, or as a
+\LaTeX{} input file. Its appearance is highly customizable; multiple
+color and B/W themes together with various levels of verbosity are
+provided.
+
+Node lists are the main building blocks of the \TeX{} engine, which
+Lua\TeX{} allows to inspect and modify. |nodetree| is inspired by a
+\href{https://gist.github.com/pgundlach/556247} {gist from Patrick
+  Gundlach}.
+
+%-----------------------------------------------------------------------
+% Usage
+%-----------------------------------------------------------------------
+
+\section{Usage}
+
+The package |nodetree| has four usage scenarios.
+It can be used as a standalone Lua module, as a plain Lua\TeX{}, a
+Lua\LaTeX{} package or as package to embed nodetree views in a
+Lua\LaTeX{} document.
+
+%-----------------------------------------------------------------------
+%
+%-----------------------------------------------------------------------
+
+\newpage
+
+\subsection{As a plain Lua\TeX{} package}
+
+Run |luatex luatex-test.tex| for example to list the nodes using
+Lua\TeX{}.
+
+\begin{minted}{latex}
+\input{nodetree.tex}
+\NodetreeRegisterCallback{postline}
+
+Lorem ipsum dolor.
+\bye
+\end{minted}
+
+\subsubsection{Available macros}
+
+\def\TmpTabularMacrosPlainTeX{
+\cmd{\NodetreeRegisterCallback}\marg{callbacks} &
+\TmpPageSecRef{sec:cmd:nodetree-register-callback} \\
+
+\cmd{\NodetreeUnregisterCallback}\marg{callbacks} &
+\TmpPageSecRef{sec:cmd:nodetree-unregister-callback} \\
+
+\cmd{\NodetreeSetOption}\oarg{option}\marg{value} &
+\TmpPageSecRef{sec:cmd:nodetree-set-option} \\
+
+\cmd{\NodetreeResetOption}\marg{option} &
+\TmpPageSecRef{sec:cmd:nodetree-reset-option} \\
+
+\cmd{\NodetreeReset} &
+\TmpPageSecRef{sec:cmd:nodetree-reset} \\
+}
+
+\begin{tabular}{ll}
+\textbf{Macro name} &
+\textbf{Reference} \\
+
+\TmpTabularMacrosPlainTeX
+
+\end{tabular}
+
+\subsubsection{Available options}
+
+\def\TmpTabularOptionsBase{\scantextokens{
+|callback| &
+\TmpPageSecRef{sec:option:callback} \\
+
+|verbosity| &
+\TmpPageSecRef{sec:option:verbosity} \\
+
+|color| &
+\TmpPageSecRef{sec:option:color} \\
+
+|unit| &
+\TmpPageSecRef{sec:option:unit} \\
+
+|decimalplaces| &
+\TmpPageSecRef{sec:option:decimalplaces} \\
+}}
+
+\begin{tabular}{ll}
+\textbf{Option name} &
+\textbf{Reference} \\
+
+\TmpTabularOptionsBase
+
+|channel| &
+\TmpPageSecRef{sec:option:channel} \\
+\end{tabular}
+
+%-----------------------------------------------------------------------
+%
+%-----------------------------------------------------------------------
+
+\newpage
+
+\subsection{As a Lua\LaTeX{} package}
+
+Run |lualatex lualatex-test.tex| to show a node tree using
+Lua\LaTeX{}. In Lua\LaTeX{} you can omit a call to
+|\NodetreeRegisterCallback{postline}|, since |\usepackage{nodetree}|
+registers the |post_linebreak_filter| by default. Use
+|\NodetreeUnregisterCallback{postline}| if you don’t want to debug the
+|post_linebreak_filter|.
+
+\begin{minted}{latex}
+\documentclass{article}
+\usepackage{nodetree}
+
+\begin{document}
+Lorem ipsum dolor.
+\end{document}
+\end{minted}
+
+\subsubsection{Available macros}
+
+\begin{tabular}{ll}
+\textbf{Macro name} &
+\textbf{Reference} \\
+
+\TmpTabularMacrosPlainTeX
+
+\cmd{\NodetreeSet}\marg{kv-options} &
+\TmpPageSecRef{sec:cmd:nodetree-set} \\
+\end{tabular}
+
+\subsubsection{Available options}
+
+\begin{tabular}{ll}
+\textbf{Option name} &
+\textbf{Reference} \\
+
+\TmpTabularOptionsBase
+
+|channel| &
+\TmpPageSecRef{sec:option:channel} \\
+\end{tabular}
+
+%-----------------------------------------------------------------------
+%
+%-----------------------------------------------------------------------
+
+\newpage
+
+\subsection{As a Lua module}
+
+Import the Lua module of the package inside
+\mintinline{latex}{\directlua{}}
+with this command:
+\mintinline{lua}{local nodetree = require('nodetree')}.
+Then use the Lua function
+\mintinline{lua}{nodetree.print(head, options)}
+to debug nodes inside your Lua code.
+
+\begin{minted}{lua}
+local nodetree = require('nodetree')
+
+local rule1 = node.new('rule')
+rule1.width  = 20 * 65536
+rule1.height = 10 * 65536
+rule1.depth  = 10 * 65536
+nodetree.print(vbox)
+\end{minted}
+
+\noindent
+The function \mintinline{lua}{nodetree.print()} takes as a second
+argument a Lua table to configure the output.
+
+\begin{minted}{lua}
+nodetree.print(vbox, { verbosity = 2, unit = 'cm' })
+\end{minted}
+
+\noindent
+These are the default options:
+
+\begin{minted}{lua}
+options =  {
+  callback = 'post_linebreak_filter',
+  channel = 'term',
+  color = 'colored',
+  decimalplaces = 2,
+  unit = 'pt',
+  verbosity = 0,
+  firstline = 1,
+  lastline = -1,
+}
+\end{minted}
+
+Options |firstline| and |lastline| only have an effect on function
+\mintinline{lua}{nodetree.input(filename)}, which is used to implement
+\cmd{\NodetreeEmbedInput} \TmpSecRef{sec:cmd:nodetree-embed-input}.
+
+The following code snippet demonstrates the usage in Lua\TeX{}.
+|head| is the current node.
+
+\begin{minted}{latex}
+\directlua{
+  local nodetree = require('nodetree')
+  local test = function (head)
+    nodetree.print(head)
+  end
+  callback.register('post_linebreak_filter', test)
+}
+
+Lorem ipsum dolor.
+\bye
+\end{minted}
+
+\noindent
+This example illustrates how the function has to be applied in
+Lua\LaTeX{}.
+
+\begin{minted}{latex}
+\documentclass{article}
+\usepackage{nodetree}
+
+\begin{document}
+
+\directlua{
+  local nodetree = require('nodetree')
+  local test = function (head)
+    nodetree.print(head)
+  end
+  luatexbase.add_to_callback('post_linebreak_filter', test, 'test')
+}
+
+Lorem ipsum dolor.
+\end{document}
+\end{minted}
+
+%-----------------------------------------------------------------------
+%
+%-----------------------------------------------------------------------
+
+\newpage
+\subsection{The package \texttt{nodetree-embed}}
+
+The single purpose of this auxiliary package is to provide a view
+similar to a terminal (console) output.
+This view mimics the output
+of |nodetree| in a terminal.
+The view can be embedded in a Lua\LaTeX{} file. You have to
+compile documents using this embedded view with the option
+|--shell-escape|.
+The main environment of this package is |NodetreeEmbed|.
+Markup
+inside this environment is written into a temporary \LaTeX{} file.
+This file is compiled in the background by |latexmk| and the
+|nodetree| output is embedded into this view.
+The following list shows each intermediate step:
+
+\begin{enumerate}
+
+\item |jobname.tex|
+
+\begin{minted}{latex}
+\begin{NodetreeEmbedEnv}
+nodetree
+\end{NodetreeEmbedEnv}
+\end{minted}
+
+\item |_nodetree-jobname/1.tex|
+
+\begin{minted}{latex}
+%!TEX program = lualatex
+\documentclass{article}
+\usepackage{nodetree}
+\NodetreeSetOption[channel]{tex}
+\NodetreeSetOption[verbosity]{0}
+\NodetreeSetOption[unit]{pt}
+\NodetreeSetOption[decimalplaces]{2}
+\NodetreeUnregisterCallback{post_linebreak_filter}
+\NodetreeRegisterCallback{post_linebreak_filter}
+\begin{document}
+nodetree
+\end{document}
+\end{minted}
+
+\item |_nodetree-jobname/1.nttex|: This temporary Lua\LaTeX{} file is
+compiled using |latexmk| and embedded in the environment |NodetreeEmbed|
+(the trailing |\| character indicates line continuation).
+
+\begin{minted}{latex}
+Callback: \textcolor{NTEred}{post\_linebreak\_filter}\par
+------------------------------------------\par
+\mbox{├─\textcolor{NTEmagentabright}{GLUE}\hspace{0.5em}(baselineskip)\
+  \textcolor{NTEyellow}{wd} 5.06\textcolor{NTEwhite}{pt}}\par
+...
+\end{minted}
+
+\item Finally the result:
+
+\begin{NodetreeEmbedEnv}
+nodetree
+\end{NodetreeEmbedEnv}
+
+\end{enumerate}
+
+\subsubsection{Available macros}
+
+\begin{tabular}{ll}
+\textbf{Macro name} &
+\textbf{Reference} \\
+
+\TmpTabularMacrosPlainTeX
+
+\cmd{\NodetreeSet}\marg{kv-options} &
+\TmpPageSecRef{sec:cmd:nodetree-set} \\
+
+\cmd{\NodetreeEmbedCmd}\oarg{kv-options}\marg{tex-markup} &
+\TmpPageSecRef{sec:cmd:nodetree-embed-cmd} \\
+
+\cmd{\NodetreeEmbedInput}\oarg{kv-options}\marg{nttex-file} &
+\TmpPageSecRef{sec:cmd:nodetree-embed-input} \\
+\end{tabular}
+
+\subsubsection{Available environment}
+
+\begin{tabular}{ll}
+\textbf{Environment name} &
+\textbf{Reference} \\
+
+|\begin{NodetreeEmbedEnv}|\oarg{kv-options} & % |\end{NodetreeEmbedEnv}|
+\TmpPageSecRef{sec:env:nodetree-embed-env} \\
+\end{tabular}
+
+\subsubsection{Available options}
+
+\begin{tabular}{ll}
+\textbf{Option name} &
+\textbf{Reference} \\
+
+\TmpTabularOptionsBase
+
+|theme| &
+\TmpPageSecRef{sec:option:theme} \\
+
+|thememode| &
+\TmpPageSecRef{sec:option:thememode} \\
+
+|font| &
+\TmpPageSecRef{sec:option:font} \\
+
+|fontsize| &
+\TmpPageSecRef{sec:option:fontsize} \\
+
+|firstline| &
+\TmpPageSecRef{sec:option:firstlastline} \\
+
+|lastline| &
+\TmpPageSecRef{sec:option:firstlastline} \\
+\end{tabular}
+
+%-----------------------------------------------------------------------
+% Macros
+%-----------------------------------------------------------------------
+\newpage
+\section{Macros}
+
+%%
+% \NodetreeRegisterCallback
+%%
+
+\subsection{\TmpMacroName{NodetreeRegisterCallback}}
+\label{sec:cmd:nodetree-register-callback}
+
+\DescribeMacro{\NodetreeRegisterCallback}
+\cmd{\NodetreeRegisterCallback}\marg{callbacks}: Globally register
+\marg{callbacks}, which is a comma-separated list of callback aliases
+\TmpSecRef{sec:option:callback}.
+
+%%
+% \NodetreeUnregisterCallback
+%%
+
+\subsection{\TmpMacroName{NodetreeUnregisterCallback}}
+\label{sec:cmd:nodetree-unregister-callback}
+
+\DescribeMacro{\NodetreeUnregisterCallback}
+\cmd{\NodetreeUnregisterCallback}\marg{callbacks}: Globally unregister
+\marg{callbacks}, which is a separated list of callback aliases
+\TmpSecRef{sec:option:callback}.
+
+%%
+% \NodetreeSetOption
+%%
+
+\subsection{\TmpMacroName{NodetreeSetOption}}
+\label{sec:cmd:nodetree-set-option}
+
+\DescribeMacro{\NodetreeSetOption}
+\cmd{\NodetreeSetOption}\oarg{option}\marg{value}: Globally set a
+single \oarg{option} to \marg{value} \TmpSecRef{sec:options}.
+
+%%
+% \NodetreeResetOption
+%%
+
+\subsection{\TmpMacroName{NodetreeResetOption}}
+\label{sec:cmd:nodetree-reset-option}
+
+\DescribeMacro{\NodetreeResetOption}
+\cmd{\NodetreeResetOption}\marg{option}: Globally reset a single
+\marg{option} to its default value \TmpSecRef{sec:options}.
+
+%%
+% \NodetreeSet
+%%
+
+\subsection{\TmpMacroName{NodetreeSet}}
+\label{sec:cmd:nodetree-set}
+
+\DescribeMacro{\NodetreeSet}
+\cmd{\NodetreeSet}\marg{kv-options}: Globally set multiple options at
+once. It can only be used along with Lua\LaTeX{}. \marg{kv-options}
+are key-value pairs.
+
+\begin{code}
+\NodetreeSet{color=no,callbacks={hpack,vpack},verbosity=2}
+\end{code}
+
+%%
+% \NodetreeReset
+%%
+
+\subsection{\TmpMacroName{NodetreeReset}}
+\label{sec:cmd:nodetree-reset}
+
+\DescribeMacro{\NodetreeReset}
+\cmd{\NodetreeReset}: Globally reset multiple options to their default
+values.
+
+%%
+%
+%%
+
+\subsection{\TmpMacroName{NodetreeEmbedCmd}}
+\label{sec:cmd:nodetree-embed-cmd}
+
+\DescribeMacro{\NodetreeEmbedCmd}
+\cmd{\NodetreeEmbedCmd}\oarg{kv-options}\marg{tex-markup}:
+
+Main macro (cmd) to evaluate some \TeX{} markup and generate a
+node tree from it. See environment version
+\TmpSecRef{sec:cmd:nodetree-embed-cmd}. Uses |xparse|'s |+v| option to
+grab the verbatim content. \marg{kv-options} are key-value pairs and
+set locally only.
+
+Only available in package |nodetree-embed|; you need option
+|--shell-escape|.
+
+%%
+% \NodetreeEmbedInput
+%%
+
+\subsection{\TmpMacroName{NodetreeEmbedInput}}
+\label{sec:cmd:nodetree-embed-input}
+
+\DescribeMacro{\NodetreeEmbedInput}
+\cmd{\NodetreeEmbedInput}\oarg{kv-options}\marg{nttex-file}: The path or
+file name of the |*.nttex| file without the extension.
+\marg{kv-options} are key-value pairs and set locally only.
+
+Only available in package |nodetree-embed|. This command works without
+option |--shell-escape|.
+
+%-----------------------------------------------------------------------
+% Environments
+%-----------------------------------------------------------------------
+
+\newpage
+\section{Environments}
+
+\subsection{\texttt{NodetreeEmbedEnv}}
+\label{sec:env:nodetree-embed-env}
+
+\DescribeEnv{NodetreeEmbedEnv}
+|\begin{NodetreeEmbedEnv}|\oarg{kv-options}\\
+\dots{} \textit{\TeX{} markup for evaluation} \dots\\
+|\end{NodetreeEmbedEnv}|
+
+Main environment (env) to evaluate some \TeX{} markup and generate a
+node tree from it. See command version
+\TmpSecRef{sec:cmd:nodetree-embed-cmd}. Uses the \cmd{\detokenize}
+command to grab the verbatim content. \marg{kv-options} are key-value
+pairs and set locally only.
+
+Only available in package |nodetree-embed|; you need option
+|--shell-escape|.
+
+%-----------------------------------------------------------------------
+% Options
+%-----------------------------------------------------------------------
+\newpage
+\section{Options}
+\label{sec:options}
+
+%%
+% callback
+%%
+
+\subsection{Option \texttt{callback}}
+\label{sec:option:callback}
+
+The option |callback| is the most important setting of the package. It
+is possible to specify an alias to select the callback. Take a look at
+the overview of callbacks (\rightarrow{} Figure~\ref{fig:callback}).
+|nodetree| supports all node-related callbacks as listed in the
+Lua\TeX{} reference manual.
+
+These macros process callback options:
+
+\begin{quote}
+  \cmd{\NodetreeRegisterCallback}\marg{callbacks}\\
+  \cmd{\NodetreeUnregisterCallback}\marg{callbacks}\\
+  \cmd{\NodetreeSet}\marg{callback=<callbacks>}\\
+  \cmd{\usepackage}\oarg{callback=<callbacks>}\marg{nodetree}
+\end{quote}
+
+The |nodetree| package can watch the node tree before and after the
+functions of a callback are executed: It is possible to prepend and/or
+append a colon (|:|) to indicate the desired watchpoint position,
+which defaults to 'before' if no colon is used.
+
+Use commas to specify multiple callbacks; trailing and leading
+whitespace is ignored.  For example, this call
+
+\begin{code}
+\NodetreeRegisterCallback{:preline, line, :postline:}
+\end{code}
+
+\noindent
+watches the node tree before the |preline| callback functions, before
+the |line| callback functions, and before and after the |postline|
+callback functions.  In case there are no callback functions
+registered for one of the |hyphenate|, |kerning|, |ligaturing|, and
+|mlist_to_hlist| callbacks, Lua\TeX{} executes some internal code
+instead.  It thus makes sense to watch the node tree before and after
+these (empty) callbacks even in this case.
+
+Wrap your callback aliases in curly braces for the macro
+|\NodetreeSet|. Note that no whitespace between |=| and |{| is
+allowed.
+
+\begin{code}
+\NodetreeSet{callback={:preline, line, :postline:}}
+\end{code}
+
+The same applies for the macro |\usepackage|:
+
+\begin{code}
+\usepackage{callback={:preline, line, :postline:}}
+\end{code}
+
+The callbacks in Figure~\ref{fig:callback} are listed in the same
+order as in the Lua\TeX{} reference manual. Note that the |ligaturing|
+and |kerning| callbacks only have an effect on ligatures and kernings,
+respectively, if the |luaotfload| package (which is the default for
+Lua\LaTeX{}, and an optional package for Lua\TeX{}) handles the
+affected font with |mode=base| (see the
+\href{http://mirrors.ctan.org/macros/luatex/generic/luaotfload/luaotfload-latex.pdf}
+{documentation} for more details).
+
+%%
+% Tabular callbacks
+%%
+
+\newcommand{\TmpCallbackRow}[3]{
+  \TmpLuaCodeInline{#1} & \TmpLuaCodeInline{#2} & \TmpLuaCodeInline{\footnotesize#3} \\
+}
+
+\begin{figure}
+\begin{tabular}{lll}
+\textbf{Callback} & \textbf{Alias} & \textbf{Alias (longer)} \\
+\TmpCallbackRow{contribute_filter}
+{contribute}
+{contributefilter}
+
+\TmpCallbackRow{buildpage_filter}
+{buildfilter} %
+{buildpagefilter}
+
+% new
+\TmpCallbackRow{build_page_insert}
+{buildinsert}
+{buildpageinsert}
+
+\TmpCallbackRow{pre_linebreak_filter}
+{preline}
+{prelinebreakfilter}
+
+\TmpCallbackRow{linebreak_filter}
+{line}
+{linebreakfilter}
+
+\TmpCallbackRow{append_to_vlist_filter}
+{append}
+{appendtovlistfilter}
+
+\TmpCallbackRow{post_linebreak_filter}
+{postline}
+{postlinebreakfilter}
+
+\TmpCallbackRow{hpack_filter}
+{hpack}
+{hpackfilter}
+
+\TmpCallbackRow{vpack_filter}
+{vpack}
+{vpackfilter}
+
+\TmpCallbackRow{hpack_quality}
+{hpackq}
+{hpackquality}
+
+\TmpCallbackRow{vpack_quality}
+{vpackq}
+{vpackquality}
+
+\TmpCallbackRow{process_rule}
+{process}
+{processrule}
+
+\TmpCallbackRow{pre_output_filter}
+{preout}
+{preoutputfilter}
+
+\TmpCallbackRow{hyphenate}
+{hyph}
+{}
+
+\TmpCallbackRow{ligaturing}
+{liga}
+{}
+
+\TmpCallbackRow{kerning}
+{kern}
+{}
+
+\TmpCallbackRow{insert_local_par}
+{insert}
+{insertlocalpar}
+
+\TmpCallbackRow{mlist_to_hlist}
+{mhlist}
+{mlisttohlist}
+\end{tabular}
+
+\caption{The callback aliases}
+\label{fig:callback}
+\end{figure}
+
+%%
+% channel
+%%
+
+\subsection{Option \texttt{channel}}
+\label{sec:option:channel}
+
+You can select the debug output channel with this option. The default
+value for the option |channel| is |term|, which displays the node tree in
+the current terminal. Specify |log| and the package creates a log file
+named |<jobname>.ntlog|. Specify |tex| and a log file named
+|<jobname>.nttex| is created. |nt...| stands for |nodetree|.
+|<jobname>| is the basename of your file you want to debug. The debug
+channel is only useful for the auxiliary package |nodetree-embed|. Paste
+the markup in the environment |NodetreeEmbedView| and you get a
+terminal-like view in your document.
+
+%%
+% verbosity
+%
+
+\subsection{Option \texttt{verbosity}}
+\label{sec:option:verbosity}
+
+Higher integer values result in a more verbose output. The default value
+for this option is~|0|. At the moment verbosity levels |0| to~|3| are
+implemented.
+
+\def\TmpExampleVerbosity#1{
+  \subsubsection{Example: \texttt{verbosity=#1}}
+  \begin{NodetreeEmbedEnv}[verbosity=#1,callback=pre_linebreak_filter,
+                           fontsize=\fontsize{5.5}{6.6}\selectfont]
+  .
+  \end{NodetreeEmbedEnv}
+}
+
+\TmpExampleVerbosity{0}
+\TmpExampleVerbosity{1}
+\TmpExampleVerbosity{2}
+\TmpExampleVerbosity{3}
+
+%%
+% color
+%%
+
+\subsection{Option \texttt{color}}
+\label{sec:option:color}
+
+The default option for |color| is |colored|. Use any other string (for
+example |none| or |no|) to disable the colored terminal output of the
+package.
+
+\begin{code}
+\usepackage[color=no]{nodetree}
+\end{code}
+
+%%
+% unit
+%%
+
+\subsection{Option \texttt{unit}}
+\label{sec:option:unit}
+
+The option |unit| sets the length unit to display all length values of
+the nodes. The default option for |unit| is |pt|. See figures
+\ref{fig:fixed-units} and~\ref{fig:relative-units} for possible values.
+
+\begin{figure}
+\begin{tabular}{lp{10cm}}
+\textbf{Unit} &
+\textbf{Description} \\
+
+pt &
+Point 1/72.27 inch. The conversion to metric units, to two decimal
+places, is 1 point = 2.85 mm = 28.45 cm. \\
+
+pc &
+Pica, 12 pt \\
+
+in &
+Inch, 72.27 pt \\
+
+bp &
+Big point, 1/72 inch. This length is the definition of a point in
+PostScript and many desktop publishing systems. \\
+
+cm &
+Centimeter \\
+
+mm &
+Millimeter \\
+
+dd &
+Didot point, 1.07 pt \\
+
+cc &
+Cicero, 12 dd \\
+
+sp &
+Scaled point, 1/65536 pt \\
+\end{tabular}
+\caption{Fixed units}
+\label{fig:fixed-units}
+\end{figure}
+
+\begin{figure}
+\begin{tabular}{lp{10cm}}
+\textbf{Unit} &
+\textbf{Description} \\
+
+ex &
+x-height of the current font \\
+
+em &
+Width of the capital letter M \\
+\end{tabular}
+\caption{Relative units}
+\label{fig:relative-units}
+\end{figure}
+
+\def\TmpExampleUnit#1{
+  \subsubsection{Example: \texttt{unit=#1}}
+  \begin{NodetreeEmbedEnv}[unit=#1,callback=pre_linebreak_filter]
+  Lorem.
+  \end{NodetreeEmbedEnv}
+}
+
+\TmpExampleUnit{pt}
+\TmpExampleUnit{sp}
+\TmpExampleUnit{cm}
+
+%%
+% decimalplaces
+%%
+
+\subsection{Option \texttt{decimalplaces}}
+\label{sec:option:decimalplaces}
+
+The options |decimalplaces| sets the number of decimal places for some
+node fields. If |decimalplaces| is set to |0| only integer values are shown.
+
+\begin{code}
+\NodetreeSetOption[decimalplaces]{4}
+\end{code}
+
+\def\TmpExampleDecimalplaces#1{
+  \subsubsection{Example: \texttt{decimalplaces=#1}}
+  \begin{NodetreeEmbedEnv}[unit=cc,decimalplaces=#1,callback=pre_linebreak_filter]
+  Lorem.
+  \end{NodetreeEmbedEnv}
+}
+
+\TmpExampleDecimalplaces{0}
+\TmpExampleDecimalplaces{2}
+\TmpExampleDecimalplaces{5}
+
+%%
+% theme and thememode
+%%
+
+\def\TmpExampleTheme#1#2{
+  \subsubsection{Example: \texttt{theme=#1} \texttt{thememode=#2}}
+  \begin{NodetreeEmbedEnv}[callback=pre_linebreak_filter,theme=#1,thememode=#2,fontsize=\small]
+  .
+  \end{NodetreeEmbedEnv}
+}
+
+\subsection{Option \texttt{theme} and \texttt{thememode}}
+\label{sec:option:theme}
+\label{sec:option:thememode}
+
+% bw
+\TmpExampleTheme{bwdark}{dark}
+\TmpExampleTheme{bwlight}{light}
+
+% monokaisoda
+\TmpExampleTheme{monokaisoda}{dark}
+\TmpExampleTheme{monokaisoda}{light}
+
+%%
+% font
+%%
+
+\subsection{Option \texttt{font}}
+\label{sec:option:font}
+
+\NodetreeSet{fontsize=\footnotesize}
+
+\def\TmpExampleFont#1{
+  \subsubsection{Example: \texttt{font=\{#1\}}}
+  \begin{NodetreeEmbedEnv}[font={#1}]
+  .
+  \end{NodetreeEmbedEnv}
+}
+
+|nodetree-embed| passes the option |font| down to the
+command |\setmonofont{}| of the |fontspec| package. The used font
+should be monospaced and have some box drawing glyphs (see
+table~\ref{fig:unicode}).
+
+\TmpExampleFont{Liberation Mono}
+\TmpExampleFont{Ubuntu Mono}
+
+%%
+% fontsize
+%%
+
+\subsection{Option \texttt{fontsize}}
+\label{sec:option:fontsize}
+
+\def\TmpExampleFontSize#1{
+  \subsubsection{Example: \TmpMacroName{#1}}
+  \begin{NodetreeEmbedEnv}[callback=pre_linebreak_filter,
+                           fontsize=\csname #1\endcsname]
+  .
+  \end{NodetreeEmbedEnv}
+}
+
+\TmpExampleFontSize{small}
+\TmpExampleFontSize{tiny}
+
+\subsection{Options \texttt{firstline} and \texttt{lastline}}
+\label{sec:option:firstlastline}
+
+These two options are for function \cmd{\NodetreeEmbedInput} only
+\TmpSecRef{sec:cmd:nodetree-embed-input}.  They specify the first
+and last shown line of the read |*.nttex| file.  Values |1|, |2|,
+\ldots, corresponds to the first line, second, line, etc.  Values
+|-1|, |-2|, \ldots, correspond to the last line, the line before the
+last line, etc.  The default values are |firstline = 1| and
+|lastline = -1| to display the whole file.
+
+%-----------------------------------------------------------------------
+% Visual tree structure
+%-----------------------------------------------------------------------
+\newpage
+\section{Visual tree structure}
+
+%%
+% Two different connections
+%%
+
+\subsection{Two different connections}
+
+Nodes in Lua\TeX{} are connected. The |nodetree| package distinguishes
+between \emph{list} and \emph{field} connections.
+
+\begin{itemize}
+ \item list: Nodes that are doubly connected by |next| and
+       |previous| fields.
+ \item field: Connections to nodes by other fields than |next| and
+       |previous|, for example, using |head| and |pre|.
+\end{itemize}
+
+%%
+% Unicode characters
+%%
+
+\subsection{Unicode characters to show the tree view}
+
+\renewcommand{\arraystretch}{1.5}
+
+The package |nodetree| uses the unicode box drawing symbols. Your
+default terminal font should contain this characters to obtain the tree
+view. Eight box drawing characters are necessary.
+
+\begin{figure}
+{
+\fontspec{DejaVu Sans Mono}
+\begin{tabular}{lcl}
+\textbf{Code} & \textbf{Character} & \textbf{Name} \\
+U+2500 & ─ & BOX DRAWINGS LIGHT HORIZONTAL \\
+U+2502 & │ & BOX DRAWINGS LIGHT VERTICAL \\
+U+2514 & └ & BOX DRAWINGS LIGHT UP AND RIGHT \\
+U+251C & ├ & BOX DRAWINGS LIGHT VERTICAL AND RIGHT \\
+U+2550 & ═ & BOX DRAWINGS DOUBLE HORIZONTAL \\
+U+2551 & ║ & BOX DRAWINGS DOUBLE VERTICAL \\
+U+255A & ╚ & BOX DRAWINGS DOUBLE UP AND RIGHT \\
+U+2560 & ╠ & BOX DRAWINGS DOUBLE VERTICAL AND RIGHT \\
+\end{tabular}
+}
+\caption{The Unicode box drawings glyphs}
+\label{fig:unicode}
+\end{figure}
+
+\noindent
+For |list| connections \emph{light} characters are shown.
+
+{
+\setmonofont{DejaVu Sans Mono}
+\begin{code}
+│ │
+│ ├─list1
+│ └─list2
+└─list3
+\end{code}
+}
+
+\noindent
+|field| connections are visialized by \emph{Double} characters.
+
+{
+\setmonofont{DejaVu Sans Mono}
+\begin{code}
+║ ║
+║ ╠═field1
+║ ╚═field2
+╚═field3
+\end{code}
+}
+
+%-----------------------------------------------------------------------
+% Examples
+%-----------------------------------------------------------------------
+\newpage
+\section{Examples}
+
+This section lists some examples of the |nodetree| output.
+
+%%
+% packagename
+%%
+
+\subsection{The node list of the package name}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,callback=post_linebreak_filter]
+nodetree
+\end{NodetreeEmbedEnv}
+
+%%
+% math
+%%
+
+\subsection{The node list of a mathematical formula}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,callback=post_linebreak_filter]
+$1+2$
+\end{NodetreeEmbedEnv}
+
+%%
+% ligatures
+%%
+
+\subsection{The node list of the word \emph{Office}}
+
+The characters \emph{ffi} are deeply nested in a discretionary node.
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,decimalplaces=0]
+Office
+\end{NodetreeEmbedEnv}
+
+%-----------------------------------------------------------------------
+% Node types
+%-----------------------------------------------------------------------
+
+\section{Node types}
+
+This section shows some node types in a |nodetree| view.
+
+\newcommand{\TmpHeadingNodeTypeSub}[4]{
+  \subsection{Type \texttt{#1(#2)}, subtype \texttt{#3(#4)}}
+}
+
+\newcommand{\TmpNodeTypeSub}[6]{
+  \subsection{Type \texttt{#1(#2)}, subtype \texttt{#3\_#4(#5)}}
+  \TmpVerbExample{#2#1#5#3#4}{#6}
+}
+
+\newcommand{\TmpHeadingNodeType}[2]{
+  \subsection{Type \texttt{#1(#2)}}
+}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{hlist}{0}{line}{1}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+Lorem
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{hlist}{0}{box}{2}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+L\hbox to 40pt{ore}m
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{hlist}{0}{indent}{3}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,unit=cm]
+\setlength{\parindent}{5cm}
+I
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeType{vlist}{1}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,decimalplaces=1]
+L\vbox to 40pt{O}L
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeType{rule}{2}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,unit=mm]
+\rule[-2mm]{10mm}{4mm}
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeType{mark}{4}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,callback=pre_output_filter]
+\mark{Lorem}.
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{disc}{7}{discretionary}{0}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+L\discretionary{}{}{}L
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{disc}{7}{explicit}{1}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+L\-O\-L
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{disc}{7}{regular}{3}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,decimalplaces=0]
+Office
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpNodeTypeSub{whatsit}{8}{pdf}{action}{22}{\fontsize{5.5}{6.6}\selectfont}
+\TmpNodeTypeSub{whatsit}{8}{pdf}{colorstack}{28}{\footnotesize}
+
+%%
+%
+%%
+
+\TmpHeadingNodeType{dir}{10}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+\textdir TRT nur {\textdir TLT run \textdir TRT NUR} nur
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{glue}{12}{baselineskip}{2}
+
+\NodetreeEmbedCmd[showmarkup=true,unit=cm]{
+\baselineskip=5cm
+Lorem
+
+Lorem
+}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{glue}{12}{parskip}{3}
+
+\NodetreeEmbedCmd[showmarkup=true,callback=pre_output_filter]{
+\parskip=5cm
+Lorem
+
+Lorem
+}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{glue}{12}{spaceskip}{13}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+\spaceskip=5cm
+a a
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{glue}{12}{leaders}{100}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+a \leavevmode\leaders\hbox{ . }\hfill\kern0pt a
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{glue}{12}{cleaders}{101}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+a \leavevmode\cleaders\hbox{ . }\hfill\kern0pt a
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{glue}{12}{xleaders}{102}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+a \leavevmode\xleaders\hbox{ . }\hfill\kern0pt a
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{glue}{12}{gleaders}{102}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+a \leavevmode\gleaders\hbox{ . }\hfill\kern0pt a
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{kern}{13}{userkern}{0}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+a\kern2pt
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{kern}{13}{fontkern}{1}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+Ve
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{kern}{13}{accentkern}{2}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+\accent96 a
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeTypeSub{kern}{13}{italiccorrection}{3}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+\textit{L}\/OL
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeType{penalty}{14}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+L \penalty 23 OL
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeType{glyph}{29}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true]
+abc
+\end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+% It seems that 'attribute' nodes don't appear in node trees.
+%
+% \TmpHeadingNodeType{attribute}{38}
+%
+% \begin{NodetreeEmbedEnv}[showmarkup=true]
+% {\attribute0=1 A}
+% \end{NodetreeEmbedEnv}
+
+%%
+%
+%%
+
+\TmpHeadingNodeType{attributelist}{40}
+
+\begin{NodetreeEmbedEnv}[showmarkup=true,callback=hpackfilter]
+{\attribute0=1 A}
+\end{NodetreeEmbedEnv}
+
+\DocInput{nodetree.dtx}
+
+\subsection{The file \texttt{nodetree.lua}}
+
+% Compilation failure:
+% lualatex: ../../../texk/web2c/luatexdir/lang/texlang.c:986: hnj_hyphenation: Assertion `(((varmem[(wordstart)].hh.u.B1) & (1 << 0)) && !((varmem[(wordstart)].hh.u.B1) & (1 << 1) ) && !((varmem[(wordstart)].hh.u.B1) & (1 << 2) ))' failed
+% \inputminted{lua}{nodetree.lua}
+There is a source code documentation of the file nodetree.lua compiled
+with Ldoc on Github:
+\url{http://josef-friedrich.github.io/nodetree/}
+
+\end{document}


Property changes on: trunk/Master/texmf-dist/doc/luatex/nodetree/nodetree-doc.tex
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/doc/luatex/nodetree/nodetree.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/source/luatex/nodetree/nodetree.dtx
===================================================================
--- trunk/Master/texmf-dist/source/luatex/nodetree/nodetree.dtx	2023-09-11 21:18:30 UTC (rev 68243)
+++ trunk/Master/texmf-dist/source/luatex/nodetree/nodetree.dtx	2023-09-11 21:18:50 UTC (rev 68244)
@@ -1,6 +1,6 @@
 % \iffalse meta-comment
 %
-% Copyright (C) 2016-2022 by Josef Friedrich <josef at friedrich.rocks>
+% Copyright (C) 2016-2023 by Josef Friedrich <josef at friedrich.rocks>
 % ----------------------------------------------------------------------
 % This work may be distributed and/or modified under the conditions of
 % the LaTeX Project Public License, either version 1.3 of this license
@@ -28,7 +28,7 @@
 %<package>\NeedsTeXFormat{LaTeX2e}[1999/12/01]
 %<package>\ProvidesPackage{nodetree}
 %<*package>
-    [2022/12/17 v2.2.1 Visualize node lists in a tree view]
+    [2023/09/10 v2.3.0 Visualize node lists in a tree view]
 %</package>
 % \fi
 %
@@ -50,34 +50,6 @@
 %   Grave accent  \`     Left brace    \{     Vertical bar  \|
 %   Right brace   \}     Tilde         \~}
 %
-%
-% \changes{v0.1}{2015/06/16}{Converted to DTX file}
-% \changes{v1.0}{2016/07/07}{Inital release}
-% \changes{v1.1}{2016/07/13}{Fix the registration of same callbacks}
-% \changes{v1.2}{2016/07/18}{Fix difference between README.md in the upload and that from nodetree.dtx}
-% \changes{v2.0}{2020/05/29}{
-%  * Switch from lowercase macro names to PascalCase names for better readability.
-%  * The Lua code is no longer developed inside the DTX file, instead in a separate file named nodetree.lua.
-%  * Add a sub package named nodetree-embed.sty for embedding nodetree views into a \LaTeX{} document.
-%  * Add support for new node subtype names.
-%  * Add support for a new Lua\TeX{} node callback.
-%  * Add support for node properties.
-%  * Less verbose representation of node attributes.
-%  * Minor tree output adjustments.
-% }
-% \changes{v2.1}{2020/10/03}{
-%  * Make the package compatible with the Harfbuzz mode of the luaotfload fontloader.
-%  * Print node properties of copied nodes.
-% }
-% \changes{v2.2}{2020/10/23}{
-%  * Fix unavailable library error (utf8 not in Lua5.1)
-% }
-% \changes{v2.2.1}{2022/12/17}{
-% * Replace non-printable unicode symbols with ???.
-% * Add missing newlines for callbacks with multiple node lists.
-% * Print subtype fields with value 0.
-% * Fix the presentation of the subtype field of a glyph as a bit field.
-% }
 % \DoNotIndex{\newcommand,\newenvironment,\def,\directlua}
 %
 % \StopEventually{}
@@ -90,7 +62,7 @@
 % \MacroTopsep = 10pt plus 2pt minus 2pt
 % \MacrocodeTopsep = 10pt plus 1.2pt minus 1pt
 % \makeatletter
-% \c at CodelineNo 25 \relax
+% \c at CodelineNo 22 \relax
 % \makeatother
 %
 % \subsection{The file \tt{nodetree.tex}}
@@ -198,7 +170,7 @@
 %    \end{macrocode}
 %
 %    \begin{macrocode}
-\DeclareStringOption[1]{verbosity}
+\DeclareStringOption[0]{verbosity}
 \define at key{NT}{verbosity}[]{\NodetreeSetOption[verbosity]{#1}}
 %    \end{macrocode}
 %
@@ -237,10 +209,16 @@
 %</package>
 %<*packageembed>
 % \fi
+% \makeatletter
+% \c at CodelineNo 22 \relax
+% \makeatother
+%
+% \subsection{The file \tt{nodetree-embed.sty}}
+%
 %    \begin{macrocode}
 \NeedsTeXFormat{LaTeX2e}[1994/06/01]
 \ProvidesPackage{nodetree-embed}
-  [2022/12/17 v2.2.1 Embed node trees into a LaTeX document]
+  [2023/09/10 v2.3.0 Embed node trees into a LaTeX document]
 %    \end{macrocode}
 %
 %    \begin{macrocode}
@@ -260,18 +238,11 @@
 %    \end{macrocode}
 %
 %    \begin{macrocode}
-\directlua{
-  nodetree = require('nodetree')
-  nodetree.check_shell_escape()
-}
-%    \end{macrocode}
-%
-%    \begin{macrocode}
 \define at key{NTE}{callback}[]{\NodetreeSetOption[callback]{#1}}
 %    \end{macrocode}
 %
 %    \begin{macrocode}
-\DeclareStringOption[1]{verbosity}
+\DeclareStringOption[0]{verbosity}
 \define at key{NTE}{verbosity}[]{\NodetreeSetOption[verbosity]{#1}}
 %    \end{macrocode}
 %
@@ -307,6 +278,16 @@
 %    \end{macrocode}
 %
 %    \begin{macrocode}
+\DeclareStringOption[1]{firstline}
+\define at key{NTE}{firstline}[]{\NodetreeSetOption[firstline]{#1}}
+%    \end{macrocode}
+%
+%    \begin{macrocode}
+\DeclareStringOption[-1]{lastline}
+\define at key{NTE}{lastline}[]{\NodetreeSetOption[lastline]{#1}}
+%    \end{macrocode}
+%
+%    \begin{macrocode}
 \DeclareBoolOption{showmarkup}
 %    \end{macrocode}
 %
@@ -404,7 +385,7 @@
 %
 % \begin{macro}{\NodetreeSet}
 % Same definition as in nodetree.sty. Only implement this command
-% if not already registers.
+% if not already registered.
 %    \begin{macrocode}
 \providecommand{\NodetreeSet}[1]{%
   \setkeys{NTE}{#1}%
@@ -414,6 +395,7 @@
 %
 %    \begin{macrocode}
 \newenvironment{NodetreeEmbedView}[1][]{
+  \directlua{nodetree.push_options()}
   \setkeys{NTE}{#1}
   \NTE at colors
   \begin{mdframed}[
@@ -424,6 +406,7 @@
   \NTE at fonts
 }{
   \end{mdframed}%
+  \directlua{nodetree.pop_options()}%
 }
 %    \end{macrocode}
 %
@@ -430,11 +413,14 @@
 % \begin{environment}{NodetreeEmbedEnv}
 %    \begin{macrocode}
 \NewDocumentEnvironment { NodetreeEmbedEnv } { O{} +b } {
+  \directlua{
+    nodetree.check_shell_escape('NodetreeEmbedEnv', false)
+    nodetree.push_options()
+  }
   \setkeys{NTE}{#1}
   \ifNTEK at showmarkup
     \noindent
     \texttt{\detokenize{#2}}
-  \else
   \fi
   \NTE at colors
   \begin{NodetreeEmbedView}
@@ -442,6 +428,7 @@
       nodetree.compile_include('\luaescapestring{\unexpanded{#2}}')
     }
   \end{NodetreeEmbedView}
+  \directlua{nodetree.pop_options()}
 }{}
 %    \end{macrocode}
 % \end{environment}
@@ -449,11 +436,14 @@
 % \begin{macro}{\NodetreeEmbedCmd}
 %    \begin{macrocode}
 \NewDocumentCommand { \NodetreeEmbedCmd } { O{} +v } {
+  \directlua{
+    nodetree.check_shell_escape('\string\\NodetreeEmbedCmd', true)
+    nodetree.push_options()
+  }
   \setkeys{NTE}{#1}
   \ifNTEK at showmarkup
     \noindent
     \texttt{#2}
-  \else
   \fi
   \NTE at colors
   \begin{NodetreeEmbedView}
@@ -461,6 +451,7 @@
       nodetree.compile_include('\luaescapestring{\unexpanded{#2}}')
     }
   \end{NodetreeEmbedView}
+  \directlua{nodetree.pop_options()}
 }
 %    \end{macrocode}
 % \end{macro}
@@ -468,10 +459,12 @@
 % \begin{macro}{\NodetreeEmbedInput}
 %    \begin{macrocode}
 \newcommand{\NodetreeEmbedInput}[2][]{
+  \directlua{nodetree.push_options()}
   \setkeys{NTE}{#1}
   \begin{NodetreeEmbedView}
-  \input{#2.nttex}
+  \directlua{nodetree.input('#2.nttex')}
   \end{NodetreeEmbedView}
+  \directlua{nodetree.pop_options()}
 }
 \let\nodetreeterminalemulator\NodetreeEmbedInput
 %    \end{macrocode}

Modified: trunk/Master/texmf-dist/source/luatex/nodetree/nodetree.ins
===================================================================
--- trunk/Master/texmf-dist/source/luatex/nodetree/nodetree.ins	2023-09-11 21:18:30 UTC (rev 68243)
+++ trunk/Master/texmf-dist/source/luatex/nodetree/nodetree.ins	2023-09-11 21:18:50 UTC (rev 68244)
@@ -1,4 +1,4 @@
-% Copyright (C) 2016-2022 by Josef Friedrich <josef at friedrich.rocks>
+% Copyright (C) 2016-2023 by Josef Friedrich <josef at friedrich.rocks>
 % ----------------------------------------------------------------------
 % This work may be distributed and/or modified under the conditions of
 % the LaTeX Project Public License, either version 1.3c of this license
@@ -21,7 +21,7 @@
 
 This is a generated file.
 
-Copyright (C) 2016-2022 by Josef Friedrich <josef at friedrich.rocks>
+Copyright (C) 2016-2023 by Josef Friedrich <josef at friedrich.rocks>
 ----------------------------------------------------------------------
 This work may be distributed and/or modified under the conditions of
 the LaTeX Project Public License, either version 1.3c of this license

Modified: trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree-embed.sty
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree-embed.sty	2023-09-11 21:18:30 UTC (rev 68243)
+++ trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree-embed.sty	2023-09-11 21:18:50 UTC (rev 68244)
@@ -8,7 +8,7 @@
 %% 
 %% This is a generated file.
 %% 
-%% Copyright (C) 2016-2022 by Josef Friedrich <josef at friedrich.rocks>
+%% Copyright (C) 2016-2023 by Josef Friedrich <josef at friedrich.rocks>
 %% ----------------------------------------------------------------------
 %% This work may be distributed and/or modified under the conditions of
 %% the LaTeX Project Public License, either version 1.3c of this license
@@ -22,7 +22,7 @@
 %% 
 \NeedsTeXFormat{LaTeX2e}[1994/06/01]
 \ProvidesPackage{nodetree-embed}
-  [2022/12/17 v2.2.1 Embed node trees into a LaTeX document]
+  [2023/09/10 v2.3.0 Embed node trees into a LaTeX document]
 \RequirePackage{xcolor,mdframed,expl3,xparse,fontspec}
 \input{nodetree}
 \RequirePackage{kvoptions}
@@ -30,12 +30,8 @@
   family=NTE,
   prefix=NTEK@
 }
-\directlua{
-  nodetree = require('nodetree')
-  nodetree.check_shell_escape()
-}
 \define at key{NTE}{callback}[]{\NodetreeSetOption[callback]{#1}}
-\DeclareStringOption[1]{verbosity}
+\DeclareStringOption[0]{verbosity}
 \define at key{NTE}{verbosity}[]{\NodetreeSetOption[verbosity]{#1}}
 \DeclareStringOption[colored]{color}
 \define at key{NTE}{color}[]{\NodetreeSetOption[color]{#1}}
@@ -47,6 +43,10 @@
 \DeclareStringOption[dark]{thememode}
 \DeclareStringOption[Ubuntu Mono]{font}
 \DeclareStringOption[\footnotesize]{fontsize}
+\DeclareStringOption[1]{firstline}
+\define at key{NTE}{firstline}[]{\NodetreeSetOption[firstline]{#1}}
+\DeclareStringOption[-1]{lastline}
+\define at key{NTE}{lastline}[]{\NodetreeSetOption[lastline]{#1}}
 \DeclareBoolOption{showmarkup}
 \ProcessKeyvalOptions{NTE}
 \ExplSyntaxOn
@@ -131,6 +131,7 @@
   \setkeys{NTE}{#1}%
 }
 \newenvironment{NodetreeEmbedView}[1][]{
+  \directlua{nodetree.push_options()}
   \setkeys{NTE}{#1}
   \NTE at colors
   \begin{mdframed}[
@@ -141,13 +142,17 @@
   \NTE at fonts
 }{
   \end{mdframed}%
+  \directlua{nodetree.pop_options()}%
 }
 \NewDocumentEnvironment { NodetreeEmbedEnv } { O{} +b } {
+  \directlua{
+    nodetree.check_shell_escape('NodetreeEmbedEnv', false)
+    nodetree.push_options()
+  }
   \setkeys{NTE}{#1}
   \ifNTEK at showmarkup
     \noindent
     \texttt{\detokenize{#2}}
-  \else
   \fi
   \NTE at colors
   \begin{NodetreeEmbedView}
@@ -155,14 +160,18 @@
       nodetree.compile_include('\luaescapestring{\unexpanded{#2}}')
     }
   \end{NodetreeEmbedView}
+  \directlua{nodetree.pop_options()}
 }{}
 
 \NewDocumentCommand { \NodetreeEmbedCmd } { O{} +v } {
+  \directlua{
+    nodetree.check_shell_escape('\string\\NodetreeEmbedCmd', true)
+    nodetree.push_options()
+  }
   \setkeys{NTE}{#1}
   \ifNTEK at showmarkup
     \noindent
     \texttt{#2}
-  \else
   \fi
   \NTE at colors
   \begin{NodetreeEmbedView}
@@ -170,12 +179,15 @@
       nodetree.compile_include('\luaescapestring{\unexpanded{#2}}')
     }
   \end{NodetreeEmbedView}
+  \directlua{nodetree.pop_options()}
 }
 \newcommand{\NodetreeEmbedInput}[2][]{
+  \directlua{nodetree.push_options()}
   \setkeys{NTE}{#1}
   \begin{NodetreeEmbedView}
-  \input{#2.nttex}
+  \directlua{nodetree.input('#2.nttex')}
   \end{NodetreeEmbedView}
+  \directlua{nodetree.pop_options()}
 }
 \let\nodetreeterminalemulator\NodetreeEmbedInput
 \endinput

Modified: trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.lua
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.lua	2023-09-11 21:18:30 UTC (rev 68243)
+++ trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.lua	2023-09-11 21:18:50 UTC (rev 68244)
@@ -1,35 +1,36 @@
---- 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
+--- This file (`nodetree.lua`) is part of the LuaTeX package
+--- 'nodetree'.
+---
+---`nodetree` uses the annotation system of the
+---[lua-language-server](https://github.com/LuaLS/lua-language-server/wiki/Annotations).
+---Install the [type definitions for LuaTeX](https://github.com/Josef-Friedrich/LuaTeX_Lua-API)
+---or the [Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=JosefFriedrich.luatex).
+---
+---The LDoc support is deprecated.
+---
+---Nodes in LuaTeX are connected. The nodetree view distinguishes
+---between *list* and *field* connections.
+---
+---* list: Nodes that are doubly connected by `next` and `previous`
+---  fields.
+---* field: Connections to nodes by other fields than `next` and
+---  `previous` fields, e.g., `head`, `pre`.
+--- at module nodetree
 
--- luacheck: globals node tex luatexbase lfs callback os unicode status modules
+---luacheck: globals node lang tex luatexbase lfs
+---luacheck: globals callback os unicode status modules
 
---- at class Node
---- at field next Node|nil # the next node in a list, or nil
---- at field id number # the node’s type (id) number
---- at field subtype number # the node subtype identifier
+---
+--- at alias ColorName 'black'|'red' |'green'|'yellow'|'blue'|'magenta'|'cyan'|'white'|'reset'
+--- at alias ColorMode 'bright'|'dim'|''
+---
+--- at alias ConnectionType 'list'|'field' # A string literal,
+---  which can be either 'list' or 'field'.
+--- at alias ConnectionState 'stop'|'continue' # A literal, which can
+---  be either `continue` or `stop`.
 
---- at alias ColorName `black` | `red` | `green` | `yellow` | `blue` | `magenta` | `cyan` | `white`
---- at alias ColorMode `bright` | `dim`
-
---- at alias ConnectionType `list` | `field` # A literal
---   is a string, which can be either `list` or `field`.
---- at alias ConnectionState `stop` | `continue` # A literal which can
---   be either `continue` or `stop`.
-
-if not modules then modules = { } end modules ['nodetree'] = {
-  version   = '2.2.1',
+if not modules then modules = {} end modules ['nodetree'] = {
+  version   = '2.3.0',
   comment   = 'nodetree',
   author    = 'Josef Friedrich',
   copyright = 'Josef Friedrich',
@@ -39,18 +40,21 @@
 local direct            = node.direct
 local todirect          = direct.todirect
 local getchar           = direct.getchar
---- Lua 5.1 does not have the utf8 library (Lua 5.1 is the default
--- version in LuajitTeX). LuaJitTeX does include the slnunicode library.
+---Lua 5.1 does not have the utf8 library (Lua 5.1 is the default
+---version in LuajitTeX). LuaJitTeX does include the slnunicode library.
 local utf8              = utf8 or unicode.utf8
 local utfchar           = utf8.char
 local properties        = direct.get_properties_table()
 
---- 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.
+---A counter for the compiled TeX examples. Some TeX code snippets
+---a written into files, wrapped with some TeX boilerplate code.
+---These written files are compiled later on.
 local example_counter = 0
 
---- The default options
+---A flag to indicate that something has been emitted by nodetree.
+local have_output = false
+
+--- The default options.
 local default_options = {
   callback = 'post_linebreak_filter',
   channel = 'term',
@@ -57,40 +61,50 @@
   color = 'colored',
   decimalplaces = 2,
   unit = 'pt',
-  verbosity = 1,
+  verbosity = 0,
+  firstline = 1,
+  lastline = -1,
 }
 
---- The current options
--- They are changed very often.
+--- The current options.
 local options = {}
 for key, value in pairs(default_options) do
   options[key] = value
 end
 
---- File descriptor
+--- The previous options.
+---We need this for functions `push_options` and `pop_options` so that
+---the effects of the `\setkeys` commands in `nodetree-embed.sty`
+---(which directly manipulates the `options` table) stay local.
+local prev_options = {}
+local option_level = 0
+
+---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
+--- The state values of the current tree item.
+---
+---`tree_state`:
+---
+---* `1` (level):
+---  * `list`: `continue`
+---  * `field`: `stop`
+---* `2`:
+---  * `list`: `continue`
+---  * `field`: `stop`
+---
+---...
 local tree_state = {}
 
 --- Format functions.
---
--- Low level template functions.
---
--- @section format
+---
+---Low-level template functions.
+---
+--- at section format
 
 local format = {
+  --- at function format.underscore
+  ---
   --- at param input string
   ---
   --- at return string
@@ -103,6 +117,8 @@
     end
   end,
 
+  --- at function format.escape
+  ---
   --- at param input string
   ---
   --- at return string
@@ -115,6 +131,8 @@
     end
   end,
 
+  --- at function format.function
+  ---
   --- at param input number
   ---
   --- at return number
@@ -123,8 +141,10 @@
     return math.floor(input * mult + 0.5) / mult
   end,
 
-  --- at param count? number # how many spaces should be output
+  --- at function format.whitespace
   ---
+  --- at param count? number # How many spaces should be output.
+  ---
   --- at return string
   whitespace = function(count)
     local whitespace
@@ -143,6 +163,8 @@
     return output
   end,
 
+  --- at function format.color_code
+  ---
   --- at param code number
   ---
   --- at return string
@@ -150,7 +172,11 @@
     return string.char(27) .. '[' .. tostring(code) .. 'm'
   end,
 
+  --- at function format.color_tex
   ---
+  --- at param color string
+  --- at param mode? string
+  ---
   --- at return string
   color_tex = function(color, mode)
     if not mode then mode = '' end
@@ -157,6 +183,7 @@
     return 'NTE' .. color .. mode
   end,
 
+  --- at function format.node_begin
   ---
   --- at return string
   node_begin = function()
@@ -167,6 +194,7 @@
     end
   end,
 
+  --- at function format.node_end
   ---
   --- at return string
   node_end = function()
@@ -177,8 +205,10 @@
     end
   end,
 
-  --- at param count? number # how many new lines should be output
+  --- at function format.new_line
   ---
+  --- at param count? number # How many new lines should be output.
+  ---
   --- at return string
   new_line = function(count)
     local output = ''
@@ -187,7 +217,7 @@
     end
     local new_line
     if options.channel == 'tex' then
-      new_line = '\\par{}'
+      new_line = '\\par\n'
     else
       new_line = '\n'
     end
@@ -198,6 +228,8 @@
     return output
   end,
 
+  --- at function format.type_id
+  ---
   --- at param id number
   ---
   --- at return string
@@ -207,8 +239,8 @@
 }
 
 --- Print the output to stdout or write it into a file (`output_file`).
--- New text is appended.
---
+---New text is appended.
+---
 --- at param text string # A text string.
 local function nodetree_print(text)
   if options.channel == 'log' or options.channel == 'tex' then
@@ -219,7 +251,8 @@
 end
 
 --- Template functions.
--- @section template
+---
+--- at section template
 
 local template = {
   node_colors = {
@@ -275,9 +308,29 @@
     shape = {'yellow'},
   },
 
-  ---
-  -- [SGR (Select Graphic Rendition) Parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters)
+  -- Field name abbreviations for verbosity level 0. A second field
+  -- limits the abbreviation to this node type.
   --
+  -- Entry '' means to omit the key, printing only the value. Entry
+  -- '()' means the same, but the value gets printed in parentheses.
+  field_abbrevs = {
+    char = {''},
+    depth = {'dp'},
+    dir = {'()', 'dir'},
+    height = {'ht'},
+    kern = {''},
+    mark = {''},
+    penalty = {'', 'penalty'},
+    shrink = {'minus'},
+    stretch = {'plus'},
+    style = {''},
+    subtype = {'()'},
+    width = {'wd'},
+  },
+
+  --- [SGR (Select Graphic Rendition)
+  -- parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters).
+  --
   -- __attributes__
   --
   -- | color      |code|
@@ -317,10 +370,12 @@
   -- | oncyan     | 46 |
   -- | onwhite    | 47 |
   --
+  --- at function template.color
+  ---
   --- at param color ColorName # A color name.
   --- at param mode? ColorMode
-  --- at param background? boolean # Colorize the background not the text.
-  --
+  --- at param background? boolean # If set, colorize the background instead of the text.
+  ---
   --- at return string
   color = function(color, mode, background)
     if options.color ~= 'colored' then
@@ -360,12 +415,12 @@
   end,
 
   --- Format the char field of a node. Try to find a textual representation that
-  -- corresponds with the number stored into the `char` field.
+  -- corresponds with the number stored in the `char` field.
   --
-  -- LuaTeX’s `node.char` are not really characters, they are font glyph indices
+  -- LuaTeX’s `node.char` values are not really characters; they are font glyph indices
   -- which sometimes happen to match valid Unicode characters. HarfBuzz shapers
-  -- differentiates between glyph IDs and characters by adding to 0x120000 to
-  -- glyph ID.
+  -- differentiate between glyph IDs and characters by adding to 0x120000 to
+  -- glyph IDs.
   --
   -- The code of this function is borrowed from the [function
   -- `get_glyph_info(n)`](https://github.com/latex3/luaotfload/blob/4c09fe264c1644792d95182280be259449e7da02/src/luaotfload-harf-plug.lua#L1018-L1031)
@@ -392,35 +447,55 @@
   -- It should also noted that this mapping is not unique, the same glyph
   -- can represent different characters in different context.
   --
+  --- at function template.char
+  --
   --- at param head Node # The head node of a node list.
   ---
-  --- at return string # A textual representation of the `char` number. In verbosity level 2 or great suffixed with `[char number]`
+  --- at return string # A textual representation of the `char` number.
   char = function(head)
-    -- See Issues #6 and #9
-    local node_id = todirect(head) -- Convert to node id
+    local node_id = todirect(head) -- Convert to node id.
     local props = properties[node_id]
     local info = props and props.glyph_info
     local textual
     local character_index = getchar(node_id)
+
     if info then
       textual = info
     elseif character_index == 0 then
       textual = '^^@'
     elseif character_index <= 31 or (character_index >= 127 and character_index <= 159) then
-      -- The C0 range [c-zero] is the characters from U+0000 to U+001F
-      -- (decimal 0-31) and U+007F (decimal 127), the C1 range is the
+      -- The C0 range [c-zero] contains characters from U+0000 to U+001F
+      -- (decimal 0-31) and U+007F (decimal 127), the C1 range covers
       -- characters from U+0080 to U+009F (decimal 128-159).
       textual = '???'
-    elseif character_index < 0x110000 then
+    elseif character_index ~= nil and character_index < 0x110000 then
       textual = utfchar(character_index)
     else
-      textual = string.format("^^^^^^%06X", character_index)
+      textual = string.format('^^^^^^%06X', character_index)
     end
-    return character_index .. ' (' .. string.format('0x%x', character_index) .. ', \''.. textual .. '\')'
+
+    if options.verbosity == 0 then
+      if textual == '???' then
+        return character_index
+      else
+        return "'" .. textual .. "'"
+      end
+    elseif options.verbosity <= 2 then
+      return character_index .. " ('" .. textual .. "')"
+    else
+      return character_index
+        .. ' ('
+        .. string.format('0x%x', character_index)
+        .. ", '"
+        .. textual
+        .. "')"
+    end
   end,
 
-  --- at param length? `long`
+  --- at function template.line
   ---
+  --- at param length string # If `long`, emit a longer line.
+  ---
   --- at return string
   line = function(length)
     local output
@@ -432,6 +507,8 @@
       return output .. format.new_line()
   end,
 
+  --- at function template.branch
+  ---
   --- at param connection_type ConnectionType
   --- at param connection_state ConnectionState
   --- at param last boolean
@@ -465,7 +542,7 @@
 --- at param number number
 --- at param order number
 --- at param field string
---
+---
 --- at return string
 function template.fill(number, order, field)
   local output
@@ -489,12 +566,12 @@
 end
 
 --- Colorize a text string.
---
---- at param text string A text string.
---- at param color ColorName A color name.
---- at param mode ColorMode
---- at param background? boolean # Colorize the background not the text.
---
+---
+--- at param text string # A text string.
+--- at param color ColorName # A color name.
+--- at param mode? ColorMode
+--- at param background? boolean # If set, colorize the background instead of the text.
+---
 --- at return string
 function template.colored_string(text, color, mode, background)
   if options.channel == 'tex' then
@@ -512,12 +589,12 @@
 end
 
 --- Format a scaled point input value into dimension string (`12pt`,
---  `1cm`)
---
+--- `1cm`)
+---
 --- at param input number
---
+---
 --- at return string
-function template.length (input)
+function template.length(input)
   local i = tonumber(input)
   if i ~= nil then
     input = i / tex.sp('1' .. options.unit)
@@ -530,16 +607,17 @@
 end
 
 --- Get all data from a table including metatables.
---
--- Properties will reside in a metatable, if nodes were copied using an
--- operation like box copy: (\copy). The 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.”
---
--- Source: https://stackoverflow.com/a/5639667 Works if __index returns
--- table, which it should as per luatex manual
---
+---
+---Properties will reside in a metatable if nodes were copied using an
+---operation like box copy: (\copy). The 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.”
+---
+---Source: [StackOverflow](https://stackoverflow.com/a/5639667) – this
+---works if `__index` returns a table, which it should as per LuaTeX
+---manual.
+---
 --- at param data table # A Lua table.
 --- at param previous_data? table # The data of a Lua table of a previous recursive call.
 ---
@@ -553,7 +631,7 @@
     output[key] = output[key] or value
   end
 
-  -- Get table’s metatable, or exit if not existing
+  -- Get table’s metatable, or exit if not existing.
   local metatable = getmetatable(data)
   if type(metatable) ~= 'table' then
     return output
@@ -565,14 +643,14 @@
     return output
   end
 
-  -- Include the data from index into data, recursively, and return.
+  -- Include the data from index into data recursively and return.
   return get_all_table_data(index, output)
 end
 
 --- Convert a Lua table into a format string.
---
---- at param table table A table to generate a inline view of.
---
+---
+--- at param table table # A table to generate an inline view of.
+---
 --- at return string
 function template.table_inline(table)
   local tex_escape = ''
@@ -598,23 +676,51 @@
   end
 end
 
---- Format a key value pair (`key: value, `).
---
+--- Format a key-value pair (`key: value, `).
+---
 --- at param key string # A key.
 --- at param value string|number # A value.
+--- at param typ? string # A node type. Had to be named typ to avoid conflict with the type() function.
 --- at param color? ColorName # A color name.
---
+---
 --- at return string
-function template.key_value(key, value, color)
+function template.key_value(key, value, typ, color)
   if type(color) ~= 'string' then
     color = 'yellow'
   end
-  if options.channel == 'tex' then
-    key = format.underscore(key)
+  key = format.underscore(key)
+
+  local output = ''
+  local abbrev = nil
+  local separator = ':'
+
+  if options.verbosity == 0 then
+    if template.field_abbrevs[key] then
+      local only_this_type = template.field_abbrevs[key][2]
+      if not only_this_type or not typ or only_this_type == typ then
+        abbrev = template.field_abbrevs[key][1]
+      end
+    end
+    separator = ''
   end
-  local output = template.colored_string(key .. ':', color)
+
+  if abbrev == nil then
+    output = template.colored_string(key .. separator, color)
+  elseif abbrev ~= '' and abbrev ~= '()' then
+    output = template.colored_string(abbrev, color)
+  end
+
   if value then
-    output = output .. ' ' .. value .. ', '
+    if abbrev == '()' then
+      -- Printing '(unused)' is confusing.
+      if value ~= 'unused' then
+        output = output .. '(' .. value .. ') '
+      end
+    elseif abbrev == '' then
+      output = output .. value .. ', '
+    else
+      output = output .. ' ' .. value .. ', '
+    end
   end
   return output
 end
@@ -625,17 +731,13 @@
 --- at return string
 function template.type(type, id)
   local output
-  if options.channel == 'tex' then
-    output = format.underscore(type)
-  else
-    output = type
-  end
+  output = format.underscore(type)
   output = string.upper(output)
   if options.verbosity > 1 then
     output = output .. format.type_id(id)
   end
   return template.colored_string(
-    output .. format.whitespace(),
+    output,
     template.node_colors[type][1],
     template.node_colors[type][2]
   )
@@ -642,13 +744,17 @@
 end
 
 --- at param callback_name string
---- at param variables? table
----
---- at return string
-function template.callback(callback_name, variables)
+--- at param variables table|nil
+--- at param where 'before'|'after' # `'before'` or `'after'`
+function template.callback(callback_name, variables, where)
+  if options.channel == 'term' or have_output == true then
+    nodetree_print(format.new_line(2))
+  end
+
+  have_output = true
+
   nodetree_print(
-    format.new_line(2) ..
-    'Callback: ' ..
+    where .. ' callback ' ..
     template.colored_string(format.underscore(callback_name), 'red', '', true) ..
     format.new_line()
   )
@@ -659,7 +765,7 @@
           '- ' ..
           format.underscore(name) ..
           ': ' ..
-          tostring(value) ..
+          format.underscore(tostring(value)) ..
           format.new_line()
         )
       end
@@ -668,6 +774,8 @@
   nodetree_print(template.line('long'))
 end
 
+--- Format the branching tree for one output line.
+---
 --- at param level number
 --- at param connection_type ConnectionType
 ---
@@ -678,7 +786,7 @@
     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
+---Format the last branches
   if connection_type == 'list' then
     output = output .. template.branch('list', tree_state[level]['list'], true)
   else
@@ -688,20 +796,21 @@
   return output
 end
 
---- Extend the node library
--- @section node_extended
+--- Node library extensions.
+---
+--- at 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: `<node    nil <    172 >    nil :
--- hlist 2>`.
---
+---
+---We have to convert the node into a string and then to extract
+---the ID from this string using a regular expression. If you convert a
+---node into a string it looks like: `<node    nil <    172 >    nil :
+---hlist 2>`.
+---
 --- at param n Node # A node.
---
+---
 --- at return string
 function node_extended.node_id(n)
   local result = string.gsub(tostring(n), '^<node%s+%S+%s+<%s+(%d+).*', '%1')
@@ -709,49 +818,46 @@
 end
 
 --- A table of all node subtype names.
---
--- __Nodes without subtypes:__
---
--- * `ins` (3)
--- * `mark` (4)
--- * `whatsit` (8)
--- * `local_par` (9)
--- * `dir` (10)
--- * `penalty` (14)
--- * `unset` (15)
--- * `style` (16)
--- * `choice` (17)
--- * `fraction` (20)
--- * `math_char` (23)
--- * `sub_box` (24)
--- * `sub_mlist` (25)
--- * `math_text_char` (26)
--- * `delim` (27)
--- * `margin_kern` (28)
--- * `align_record` (30)
--- * `pseudo_file` (31)
--- * `pseudo_line` (32)
--- * `page_insert` (33)
--- * `split_insert` (34)
--- * `expr_stack` (35)
--- * `nested_list` (36)
--- * `span` (37)
--- * `attribute` (38)
--- * `glue_spec` (39)
--- * `attribute_list` (40)
--- * `temp` (41)
--- * `align_stack` (42)
--- * `movement_stack` (43)
--- * `if_stack` (44)
--- * `unhyphenated` (45)
--- * `hyphenated` (46)
--- * `delta` (47)
--- * `passive` (48)
--- * `shape` (49)
---
+---
+---__Nodes without subtypes:__
+---
+---* `ins` (3)
+---* `local_par` (9)
+---* `penalty` (14)
+---* `unset` (15)
+---* `style` (16)
+---* `choice` (17)
+---* `fraction` (20)
+---* `math_char` (23)
+---* `sub_box` (24)
+---* `sub_mlist` (25)
+---* `math_text_char` (26)
+---* `delim` (27)
+---* `margin_kern` (28)
+---* `align_record` (30)
+---* `pseudo_file` (31)
+---* `pseudo_line` (32)
+---* `page_insert` (33)
+---* `split_insert` (34)
+---* `expr_stack` (35)
+---* `nested_list` (36)
+---* `span` (37)
+---* `attribute` (38)
+---* `glue_spec` (39)
+---* `attribute_list` (40)
+---* `temp` (41)
+---* `align_stack` (42)
+---* `movement_stack` (43)
+---* `if_stack` (44)
+---* `unhyphenated` (45)
+---* `hyphenated` (46)
+---* `delta` (47)
+---* `passive` (48)
+---* `shape` (49)
+---
 --- at return table
-local function get_node_subtypes ()
-    local subtypes = {
+local function get_node_subtypes()
+  local subtypes = {
     -- hlist (0)
     hlist = {
       [0] = 'unknown',
@@ -803,6 +909,11 @@
       [8] = 'radical',
       [9] = 'outline',
     },
+    -- mark (4)
+    -- The subtype is not used.
+    mark = {
+      [0] = 'unused',
+    },
     -- adjust (5)
     adjust = {
       [0] = 'normal',
@@ -816,7 +927,7 @@
       [3] = 'word',
     },
     -- disc (7)
-    disc  = {
+    disc = {
       [0] = 'discretionary',
       [1] = 'explicit',
       [2] = 'automatic',
@@ -824,6 +935,13 @@
       [4] = 'first',
       [5] = 'second',
     },
+    -- dir (10)
+    -- This is an internal detail, see luatex source code file
+    -- `texnodes.h`.
+    -- dir = {
+    --   [0] = 'normal_dir',
+    --   [1] = 'cancel_dir',
+    -- },
     -- math (11)
     math = {
       [0] = 'beginmath',
@@ -923,8 +1041,8 @@
       [1] = 'right',
     },
     -- glyph (29)
-    -- the subtype for this node is a bit field, not an enumeration;
-    -- bit 0 gets handled separately
+    -- The subtype for this node is a bit field, not an enumeration;
+    -- bit 0 gets handled separately.
     glyph = {
       [1] = 'ligature',
       [2] = 'ghost',
@@ -945,7 +1063,7 @@
   local output = ''
   if subtypes[typ] then
     if typ == 'glyph' then
-      -- only handle the lowest five bits
+      -- Only handle the lowest five bits.
       if n.subtype & 1 == 0 then
         output = output .. 'glyph'
       else
@@ -975,8 +1093,9 @@
   end
 end
 
---- Build the node tree.
--- @section tree
+--- Node tree building functions.
+---
+--- at section tree
 
 local tree = {}
 
@@ -983,30 +1102,37 @@
 ---
 --- at param head Node # The head node of a node list.
 --- at param field string
---
+---
 --- at return string
 function tree.format_field(head, field)
   local output
+  local typ = node.type(head.id)
 
-  -- subtypes with IDs 0 are were not printed, see #12
-  if head[field] ~= nil and field == "subtype" then
-    return template.key_value(field, format.underscore(node_extended.subtype(head)))
+  -- Print subtypes also for nodes with ID=0. However, suppress the
+  -- internal 'subtype' field for 'dir' nodes.
+  if field == 'subtype' then
+    if typ == 'dir' then
+      return ''
+    elseif head[field] ~= nil then
+      return template.key_value(field,
+                                format.underscore(node_extended.subtype(head)))
+    end
   end
 
-  -- Character "0" should be printed in a tree, because in TeX fonts the
-  -- 0 slot usually has a symbol.
-  if head[field] == nil or (head[field] == 0 and field ~= "char") then
+  -- Character 0 should be printed in a tree because the corresponding slot
+  -- zero in a TeX font usually contains a symbol.
+  if head[field] == nil 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
+    -- Don't drop the 'dir' field of the 'dir' node.
+    (field == 'dir' and typ ~= 'dir') or
     field == 'glue_order' or
     field == 'glue_sign' or
     field == 'glue_set' or
@@ -1016,8 +1142,7 @@
   elseif options.verbosity < 3 and
     field == 'prev' or
     field == 'next' or
-    field == 'id'
-  then
+    field == 'id' then
     return ''
   end
 
@@ -1037,37 +1162,50 @@
   elseif field == 'stretch' or field == 'shrink' then
     output = template.fill(head[field], head[field .. '_order'], field)
   else
-    output = tostring(head[field])
+    -- Surround strings with single quotes except values of fields
+    -- that get potentially abbreviated (and thus don't really need
+    -- quotes).
+    if type(head[field]) == 'string' and not template.field_abbrevs[field] then
+      output = template.colored_string("'", 'yellow') ..
+        head[field] ..
+        template.colored_string("'", 'yellow')
+    elseif type(head[field]) == 'table' then
+      output = '<table>'
+    else
+      output = tostring(head[field])
+    end
   end
 
-  return template.key_value(field, output)
+  return template.key_value(field, output, node.type(head.id))
 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.
---
+---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.
+---
 --- at param head Node # The head node of a node list.
---
+---
 --- at return string
 function tree.format_attributes(head)
   if not head then
     return ''
   end
+  local space = ''
   local output = ''
-  local attr = head.next
+  local attr = head.next --[[@as AttributeNode]]
   while attr do
     if attr.number ~= 0 or (attr.number == 0 and attr.value ~= 0) then
-      output = output .. tostring(attr.number) .. '=' .. tostring(attr.value) .. ' '
+      output = output .. space .. tostring(attr.number) .. '=' .. tostring(attr.value)
+      space = ' '
     end
-    attr = attr.next
+    attr = attr.next --[[@as AttributeNode]]
   end
   return output
 end
 
 ---
---- at param level number # `level` is a integer beginning with 1.
+--- at param level number # `level` is an integer beginning with 1.
 --- at param connection_type ConnectionType
 --- at param connection_state ConnectionState
 function tree.set_state(level, connection_type, connection_state)
@@ -1079,7 +1217,7 @@
 
 ---
 --- at param fields table
---- at param level number
+--- at param level number # The current recursion level.
 function tree.analyze_fields(fields, level)
   local max = 0
   local connection_state
@@ -1108,10 +1246,11 @@
 
 ---
 --- at param head Node # The head node of a node list.
---- at param level number
+--- at param level number # The current recursion level.
 function tree.analyze_node(head, level)
   local connection_state
   local output
+  local need_whitespace = true
   if head.next then
     connection_state = 'continue'
   else
@@ -1121,16 +1260,20 @@
   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))
+    output = output ..
+      format.whitespace() ..
+      template.key_value('no', node_extended.node_id(head))
+    need_whitespace = false
   end
 
-  -- We store the attributes output to append it to the field list.
+  -- We store the attributes output so that we can append it to the field
+  -- list later on.
   local attributes
 
   -- We store fields which are nodes for later treatment.
   local fields = {}
 
-  -- Inline fields, for example: char: 'm', width: 25pt, height: 13.33pt,
+  -- 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
@@ -1143,12 +1286,19 @@
     end
   end
   if output_fields ~= '' then
+    if need_whitespace == true then
+      output = output .. format.whitespace()
+      need_whitespace = false
+    end
     output = output .. output_fields
   end
 
-  -- Append the attributes output if available
-  if attributes ~= '' then
-    output = output .. template.key_value('attr', attributes, 'blue')
+  -- Append the attributes output if available.
+  if attributes and attributes ~= '' then
+    if need_whitespace == true then
+      output = output .. format.whitespace()
+    end
+    output = output .. template.key_value('attr', attributes, nil, 'blue')
   end
 
   output = output:gsub(', $', '')
@@ -1162,11 +1312,19 @@
 
   local property = node.getproperty(head)
   if property then
+    local props
+    if options.verbosity == 0 then
+      props = 'props'
+    else
+      props = 'properties:'
+    end
+
+    -- Print attributes in a separate line.
     nodetree_print(
       format.node_begin() ..
       template.branches(level, 'field') ..
       '  ' ..
-      template.colored_string('properties:', 'blue') .. ' ' ..
+      template.colored_string(props, 'blue') .. ' ' ..
       template.table_inline(property) ..
       format.node_end() ..
       format.new_line()
@@ -1176,9 +1334,10 @@
   tree.analyze_fields(fields, level)
 end
 
+--- Recurse over the current node list.
 ---
 --- at param head Node # The head node of a node list.
---- at param level number
+--- at param level number # The current recursion level.
 function tree.analyze_list(head, level)
   while head do
     tree.analyze_node(head, level)
@@ -1186,36 +1345,91 @@
   end
 end
 
+--- The top-level internal entry point.
 ---
 --- at param head Node # The head node of a node list.
 function tree.analyze_callback(head)
   tree.analyze_list(head, 1)
-  nodetree_print(template.line('short') .. format.new_line())
+  nodetree_print(template.line('short'))
 end
 
---- Callback wrapper.
--- @section callbacks
+local orig_callbacks = {}
+local orig_descriptions = {}
 
-local callbacks = {
+local print_positions = {}
+local callback_has_default_action = {
+  hyphenate = true,
+  ligaturing = true,
+  kerning = true,
+  mlist_to_hlist = true
+}
 
+--- Callback wrappers.
+---
+---Nodetree uses luatexbase's functions to manage callbacks if
+---available. Otherwise a simplistic approach is taken by prepending
+---or appending nodetree's diagnostic callbacks to the existing ones
+---(and also removing them again if requested).
+---
+---Each function in the `callback_wrappers` table consists of three
+---parts:
+---
+---* before-callback inspection
+---* original callback or default function call
+---* after-callback inspection
+---
+---The actual callback functions are stored in the `callbacks` table.
+---
+--- at section callbacks
+
+local callback_wrappers = {
+  --- at function callbacks.contribute_filter
   ---
   --- at param extrainfo string
-  ---
-  --- at return boolean
-  contribute_filter = function(extrainfo)
-    template.callback('contribute_filter', {extrainfo = extrainfo})
-    return true
+  --- at param where string
+  contribute_filter = function(extrainfo, where)
+    local cb = 'contribute_filter'
+    local before, after = template.get_print_position(where)
+
+    if before then
+      template.callback(cb, {extrainfo = extrainfo}, before)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        orig_callbacks[cb](extrainfo)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {extrainfo = extrainfo}, after)
+    end
   end,
 
+  --- at function callbacks.buildpage_filter
   ---
   --- at param extrainfo string
-  ---
-  --- at return boolean
-  buildpage_filter = function(extrainfo)
-    template.callback('buildpage_filter', {extrainfo = extrainfo})
-    return true
+  --- at param where string
+  buildpage_filter = function(extrainfo, where)
+    local cb = 'buildpage_filter'
+    local before, after = template.get_print_position(where)
+
+    if before then
+      template.callback(cb, {extrainfo = extrainfo}, before)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        orig_callbacks[cb](extrainfo)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {extrainfo = extrainfo}, after)
+    end
   end,
 
+  --- at function callbacks.build_page_insert
   ---
   --- at param n string
   --- at param i string
@@ -1222,21 +1436,59 @@
   ---
   --- at return number
   build_page_insert = function(n, i)
-    template.callback('build_page_insert', {n = n, i = i})
-    return 0
+    local cb = 'build_page_insert'
+    local before, after = template.get_print_position(cb)
+    local retval = 0
+
+    if before then
+      template.callback(cb, {n = n, i = i}, before)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](n, i)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {n = n, i = i}, after)
+    end
+
+    return retval
   end,
 
+  --- at function callbacks.pre_linebreak_filter
   ---
   --- at param head Node # The head node of a node list.
   --- at param groupcode string
+  --- at param where string
   ---
   --- at return boolean
-  pre_linebreak_filter = function(head, groupcode)
-    template.callback('pre_linebreak_filter', {groupcode = groupcode})
-    tree.analyze_callback(head)
-    return true
+  pre_linebreak_filter = function(head, groupcode, where)
+    local cb = 'pre_linebreak_filter'
+    local before, after = template.get_print_position(where)
+    local retval = true
+
+    if before then
+      template.callback(cb, {groupcode = groupcode}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](head, groupcode)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {groupcode = groupcode}, after)
+      tree.analyze_callback(head)
+    end
+
+    return retval
   end,
 
+  --- at function callbacks.linebreak_filter
   ---
   --- at param head Node # The head node of a node list.
   --- at param is_display boolean
@@ -1243,38 +1495,98 @@
   ---
   --- at return boolean
   linebreak_filter = function(head, is_display)
-    template.callback('linebreak_filter', {is_display = is_display})
-    tree.analyze_callback(head)
-    return true
+    local cb = 'linebreak_filter'
+    local before, after = template.get_print_position(cb)
+    local retval = true
+
+    if before then
+      template.callback(cb, {is_display = is_display}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](head, is_display)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {is_display = is_display}, after)
+      tree.analyze_callback(head)
+    end
+
+    return retval
   end,
 
+  --- at function callbacks.append_to_vlist_filter
   ---
   --- at param box Node
   --- at param locationcode string
   --- at param prevdepth number
   --- at param mirrored boolean
+  ---
+  --- at return Node
+  --- at return number
   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
+    local cb = 'append_to_vlist_filter'
+    local before, after = template.get_print_position(cb)
+
+    if before then
+      template.callback(cb, {locationcode = locationcode,
+                             prevdepth = prevdepth,
+                             mirrored = mirrored}, before)
+      tree.analyze_callback(box)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        box, prevdepth = orig_callbacks[cb](box, locationcode,
+                                            prevdepth, mirrored)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {locationcode = locationcode,
+                             prevdepth = prevdepth,
+                             mirrored = mirrored}, after)
+      tree.analyze_callback(box)
+    end
+
+    return box, prevdepth
   end,
 
+  --- at function callbacks.post_linebreak_filter
   ---
   --- at param head Node # The head node of a node list.
   --- at param groupcode string
+  --- at param where string
   ---
   --- at return boolean
-  post_linebreak_filter = function(head, groupcode)
-    template.callback('post_linebreak_filter', {groupcode = groupcode})
-    tree.analyze_callback(head)
-    return true
+  post_linebreak_filter = function(head, groupcode, where)
+    local cb = 'post_linebreak_filter'
+    local before, after = template.get_print_position(where)
+    local retval = true
+
+    if before then
+      template.callback(cb, {groupcode = groupcode}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](head, groupcode)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {groupcode = groupcode}, after)
+      tree.analyze_callback(head)
+    end
+
+    return retval
   end,
 
+  --- at function callbacks.hpack_filter
   ---
   --- at param head Node # The head node of a node list.
   --- at param groupcode string
@@ -1282,21 +1594,45 @@
   --- at param packtype string
   --- at param direction string
   --- at param attributelist Node
+  --- at param where string
   ---
   --- at return boolean
-  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
+  hpack_filter = function(head, groupcode, size, packtype,
+                          direction, attributelist, where)
+    local cb = 'hpack_filter'
+    local before, after = template.get_print_position(where)
+    local retval = true
+
+    if before then
+      template.callback(cb, {groupcode = groupcode,
+                             size = size,
+                             packtype = packtype,
+                             direction = direction,
+                             attributelist = attributelist}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](head, groupcode, size,
+                                    packtype, direction,
+                                    attributelist)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {groupcode = groupcode,
+                             size = size,
+                             packtype = packtype,
+                             direction = direction,
+                             attributelist = attributelist}, after)
+      tree.analyze_callback(head)
+    end
+
+    return retval
   end,
 
+  --- at function callbacks.vpack_filter
   ---
   --- at param head Node # The head node of a node list.
   --- at param groupcode string
@@ -1305,22 +1641,49 @@
   --- at param maxdepth number
   --- at param direction string
   --- at param attributelist Node
+  --- at param where string
   ---
   --- at return boolean
-  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
+  vpack_filter = function(head, groupcode, size, packtype,
+                          maxdepth, direction, attributelist, where)
+    local cb = 'vpack_filter'
+    local before, after = template.get_print_position(where)
+    local retval = true
+
+    if before then
+      template.callback(cb, {groupcode = groupcode,
+                             size = size,
+                             packtype = packtype,
+                             maxdepth = template.length(maxdepth),
+                             direction = direction,
+                             attributelist = attributelist}, before)
+      tree.analyze_callback(head)
+      tree.analyze_callback(attributelist)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](head, groupcode, size, packtype,
+                                    maxdepth, direction,
+                                    attributelist)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {groupcode = groupcode,
+                             size = size,
+                             packtype = packtype,
+                             maxdepth = template.length(maxdepth),
+                             direction = direction,
+                             attributelist = attributelist}, after)
+      tree.analyze_callback(head)
+      tree.analyze_callback(attributelist)
+    end
+
+    return retval
   end,
 
+  --- at function callbacks.hpack_quality
   ---
   --- at param incident string
   --- at param detail number
@@ -1327,17 +1690,39 @@
   --- at param head Node # The head node of a node list.
   --- at param first number
   --- at param last number
+  ---
+  --- at return Node
   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)
+    local cb = 'hpack_quality'
+    local before, after = template.get_print_position(cb)
+    local retval = nil
+
+    if before then
+      template.callback(cb, {incident = incident,
+                             detail = detail,
+                             first = first,
+                             last = last}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](incident, detail, head, first, last)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {incident = incident,
+                             detail = detail,
+                             first = first,
+                             last = last}, after)
+      tree.analyze_callback(head)
+    end
+
+    return retval
   end,
 
+  --- at function callbacks.vpack_quality
   ---
   --- at param incident string
   --- at param detail number
@@ -1345,32 +1730,59 @@
   --- at param first number
   --- at param last number
   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)
+    local cb = 'vpack_quality'
+    local before, after = template.get_print_position(cb)
+
+    if before then
+      template.callback(cb, {incident = incident,
+                             detail = detail,
+                             first = first,
+                             last = last}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        orig_callbacks[cb](incident, detail, head, first, last)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {incident = incident,
+                             detail = detail,
+                             first = first,
+                             last = last}, after)
+      tree.analyze_callback(head)
+    end
   end,
 
+  --- at function callbacks.process_rule
   ---
   --- at param head Node # The head node of a node list.
   --- at param width number
   --- at param height number
-  ---
-  --- at return boolean
   process_rule = function(head, width, height)
-    local variables = {
-      width = width,
-      height = height,
-    }
-    template.callback('process_rule', variables)
-    tree.analyze_callback(head)
-    return true
+    local cb = 'process_rule'
+    local before, after = template.get_print_position(cb)
+
+    if before then
+      template.callback(cb, {width = width, height = height}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        orig_callbacks[cb](head, width, height)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {width = width, height = height}, after)
+      tree.analyze_callback(head)
+    end
   end,
 
+  --- at function callbacks.pre_output_filter
   ---
   --- at param head Node # The head node of a node list.
   --- at param groupcode string
@@ -1378,98 +1790,437 @@
   --- at param packtype string
   --- at param maxdepth number
   --- at param direction string
+  --- at param where string
   ---
   --- at return boolean
-  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
+  pre_output_filter = function(head, groupcode, size, packtype,
+                               maxdepth, direction, where)
+    local cb = 'pre_output_filter'
+    local before, after = template.get_print_position(where)
+    local retval = true
+
+    if before then
+      template.callback(cb, {groupcode = groupcode,
+                             size = size,
+                             packtype = packtype,
+                             maxdepth = maxdepth,
+                             direction = direction}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](head, groupcode, size,
+                                    packtype, maxdepth,
+                                    direction)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {groupcode = groupcode,
+                             size = size,
+                             packtype = packtype,
+                             maxdepth = maxdepth,
+                             direction = direction}, after)
+      tree.analyze_callback(head)
+    end
+
+    return retval
   end,
 
+  --- at function callbacks.hyphenate
   ---
   --- at param head Node # The head node of a node list.
   --- at param tail Node
-  hyphenate = function(head, tail)
-    template.callback('hyphenate')
-    nodetree_print('head:' .. format.new_line())
-    tree.analyze_callback(head)
-    nodetree_print('tail:' .. format.new_line())
-    tree.analyze_callback(tail)
+  --- at param where string
+  hyphenate = function(head, tail, where)
+    local cb = 'hyphenate'
+    local before, after = template.get_print_position(where)
+
+    if before then
+      template.callback(cb, nil, before)
+      nodetree_print('head:' .. format.new_line())
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        orig_callbacks[cb](head, tail)
+      end
+    else
+      template.no_callback(cb, true)
+      lang.hyphenate(head, tail)
+    end
+    if after then
+      template.callback(cb, nil, after)
+      nodetree_print('head:' .. format.new_line())
+      tree.analyze_callback(head)
+    end
   end,
 
+  --- at function callbacks.ligaturing
   ---
   --- at param head Node # The head node of a node list.
   --- at param tail Node
-  ligaturing = function(head, tail)
-    template.callback('ligaturing')
-    nodetree_print('head:' .. format.new_line())
-    tree.analyze_callback(head)
-    nodetree_print('tail:' .. format.new_line())
-    tree.analyze_callback(tail)
+  --- at param where string
+  ligaturing = function(head, tail, where)
+    local cb = 'ligaturing'
+    local before, after = template.get_print_position(where)
+
+    if before then
+      template.callback(cb, nil, before)
+      nodetree_print('head:' .. format.new_line())
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        orig_callbacks[cb](head, tail)
+      end
+    else
+      template.no_callback(cb, true)
+      node.ligaturing(head, tail)
+    end
+    if after then
+      template.callback(cb, nil, after)
+      nodetree_print('head:' .. format.new_line())
+      tree.analyze_callback(head)
+    end
   end,
 
+  --- at function callbacks.kerning
   ---
   --- at param head Node # The head node of a node list.
   --- at param tail Node
-  kerning = function(head, tail)
-    template.callback('kerning')
-    nodetree_print('head:' .. format.new_line())
-    tree.analyze_callback(head)
-    nodetree_print('tail:' .. format.new_line())
-    tree.analyze_callback(tail)
+  --- at param where string
+  kerning = function(head, tail, where)
+    local cb = 'kerning'
+    local before, after = template.get_print_position(where)
+
+    if before then
+      template.callback(cb, nil, before)
+      nodetree_print('head:' .. format.new_line())
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        orig_callbacks[cb](head, tail)
+      end
+    else
+      template.no_callback(cb, true)
+      node.kerning(head, tail)
+    end
+    if after then
+      template.callback(cb, nil, after)
+      nodetree_print('head:' .. format.new_line())
+      tree.analyze_callback(head)
+    end
   end,
 
+  --- at function callbacks.insert_local_par
   ---
   --- at param local_par Node
   --- at param location string
-  ---
-  --- at return boolean
-  insert_local_par = function(local_par, location)
-    template.callback('insert_local_par', {location = location})
-    tree.analyze_callback(local_par)
-    return true
+  --- at param where string
+  insert_local_par = function(local_par, location, where)
+    local cb = 'insert_local_par'
+    local before, after = template.get_print_position(where)
+
+    if before then
+      template.callback(cb, {location = location}, before)
+      tree.analyze_callback(local_par)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        orig_callbacks[cb](local_par, location)
+      end
+    else
+      template.no_callback(cb)
+    end
+    if after then
+      template.callback(cb, {location = location}, after)
+      tree.analyze_callback(local_par)
+    end
   end,
 
+  --- at function callbacks.mlist_to_hlist
   ---
   --- at param head Node # The head node of a node list.
   --- at param display_type string
   --- at param need_penalties boolean
+  ---
+  --- at return Node
   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)
+    local cb = 'mlist_to_hlist'
+    local before, after = template.get_print_position(cb)
+    local retval
+
+    if before then
+      template.callback(cb, {display_type = display_type,
+                             need_penalties = need_penalties}, before)
+      tree.analyze_callback(head)
+    end
+    if orig_callbacks[cb] then
+      if orig_callbacks[cb] ~= '' then
+        retval = orig_callbacks[cb](head, display_type, need_penalties)
+      end
+    else
+      template.no_callback(cb, true)
+      retval = node.mlist_to_hlist(head, display_type, need_penalties)
+    end
+    if after then
+      template.callback(cb, {display_type = display_type,
+                             need_penalties = need_penalties}, after)
+      tree.analyze_callback(head)
+    end
+
+    return retval
+  end
+}
+
+-- The actual callback functions. The `*_before` and `*_after`
+-- variants are needed for luatexbase. For 'exclusive' callbacks we
+-- directly use the corresponding functions from the
+-- `callback_wrappers` table.
+
+local callbacks = {
+  contribute_filter = function(extrainfo)
+    callback_wrappers.contribute_filter(extrainfo, 'contribute_filter')
   end,
+  contribute_filter_before = function(extrainfo)
+    callback_wrappers.contribute_filter(extrainfo, 'before')
+  end,
+  contribute_filter_after = function(extrainfo)
+    callback_wrappers.contribute_filter(extrainfo, 'after')
+  end,
+
+  buildpage_filter = function(extrainfo)
+    callback_wrappers.buildpage_filter(extrainfo, 'buildpage_filter')
+  end,
+  buildpage_filter_before = function(extrainfo)
+    callback_wrappers.buildpage_filter(extrainfo, 'before')
+  end,
+  buildpage_filter_after = function(extrainfo)
+    callback_wrappers.buildpage_filter(extrainfo, 'after')
+  end,
+
+  build_page_insert = callback_wrappers.build_page_insert,
+
+  pre_linebreak_filter = function(head, groupcode)
+    return callback_wrappers.pre_linebreak_filter(head, groupcode,
+                                                  'pre_linebreak_filter')
+  end,
+  pre_linebreak_filter_before = function(head, groupcode)
+    return callback_wrappers.pre_linebreak_filter(head, groupcode, 'before')
+  end,
+  pre_linebreak_filter_after = function(head, groupcode)
+    return callback_wrappers.pre_linebreak_filter(head, groupcode, 'after')
+  end,
+
+  linebreak_filter = callback_wrappers.linebreak_filter,
+  append_to_vlist_filter = callback_wrappers.append_to_vlist_filter,
+
+  post_linebreak_filter = function(head, groupcode)
+    return callback_wrappers.post_linebreak_filter(head, groupcode,
+                                                   'post_linebreak_filter')
+  end,
+  post_linebreak_filter_before = function(head, groupcode)
+    return callback_wrappers.post_linebreak_filter(head, groupcode, 'before')
+  end,
+  post_linebreak_filter_after = function(head, groupcode)
+    return callback_wrappers.post_linebreak_filter(head, groupcode, 'after')
+  end,
+
+  hpack_filter = function(head, groupcode, size, packtype,
+                          direction, attributelist)
+    return callback_wrappers.hpack_filter(head, groupcode, size, packtype,
+                                          direction, attributelist,
+                                          'hpack_filter')
+  end,
+  hpack_filter_before = function(head, groupcode, size, packtype,
+                                 direction, attributelist)
+    return callback_wrappers.hpack_filter(head, groupcode, size, packtype,
+                                          direction, attributelist, 'before')
+  end,
+  hpack_filter_after = function(head, groupcode, size, packtype,
+                                direction, attributelist)
+    return callback_wrappers.hpack_filter(head, groupcode, size, packtype,
+                                          direction, attributelist, 'after')
+  end,
+
+  vpack_filter = function(head, groupcode, size, packtype,
+                          maxdepth, direction, attributelist)
+    return callback_wrappers.vpack_filter(head, groupcode, size, packtype,
+                                          maxdepth, direction, attributelist,
+                                          'vpack_filter')
+  end,
+  vpack_filter_before = function(head, groupcode, size, packtype,
+                                 maxdepth, direction, attributelist)
+    return callback_wrappers.vpack_filter(head, groupcode, size, packtype,
+                                          maxdepth, direction, attributelist,
+                                          'before')
+  end,
+  vpack_filter_after = function(head, groupcode, size, packtype,
+                                maxdepth, direction, attributelist)
+    callback_wrappers.vpack_filter(head, groupcode, size, packtype,
+                                   maxdepth, direction, attributelist,
+                                   'after')
+  end,
+
+  hpack_quality = callback_wrappers.hpack_quality,
+  vpack_quality = callback_wrappers.vpack_quality,
+  process_rule = callback_wrappers.process_rule,
+
+  pre_output_filter = function(head, groupcode, size, packtype,
+                               maxdepth, direction)
+    return callback_wrappers.pre_output_filter(head, groupcode, size,
+                                               packtype, maxdepth, direction,
+                                               'pre_output_filter')
+  end,
+  pre_output_filter_before = function(head, groupcode, size, packtype,
+                                      maxdepth, direction)
+    return callback_wrappers.pre_output_filter(head, groupcode, size,
+                                               packtype, maxdepth, direction,
+                                               'before')
+  end,
+  pre_output_filter_after = function(head, groupcode, size, packtype,
+                                     maxdepth, direction)
+    return callback_wrappers.pre_output_filter(head, groupcode, size,
+                                               packtype, maxdepth, direction,
+                                               'after')
+  end,
+
+  hyphenate = function(head, tail)
+    callback_wrappers.hyphenate(head, tail, 'hyphenate')
+  end,
+  hyphenate_before = function(head, tail)
+    callback_wrappers.hyphenate(head, tail, 'before')
+  end,
+  hyphenate_after = function(head, tail)
+    callback_wrappers.hyphenate(head, tail, 'after')
+  end,
+
+  ligaturing = function(head, tail)
+    callback_wrappers.ligaturing(head, tail, 'ligaturing')
+  end,
+  ligaturing_before = function(head, tail)
+    callback_wrappers.ligaturing(head, tail, 'before')
+  end,
+  ligaturing_after = function(head, tail)
+    callback_wrappers.ligaturing(head, tail, 'after')
+  end,
+
+  kerning = function(head, tail)
+    callback_wrappers.kerning(head, tail, 'kerning')
+  end,
+  kerning_before = function(head, tail)
+    callback_wrappers.kerning(head, tail, 'before')
+  end,
+  kerning_after = function(head, tail)
+    callback_wrappers.kerning(head, tail, 'after')
+  end,
+
+  insert_local_par = function(head, tail)
+    callback_wrappers.insert_local_par(head, tail, 'insert_local_par')
+  end,
+  insert_local_par_before = function(head, tail)
+    callback_wrappers.insert_local_par(head, tail, 'before')
+  end,
+  insert_local_par_after = function(head, tail)
+    callback_wrappers.insert_local_par(head, tail, 'after')
+  end,
+
+  mlist_to_hlist = callback_wrappers.mlist_to_hlist
 }
 
---- Set a single option key value pair.
---
+--- Messages, options
+---
+--- at section messages
+
+--- Emit a warning or error message.
+---
+---This works for plain TeX, Texinfo, and LaTeX (for plain TeX and
+---Texinfo we make the message look identical to the LaTeX case).
+---Note that a full stop gets appended to `what`.
+---
+--- at param why string # `'error'` or `'warning'`.
+--- at param where string # In which package the error happened.
+--- at param what string # The warning message to emit.
+--- at param help? string # Additional help text for errors.
+local function message(why, where, what, help)
+  local msg
+
+  what = string.gsub(what, '\n', '\\MessageBreak ')
+
+  if why == 'error' then
+    if not help then
+      help = ''
+    end
+
+    msg = {
+      '\\ifx\\mbox\\undefined',
+      '  \\errhelp{' .. help .. '}%',
+      '  \\begingroup',
+      '    \\newlinechar`\\^^J%',
+      '    \\def\\MessageBreak{^^J(' .. where .. ')' .. string.rep('\\space', 16) .. '}%',
+      '    \\errmessage{Package ' .. where .. ' Error: ' .. what .. '}%',
+      '  \\endgroup',
+      '\\else',
+      '  \\PackageError{' .. where .. '}{' .. what .. '}{' .. help .. '}%',
+      '\\fi'
+    }
+  else
+    msg = {
+      '\\ifx\\mbox\\undefined',
+      '  \\begingroup',
+      '    \\newlinechar`\\^^J%',
+      '    \\def\\MessageBreak{^^J(' .. where .. ')' .. string.rep('\\space', 16) .. '}%',
+      '    \\message{Package ' .. where .. ' Warning: ' .. what .. '}%',
+      '  \\endgroup',
+      '\\else',
+      '  \\PackageWarning{' .. where .. '}{' .. what .. '}%',
+      '\\fi'
+    }
+  end
+
+  if tex.escapechar == utf8.codepoint('@') then
+    table.insert(msg, 1, '@tex')
+    table.insert(msg, '@end tex')
+  end
+
+  tex.print(msg)
+end
+
+--- Set a single-option key-value pair.
+---
 --- at param key string # The key of the option pair.
 --- at param value number|string # The value of the option pair.
 local function set_option(key, value)
+  if not default_options[key] then
+    message(
+      'warning',
+      'nodetree',
+      "Ignoring unknown option '" .. key .. "'"
+    )
+    return
+  end
   if not options then
     options = {}
   end
-  if key == 'verbosity' or key == 'decimalplaces' then
-    options[key] = tonumber(value)
+  if key == 'verbosity' then
+    options[key] = tonumber(value) or default_options.verbosity
+  elseif key == 'decimalplaces' then
+    options[key] = tonumber(value) or default_options.decimalplaces
+  elseif key == 'firstline' then
+    options[key] = tonumber(value) or default_options.firstline
+  elseif key == 'lastline' then
+    options[key] = tonumber(value) or default_options.lastline
   else
     options[key] = value
   end
 end
 
---- Set multiple key value pairs using a table.
---
---- at param opts table # Options
+--- Set multiple key-value option pairs using a table.
+---
+--- at param opts table # Options.
 local function set_options(opts)
   if not options then
     options = {}
@@ -1479,20 +2230,62 @@
   end
 end
 
---- Check if the given callback name exists.
---
--- Throw an error if it doen’t.
---
+--- Callback management
+---
+--- at section callback_management
+
+---
+--- at param what? string|'before'|'after' # The name of a callback, or either the string `before` or `after`.
+---
+--- at return 'before'|nil # 'before'` or `nil`.
+--- at return 'after'|nil # `'after'` or `nil`.
+function template.get_print_position(what)
+  local before, after
+
+  if what == 'before' then
+    before = what
+    after = nil
+  elseif what == 'after' then
+    before = nil
+    after = what
+  else
+    before = print_positions[what][1]
+    after = print_positions[what][2]
+  end
+
+  return before, after
+end
+
+---
+--- at param name string
+--- at param internal? string|boolean
+function template.no_callback(name, internal)
+  local more = ''
+  if internal == true then
+    more = ',' .. format.new_line() ..
+      ' LuaTeX uses internal function instead'
+  end
+  nodetree_print(
+    format.new_line() ..
+    "<no registered function for '" ..
+    format.underscore(name) .. "' callback" .. more .. ">")
+end
+
+--- Check whether the given callback name exists.
+---
+---Throw an error if it doesn’t.
+---
 --- at param callback_name string # The name of a callback to check.
---
+---
 --- at return 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 ..
-      '"'
+    message(
+      'error',
+      'nodetree',
+      'Unknown callback name or callback alias\n'
+      .. "'" .. callback_name .. "'"
     )
   end
   return callback_name
@@ -1499,14 +2292,12 @@
 end
 
 --- Get the real callback name from an alias string.
---
---- at param alias string The alias of a callback name or the callback
--- name itself.
---
+---
+--- at param alias string # The alias of a callback name or the callback name itself.
+---
 --- at return 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'
 
@@ -1527,7 +2318,6 @@
   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'
 
@@ -1571,35 +2361,139 @@
 end
 
 --- Register a callback.
---
+---
+--- Doing this for plain TeX is simple; we have access to LuaTeX's
+--- base function `callback.register` and thus can easily add our
+--- callback wrapper, which in turn calls nodetree's functions at the
+--- very beginning and/or at the very end.
+---
+--- Enter LaTeX. It comes with its own callback management that can
+--- register multiple callbacks, also taking care of the calling
+--- order. Unfortunately, however, it is also more restrictive: for
+--- example, some callbacks like `linebreak_filter` are tagged as
+--- 'exclusive', only accepting a single callback. While this makes
+--- sense for the end user, it complicates the situation for nodetree
+--- to install its non-intrusive, observing-only callbacks.
+---
+--- We thus take the following route.
+---
+--- * If there is no function for callback `<foo>` installed, register
+---   `callbacks.<foo>`.
+---
+--- * If there is a (single) function for callback `<foo>` of type
+---   three ('exclusive'), remove it, wrap it into `callbacks.<foo>`
+---   (via `orig_callbacks`) and install `callbacks.<foo>`.
+---
+--- * Otherwise register `callbacks.<foo>_{before,after}` as
+---   necessary.
+---
 --- at param cb string # The name of a callback.
 local function register_callback(cb)
   if luatexbase then
-    luatexbase.add_to_callback(cb, callbacks[cb], 'nodetree')
+    local descriptions = luatexbase.callback_descriptions(cb)
+
+    if #descriptions == 0 then
+      -- No callback installed. If there is no default action
+      -- (according to the LuaTeX manual), use only `before`, ignoring
+      -- the position requested by the user.
+      if not callback_has_default_action[cb] then
+        print_positions[cb] = { 'before', nil }
+      end
+      luatexbase.add_to_callback(cb, callbacks[cb], 'nodetree')
+    elseif luatexbase.callbacktypes[cb] == 3 then
+      -- A single, 'exclusive' callback.
+      orig_callbacks[cb], orig_descriptions[cb] =
+        luatexbase.remove_from_callback(cb, descriptions[1])
+      luatexbase.add_to_callback(cb, callbacks[cb], 'nodetree')
+    else
+      -- All other callback types.
+      local funcs = {}
+      local descr = {}
+      local before, after = template.get_print_position(cb)
+
+      -- XXX Is this correct for 'reverselist' callback type?
+
+      -- This makes the callback wrapper call neither the old nor the
+      -- new function.
+      orig_callbacks[cb] = ''
+
+      for i, description in ipairs(descriptions) do
+        funcs[i], descr[i] = luatexbase.remove_from_callback(cb, description)
+      end
+
+      if before then
+        luatexbase.add_to_callback(cb, callbacks[cb .. '_before'],
+                                   'nodetree before')
+      end
+      for i, _ in ipairs(funcs) do
+        luatexbase.add_to_callback(cb, funcs[i], descr[i])
+      end
+      if after then
+        luatexbase.add_to_callback(cb, callbacks[cb .. '_after'],
+                                   'nodetree after')
+      end
+    end
   else
+    orig_callbacks[cb] = callback.find(cb)
     callback.register(cb, callbacks[cb])
   end
 end
 
 --- Unregister a callback.
---
+---
+--- We follow the same logic as with `register_callback`.
+---
 --- at param cb string # The name of a callback.
 local function unregister_callback(cb)
   if luatexbase then
-    luatexbase.remove_from_callback(cb, 'nodetree')
+    local descriptions = luatexbase.callback_descriptions(cb)
+
+    if #descriptions == 0 then
+      return
+    elseif #descriptions == 1 then
+      luatexbase.remove_from_callback(cb, 'nodetree')
+      if orig_callbacks[cb] then
+        luatexbase.add_to_callback(cb,
+                                   orig_callbacks[cb],
+                                   orig_descriptions[cb])
+      end
+      orig_callbacks[cb] = nil
+      orig_descriptions[cb] = nil
+    else
+      local funcs = {}
+      local descr = {}
+
+      local i = 1
+      for _, description in ipairs(descriptions) do
+        if description == 'nodetree before' or
+          description == 'nodetree after' then
+          luatexbase.remove_from_callback(cb, description)
+        else
+          funcs[i], descr[i] = luatexbase.remove_from_callback(cb,
+                                                               description)
+          i = i + 1
+        end
+      end
+
+      for n, _ in ipairs(funcs) do
+        luatexbase.add_to_callback(cb, funcs[n], descr[n])
+      end
+    end
   else
-    register_callback(cb, nil)
+    callback.register(cb, nil)
+    callback.register(cb, orig_callbacks[cb])
   end
 end
 
 --- Exported functions.
--- @section export
+---
+--- at section export
 
 local export = {
   set_option = set_option,
   set_options = set_options,
 
-  ---
+  --- at function export.register_callbacks
   register_callbacks = function()
     if options.channel == 'log' or options.channel == 'tex' then
       -- nt = nodetree
@@ -1606,18 +2500,43 @@
       -- jobname.nttex
       -- jobname.ntlog
       local file_name = tex.jobname .. '.nt' .. options.channel
-      io.open(file_name, 'w'):close() -- Clear former content
+      io.open(file_name, 'w'):close() -- Clear former content.
       output_file = io.open(file_name, 'a')
     end
+
+    -- Split string at ','.
     for alias in string.gmatch(options.callback, '([^,]+)') do
-      register_callback(get_callback_name(alias))
+      -- Trim whitespace.
+      alias = string.gsub(alias, '^%s*(.-)%s*$', '%1')
+
+      -- Check where to position nodetree's inspection callback(s).
+      local before, after
+      if string.sub(alias, 1, 1) == ':' then
+        before = 'before'
+        alias = string.sub(alias, 2, -1)
+      end
+      if string.sub(alias, -1, -1) == ':' then
+        after = 'after'
+        alias = string.sub(alias, 1, -2)
+      end
+      if not before and not after then
+        before = 'before'
+      end
+      local name = get_callback_name(alias)
+      print_positions[name] = {before, after}
+      register_callback(name)
     end
   end,
 
-  ---
+  --- at function export.unregister_callbacks
   unregister_callbacks = function()
     for alias in string.gmatch(options.callback, '([^,]+)') do
-      unregister_callback(get_callback_name(alias))
+      -- Split string at ',', then trim whitespace. For symmetry with
+      -- `register_callbacks`, also remove a leading and/or trailing
+      -- ':' character.
+      unregister_callback(
+        get_callback_name(string.gsub(alias, '^%s*:?(.-):?%s*$', '%1'))
+      )
     end
   end,
 
@@ -1624,12 +2543,12 @@
   --- 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
+  -- file using `latexmk`, read the generated `*.nttex` file, and
   -- return its content.
   --
+  --- at function export.compile_include
+  --
   --- at param tex_markup string
-  --
-  --- at return string
   compile_include = function(tex_markup)
     -- Generate a subfolder for all tempory files: _nodetree-jobname.
     local parent_path = lfs.currentdir() .. '/' .. '_nodetree-' .. tex.jobname
@@ -1641,12 +2560,12 @@
     local absolute_path_tex = parent_path .. '/' .. filename_tex
     output_file = io.open(absolute_path_tex, 'w')
 
-    local format_option = function (key, value)
+    local format_option = function(key, value)
       return '\\NodetreeSetOption[' .. key .. ']{' .. value .. '}' .. '\n'
     end
 
-    -- Process the options
-    local options =
+    -- Process the options.
+    local option_lines =
       format_option('channel', 'tex') ..
       format_option('verbosity', options.verbosity) ..
       format_option('unit', options.unit) ..
@@ -1657,32 +2576,58 @@
     local prefix = '%!TEX program = lualatex\n' ..
                   '\\documentclass{article}\n' ..
                   '\\usepackage{nodetree}\n' ..
-                  options .. '\n' ..
+                  option_lines .. '\n' ..
                   '\\begin{document}\n'
     local suffix = '\n\\end{document}'
-    output_file:write(prefix .. tex_markup .. suffix)
-    output_file:close()
+    if output_file ~= nil then
+      output_file:write(prefix .. tex_markup .. suffix)
+      output_file:close()
+    end
 
     -- 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")
+    local include_file = assert(io.open(parent_path .. '/' .. example_counter .. '.nttex', 'r'))
+    local include_content = include_file:read('*all')
     include_file:close()
-    include_content = include_content:gsub('[\r\n]', '')
+    -- To make the newline character be handled properly by the TeX engine
+    -- it would be necessary to set up its correct catcode. However, it is
+    -- simpler to just replace all newlines with '{}'.
+    include_content = include_content:gsub('[\r\n]', '{}')
     tex.print(include_content)
   end,
 
-  --- Check for `--shell-escape`
-  --
-  check_shell_escape = function()
+  --- Check for `\--shell-escape` within a command or environment.
+  ---
+  --- at function export.check_shell_escape
+  ---
+  --- at param what string # The name of the command or environment.
+  --- at param is_command boolean # Set if `what` is a command.
+  check_shell_escape = function(what, is_command)
     local info = status.list()
-    if info.shell_escape == 0 then
-      tex.error('Package "nodetree-embed": You have to use the --shell-escape option')
+    if info ~= nil and info.shell_escape ~= 1 then
+      local typ, stuff
+      if is_command == true then
+        typ = 'command'
+        stuff = 'argument'
+      else
+        typ = 'environment'
+        stuff = 'contents'
+      end
+      message(
+        'error',
+        'nodetree-embed',
+        what .. ' needs option --shell-escape',
+        "You must process this document with 'lualatex --shell-escape ...'\n"
+        .. "so that 'latexmk' can be executed to generate the nodetree view\n"
+        .. 'for the ' .. stuff .. ' of this ' .. typ .. '.'
+      )
     end
   end,
 
   --- Print a node tree.
   ---
+  --- at function export.print
+  ---
   --- at param head Node # The head node of a node list.
   --- at param opts table # Options as a table.
   print = function(head, opts)
@@ -1693,9 +2638,11 @@
     tree.analyze_list(head, 1)
   end,
 
-  --- Format a scaled point value into a formated string.
+  --- Format a scaled point value as a formatted string.
   --
-  --- at param sp number # A scaled point value
+  --- at function export.format_dim
+  ---
+  --- at param sp number # A scaled point value.
   --
   --- at return string
   format_dim = function(sp)
@@ -1703,17 +2650,98 @@
   end,
 
   --- Get a default option that is not changed.
+  ---
+  --- at function export.get_default_option
+  ---
   --- at param key string # The key of the option.
   --
   --- at return string|number|boolean
   get_default_option = function(key)
     return default_options[key]
+  end,
+
+  --- Push current options.
+  ---
+  --- at function export.push_options
+  push_options = function()
+    prev_options[option_level] = {}
+    for k, v in pairs(options) do
+      prev_options[option_level][k] = v
+    end
+    option_level = option_level + 1
+  end,
+
+  --- Pop previous options.
+  ---
+  --- at function export.pop_options
+  pop_options = function()
+    if option_level > 0 then
+      prev_options[option_level] = nil
+      option_level = option_level - 1
+      for k, v in pairs(prev_options[option_level]) do
+        options[k] = v
+      end
+    end
+  end,
+
+  --- Read a LaTeX input file and emit it immediately, obeying options
+  --- `firstline` and `lastline`.
+  ---
+  --- at function export.input
+  ---
+  --- at param filename string
+  input = function(filename)
+    local file = assert(io.open(filename, 'r'))
+    local lines_in = {}
+    for line in file:lines() do
+      table.insert(lines_in, line)
+    end
+
+    local firstline = options.firstline
+    local lastline = options.lastline
+
+    -- Handle negative line numbers.
+    if firstline < 0 then
+      firstline = #lines_in + 1 + firstline
+    elseif firstline == 0 then
+      firstline = 1
+    end
+    if lastline < 0 then
+      lastline = #lines_in + 1 + lastline
+    elseif lastline == 0 then
+      lastline = 1
+    end
+
+    -- Clamp values.
+    if firstline < 1 then
+      firstline = 1
+    elseif firstline > #lines_in then
+      firstline = #lines_in
+    end
+    if lastline < 1 then
+      lastline = 1
+    elseif lastline > #lines_in then
+      lastline = #lines_in
+    end
+
+    if firstline > lastline then
+      local tmp = firstline
+      firstline = lastline
+      lastline = tmp
+    end
+
+    local lines_out = {}
+    for i, line in ipairs(lines_in) do
+      if firstline <= i and i <= lastline then
+        table.insert(lines_out, line)
+      end
+    end
+
+    tex.print(lines_out)
   end
 }
 
---- Use export.print
---
---- at param head Node # The head node of a node list.
+--- Set to `export.print`.
 export.analyze = export.print
 
 return export

Modified: trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.sty
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.sty	2023-09-11 21:18:30 UTC (rev 68243)
+++ trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.sty	2023-09-11 21:18:50 UTC (rev 68244)
@@ -8,7 +8,7 @@
 %% 
 %% This is a generated file.
 %% 
-%% Copyright (C) 2016-2022 by Josef Friedrich <josef at friedrich.rocks>
+%% Copyright (C) 2016-2023 by Josef Friedrich <josef at friedrich.rocks>
 %% ----------------------------------------------------------------------
 %% This work may be distributed and/or modified under the conditions of
 %% the LaTeX Project Public License, either version 1.3c of this license
@@ -22,7 +22,7 @@
 %% 
 \NeedsTeXFormat{LaTeX2e}[1999/12/01]
 \ProvidesPackage{nodetree}
-    [2022/12/17 v2.2.1 Visualize node lists in a tree view]
+    [2023/09/10 v2.3.0 Visualize node lists in a tree view]
 \input{nodetree}
 \RequirePackage{kvoptions}
 \SetupKeyvalOptions{
@@ -33,7 +33,7 @@
 \define at key{NT}{channel}[]{\NodetreeSetOption[channel]{#1}}
 \DeclareStringOption[postlinebreak]{callback}
 \define at key{NT}{callback}[]{\NodetreeSetOption[callback]{#1}}
-\DeclareStringOption[1]{verbosity}
+\DeclareStringOption[0]{verbosity}
 \define at key{NT}{verbosity}[]{\NodetreeSetOption[verbosity]{#1}}
 \DeclareStringOption[colored]{color}
 \define at key{NT}{color}[]{\NodetreeSetOption[color]{#1}}

Modified: trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.tex
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.tex	2023-09-11 21:18:30 UTC (rev 68243)
+++ trunk/Master/texmf-dist/tex/luatex/nodetree/nodetree.tex	2023-09-11 21:18:50 UTC (rev 68244)
@@ -8,7 +8,7 @@
 %% 
 %% This is a generated file.
 %% 
-%% Copyright (C) 2016-2022 by Josef Friedrich <josef at friedrich.rocks>
+%% Copyright (C) 2016-2023 by Josef Friedrich <josef at friedrich.rocks>
 %% ----------------------------------------------------------------------
 %% This work may be distributed and/or modified under the conditions of
 %% the LaTeX Project Public License, either version 1.3c of this license



More information about the tex-live-commits mailing list.