New Upstream Release - sphinx-a4doc

Ready changes

Summary

Merged new upstream version: 1.6.0 (was: 1.3.0).

Resulting package

Built on 2023-08-10T19:28 (took 5m53s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-sphinx-a4doc

Lintian Result

Diff

diff --git a/.github/workflows/build-prod.yml b/.github/workflows/build-prod.yml
index b8e60b5..e1dfde8 100644
--- a/.github/workflows/build-prod.yml
+++ b/.github/workflows/build-prod.yml
@@ -48,6 +48,11 @@ jobs:
         asset_path: dist/sphinx-a4doc-${{ steps.get_version.outputs.VERSION }}.tar.gz
         asset_name: sphinx-a4doc-${{ steps.get_version.outputs.VERSION }}.tar.gz
         asset_content_type: application/tar+gzip
+    - name: Publish distribution to Test PyPI
+      uses: pypa/gh-action-pypi-publish@master
+      with:
+        password: ${{ secrets.TEST_PYPI_PASSWORD }}
+        repository_url: https://test.pypi.org/legacy/
     - name: Publish distribution to PyPI
       uses: pypa/gh-action-pypi-publish@master
       with:
diff --git a/README.md b/README.md
index ff984b4..65a4397 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,20 @@ pip3 install sphinx-a4doc
 
 ## Changelog
 
-*v1.2.5*
+*v1.6.0*
+
+- Support LaTeX builder.
+
+*v1.5.0*
+
+- Fixed position of text in diagram nodes in Firefox.
+- Added an option to set custom classes to diagram nodes: `//@ doc:css-class`.
+
+*v1.4.0*
+
+- Fixed compatibility with `singlehtml` mode (see [#15](https://github.com/taminomara/sphinx-a4doc/issues/15)).
+
+*v1.3.0*
 
 - Fixed python 3.9 compatibility issue (by [@sandrotosi](https://github.com/sandrotosi)).
 
diff --git a/debian/changelog b/debian/changelog
index 6b9e010..6d10e52 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+sphinx-a4doc (1.6.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 10 Aug 2023 19:22:39 -0000
+
 sphinx-a4doc (1.3.0-1) unstable; urgency=medium
 
   * New upstream release
diff --git a/debian/patches/dont-use-setuptools-scm.patch b/debian/patches/dont-use-setuptools-scm.patch
index 19ef578..026f493 100644
--- a/debian/patches/dont-use-setuptools-scm.patch
+++ b/debian/patches/dont-use-setuptools-scm.patch
@@ -1,5 +1,7 @@
---- a/setup.py
-+++ b/setup.py
+Index: sphinx-a4doc.git/setup.py
+===================================================================
+--- sphinx-a4doc.git.orig/setup.py
++++ sphinx-a4doc.git/setup.py
 @@ -6,7 +6,4 @@ setup(
          'Source': 'https://github.com/taminomara/sphinx-a4doc',
          'Tracker': 'https://github.com/taminomara/sphinx-a4doc/issues',
@@ -8,11 +10,13 @@
 -        "local_scheme": "no-local-version"
 -    }
  )
---- a/setup.cfg
-+++ b/setup.cfg
-@@ -30,10 +30,7 @@ install_requires =
-     antlr4-python3-runtime==4.7.1
+Index: sphinx-a4doc.git/setup.cfg
+===================================================================
+--- sphinx-a4doc.git.orig/setup.cfg
++++ sphinx-a4doc.git/setup.cfg
+@@ -31,10 +31,7 @@ install_requires =
      PyYAML
+     svglib
  setup_requires =
 -    setuptools-scm>=3.5.0
      setuptools>=42.0
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 863a4f9..45044ed 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -345,6 +345,11 @@ The list of control comments includes:
 - ``//@ doc:name <str>`` -- set a human-readable name for this rule.
   See :rst:opt:`a4:rule:name` option.
 
+- ``//@ doc:css-class`` -- add a custom CSS class to all diagrams
+  referencing this rule.
+
+  .. versionadded:: 1.5.0
+
 
 .. _config:
 
@@ -360,6 +365,11 @@ To customize diagram style, one can replace
 `the default css file <https://github.com/taminomara/sphinx-a4doc/blob/master/sphinx_a4doc/_static/a4_railroad_diagram.css>`_
 by placing a ``a4_railroad_diagram.css`` file to the ``_static`` directory.
 
+.. versionadded:: 1.6.0
+
+   to customise how diagrams look in latex build,
+   place a ``a4_railroad_diagram_latex.css`` file to the ``_static`` directory.
+
 .. . .. _custom_lookup:
 
 .. . Customizing process of grammar files lookup
diff --git a/push_docs.py b/push_docs.py
index 11f9793..a349777 100755
--- a/push_docs.py
+++ b/push_docs.py
@@ -44,7 +44,7 @@ def push_html():
             os.system('git init')
             os.system('git add .')
             os.system('git commit -m "update docs"')
-            os.system('git push -f ' + REPO + ' master:gh-pages')
+            os.system('git push -f ' + REPO + ' main:gh-pages')
 
 
 if __name__ == '__main__':
diff --git a/setup.cfg b/setup.cfg
index da68ede..9d3ca7b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -29,6 +29,7 @@ install_requires =
     sphinx>=1.8.0
     antlr4-python3-runtime==4.7.1
     PyYAML
+    svglib
 setup_requires =
     setuptools-scm>=3.5.0
     setuptools>=42.0
diff --git a/sphinx_a4doc/__init__.py b/sphinx_a4doc/__init__.py
index de0d58a..7138a06 100644
--- a/sphinx_a4doc/__init__.py
+++ b/sphinx_a4doc/__init__.py
@@ -10,7 +10,7 @@ from sphinx_a4doc.autodoc_directive import AutoGrammar, AutoRule
 
 def config_inited(app, config):
     static_path = os.path.join(os.path.dirname(__file__), '_static')
-    config.html_static_path.insert(0, static_path)
+    config.html_static_path.append(static_path)
 
 
 def setup(app: sphinx.application.Sphinx):
@@ -20,9 +20,13 @@ def setup(app: sphinx.application.Sphinx):
 
     app.add_node(RailroadDiagramNode,
                  text=(RailroadDiagramNode.visit_node_text,
-                       None),
+                       RailroadDiagramNode.depart_node),
                  html=(RailroadDiagramNode.visit_node_html,
-                       RailroadDiagramNode.depart_node))
+                       RailroadDiagramNode.depart_node),
+                 latex=(RailroadDiagramNode.visit_node_latex,
+                        RailroadDiagramNode.depart_node),
+                 man=(RailroadDiagramNode.visit_node_man,
+                      RailroadDiagramNode.depart_node))
 
     app.add_directive('railroad-diagram', RailroadDiagram)
     app.add_directive('lexer-rule-diagram', LexerRuleDiagram)
diff --git a/sphinx_a4doc/_static/a4_railroad_diagram.css b/sphinx_a4doc/_static/a4_railroad_diagram_latex.css
similarity index 90%
rename from sphinx_a4doc/_static/a4_railroad_diagram.css
rename to sphinx_a4doc/_static/a4_railroad_diagram_latex.css
index c785176..4ad1701 100644
--- a/sphinx_a4doc/_static/a4_railroad_diagram.css
+++ b/sphinx_a4doc/_static/a4_railroad_diagram_latex.css
@@ -8,8 +8,7 @@
     font-size: 14px;
     font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
     text-anchor: middle;
-    alignment-baseline: central;
-    font-weight: bold;
+    dy: 4;
 }
 
 .railroad-diagram a {
diff --git a/sphinx_a4doc/autodoc_directive.py b/sphinx_a4doc/autodoc_directive.py
index c70ef06..8c34036 100644
--- a/sphinx_a4doc/autodoc_directive.py
+++ b/sphinx_a4doc/autodoc_directive.py
@@ -352,7 +352,9 @@ class AutoGrammar(Grammar, ModelLoaderMixin, DocsRendererMixin):
                 ):
                     settings = dataclasses.replace(settings, end_class=EndClass.COMPLEX)
                 desc_content.append(
-                    RailroadDiagramNode(dia, settings, grammar)
+                    RailroadDiagramNode(
+                        '', diagram=dia, options=settings, grammar=grammar
+                    )
                 )
 
             self.render_docs(rule.position.file, docs, desc_content)
@@ -472,7 +474,9 @@ class AutoRule(Rule, ModelLoaderMixin, DocsRendererMixin):
                     settings = self.diagram_settings
 
                     doc_node.append(
-                        RailroadDiagramNode(dia, settings, grammar)
+                        RailroadDiagramNode(
+                            '', diagram=dia, options=settings, grammar=grammar
+                        )
                     )
 
                 self.render_docs(rule.position.file, docs, doc_node)
diff --git a/sphinx_a4doc/contrib/railroad_diagrams.py b/sphinx_a4doc/contrib/railroad_diagrams.py
index 2442188..0cc6082 100644
--- a/sphinx_a4doc/contrib/railroad_diagrams.py
+++ b/sphinx_a4doc/contrib/railroad_diagrams.py
@@ -67,7 +67,7 @@ def ensure_type(name, x, *types):
     if not isinstance(x, types):
         types_str = ', '.join([t.__name__ for t in types])
         raise ValueError(f'{name} should be {types_str}, '
-                         f'got {type(x)} instead')
+                         f'got {type(x)} ({x!r}) instead')
 
 
 def ensure_empty_dict(name, x):
