Skip to content

Add pyluatex-pysub package for PythonTeX-like variable substitution #30

@kzhk75

Description

@kzhk75

Hi! Thank you for developing PyLuaTeX. It is an incredibly fast and useful package.

I would like to propose an add-on package, pyluatex-pysub.sty, which introduces a pythontex-like syntax (!{...}) for inline variable substitution.

Some users migrating from pythontex to pyluatex miss the \begin{pysub} environment, which allows for clean and readable string interpolation without explicitly writing \py{...} every time.

I believe this would make PyLuaTeX even more accessible and user-friendly, especially for those creating dynamic educational materials or exams.

Please let me know if you have any suggestions for improvement. I'd be happy to make any changes or integrate it directly into the core code if you prefer!

Key Features

  1. Familiar Syntax: Introduces the pysub environment and \pys{} command. Within these, users can simply write !{var} instead of \py{var}.
  2. Built-in Auto-Escaping: Added a new !e{...} syntax that automatically escapes LaTeX special characters (like _, &, %) via a Python helper function. This prevents compilation errors when dealing with arbitrary strings.
  3. Nested Braces Support: The Lua backend uses %b{} for pattern matching, fully supporting nested braces inside the tag. This allows for complex Python expressions like !{ data["scores"]["math"] } without breaking the parser.

Minimal Working Example

\documentclass{article}
\usepackage{pyluatex-pysub} % please refer to `pyluatex-pysub.sty` below

\begin{pythonq}
import math
username = "user_name & co"

# A dictionary with nested structures
data = {
    "scores": {
        "math": 95, 
        "physics": 88
    }
}
\end{pythonq}

\begin{document}

\begin{pysub}
% 1. Raw evaluation for numbers/math
Pi is approx !{round(math.pi, 2)}.

% 2. Nested braces support (Dictionary/List access)
My math score is !{ data["scores"]["math"] }.

% 3. Auto-escaped strings
Username is !e{username}.
\end{pysub}

\end{document}

pyluatex-pysub.sty

\ProvidesPackage{pyluatex-pysub}[2026/02/13 PyLuaTeX add-on for PythonTeX-like variable substitution]

\RequirePackage{pyluatex}
\RequirePackage{luacode}
\RequirePackage{environ}

% ============================================================
% 1. Python Side: Helper function for LaTeX escaping
% ============================================================
\begin{pythonq}
def latex_escape(s):
    """
    Escapes LaTeX special characters in a string to prevent compilation errors.
    Useful for safely printing arbitrary Python strings in LaTeX.
    """
    if s is None: 
        return ""
    s = str(s)
    # The order of replacement matters
    s = s.replace("\\", r"\textbackslash{}")
    s = s.replace("_", r"\_")
    s = s.replace("&", r"\&")
    s = s.replace("%", r"\%")
    s = s.replace("$", r"\$")
    s = s.replace("#", r"\#")
    s = s.replace("{", r"\{")
    s = s.replace("}", r"\}")
    s = s.replace("^", r"\textasciicircum{}")
    s = s.replace("~", r"\textasciitilde{}")
    return s
\end{pythonq}

% ============================================================
% 2. Lua Side: Substitution engine supporting nested braces
% ============================================================
\begin{luacode*}
function process_pysub(content)
    -- First, process !e{...} : Escaped output (safe for strings with special characters)
    -- Using %b{} ensures that nested braces (e.g., !e{ data["key"] }) are handled correctly.
    local result = string.gsub(content, "!e(%b{})", function(s)
        local inner = string.sub(s, 2, -2) -- Remove the outermost braces
        return "\\py{latex_escape(" .. inner .. ")}"
    end)
    
    -- Next, process !{...} : Raw output (for numbers, SymPy LaTeX output, etc.)
    result = string.gsub(result, "!(%b{})", function(s)
        local inner = string.sub(s, 2, -2)
        return "\\py{" .. inner .. "}"
    end)
    
    tex.print(result)
end
\end{luacode*}

% ============================================================
% 3. LaTeX Side: Environments and Commands
% ============================================================

% The pysub environment allows multi-line substitution
\NewEnviron{pysub}{%
    \directlua{process_pysub(\luastringO{\BODY})}%
}

% The \pys command allows inline substitution
\newcommand{\pys}[1]{%
    \directlua{process_pysub(\luastringO{#1})}%
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions