1# $Id$
2# Authors: David Goodger <goodger@python.org>; Ueli Schlaepfer
3# Copyright: This module has been placed in the public domain.
4
5"""
6This package contains modules for standard tree transforms available
7to Docutils components. Tree transforms serve a variety of purposes:
8
9- To tie up certain syntax-specific "loose ends" that remain after the
10 initial parsing of the input plaintext. These transforms are used to
11 supplement a limited syntax.
12
13- To automate the internal linking of the document tree (hyperlink
14 references, footnote references, etc.).
15
16- To extract useful information from the document tree. These
17 transforms may be used to construct (for example) indexes and tables
18 of contents.
19
20Each transform is an optional step that a Docutils component may
21choose to perform on the parsed document.
22"""
23
24from __future__ import annotations
25
26__docformat__ = 'reStructuredText'
27
28import warnings
29
30from docutils import languages, ApplicationError, TransformSpec
31
32
33class TransformError(ApplicationError):
34 pass
35
36
37class Transform:
38 """Docutils transform component abstract base class."""
39
40 default_priority = None
41 """Numerical priority of this transform, 0 through 999 (override)."""
42
43 def __init__(self, document, startnode=None) -> None:
44 """
45 Initial setup for in-place document transforms.
46 """
47
48 self.document = document
49 """The document tree to transform."""
50
51 self.startnode = startnode
52 """Node from which to begin the transform. For many transforms which
53 apply to the document as a whole, `startnode` is not set (i.e. its
54 value is `None`)."""
55
56 self.language = languages.get_language(
57 document.settings.language_code, document.reporter)
58 """Language module local to this document."""
59
60 def apply(self, **kwargs):
61 """Override to apply the transform to the document tree."""
62 raise NotImplementedError('subclass must override this method')
63
64
65class Transformer(TransformSpec):
66 """
67 Store "transforms" and apply them to the document tree.
68
69 Collect lists of `Transform` instances from Docutils
70 components (`TransformSpec` instances).
71 Apply collected "transforms" to the document tree.
72
73 Also keeps track of components by component type name.
74
75 https://docutils.sourceforge.io/docs/peps/pep-0258.html#transformer
76 """
77
78 def __init__(self, document) -> None:
79 self.transforms = []
80 """List of transforms to apply. Each item is a 4-tuple:
81 ``(priority string, transform class, pending node or None, kwargs)``.
82 """
83
84 self.unknown_reference_resolvers = []
85 """List of hook functions which assist in resolving references.
86
87 Deprecated. Will be removed in Docutils 1.0.
88 """
89
90 self.document = document
91 """The `nodes.document` object this Transformer is attached to."""
92
93 self.applied = []
94 """Transforms already applied, in order."""
95
96 self.sorted = False
97 """Boolean: is `self.tranforms` sorted?"""
98
99 self.components = {}
100 """Mapping of component type name to component object.
101
102 Set by `self.populate_from_components()`.
103 """
104
105 self.serialno = 0
106 """Internal serial number to keep track of the add order of
107 transforms."""
108
109 def add_transform(self, transform_class, priority=None, **kwargs) -> None:
110 """
111 Store a single transform. Use `priority` to override the default.
112 `kwargs` is a dictionary whose contents are passed as keyword
113 arguments to the `apply` method of the transform. This can be used to
114 pass application-specific data to the transform instance.
115 """
116 if priority is None:
117 priority = transform_class.default_priority
118 priority_string = self.get_priority_string(priority)
119 self.transforms.append(
120 (priority_string, transform_class, None, kwargs))
121 self.sorted = False
122
123 def add_transforms(self, transform_list) -> None:
124 """Store multiple transforms, with default priorities."""
125 for transform_class in transform_list:
126 priority_string = self.get_priority_string(
127 transform_class.default_priority)
128 self.transforms.append(
129 (priority_string, transform_class, None, {}))
130 self.sorted = False
131
132 def add_pending(self, pending, priority=None) -> None:
133 """Store a transform with an associated `pending` node."""
134 transform_class = pending.transform
135 if priority is None:
136 priority = transform_class.default_priority
137 priority_string = self.get_priority_string(priority)
138 self.transforms.append(
139 (priority_string, transform_class, pending, {}))
140 self.sorted = False
141
142 def get_priority_string(self, priority) -> str:
143 """
144 Return a string, `priority` combined with `self.serialno`.
145
146 This ensures FIFO order on transforms with identical priority.
147 """
148 self.serialno += 1
149 return '%03d-%03d' % (priority, self.serialno)
150
151 def populate_from_components(self, components) -> None:
152 """
153 Store each component's default transforms and reference resolvers.
154
155 Transforms are stored with default priorities for later sorting.
156 "Unknown reference resolvers" are sorted and stored.
157 Components that don't inherit from `TransformSpec` are ignored.
158
159 Also, store components by type name in a mapping for later lookup.
160 """
161 resolvers = []
162 for component in components:
163 if not isinstance(component, TransformSpec):
164 continue
165 self.add_transforms(component.get_transforms())
166 self.components[component.component_type] = component
167 resolvers.extend(component.unknown_reference_resolvers)
168 self.sorted = False # sort transform list in self.apply_transforms()
169
170 # Sort and add hook functions helping to resolve unknown references.
171 def keyfun(f):
172 return f.priority
173 resolvers.sort(key=keyfun)
174 self.unknown_reference_resolvers += resolvers
175 if self.unknown_reference_resolvers:
176 warnings.warn('The `unknown_reference_resolvers` hook chain '
177 'will be removed in Docutils 1.0.\n'
178 'Use a transform to resolve references.',
179 DeprecationWarning, stacklevel=2)
180
181 def apply_transforms(self) -> None:
182 """Apply all of the stored transforms, in priority order."""
183 self.document.reporter.attach_observer(
184 self.document.note_transform_message)
185 while self.transforms:
186 if not self.sorted:
187 # Unsorted initially, and whenever a transform is added
188 # (transforms may add other transforms).
189 self.transforms.sort(reverse=True)
190 self.sorted = True
191 priority, transform_class, pending, kwargs = self.transforms.pop()
192 transform = transform_class(self.document, startnode=pending)
193 transform.apply(**kwargs)
194 self.applied.append((priority, transform_class, pending, kwargs))
195 self.document.reporter.detach_observer(
196 self.document.note_transform_message)