@@ -160,11 +160,11 @@ class Diagram:
     def node(self, text: str, href: Optional[str] = None, css_class: str = '', radius: int = 0, padding: int = 20, resolve: bool = False, title_is_weak: bool = False) -> 'DiagramItem':
         return Node(self, text, href, css_class, radius, padding, resolve, title_is_weak)
 
-    def terminal(self, text: str, href: Optional[str] = None, resolve: bool = True, title_is_weak: bool = False):
-        return self.node(text, href, 'node terminal', 10, 20, resolve, title_is_weak)
+    def terminal(self, text: str, href: Optional[str] = None, css_class: str = '', resolve: bool = True, title_is_weak: bool = False):
+        return self.node(text, href, 'node terminal ' + css_class, 10, 20, resolve, title_is_weak)
 
-    def non_terminal(self, text: str, href: Optional[str] = None, resolve: bool = True, title_is_weak: bool = False):
-        return self.node(text, href, 'node non-terminal', 0, 20, resolve, title_is_weak)
+    def non_terminal(self, text: str, href: Optional[str] = None, css_class: str = '', resolve: bool = True, title_is_weak: bool = False):
+        return self.node(text, href, 'node non-terminal ' + css_class, 0, 20, resolve, title_is_weak)
 
     def comment(self, text: str, href: Optional[str] = None):
         return self.node(text, href, 'node comment', 0, 5)
