texlive[75037] Master/texmf-dist: jupynotex (27apr25)

commits+karl at tug.org commits+karl at tug.org
Sun Apr 27 22:19:05 CEST 2025


Revision: 75037
          https://tug.org/svn/texlive?view=revision&revision=75037
Author:   karl
Date:     2025-04-27 22:19:05 +0200 (Sun, 27 Apr 2025)
Log Message:
-----------
jupynotex (27apr25)

Modified Paths:
--------------
    trunk/Master/texmf-dist/doc/latex/jupynotex/README.md
    trunk/Master/texmf-dist/tex/latex/jupynotex/jupynotex.py
    trunk/Master/texmf-dist/tex/latex/jupynotex/jupynotex.sty

Modified: trunk/Master/texmf-dist/doc/latex/jupynotex/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/latex/jupynotex/README.md	2025-04-27 20:18:52 UTC (rev 75036)
+++ trunk/Master/texmf-dist/doc/latex/jupynotex/README.md	2025-04-27 20:19:05 UTC (rev 75037)
@@ -4,7 +4,7 @@
 
 ## Wait, what?
 
-A TeX package that you can use in your project to include Jupyter Notebooks (all of them, or some specific cells) as part of your text. 
+A TeX package that you can use in your project to include Jupyter Notebooks (all of them, or some specific cells) as part of your text.
 
 It will convert the Jupyter Notebook format to proper LaTeX so it gets included seamless, supporting text, latex, images, etc.
 
@@ -38,7 +38,7 @@
 - include cells 1, 3, and 6, 7, and 8 from the range:
 
     `\jupynotex[1,3,6-8]{sample.ipynb}`
-    
+
 - include everything up to the fourth cell, and the eigth:
 
     `\jupynotex[-4,8]{whatever.ipynb}`
@@ -47,7 +47,17 @@
 
     `\jupynotex[3,12-]{somenote.ipynb}`
 
+If the cell number or the range ends with an `i` or an `o` it will only show the input or output, correspondingly. E.g.:
 
+- include cell 1 completely, only the input of cell 2, and cells 3 and 4
+
+    `\jupynotex[1,2i,3-4]{sample.ipynb}`
+
+- include cell 3 completely, and outputs of 5, 6, and 7
+
+    `\jupynotex[3,5-7o]{sample.ipynb}`
+
+
 ## Configurations available
 
 The whole package can be configured when included in your project:
@@ -57,10 +67,16 @@
 Global options available:
 
 - `output-text-limit=N` where N is a number; it will wrap all outputs that exceed that quantity of columns
+- `cells-id-template=TPL`: Where TPL is a template to build the title of each cell using Python's format syntax; available variables are 'number' and 'filename', it defaults to `Cell {number:02d}`
+- `first-cell-id-template=TPL`: Same than `cells-id-template` but only applies to the first cell of each file; it defaults to the value of `cells-id-template`
 
+A note regarding these configurations per project: as they use Python's format syntax, it may get weird with curly braces, which you must use for LaTeX to respect spaces and other characters. E.g., see this config that changes the title of all cells to just the number using three digits surrounded by dots, see how there is the `{}` for latex to delimit the whole value of the config variable, and the `{}` inside for Python formatting:
+
+    \usepackage[cells-id-template={...{number:03d}...}]{jupynotex}
+
 Also, each cell(s) can be configured when included in your .tex files:
 
-    `\jupynotex[3, OPTIONS]{yournotebook.ipynb}`
+    \jupynotex[3, OPTIONS]{yournotebook.ipynb}
 
 Cell options available:
 
@@ -118,7 +134,7 @@
 Please open any issue or ask any question [here](https://github.com/facundobatista/jupynotex/issues/new).
 
 To run the tests (need to have [fades](https://github.com/pyar/fades) installed):
-    
+
     ./tests/run
 
 This material is subject to the Apache 2.0 license.

Modified: trunk/Master/texmf-dist/tex/latex/jupynotex/jupynotex.py
===================================================================
--- trunk/Master/texmf-dist/tex/latex/jupynotex/jupynotex.py	2025-04-27 20:18:52 UTC (rev 75036)
+++ trunk/Master/texmf-dist/tex/latex/jupynotex/jupynotex.py	2025-04-27 20:19:05 UTC (rev 75037)
@@ -1,4 +1,4 @@
-# Copyright 2020-2024 Facundo Batista
+# Copyright 2020-2025 Facundo Batista
 # All Rights Reserved
 # Licensed under Apache 2.0
 
@@ -14,6 +14,7 @@
 import tempfile
 import textwrap
 import traceback
+from dataclasses import dataclass
 
 # message to help people to report potential problems
 REPORT_MSG = """
@@ -34,11 +35,24 @@
 }
 
 # the different formats to be used when error or all ok
-FORMAT_ERROR = r"colback=red!5!white,colframe=red!75!"
-FORMAT_OK = (
-    r"coltitle=red!75!black, colbacktitle=black!10!white, "
-    r"halign title=right, fonttitle=\sffamily\mdseries\scshape\footnotesize")
+_style_formats = [
+    # text background color
+    "colback=black!2",
+    # frame color and width
+    "colframe=black!60",
+    "boxrule=0.3mm",
+    # to control how rounded corners are
+    "arc=0.3mm",
+    # title color(foreground and background), alignment and font size
+    "coltitle=red!75!black",
+    "colbacktitle=black!8!white",
+    "halign title=right",
+    r"fonttitle=\sffamily\mdseries\scshape\footnotesize",
+]
+FORMAT_OK = ",".join(_style_formats)
+FORMAT_ERROR = "colback=red!5!white,colframe=red!75!"
 
+
 # a little mark to put in the continuation line(s) when text is wrapped
 WRAP_MARK = "↳"
 
@@ -45,9 +59,49 @@
 # the options available for command line
 CMDLINE_OPTION_NAMES = {
     "output-text-limit": "The column limit for the output text of a cell",
+    "cells-id-template": (
+        "A template to build the title of each cell using Python's format syntax; "
+        "available variables 'number' and 'filename', defaults to 'Cell {number:02d}'"
+    ),
+    "first-cell-id-template": (
+        "Same than cells-id-template but only applies to the first cell of each file; "
+        "defaults to the value of cells-id-template"
+    ),
 }
 
 
+ at dataclass(order=True, eq=True, frozen=True)
+class CellSelection:
+    """The selection of a cell.
+
+    Its number (`index`) and an indication if what part is used: (A)ll, only the (I)nput, or only
+    the (O)utput.
+    """
+    index: int
+    partial: str = "a"
+
+
+LATEX_ESCAPE = [
+    ("\\", r"\textbackslash"),  # needs to go first, otherwise transforms other escapings
+    ("&", r"\&"),
+    ("%", r"\%"),
+    ("$", r"\$"),
+    ("#", r"\#"),
+    ("_", r"\_"),
+    ("{", r"\{"),
+    ("}", r"\}"),
+    ("~", r"\textasciitilde"),
+    ("^", r"\textasciicircum"),
+]
+
+
+def latex_escape(text):
+    """Escape some chars in latex."""
+    for src, dst in LATEX_ESCAPE:
+        text = text.replace(src, dst)
+    return text
+
+
 def _validator_positive_int(value):
     """Validate value is a positive integer."""
     value = value.strip()
@@ -130,7 +184,13 @@
         with open(svg_fname, 'wb') as fh:
             fh.write(raw_svg)
 
-        cmd = ['inkscape', '--export-text-to-path', '--export-pdf={}'.format(pdf_fname), svg_fname]
+        cmd = [
+            'inkscape',
+            '--export-text-to-path',
+            '--export-type=pdf',
+            f'--export-filename={pdf_fname}',
+            svg_fname,
+        ]
         subprocess.run(cmd)
 
         return pdf_fname
@@ -158,7 +218,7 @@
 class Notebook:
     """The notebook converter to latex."""
 
-    GLOBAL_CONFIGS = {
+    _configs_validator = {
         "output-text-limit": _validator_positive_int,
     }
 
@@ -177,9 +237,10 @@
     def _validate_config(self, config):
         """Validate received configuration."""
         for key, value in list(config.items()):
-            validator = self.GLOBAL_CONFIGS[key]
-            new_value = validator(value)
-            config[key] = new_value
+            validator = self._configs_validator.get(key)
+            if validator is not None:
+                new_value = validator(value)
+                config[key] = new_value
         return config
 
     def _proc_src(self, content):
@@ -186,6 +247,11 @@
         """Process the source of a cell."""
         source = content['source']
         result = []
+
+        # Ensure `source` is a list of strings
+        if isinstance(source, str):
+            source = [source]  # Convert single string to a list
+
         if content['cell_type'] == 'code':
             begin, end = self._highlight_delimiters
             result.extend(begin)
@@ -252,11 +318,18 @@
         groups = [x.strip() for x in spec.split(',')]
         valid_chars = set('0123456789-,')
         for group in groups:
+            # check if it's a config option
             if '=' in group:
                 k, v = group.split("=", maxsplit=1)
                 options[k] = v
                 continue
 
+            # if there is a partial indication, save it and remove it from the rest of processing
+            partial = "a"
+            if group[-1] in "io":
+                partial = group[-1]
+                group = group[:-1]
+
             if set(group) - valid_chars:
                 raise ValueError(
                     "Found forbidden characters in cells definition (allowed digits, '-' and ',')")
@@ -268,16 +341,17 @@
                 if cfrom >= cto:
                     raise ValueError(
                         "Range 'from' need to be smaller than 'to' (got {!r})".format(group))
-                cells.update(range(cfrom, cto + 1))
+                cells.update(CellSelection(x, partial=partial) for x in range(cfrom, cto + 1))
             else:
-                cells.add(int(group))
+                cells.add(CellSelection(int(group), partial=partial))
         cells = sorted(cells)
 
-        if any(x < 1 for x in cells):
+        if any(x.index < 1 for x in cells):
             raise ValueError("Cells need to be >=1")
-        if maxlen < cells[-1]:
-            raise ValueError(
-                f"Notebook loaded of len {maxlen}, smaller than requested cells: {cells}")
+        if len(cells) != len(set(cell.index for cell in cells)):
+            raise ValueError("Mixed different parts indication for the same cell.")
+        if maxlen < cells[-1].index:
+            raise ValueError(f"Notebook loaded of len {maxlen}, smaller than requested cells")
 
         self.cell_options = options
         return cells
@@ -288,12 +362,18 @@
     nb = Notebook(notebook_path, config_options)
     cells = nb.parse_cells(cells_spec)
 
+    # get templates from config
+    cells_id_template = config_options.get("cells-id-template", "Cell {number:02d}")
+    first_cell_id_template = config_options.get("first-cell-id-template", cells_id_template)
+
+    escaped_path_name = latex_escape(notebook_path.name)
+    tcolorbox_begin_template = r"\begin{{tcolorbox}}[{}, breakable, title={}]"
     for cell in cells:
         try:
-            src, out = nb.get(cell)
+            src, out = nb.get(cell.index)
         except Exception as exc:
-            title = "ERROR when parsing cell {}".format(cell)
-            print(r"\begin{{tcolorbox}}[{}, title={{{}}}]".format(FORMAT_ERROR, title))
+            title = "ERROR when parsing cell {}".format(cell.index)
+            print(tcolorbox_begin_template.format(FORMAT_ERROR, title))
             print(exc)
             _parts = _process_plain_text(REPORT_MSG.split('\n'))
             print('\n'.join(_parts))
@@ -304,12 +384,23 @@
             print(tb, file=sys.stderr)
             continue
 
-        print(r"\begin{{tcolorbox}}[{}, title=Cell {{{:02d}}}]".format(FORMAT_OK, cell))
-        print(src)
-        if out:
-            print(r"\tcblower")
+        template = first_cell_id_template if cell.index == 1 else cells_id_template
+        title = template.format(number=cell.index, filename=escaped_path_name)
+        print(tcolorbox_begin_template.format(FORMAT_OK, title))
+
+        if cell.partial == "i":
+            print(src)
+        elif cell.partial == "o" and out:
             print(out)
+        else:
+            # more usual case, both input and outputs (separated by a line)
+            print(src)
+            if out:
+                print(r"\tcblower")
+                print(out)
+
         print(r"\end{tcolorbox}")
+        print()  # extra new line so boxes are separated in the LaTeX PoV
 
 
 if __name__ == "__main__":
@@ -327,5 +418,10 @@
         parser.add_argument(option, type=str, help=explanation)
     args = parser.parse_args()
 
-    config_options = {option: getattr(args, option) for option in CMDLINE_OPTION_NAMES}
+    # get config options from command line (ignoring '', which is the default in .sty file)
+    config_options = {}
+    for option in CMDLINE_OPTION_NAMES:
+        value = getattr(args, option)
+        if value:
+            config_options[option] = value
     main(args.notebook_path, args.cells_spec, config_options)

Modified: trunk/Master/texmf-dist/tex/latex/jupynotex/jupynotex.sty
===================================================================
--- trunk/Master/texmf-dist/tex/latex/jupynotex/jupynotex.sty	2025-04-27 20:18:52 UTC (rev 75036)
+++ trunk/Master/texmf-dist/tex/latex/jupynotex/jupynotex.sty	2025-04-27 20:19:05 UTC (rev 75037)
@@ -1,9 +1,11 @@
-\ProvidesPackage{jupynotex}[1.0]
+\ProvidesPackage{jupynotex}[1.1]
 
-\usepackage{tcolorbox}
+\usepackage[breakable]{tcolorbox}
 \usepackage{pgfopts}
 
 \newcommand*\jupynotex at outputtextlimit@value{}
+\newcommand*\jupynotex at cellsidtemplate@value{}
+\newcommand*\jupynotex at firstcellidtemplate@value{}
 
 
 \pgfkeys{
@@ -10,11 +12,19 @@
   /jupynotex/.cd ,
     output-text-limit/.store in=\jupynotex at outputtextlimit@value
 }
+\pgfkeys{
+  /jupynotex/.cd ,
+    cells-id-template/.store in=\jupynotex at cellsidtemplate@value
+}
+\pgfkeys{
+  /jupynotex/.cd ,
+    first-cell-id-template/.store in=\jupynotex at firstcellidtemplate@value
+}
 
 \ProcessPgfPackageOptions{/jupynotex}
 
 \newcommand{\jupynotex}[2][-]{
-    \input|"python3 jupynotex.py '#2' '#1' '\jupynotex at outputtextlimit@value'"
+    \input|"python3 jupynotex.py '#2' '#1' '\jupynotex at outputtextlimit@value' '\jupynotex at cellsidtemplate@value' '\jupynotex at firstcellidtemplate@value'"
 }
 
 \endinput



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