diff --git a/CHANGES.rst b/CHANGES.rst index bcc64e9..86921f6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ - Add an ``ansi`` extra to install ``erbsland-sphinx-ansi``. Version 1.2.4 or later is required. - Explicitly list ``docutils`` as a dependency. +- Add the ability to specify a ``:class:`` argument to set CSS classes + on programoutput nodes. 0.19 (2026-02-20) diff --git a/src/sphinxcontrib/programoutput/__init__.py b/src/sphinxcontrib/programoutput/__init__.py index a35c6b0..8ca1b36 100644 --- a/src/sphinxcontrib/programoutput/__init__.py +++ b/src/sphinxcontrib/programoutput/__init__.py @@ -128,7 +128,7 @@ class ProgramOutputDirective(rst.Directive): ellipsis=_slice, extraargs=unchanged, returncode=nonnegative_int, cwd=unchanged, caption=unchanged, name=unchanged, - language=unchanged) + language=unchanged, **{'class': unchanged}) def run(self): env = self.state.document.settings.env @@ -151,9 +151,16 @@ def run(self): node['language'] = self.options.get('language', 'text') if 'ellipsis' in self.options: node['strip_lines'] = self.options['ellipsis'] + + classes = self.options.get('class', '').split() if 'class' in self.options else [] + if classes: + node['classes'] = classes + if 'caption' in self.options: caption = self.options['caption'] or self.arguments[0] node = _container_wrapper(self, node, caption) + if classes: + node['classes'].extend(classes) self.add_name(node) return [node] @@ -347,6 +354,8 @@ def run_programs(app, doctree): output, app.config.programoutput_use_ansi, app ) new_node['language'] = node['language'] + if 'classes' in node: + new_node['classes'].extend(node['classes']) node.replace_self(new_node) diff --git a/src/sphinxcontrib/programoutput/tests/test_directive.py b/src/sphinxcontrib/programoutput/tests/test_directive.py index 4d5da3c..c6d2324 100644 --- a/src/sphinxcontrib/programoutput/tests/test_directive.py +++ b/src/sphinxcontrib/programoutput/tests/test_directive.py @@ -482,6 +482,39 @@ def test_use_ansi_enabled_extension(self): '\x1b[31mspam\x1b[0m' ) + @with_content("""\ + .. program-output:: echo spam + :class: myclass""") + def test_class(self): + literal = self.doctree.next_node(literal_block) + self.assertTrue(literal) + self.assertIn('myclass', literal.get('classes')) + self.assert_output(self.doctree, 'spam') + self.assert_cache(self.app, 'echo spam', 'spam') + + @with_content("""\ + .. program-output:: echo spam + :class: myclass anotherclass""") + def test_multiple_classes(self): + literal = self.doctree.next_node(literal_block) + self.assertTrue(literal) + classes = literal.get('classes') + self.assertIn('myclass', classes) + self.assertIn('anotherclass', classes) + self.assert_output(self.doctree, 'spam') + self.assert_cache(self.app, 'echo spam', 'spam') + + @with_content("""\ + .. program-output:: echo spam + :class: myclass + :caption: mycaption""") + def test_class_with_caption(self): + container_node = self.doctree.next_node(container) + self.assertTrue(container_node) + self.assertIn('myclass', container_node.get('classes')) + self.assert_output(self.doctree, 'spam', caption='mycaption') + self.assert_cache(self.app, 'echo spam', 'spam') + def test_suite(): return unittest.defaultTestLoader.loadTestsFromName(__name__)