@@ -300,6 +300,7 @@ class Diagram:
             a, kw, self.terminal, (str,), lambda s: ([s], {}),
             {
                 'href':          ((str,                  ), None           ),
+                'css_class':     ((str,                  ), None           ),
                 'resolve':       ((bool,                 ), None           ),
                 'title_is_weak': ((bool,                 ), None           ),
             }
@@ -310,6 +311,7 @@ class Diagram:
             a, kw, self.non_terminal, (str,), lambda s: ([s], {}),
             {
                 'href':          ((str,                  ), None           ),
+                'css_class':     ((str,                  ), None           ),
                 'resolve':       ((bool,                 ), None           ),
                 'title_is_weak': ((bool,                 ), None           ),
             }
@@ -396,12 +398,12 @@ class Diagram:
         return [self.load(x)], {}
 
     @overload
-    def render(self, root: 'DiagramItem', output: None = None) -> str: ...
+    def render(self, root: 'DiagramItem', output: None = None, style=None) -> str: ...
 
     @overload
-    def render(self, root: 'DiagramItem', output: TextIO) -> None: ...
+    def render(self, root: 'DiagramItem', output: TextIO, style=None) -> None: ...
 
-    def render(self, root, output=None):
+    def render(self, root, output=None, style=None):
         root = self.sequence(
             self.start(),
             root,
@@ -423,6 +425,11 @@ class Diagram:
         svg.attrs['class'] = 'railroad-diagram'
         svg = svg.format()
 
+        if style:
+            style_r = self.element('style').format()
+            style_r.children.append(style)
+            style_r.add_to(svg)
+
         g = self.element('g')
         if self.settings.translate_half_pixel:
             g.attrs['transform'] = 'translate(.5 .5)'
diff --git a/sphinx_a4doc/diagram_directive.py b/sphinx_a4doc/diagram_directive.py
index 4e12761..08262e1 100644
--- a/sphinx_a4doc/diagram_directive.py
+++ b/sphinx_a4doc/diagram_directive.py
@@ -1,3 +1,6 @@
+import json
+import os.path
+
 import docutils.parsers.rst
 import docutils.nodes
 import docutils.utils
@@ -5,6 +8,9 @@ import sphinx.addnodes
 import sphinx.util.docutils
 import sphinx.writers.html
 import sphinx.writers.text
+import sphinx.writers.latex
+import sphinx.writers.manpage
+import sphinx.util.docutils
 import sphinx.util.logging
 import sphinx.environment
 
@@ -54,7 +60,10 @@ class DomainResolver(HrefResolver):
         builder = self.builder
         env = builder.env
         domain = env.get_domain('a4')
-        docname = builder.current_docname
+        if hasattr(builder, 'current_docname'):
+            docname = builder.current_docname
+        else:
+            docname = None
 
         xref = sphinx.addnodes.pending_xref(
             '',
@@ -92,19 +101,46 @@ class DomainResolver(HrefResolver):
 
 
 class RailroadDiagramNode(docutils.nodes.Element, docutils.nodes.General):
-    def __init__(self, diagram: dict, options: DiagramSettings, grammar: str):
-        super().__init__('', diagram=diagram, options=options, grammar=grammar)
+    def __init__(
+        self,
+        rawsource='',
+        *args,
+        diagram: dict,
+        options: DiagramSettings,
+        grammar: str,
+        **kwargs
+    ):
+        super().__init__(
+            rawsource,
+            *args,
+            diagram=diagram,
+            options=options,
+            grammar=grammar,
+            **kwargs
+        )
 
     @staticmethod
-    def visit_node_html(self: sphinx.writers.html.HTMLTranslator, node):
+    def node_to_svg(self: sphinx.util.docutils.SphinxTranslator, node, add_style=False):
         resolver = DomainResolver(self.builder, node['grammar'])
         dia = Diagram(settings=node['options'], href_resolver=resolver)
+        style = None
+        if add_style:
+            for basedir in self.config.html_static_path:
+                path = os.path.join(self.builder.confdir, basedir, 'a4_railroad_diagram_latex.css')
+                if os.path.exists(path):
+                    with open(path, 'r') as f:
+                        style = f.read()
+                    break
         try:
             data = dia.load(node['diagram'])
-            svg = dia.render(data)
+            return dia.render(data, style=style)
         except Exception as e:
             logger.exception(f'{node.source}:{node.line}: WARNING: {e}')
-        else:
+
+    @staticmethod
+    def visit_node_html(self: sphinx.writers.html.HTMLTranslator, node):
+        svg = RailroadDiagramNode.node_to_svg(self, node)
+        if svg:
             self.body.append('<p class="railroad-diagram-container">')
             self.body.append(svg)
             self.body.append('</p>')
@@ -115,9 +151,42 @@ class RailroadDiagramNode(docutils.nodes.Element, docutils.nodes.General):
             self.add_text('{}'.format(node['options'].alt))
         else:
             self.add_text(yaml.dump(node['diagram']))
-        raise docutils.nodes.SkipNode
 
     @staticmethod
+    def visit_node_latex(self: sphinx.writers.latex.LaTeXTranslator, node):
+        from svglib.svglib import svg2rlg
+        from reportlab.graphics import renderPDF
+        import io
+        import hashlib
+
+        outdir = os.path.join(self.builder.outdir, 'railroad_diagrams')
+        os.makedirs(outdir, exist_ok=True)
+
+        hash = hashlib.sha256()
+        hash.update(
+            yaml.safe_dump(node['diagram'], sort_keys=True, canonical=True).encode())
+        hash.update(
+            repr(node['options']).encode())
+        pdf_file = f'diagram:{node["grammar"]}:{hash.hexdigest()}.pdf'
+        pdf_file = os.path.join(outdir, pdf_file)
+
+        svg = RailroadDiagramNode.node_to_svg(self, node, add_style=True)
+        svg_file = io.StringIO(svg)
+        rlg = svg2rlg(svg_file)
+
+        renderPDF.drawToFile(rlg, pdf_file)
+
+        self.body.append(
+            f'\n\n\\includegraphics[scale=0.6]{{{pdf_file}}}\n\n'
+        )
+
+    @staticmethod
+    def visit_node_man(self: sphinx.writers.manpage.ManualPageTranslator, node):
+        if node['options'].alt:
+            self.body.append('{}'.format(node['options'].alt))
+        else:
+            self.body.append(yaml.dump(node['diagram']))
+
     def depart_node(self, node):
         pass
 
@@ -390,7 +459,11 @@ class RailroadDiagram(sphinx.util.docutils.SphinxDirective, ManagedDirective):
                     line=self.lineno
                 )
             ]
