2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
6 Transforms for PEP processing.
8 - `Headers`: Used to transform a PEP's initial RFC-2822 header. It remains a
9 field list, but some entries get processed.
10 - `Contents`: Auto-inserts a table of contents.
11 - `PEPZero`: Special processing for PEP 0.
14 __docformat__
= 'reStructuredText'
20 from docutils
import nodes
, utils
, languages
21 from docutils
import ApplicationError
, DataError
22 from docutils
.transforms
import Transform
, TransformError
23 from docutils
.transforms
import parts
, references
, misc
26 class Headers(Transform
):
29 Process fields in a PEP's initial RFC-2822 header.
32 default_priority
= 360
35 pep_cvs_url
= ('http://hg.python.org'
36 '/peps/file/default/pep-%04d.txt')
37 rcs_keyword_substitutions
= (
38 (re
.compile(r
'\$' r
'RCSfile: (.+),v \$$', re
.IGNORECASE
), r
'\1'),
39 (re
.compile(r
'\$[a-zA-Z]+: (.+) \$$'), r
'\1'),)
42 if not len(self
.document
):
43 # @@@ replace these DataErrors with proper system messages
44 raise DataError('Document tree is empty.')
45 header
= self
.document
[0]
46 if not isinstance(header
, nodes
.field_list
) or \
47 'rfc2822' not in header
['classes']:
48 raise DataError('Document does not begin with an RFC-2822 '
49 'header; it is not a PEP.')
52 if field
[0].astext().lower() == 'pep': # should be the first field
53 value
= field
[1].astext()
56 cvs_url
= self
.pep_cvs_url
% pep
60 msg
= self
.document
.reporter
.warning(
61 '"PEP" header must contain an integer; "%s" is an '
62 'invalid value.' % pep
, base_node
=field
)
63 msgid
= self
.document
.set_id(msg
)
64 prb
= nodes
.problematic(value
, value
or '(none)',
66 prbid
= self
.document
.set_id(prb
)
67 msg
.add_backref(prbid
)
69 field
[1][0][:] = [prb
]
71 field
[1] += nodes
.paragraph('', '', prb
)
74 raise DataError('Document does not contain an RFC-2822 "PEP" '
77 # Special processing for PEP 0.
78 pending
= nodes
.pending(PEPZero
)
79 self
.document
.insert(1, pending
)
80 self
.document
.note_pending(pending
)
81 if len(header
) < 2 or header
[1][0].astext().lower() != 'title':
82 raise DataError('No title!')
84 name
= field
[0].astext().lower()
87 raise DataError('PEP header field body contains multiple '
88 'elements:\n%s' % field
.pformat(level
=1))
90 if not isinstance(body
[0], nodes
.paragraph
):
91 raise DataError('PEP header field body may only contain '
92 'a single paragraph:\n%s'
93 % field
.pformat(level
=1))
94 elif name
== 'last-modified':
97 time
.localtime(os
.stat(self
.document
['source'])[8]))
99 body
+= nodes
.paragraph(
100 '', '', nodes
.reference('', date
, refuri
=cvs_url
))
107 if isinstance(node
, nodes
.reference
):
108 node
.replace_self(mask_email(node
))
109 elif name
== 'discussions-to':
111 if isinstance(node
, nodes
.reference
):
112 node
.replace_self(mask_email(node
, pep
))
113 elif name
in ('replaces', 'replaced-by', 'requires'):
115 space
= nodes
.Text(' ')
116 for refpep
in re
.split(',?\s+', body
.astext()):
118 newbody
.append(nodes
.reference(
120 refuri
=(self
.document
.settings
.pep_base_url
121 + self
.pep_url
% pepno
)))
122 newbody
.append(space
)
123 para
[:] = newbody
[:-1] # drop trailing space
124 elif name
== 'last-modified':
125 utils
.clean_rcs_keywords(para
, self
.rcs_keyword_substitutions
)
128 para
[:] = [nodes
.reference('', date
, refuri
=cvs_url
)]
129 elif name
== 'content-type':
130 pep_type
= para
.astext()
131 uri
= self
.document
.settings
.pep_base_url
+ self
.pep_url
% 12
132 para
[:] = [nodes
.reference('', pep_type
, refuri
=uri
)]
133 elif name
== 'version' and len(body
):
134 utils
.clean_rcs_keywords(para
, self
.rcs_keyword_substitutions
)
137 class Contents(Transform
):
140 Insert an empty table of contents topic and a transform placeholder into
141 the document after the RFC 2822 header.
144 default_priority
= 380
147 language
= languages
.get_language(self
.document
.settings
.language_code
,
148 self
.document
.reporter
)
149 name
= language
.labels
['contents']
150 title
= nodes
.title('', name
)
151 topic
= nodes
.topic('', title
, classes
=['contents'])
152 name
= nodes
.fully_normalize_name(name
)
153 if not self
.document
.has_name(name
):
154 topic
['names'].append(name
)
155 self
.document
.note_implicit_target(topic
)
156 pending
= nodes
.pending(parts
.Contents
)
158 self
.document
.insert(1, topic
)
159 self
.document
.note_pending(pending
)
162 class TargetNotes(Transform
):
165 Locate the "References" section, insert a placeholder for an external
166 target footnote insertion transform at the end, and schedule the
167 transform to run immediately.
170 default_priority
= 520
175 refsect
= copyright
= None
176 while i
>= 0 and isinstance(doc
[i
], nodes
.section
):
177 title_words
= doc
[i
][0].astext().lower().split()
178 if 'references' in title_words
:
181 elif 'copyright' in title_words
:
185 refsect
= nodes
.section()
186 refsect
+= nodes
.title('', 'References')
189 # Put the new "References" section before "Copyright":
190 doc
.insert(copyright
, refsect
)
192 # Put the new "References" section at end of doc:
194 pending
= nodes
.pending(references
.TargetNotes
)
195 refsect
.append(pending
)
196 self
.document
.note_pending(pending
, 0)
197 pending
= nodes
.pending(misc
.CallBack
,
198 details
={'callback': self
.cleanup_callback
})
199 refsect
.append(pending
)
200 self
.document
.note_pending(pending
, 1)
202 def cleanup_callback(self
, pending
):
204 Remove an empty "References" section.
206 Called after the `references.TargetNotes` transform is complete.
208 if len(pending
.parent
) == 2: # <title> and <pending>
209 pending
.parent
.parent
.remove(pending
.parent
)
212 class PEPZero(Transform
):
215 Special processing for PEP 0.
218 default_priority
=760
221 visitor
= PEPZeroSpecial(self
.document
)
222 self
.document
.walk(visitor
)
223 self
.startnode
.parent
.remove(self
.startnode
)
226 class PEPZeroSpecial(nodes
.SparseNodeVisitor
):
229 Perform the special processing needed by PEP 0:
231 - Mask email addresses.
233 - Link PEP numbers in the second column of 4-column tables to the PEPs
237 pep_url
= Headers
.pep_url
239 def unknown_visit(self
, node
):
242 def visit_reference(self
, node
):
243 node
.replace_self(mask_email(node
))
245 def visit_field_list(self
, node
):
246 if 'rfc2822' in node
['classes']:
249 def visit_tgroup(self
, node
):
250 self
.pep_table
= node
['cols'] == 4
253 def visit_colspec(self
, node
):
255 if self
.pep_table
and self
.entry
== 2:
256 node
['classes'].append('num')
258 def visit_row(self
, node
):
261 def visit_entry(self
, node
):
263 if self
.pep_table
and self
.entry
== 2 and len(node
) == 1:
264 node
['classes'].append('num')
266 if isinstance(p
, nodes
.paragraph
) and len(p
) == 1:
270 ref
= (self
.document
.settings
.pep_base_url
271 + self
.pep_url
% pep
)
272 p
[0] = nodes
.reference(text
, text
, refuri
=ref
)
277 non_masked_addresses
= ('peps@python.org',
278 'python-list@python.org',
279 'python-dev@python.org')
281 def mask_email(ref
, pepno
=None):
283 Mask the email address in `ref` and return a replacement node.
285 `ref` is returned unchanged if it contains no email address.
287 For email addresses such as "user@host", mask the address as "user at
288 host" (text) to thwart simple email address harvesters (except for those
289 listed in `non_masked_addresses`). If a PEP number (`pepno`) is given,
290 return a reference including a default email subject.
292 if ref
.hasattr('refuri') and ref
['refuri'].startswith('mailto:'):
293 if ref
['refuri'][8:] in non_masked_addresses
:
296 replacement_text
= ref
.astext().replace('@', ' at ')
297 replacement
= nodes
.raw('', replacement_text
, format
='html')
301 ref
['refuri'] += '?subject=PEP%%20%s' % pepno
302 ref
[:] = [replacement
]