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
24__docformat__ = 'reStructuredText'
25
26
27from docutils import languages, ApplicationError, TransformSpec
28
29
30class TransformError(ApplicationError):
31 pass
32
33
34class Transform:
35 """Docutils transform component abstract base class."""
36
37 default_priority = None
38 """Numerical priority of this transform, 0 through 999 (override)."""
39
40 def __init__(self, document, startnode=None):
41 """
42 Initial setup for in-place document transforms.
43 """
44
45 self.document = document
46 """The document tree to transform."""
47
48 self.startnode = startnode
49 """Node from which to begin the transform. For many transforms which
50 apply to the document as a whole, `startnode` is not set (i.e. its
51 value is `None`)."""
52
53 self.language = languages.get_language(
54 document.settings.language_code, document.reporter)
55 """Language module local to this document."""
56
57 def apply(self, **kwargs):
58 """Override to apply the transform to the document tree."""
59 raise NotImplementedError('subclass must override this method')
60
61
62class Transformer(TransformSpec):
63 """
64 Store "transforms" and apply them to the document tree.
65
66 Collect lists of `Transform` instances and "unknown_reference_resolvers"
67 from Docutils components (`TransformSpec` instances).
68 Apply collected "transforms" to the document tree.
69
70 Also keeps track of components by component type name.
71
72 https://docutils.sourceforge.io/docs/peps/pep-0258.html#transformer
73 """
74
75 def __init__(self, document):
76 self.transforms = []
77 """List of transforms to apply. Each item is a 4-tuple:
78 ``(priority string, transform class, pending node or None, kwargs)``.
79 """
80
81 self.unknown_reference_resolvers = []
82 """List of hook functions which assist in resolving references."""
83
84 self.document = document
85 """The `nodes.document` object this Transformer is attached to."""
86
87 self.applied = []
88 """Transforms already applied, in order."""
89
90 self.sorted = False
91 """Boolean: is `self.tranforms` sorted?"""
92
93 self.components = {}
94 """Mapping of component type name to component object.
95
96 Set by `self.populate_from_components()`.
97 """
98
99 self.serialno = 0
100 """Internal serial number to keep track of the add order of
101 transforms."""
102
103 def add_transform(self, transform_class, priority=None, **kwargs):
104 """
105 Store a single transform. Use `priority` to override the default.
106 `kwargs` is a dictionary whose contents are passed as keyword
107 arguments to the `apply` method of the transform. This can be used to
108 pass application-specific data to the transform instance.
109 """
110 if priority is None:
111 priority = transform_class.default_priority
112 priority_string = self.get_priority_string(priority)
113 self.transforms.append(
114 (priority_string, transform_class, None, kwargs))
115 self.sorted = False
116
117 def add_transforms(self, transform_list):
118 """Store multiple transforms, with default priorities."""
119 for transform_class in transform_list:
120 priority_string = self.get_priority_string(
121 transform_class.default_priority)
122 self.transforms.append(
123 (priority_string, transform_class, None, {}))
124 self.sorted = False
125
126 def add_pending(self, pending, priority=None):
127 """Store a transform with an associated `pending` node."""
128 transform_class = pending.transform
129 if priority is None:
130 priority = transform_class.default_priority
131 priority_string = self.get_priority_string(priority)
132 self.transforms.append(
133 (priority_string, transform_class, pending, {}))
134 self.sorted = False
135
136 def get_priority_string(self, priority):
137 """
138 Return a string, `priority` combined with `self.serialno`.
139
140 This ensures FIFO order on transforms with identical priority.
141 """
142 self.serialno += 1
143 return '%03d-%03d' % (priority, self.serialno)
144
145 def populate_from_components(self, components):
146 """
147 Store each component's default transforms and reference resolvers
148
149 Transforms are stored with default priorities for later sorting.
150 "Unknown reference resolvers" are sorted and stored.
151 Components that don't inherit from `TransformSpec` are ignored.
152
153 Also, store components by type name in a mapping for later lookup.
154 """
155 resolvers = []
156 for component in components:
157 if not isinstance(component, TransformSpec):
158 continue
159 self.add_transforms(component.get_transforms())
160 self.components[component.component_type] = component
161 resolvers.extend(component.unknown_reference_resolvers)
162 self.sorted = False # sort transform list in self.apply_transforms()
163
164 # Sort and add helper functions to help resolve unknown references.
165 def keyfun(f):
166 return f.priority
167 resolvers.sort(key=keyfun)
168 self.unknown_reference_resolvers += resolvers
169
170 def apply_transforms(self):
171 """Apply all of the stored transforms, in priority order."""
172 self.document.reporter.attach_observer(
173 self.document.note_transform_message)
174 while self.transforms:
175 if not self.sorted:
176 # Unsorted initially, and whenever a transform is added
177 # (transforms may add other transforms).
178 self.transforms.sort(reverse=True)
179 self.sorted = True
180 priority, transform_class, pending, kwargs = self.transforms.pop()
181 transform = transform_class(self.document, startnode=pending)
182 transform.apply(**kwargs)
183 self.applied.append((priority, transform_class, pending, kwargs))
184 self.document.reporter.detach_observer(
185 self.document.note_transform_message)