-        return [RailroadDiagramNode(content, self.settings, grammar)]
+        return [
+            RailroadDiagramNode(
+                diagram=content, options=self.settings, grammar=grammar
+            )
+        ]
 
     def get_content(self):
         return yaml.safe_load('\n'.join(self.content))
diff --git a/sphinx_a4doc/model/impl.py b/sphinx_a4doc/model/impl.py
index 588b58e..884e26c 100644
--- a/sphinx_a4doc/model/impl.py
+++ b/sphinx_a4doc/model/impl.py
@@ -236,6 +236,7 @@ class MetaLoader(ParserVisitor):
                 is_doxygen_nodoc=True,
                 is_doxygen_inline=True,
                 is_doxygen_no_diagram=True,
+                css_class=None,
                 importance=1,
                 documentation='',
                 section=None,
@@ -377,6 +378,7 @@ class LexerRuleLoader(RuleLoader):
             is_doxygen_nodoc=doc_info['is_doxygen_nodoc'],
             is_doxygen_inline=doc_info['is_doxygen_inline'],
             is_doxygen_no_diagram=doc_info['is_doxygen_no_diagram'],
+            css_class=doc_info['css_class'],
             importance=doc_info['importance'],
             documentation=doc_info['documentation'],
             is_fragment=bool(ctx.frag),
