SF.net SVN: docutils:[10202 ] trunk/docutils
milde--- via Docutils-checkins <[email protected]> Fri, 15 Aug 2025 17:37:58 +0000
| Newsgroups | gmane.text.docutils.cvs |
|---|---|
| Message-ID | <[email protected]> |
Revision: 10202
http://sourceforge.net/p/docutils/code/10202
Author: milde
Date: 2025-08-15 17:37:57 +0000 (Fri, 15 Aug 2025)
Log Message:
-----------
Fixes for the section level determination.
Simplify the fix in [r10200].
Don't add elements without valid parent to the list returned by
`nodes.Element.section_hierarchy()` instead of removing them later.
Remove the check/report for `new_parent is None` (we don't add
elements with .parent None to the `parent_sections` list any more).
Don't add a new section title style to the list of established title styles
if it is inconsistent.
Annotate and document arguments of `RSTState.nested_parse()`.
Modified Paths:
--------------
trunk/docutils/docutils/nodes.py
trunk/docutils/docutils/parsers/rst/states.py
trunk/docutils/test/test_parsers/test_rst/test_misc.py
trunk/docutils/test/test_parsers/test_rst/test_section_headers.py
Modified: trunk/docutils/docutils/nodes.py
===================================================================
--- trunk/docutils/docutils/nodes.py 2025-08-15 17:37:47 UTC (rev 10201)
+++ trunk/docutils/docutils/nodes.py 2025-08-15 17:37:57 UTC (rev 10202)
@@ -820,18 +820,21 @@
def section_hierarchy(self) -> list[section]:
"""Return the element's section hierarchy.
- Return a list of all <section> elements containing `self`
- (including `self` if it is a <section>).
+ Return a list of all <section> elements that contain `self`
+ (including `self` if it is a <section>) and have a parent node.
List item ``[i]`` is the parent <section> of level i+1
(1: section, 2: subsection, 3: subsubsection, ...).
The length of the list is the element's section level.
+ See `docutils.parsers.rst.states.RSTState.check_subsection()`
+ for a usage example.
+
Provisional. May be changed or removed without warning.
"""
sections = []
node = self
- while node is not None:
+ while node.parent is not None:
if isinstance(node, section):
sections.append(node)
node = node.parent
Modified: trunk/docutils/docutils/parsers/rst/states.py
===================================================================
--- trunk/docutils/docutils/parsers/rst/states.py 2025-08-15 17:37:47 UTC (rev 10201)
+++ trunk/docutils/docutils/parsers/rst/states.py 2025-08-15 17:37:57 UTC (rev 10202)
@@ -121,7 +121,11 @@
from docutils.utils._roman_numerals import (InvalidRomanNumeralError,
RomanNumeral)
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from docutils.statemachine import StringList
+
class MarkupError(DataError): pass
class UnknownInterpretedRoleError(DataError): pass
class InterpretedRoleNotImplementedError(DataError): pass
@@ -250,11 +254,37 @@
"""Called at beginning of file."""
return [], []
- def nested_parse(self, block, input_offset, node, match_titles=False,
- state_machine_class=None, state_machine_kwargs=None):
+ def nested_parse(self,
+ block: StringList,
+ input_offset: int,
+ node: nodes.Element,
+ match_titles: bool = False,
+ state_machine_class: StateMachineWS|None = None,
+ state_machine_kwargs: dict|None = None
+ ) -> int:
"""
- Create a new StateMachine rooted at `node` and run it over the input
- `block`.
+ Parse the input `block` with a nested state-machine rooted at `node`.
+
+ :block:
+ reStructuredText source extract.
+ :input_offset:
+ Line number at start of the block.
+ :node:
+ Root node. Generated nodes will be appended to this node
+ (unless a new section with lower level is encountered).
+ :match_titles:
+ Allow section titles?
+ If True, `node` should be attached to the document
+ so that section levels can be computed correctly
+ and moving up in the section hierarchy works.
+ :state_machine_class:
+ Default: `NestedStateMachine`.
+ :state_machine_kwargs:
+ Keyword arguments for the state-machine instantiation.
+ Default: `self.nested_sm_kwargs`.
+
+ Create a new state-machine instance if required.
+ Return new offset.
"""
use_default = 0
if state_machine_class is None:
@@ -263,8 +293,6 @@
if state_machine_kwargs is None:
state_machine_kwargs = self.nested_sm_kwargs
use_default += 1
- block_length = len(block)
-
state_machine = None
if use_default == 2:
try:
@@ -274,8 +302,11 @@
if not state_machine:
state_machine = state_machine_class(debug=self.debug,
**state_machine_kwargs)
+ # run the statemachine and populate `node`:
+ block_length = len(block)
state_machine.run(block, input_offset, memo=self.memo,
node=node, match_titles=match_titles)
+ # clean up
if use_default == 2:
self.nested_sm_cache.append(state_machine)
else:
@@ -332,12 +363,6 @@
"""
title_styles = self.memo.title_styles
parent_sections = self.parent.section_hierarchy()
- # Adding a new <section> at level "i" is done by appending to
- # ``parent_sections[i-1].parent``.
- # However, in nested parsing the root `node` may be a <section>.
- # Then ``parent_sections[0]`` has no parent and must be discarded:
- if parent_sections and parent_sections[0].parent is None:
- parent_sections.pop(0)
# current section level: (0 root, 1 section, 2 subsection, ...)
oldlevel = len(parent_sections)
# new section level:
@@ -344,8 +369,7 @@
try: # check for existing title style
newlevel = title_styles.index(style) + 1
except ValueError: # new title style
- title_styles.append(style)
- newlevel = len(title_styles)
+ newlevel = len(title_styles) + 1
# The new level must not be deeper than an immediate child
# of the current level:
if newlevel > oldlevel + 1:
@@ -358,23 +382,12 @@
line=lineno)
return False
# Update parent state:
+ if newlevel > len(title_styles):
+ title_styles.append(style)
self.memo.section_level = newlevel
if newlevel <= oldlevel:
# new section is sibling or higher up in the section hierarchy
- new_parent = parent_sections[newlevel-1].parent
- if new_parent is None:
- styles = ' '.join('/'.join(style) for style in title_styles)
- self.parent += self.reporter.error(
- f'Cannot skip from level {oldlevel} to {newlevel}.'
- ' Current element has only {len(self.parent_sections)}'
- ' parent sections.'
- ' (Mismatch of `memo.section_styles`,'
- ' and the root node of a nested parser?)',
- nodes.literal_block('', source),
- nodes.paragraph('', f'Established title styles: {styles}'),
- line=lineno)
- return False
- self.parent = new_parent
+ self.parent = parent_sections[newlevel-1].parent
return True
def title_inconsistent(self, sourcetext, lineno):
Modified: trunk/docutils/test/test_parsers/test_rst/test_misc.py
===================================================================
--- trunk/docutils/test/test_parsers/test_rst/test_misc.py 2025-08-15 17:37:47 UTC (rev 10201)
+++ trunk/docutils/test/test_parsers/test_rst/test_misc.py 2025-08-15 17:37:57 UTC (rev 10202)
@@ -9,6 +9,7 @@
from pathlib import Path
import sys
+import types
import unittest
if __name__ == '__main__':
@@ -15,8 +16,9 @@
# prepend the local "docutils root" to the Python library path
sys.path.insert(0, Path(__file__).resolve().parents[2].as_posix())
-from docutils import frontend, utils
+from docutils import frontend, nodes, statemachine, utils
import docutils.parsers.rst
+from docutils.parsers.rst import states
class RstParserTests(unittest.TestCase):
@@ -30,5 +32,126 @@
parser.parse(b'hol', document)
+class RSTStateTests(unittest.TestCase):
+
+ # state machine
+ machine = states.RSTStateMachine(state_classes=states.state_classes,
+ initial_state='Body')
+ # "generic" state
+ state = states.RSTState(machine)
+
+ def title_markup(self, text, adornment='-'):
+ underline = adornment * len(text)
+ return statemachine.StringList([text, underline], 'section block')
+
+ def setUp(self):
+ # state machine runtime initialization (cf. RSTStateMachine.run())
+ # Only for test:
+ # don't use this low-level approach in production code!
+
+ # empty <document> and settings:
+ settings = frontend.get_default_settings(docutils.parsers.rst.Parser)
+ settings.halt_level = 2
+ settings.warning_stream = ''
+ document = self.document = utils.new_document('test data', settings)
+ # language module (localized directive and role names)
+ # self.machine.language = languages.get_language(
+ # document.settings.language_code)
+ # self.machine.match_titles = True # support sections
+ # "memo": Container for document-wide auxiliary data
+ inliner = states.Inliner() # rST parser for inline markup
+ inliner.init_customizations(document.settings)
+ self.machine.memo = types.SimpleNamespace(document=document,
+ reporter=document.reporter,
+ language='en',
+ title_styles=[],
+ inliner=inliner)
+ # self.machine.document = document
+ # self.machine.reporter = document.reporter
+ self.machine.node = document
+ # initialize `state` object
+ self.state.runtime_init()
+
+ def test_nested_parse(self):
+ # parse a text block in a nested parser
+ text = statemachine.StringList(['test input'], 'nested block')
+ tip = nodes.tip('') # base `node`
+
+ # plain text -> attach <paragraph> to `node`:
+ self.state.nested_parse(text, input_offset=0, node=tip)
+ self.assertEqual('<tip><paragraph>test input</paragraph></tip>',
+ str(tip))
+
+ # by default, section titles are not supported:
+ title = self.title_markup('top', '-')
+ with self.assertRaisesRegex(utils.SystemMessage,
+ 'Unexpected section title.'):
+ self.state.nested_parse(title, input_offset=0, node=tip)
+
+ # Nested parsing with section markup is supported with
+ # ``match_titles=True`` but not used in Docutils.
+ # (However, Sphinx "autodoc" and some contributed extensions use it.)
+ def test_nested_parse_with_sections_base_attached(self):
+ # If the base `node` is attached to the `document`,
+ # the document-wide section style hierarchy is used.
+
+ # base`node` is a <section>
+ section = nodes.section('')
+ self.document += section # attach to document
+ self.machine.memo.title_styles.append('-') # register title style
+ # parse level-2 section title
+ title = self.title_markup('sub', '~')
+ # -> append new <section> to the parsers base `node`
+ self.state.nested_parse(title, 0, node=section, match_titles=True)
+ self.assertEqual('<document source="test data">\n'
+ ' <section>\n'
+ ' <section ids="sub" names="sub">\n'
+ ' <title>\n'
+ ' sub\n',
+ str(self.document.pformat()))
+ self.assertEqual(['-', '~'], self.machine.memo.title_styles)
+
+ # parse top-level section title
+ title = self.title_markup('top', '-')
+ # -> move 1 level up and attach <section> to document
+ self.state.nested_parse(title, 0, node=section, match_titles=True)
+ self.assertEqual('<document source="test data">\n'
+ ' <section>\n'
+ ' <section ids="sub" names="sub">\n'
+ ' <title>\n'
+ ' sub\n'
+ ' <section ids="top" names="top">\n'
+ ' <title>\n'
+ ' top\n',
+ str(self.document.pformat()))
+
+ # base`node` is a <paragraph>
+ paragraph = nodes.paragraph('', 'base node')
+ section += paragraph # attach (indirectly)
+ # parse top-level section title
+ title = self.title_markup('top 2', '-')
+ # -> move 1 level up and attach <section> to document
+ self.state.nested_parse(title, 0, node=paragraph, match_titles=True)
+ self.assertEqual('<section ids="top-2" names="top\\ 2">\n'
+ ' <title>\n'
+ ' top 2\n',
+ self.document[-1].pformat())
+
+ # new (2nd-level) section title
+ # TODO: don't append <section> to <paragraph>!
+ title = self.title_markup('sub 2', '~')
+ self.state.nested_parse(title, 0, node=paragraph, match_titles=True)
+ self.assertEqual('<section>\n'
+ ' <section ids="sub" names="sub">\n'
+ ' <title>\n'
+ ' sub\n'
+ ' <paragraph>\n'
+ ' base node\n'
+ ' <section ids="sub-2" names="sub\\ 2">\n'
+ ' <title>\n'
+ ' sub 2\n',
+ section.pformat())
+
+
if __name__ == '__main__':
unittest.main()
Modified: trunk/docutils/test/test_parsers/test_rst/test_section_headers.py
===================================================================
--- trunk/docutils/test/test_parsers/test_rst/test_section_headers.py 2025-08-15 17:37:47 UTC (rev 10201)
+++ trunk/docutils/test/test_parsers/test_rst/test_section_headers.py 2025-08-15 17:37:57 UTC (rev 10202)
@@ -502,7 +502,7 @@
Title 4
```````
<paragraph>
- Established title styles: = - `
+ Established title styles: = -
<paragraph>
Paragraph 4.
"""],
@@ -556,7 +556,7 @@
Title 4
```````
<paragraph>
- Established title styles: =/= -/- `/`
+ Established title styles: =/= -/-
<paragraph>
Paragraph 4.
"""],
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.