Smartquotes: correct "educating" of quotes around inline markup.
[docutils.git] / docutils / transforms / universal.py
blob4f5626c1dd0387ccc87a20ad5206109da53e3d29
1 # $Id$
2 # Authors: David Goodger <goodger@python.org>; Ueli Schlaepfer
3 # Copyright: This module has been placed in the public domain.
5 """
6 Transforms needed by most or all documents:
8 - `Decorations`: Generate a document's header & footer.
9 - `Messages`: Placement of system messages stored in
10 `nodes.document.transform_messages`.
11 - `TestMessages`: Like `Messages`, used on test runs.
12 - `FinalReferences`: Resolve remaining references.
13 """
15 __docformat__ = 'reStructuredText'
17 import re
18 import sys
19 import time
20 from docutils import nodes, utils
21 from docutils.transforms import TransformError, Transform
22 from docutils.utils import smartquotes
24 class Decorations(Transform):
26 """
27 Populate a document's decoration element (header, footer).
28 """
30 default_priority = 820
32 def apply(self):
33 header_nodes = self.generate_header()
34 if header_nodes:
35 decoration = self.document.get_decoration()
36 header = decoration.get_header()
37 header.extend(header_nodes)
38 footer_nodes = self.generate_footer()
39 if footer_nodes:
40 decoration = self.document.get_decoration()
41 footer = decoration.get_footer()
42 footer.extend(footer_nodes)
44 def generate_header(self):
45 return None
47 def generate_footer(self):
48 # @@@ Text is hard-coded for now.
49 # Should be made dynamic (language-dependent).
50 settings = self.document.settings
51 if settings.generator or settings.datestamp or settings.source_link \
52 or settings.source_url:
53 text = []
54 if settings.source_link and settings._source \
55 or settings.source_url:
56 if settings.source_url:
57 source = settings.source_url
58 else:
59 source = utils.relative_path(settings._destination,
60 settings._source)
61 text.extend([
62 nodes.reference('', 'View document source',
63 refuri=source),
64 nodes.Text('.\n')])
65 if settings.datestamp:
66 datestamp = time.strftime(settings.datestamp, time.gmtime())
67 text.append(nodes.Text('Generated on: ' + datestamp + '.\n'))
68 if settings.generator:
69 text.extend([
70 nodes.Text('Generated by '),
71 nodes.reference('', 'Docutils', refuri=
72 'http://docutils.sourceforge.net/'),
73 nodes.Text(' from '),
74 nodes.reference('', 'reStructuredText', refuri='http://'
75 'docutils.sourceforge.net/rst.html'),
76 nodes.Text(' source.\n')])
77 return [nodes.paragraph('', '', *text)]
78 else:
79 return None
82 class ExposeInternals(Transform):
84 """
85 Expose internal attributes if ``expose_internals`` setting is set.
86 """
88 default_priority = 840
90 def not_Text(self, node):
91 return not isinstance(node, nodes.Text)
93 def apply(self):
94 if self.document.settings.expose_internals:
95 for node in self.document.traverse(self.not_Text):
96 for att in self.document.settings.expose_internals:
97 value = getattr(node, att, None)
98 if value is not None:
99 node['internal:' + att] = value
102 class Messages(Transform):
105 Place any system messages generated after parsing into a dedicated section
106 of the document.
109 default_priority = 860
111 def apply(self):
112 unfiltered = self.document.transform_messages
113 threshold = self.document.reporter.report_level
114 messages = []
115 for msg in unfiltered:
116 if msg['level'] >= threshold and not msg.parent:
117 messages.append(msg)
118 if messages:
119 section = nodes.section(classes=['system-messages'])
120 # @@@ get this from the language module?
121 section += nodes.title('', 'Docutils System Messages')
122 section += messages
123 self.document.transform_messages[:] = []
124 self.document += section
127 class FilterMessages(Transform):
130 Remove system messages below verbosity threshold.
133 default_priority = 870
135 def apply(self):
136 for node in self.document.traverse(nodes.system_message):
137 if node['level'] < self.document.reporter.report_level:
138 node.parent.remove(node)
141 class TestMessages(Transform):
144 Append all post-parse system messages to the end of the document.
146 Used for testing purposes.
149 default_priority = 880
151 def apply(self):
152 for msg in self.document.transform_messages:
153 if not msg.parent:
154 self.document += msg
157 class StripComments(Transform):
160 Remove comment elements from the document tree (only if the
161 ``strip_comments`` setting is enabled).
164 default_priority = 740
166 def apply(self):
167 if self.document.settings.strip_comments:
168 for node in self.document.traverse(nodes.comment):
169 node.parent.remove(node)
172 class StripClassesAndElements(Transform):
175 Remove from the document tree all elements with classes in
176 `self.document.settings.strip_elements_with_classes` and all "classes"
177 attribute values in `self.document.settings.strip_classes`.
180 default_priority = 420
182 def apply(self):
183 if not (self.document.settings.strip_elements_with_classes
184 or self.document.settings.strip_classes):
185 return
186 # prepare dicts for lookup (not sets, for Python 2.2 compatibility):
187 self.strip_elements = dict(
188 [(key, None)
189 for key in (self.document.settings.strip_elements_with_classes
190 or [])])
191 self.strip_classes = dict(
192 [(key, None) for key in (self.document.settings.strip_classes
193 or [])])
194 for node in self.document.traverse(self.check_classes):
195 node.parent.remove(node)
197 def check_classes(self, node):
198 if isinstance(node, nodes.Element):
199 for class_value in node['classes'][:]:
200 if class_value in self.strip_classes:
201 node['classes'].remove(class_value)
202 if class_value in self.strip_elements:
203 return 1
205 class SmartQuotes(Transform):
208 Replace ASCII quotation marks with typographic form.
210 Also replace multiple dashes with em-dash/en-dash characters.
213 default_priority = 850
215 texttype = {True: 'literal',
216 False: 'plain'}
218 def apply(self):
219 if self.document.settings.smart_quotes is False:
220 return
222 # "Educate" quotes in normal text. Handle each block of text
223 # (TextElement node) as a unit to keep context around inline nodes:
224 for node in self.document.traverse(nodes.TextElement):
225 # skip preformatted text blocks and special elements:
226 if isinstance(node, (nodes.FixedTextElement, nodes.Special)):
227 continue
228 # nested TextElements are not "block-level" elements:
229 if isinstance(node.parent, nodes.TextElement):
230 continue
232 # list of text nodes in the "text block":
233 txtnodes = [txtnode for txtnode in node.traverse(nodes.Text)
234 if not isinstance(txtnode.parent,
235 nodes.option_string)]
236 # smartquotes.educate_tokens() iterates over
237 # ``(texttype, nodetext)`` tuples. `texttype` is "literal"
238 # or "plain" where "literal" text is not changed:
239 tokens = [(self.texttype[isinstance(txtnode.parent,
240 (nodes.literal,
241 nodes.math,
242 nodes.image,
243 nodes.raw,
244 nodes.problematic))],
245 txtnode.astext()) for txtnode in txtnodes]
247 # Iterator educating quotes in plain text
248 # 2 : set all, using old school en- and em- dash shortcuts
249 teacher = smartquotes.educate_tokens(tokens, attr='2')
251 for txtnode, newtext in zip(txtnodes, teacher):
252 txtnode.parent.replace(txtnode, nodes.Text(newtext))