@@ -484,6 +486,7 @@ class ParserRuleLoader(RuleLoader):
             is_doxygen_nodoc=doc_info['is_doxygen_nodoc'],
             is_doxygen_inline=doc_info['is_doxygen_inline'],
             is_doxygen_no_diagram=doc_info['is_doxygen_no_diagram'],
+            css_class=doc_info['css_class'],
             importance=doc_info['importance'],
             documentation=doc_info['documentation'],
             section=self._current_section,
@@ -575,6 +578,7 @@ def load_docs(model, tokens, allow_cmd=True):
         is_doxygen_nodoc = False
         is_doxygen_inline = False
         is_doxygen_no_diagram = False
+        css_class = None
         importance = 1
         name = None
         docs: List[Tuple[int, str]] = []
@@ -617,6 +621,11 @@ def load_docs(model, tokens, allow_cmd=True):
                     if not name:
                         logger.error(f'{position}: WARNING: name command requires an argument')
                         continue
+                elif cmd == 'css-class':
+                    css_class = match['ctx'].strip()
+                    if not name:
+                        logger.error(f'{position}: WARNING: css-class command requires an argument')
+                        continue
                 else:
                     logger.error(f'{position}: WARNING: unknown command {cmd!r}')
 
@@ -655,6 +664,7 @@ def load_docs(model, tokens, allow_cmd=True):
             is_doxygen_inline=is_doxygen_inline,
             is_doxygen_nodoc=is_doxygen_nodoc,
             is_doxygen_no_diagram=is_doxygen_no_diagram,
