What’s new in pylatexenc 3¶
Wow, a lot of stuff has changed in the latexwalker and macrospec modules. There’s even a new latexnodes module. I don’t know where to start!
The good news is, if you’re simply using the latex-to-unicode and
unicode-to-latex conversion tools, your code depending on pylatexenc 2 should
run without any changes. The text that comes out might look a little
different here and there, though — see below. You might get some deprecation
warnings which you can silence using python’s warnings filter management (e.g.,
python -W 'ignore::DeprecationWarning' or using
warnings.simplefilter()). We did our best to make the API as
backwards-compatible as reasonably possible, so there’s a great chance your code
continues to run as-is.
Compatibility with Python 2 was dropped. We’ll do our best to remain compatible with Python ≥ 3.6 moving forward.
Overall, the differences between pylatexenc v2 and v3 are mostly associated with
the parser-related modules latexwalker,
macrospec, and the new module
latexnodes. The latex2text module
gained a few helpers, support for unicode text and math font formatting, and a
better math mode formatter. (The latexencode module
underwent only minor changes.)
The parser-related modules have underwent a restructuring for a better architecture with extended and more flexible functionality:
New parsing mechanism in a new latexnodes module — everything gets delegated to “parser objects” that are specialized in parsing a specific construct. See
pylatexenc.latexnodes.parsers.LatexParserBase. The reusable parsers live in pylatexenc.latexnodes.parsers; you can write your own by deriving from LatexParserBase.We’ve introduced a new entry point interface for parsing:
LatexWalker.parse_content(), with a suitable parser object frompylatexenc.latexnodes.parsers(such asLatexGeneralNodesParser). The earlier interface exposed by pylatexenc 2, usingLatexWalker— get_latex_nodes(), get_token(), get_latex_expression(), get_latex_braced_group(), get_latex_environment(), and get_latex_maybe_optional_arg() keeps working as it did.Lists of latex node objects (
LatexNode) are now wrapped in a special object for node lists →pylatexenc.latexnodes.nodes.LatexNodeList.The parser has new enhanced handling of macro, environment, and specials arguments. Arguments can be named for easier lookup when traversing the node tree. Also, macro, environment and specials arguments are now stored in a
pylatexenc.latexnodes.ParsedArgumentsobject. (The name macrospec.ParsedMacroArgs still refers to that same class, so existing isinstance() tests keep working.) Usepylatexenc.latexnodes.ParsedArgumentsInfoto look up arguments comfortably, including by name.New standard argument parsers for custom type arguments: You can now declare macros, environments, and specials that take arguments with extended syntax such as
\mymacro<One>{Two}[Optional three]. The argument specification is inspired by LaTeX’ xparse package specification. For a full list of argument types, seeMacroSpecandLatexStandardArgumentParser.Parsing state changes are described by explicit parsing state delta objects (
ParsingStateDelta, so a construct can now legitimately alter how the rest of the document is parsed — including extending the macro database mid-document.Errors got their own little hierarchy. LatexWalkerParseError now derives from a new
LatexWalkerLocatedError, and the walker actually raises the more specificLatexWalkerNodesParseErrorandLatexWalkerTokenParseError. Catching LatexWalkerParseError still works; matching on the error message doesn’t, because the messages and the “open blocks” trace were rewritten.LatexContextDbobjects have a lifecycle now: you build them with add_context_category(), then freeze() them, and then you can derive new ones with filtered_context() (which replaces filter_context()) and extended_with(). Note extended_with() insists that the object be frozen first.Two new tools you might enjoy:
LatexNodesLatexRecomposerturns a node tree back into latex code, andLatexNodesVisitor(along withnode.accept_node_visitor()) gives you a visitor pattern for walking a tree.The len attribute in node objects is replaced by a pos_end attribute. The len attribute can still be accessed as a read-only computed attribute for compatibility with existing code using pylatexenc 2.
Node objects carry more information than they used to (the parsing state, the latex walker, the spec object, …), so
repr(node)is now much longer than in pylatexenc 2, and the JSON export produced bylatexwalker --output-format=jsonchanged shape accordingly. If you were comparing reprs in doctests or scraping them from logs, you’ll want to look at the node fields directly instead.
The latex2text module was improved:
latex2textnow keeps track of the surroundings in which a node is being converted, in apylatexenc.latex2text.TextConversionStateobject available asLatexNodes2Text.state.latex2textrenders list environments ({itemize},{enumerate},{description}) properly, including nested lists: items are numbered where relevant, the item marker style follows the nesting depth as it does in LaTeX, and the contents of an item that spans several lines is aligned with the item text. Note the default bullet for\itemis now•and no longer*.Support for unicode alphabets for bold and italic text.
A new default renderer for math content,
math_mode='fancy', now formats math content in a way that is much more visually appealing, so that$\frac{4 \pi c}{2}\sin(x+y)$gives(4π𝑐)/2 sin(𝑥 + 𝑦)instead of4 π c/2sin(x+y). We also support unicode fonts for text and math. (Usemath_mode='text', math_fontstyle=None, text_fontstyle=Falseto restore pylatexenc v2’s behavior; seeLatexNodes2Text).Subscripts and superscripts are now rendered with unicode characters whenever there are suitable ones:
$x^2$givesx²,$H_2O$givesH₂O, and$\sum_{i=1}^n x_i$gives∑ᵢ₌₁ⁿ xᵢ. Same for\sqrt, which gives√and∛.
The latexencode module has barely changed.
A couple things to look out for¶
LatexNodes2Textrenders formulas with the newmath_mode='fancy'engine by default, which improves math content spacing and uses cleverer unicode constructs such as for sub/superscripts and fractions. The renderer now also uses unicode mathematical font characters where pylatexenc 2 produced plain ASCII letters. That is,$x+y$gives𝑥 + 𝑦, and\textbf{bold}gives𝐛𝐨𝐥𝐝. If you are indexing the output, comparing it against stored strings, or feeding it to a program that expects ASCII, switch the unicode alphabets off withmath_fontstyle=False, text_fontstyle=False.To restore pylatexenc 2’s behavior, set
math_fontstyle=None, text_fontstyle=False, math_mode='text'. For example:from pylatexenc.latex2text import LatexNodes2Text latex = r'$\gamma x+y + \mathcal{Z}$ and \textbf{bold}' # pylatexenc 3 default LatexNodes2Text().latex_to_text(latex) # -> 'γ𝑥 + 𝑦 + 𝒵 and 𝐛𝐨𝐥𝐝' # the new math renderer, with all unicode alphabets dropped: LatexNodes2Text(math_fontstyle=False, text_fontstyle=False).latex_to_text(latex) # -> 'γx + y + Z and bold' # Restore pylatexenc 2 behavior: legacy rendering engine; upright # default math font, but \mathcal{}, \mathbb{} still produce # the relevant unicode fonts LatexNodes2Text(math_mode='text', math_fontstyle=None, text_fontstyle=False).latex_to_text(latex) # -> 'γ x+y + 𝒵 and bold'
The command-line tool takes
--text-fontstyle=off --math-fontstyle=none --math-mode=textfor the same purposes.Paragraph breaks (
\n\n) as well as^and_are now reported as their own specials nodes. This means that text which used to come back as a singleLatexCharsNodeis now split into several nodes:'para one\n\npara two' pylatexenc 2: chars('para one\n\npara two') pylatexenc 3: chars('para one') + specials('\n\n') + chars('para two')
If you were looking for
"\n\n"insidenode.charsto split paragraphs, or simply concatenating thecharsof every chars node in a list, you’ll need to handle those specials nodes as well.To restore pylatexenc v2’s behavior (nn kept as chars, _/^ not specials), exclude the corresponding categories from the default latex context DB:
from pylatexenc import latexwalker, latex2text # restore pylatexenc v2 behavior: \n\n, _, ^ as chars and not specials latex_context = latexwalker.get_default_latex_context_db().filtered_context( exclude_categories=["latex-paragraph", "latex-base-subsuperscripts"] ) # Use in latex2text directly -- kwargs of latex_to_text are directly # handed over as LatexWalker constructor arguments text = latex2text.LatexNodes2Text().latex_to_text( …, latex_context=latex_context ) # Alternatively, if you want to create a walker object yourself: walker = latexwalker.LatexWalker(…, latex_context=latex_context) nodelist, _ = walker.parse_content() # returns (nodes, parsing_state_delta) text = latex2text.LatexNodes2Text().nodelist_to_text( nodelist )The
latex2textmodule keeps a database of its own, in which the text replacements for^and_sit in a category of the same name. One could disable those definitions there as well with a correspondinglatex2text.get_default_latex_context_db().filtered_context(…)call; however, it is not necessary to do so because a walker that no longer reports^and_as specials hands them to latex2text inside ordinary chars nodes, so their text replacements are never looked up.If you created a
LatexContextDbdatabase from scratch, you might suddenly get errors about unknown macros. The default initialization for unknown macro, environment and specials specification objects forLatexContextDbwas, and remains, None. What has changed is the interpretation of this None: Now, the latex walker (more precisely,LatexNodesCollector) reports an error, whereas previously, the parser would simply assume the macro doesn’t accept any arguments. To restore the earlier behavior, simply set the spec objects for unknown macro/environment/specials in your latex context db object:latex_context_db = macrospec.LatexContextDb() # ... latex_context_db.add_context_category( ... ) # ... latex_context_db.set_unknown_macro_spec(macrospec.MacroSpec('')) latex_context_db.set_unknown_environment_spec(macrospec.EnvironmentSpec('')) # # unknown macros and environments are now accepted and are assumed # not to take any arguments #
Node lists are now encapsulated in a
LatexNodeList. It behaves very much like a list in all respects (indexing, slicing, etc.), except that it does not satisfyisinstance(nodelist, list). If you relied on such tests, you’ll need to update them to the liking ofisinstance(nodelist, (LatexNodeList, list)).Verbatim arguments to verb are now reported with a
LatexGroupNodethat contains a chars node, rather than aLatexCharsNodedirectly. Note also thatisinstance(node.nodeargd, ParsedVerbatimArgs)is no longer true for\verb; the verbatim_text attribute still works as before.Building macro specifications with the pylatexenc 2 args_parser= argument still works, but it is deprecated and now warns.
Some bug fixes in behavior¶
The
verbatimandlstlistingenvironments no longer report the newline that immediately follows\begin{...}as part of their contents, mirroring what LaTeX itself does.An unterminated
lstlistingenvironment no longer loses its parsed arguments in tolerant parsing mode;nodeargdused to be None there.Fixes in latex2text, including
\href{...}{...},\url{...},\paragraph{...},{verbatim}and{lstlisting}.Numbered and hierarchical
{enumerate}items are numbered.More extensive support for math operators and symbols in latex2text.
Several bug fixes, especially in tolerant parsing mode.
You might notice other fixes that we forgot to include here.
Details that v3 might get different¶
Pylatexenc v3’s behavior might get some minor things a bit differently. We don’t expect these to lead to downstream issues, but we’re listing the ones we’re aware of here for completeness:
The
latexencodemodule no longer uses the dotless\icommand when encoding the accented characters ‘ì’, ‘í’, ‘î’ and ‘ï’ (U+00EC–U+00EF), producing\'iinstead of{\'\i}. This follows a long-standing change in LaTeX where\iis no longer needed for such accented characters. (If you prefer to keep braces around your replacement, remember to use a “latex replacement protection” option, seepylatexenc.latexencode.UnicodeToLatexEncoder.replacement_latex_protection.)Parse error messages were rewritten, and the reported column can be off by one compared with pylatexenc 2. The exception classes are still the ones you know (see above), so code that catches them is fine; code that matches on the message text isn’t.
Recomposing a node tree back to latex code with
LatexNodesLatexRecomposernormalizes the white space around a paragraph break to exactly\n\n, because that’s what the specials node records. E.g.'a\n\n\n\nb'comes back as'a\n\nb'.You might notice other differences that we forgot to include here.