+            css_class=css_class,
             name=name,
             documentation=docs
         )
diff --git a/sphinx_a4doc/model/model.py b/sphinx_a4doc/model/model.py
index 90b7c7e..76cfb72 100644
--- a/sphinx_a4doc/model/model.py
+++ b/sphinx_a4doc/model/model.py
@@ -261,6 +261,10 @@ class RuleBase:
     If true, generators should not produce railroad diagram for this rule.
     """
 
+    css_class: Optional[str]
+    """Custom css class set via `//@ doc:css_class`.
+    """
+
     is_doxygen_inline: bool
     """Indicates that the `'inline'` flag is set for this rule.
     If true, generators should not output any content for this rule.
diff --git a/sphinx_a4doc/model/model_renderer.py b/sphinx_a4doc/model/model_renderer.py
index a15695d..69e2e13 100644
--- a/sphinx_a4doc/model/model_renderer.py
+++ b/sphinx_a4doc/model/model_renderer.py
@@ -97,12 +97,12 @@ class Renderer(CachedRuleContentVisitor[dict]):
         return dict(zero_or_more=item, repeat=repeat)
 
     @staticmethod
-    def _terminal(text: str, href: Optional[str]=None, resolve: bool = True, title_is_weak: bool = False):
-        return dict(terminal=text, href=href, resolve=resolve, title_is_weak=title_is_weak)
+    def _terminal(text: str, href: Optional[str]=None, resolve: bool = True, title_is_weak: bool = False, css_class: Optional[str] = None):
+        return dict(terminal=text, href=href, resolve=resolve, title_is_weak=title_is_weak, css_class=css_class)
 
     @staticmethod
-    def _non_terminal(text: str, href: Optional[str]=None, resolve: bool = True, title_is_weak: bool = False):
-        return dict(non_terminal=text, href=href, resolve=resolve, title_is_weak=title_is_weak)
+    def _non_terminal(text: str, href: Optional[str]=None, resolve: bool = True, title_is_weak: bool = False, css_class: Optional[str] = None):
+        return dict(non_terminal=text, href=href, resolve=resolve, title_is_weak=title_is_weak, css_class=css_class)
 
     @staticmethod
     def _comment(text: str, href: Optional[str]=None):
@@ -163,15 +163,16 @@ class Renderer(CachedRuleContentVisitor[dict]):
                 literal = str(rule.content)
                 if self.literal_rendering is LiteralRendering.CONTENTS_UNQUOTED:
                     literal = literal[1:-1]
-                return self._terminal(literal, path)
+                return self._terminal(literal, path, css_class=rule.css_class)
             else:
                 name = rule.display_name or self._cc_to_dash(rule.name)
-                return self._terminal(name, path, title_is_weak=True)
+                return self._terminal(name, path, title_is_weak=True, css_class=rule.css_class)
         elif isinstance(rule, ParserRule):
             return self._non_terminal(
                 rule.display_name or self._cc_to_dash(rule.name),
                 f'{rule.model.get_name()}.{rule.name}',
-                title_is_weak=True)
+                title_is_weak=True,
+                css_class=rule.css_class)
         else:
             assert False
 

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sphinx_a4doc/_static/a4_railroad_diagram_latex.css

Control files: lines which differ (wdiff format)

  • Depends: python3-antlr4, python3-sphinx, python3-svglib, python3-yaml, python3:any

More details

Full run details