Website: add template and basic stylesheet with menu. Update buildhtml.py (and theref...
[docutils.git] / sandbox / gitpull / web_stylesheet_and_menu / docutils / transforms / peps.py
blobb89cf41c79cdc5ece76f4a042aeb9bc0abc22513
1 # $Id$
2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
5 """
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.
12 """
14 __docformat__ = 'reStructuredText'
16 import sys
17 import os
18 import re
19 import time
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):
28 """
29 Process fields in a PEP's initial RFC-2822 header.
30 """
32 default_priority = 360
34 pep_url = 'pep-%04d'
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'),)
41 def apply(self):
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.')
50 pep = None
51 for field in header:
52 if field[0].astext().lower() == 'pep': # should be the first field
53 value = field[1].astext()
54 try:
55 pep = int(value)
56 cvs_url = self.pep_cvs_url % pep
57 except ValueError:
58 pep = value
59 cvs_url = None
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)',
65 refid=msgid)
66 prbid = self.document.set_id(prb)
67 msg.add_backref(prbid)
68 if len(field[1]):
69 field[1][0][:] = [prb]
70 else:
71 field[1] += nodes.paragraph('', '', prb)
72 break
73 if pep is None:
74 raise DataError('Document does not contain an RFC-2822 "PEP" '
75 'header.')
76 if pep == 0:
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!')
83 for field in header:
84 name = field[0].astext().lower()
85 body = field[1]
86 if len(body) > 1:
87 raise DataError('PEP header field body contains multiple '
88 'elements:\n%s' % field.pformat(level=1))
89 elif len(body) == 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':
95 date = time.strftime(
96 '%d-%b-%Y',
97 time.localtime(os.stat(self.document['source'])[8]))
98 if cvs_url:
99 body += nodes.paragraph(
100 '', '', nodes.reference('', date, refuri=cvs_url))
101 else:
102 # empty
103 continue
104 para = body[0]
105 if name == 'author':
106 for node in para:
107 if isinstance(node, nodes.reference):
108 node.replace_self(mask_email(node))
109 elif name == 'discussions-to':
110 for node in para:
111 if isinstance(node, nodes.reference):
112 node.replace_self(mask_email(node, pep))
113 elif name in ('replaces', 'replaced-by', 'requires'):
114 newbody = []
115 space = nodes.Text(' ')
116 for refpep in re.split(',?\s+', body.astext()):
117 pepno = int(refpep)
118 newbody.append(nodes.reference(
119 refpep, refpep,
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)
126 if cvs_url:
127 date = para.astext()
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
146 def apply(self):
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)
157 topic += pending
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
172 def apply(self):
173 doc = self.document
174 i = len(doc) - 1
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:
179 refsect = doc[i]
180 break
181 elif 'copyright' in title_words:
182 copyright = i
183 i -= 1
184 if not refsect:
185 refsect = nodes.section()
186 refsect += nodes.title('', 'References')
187 doc.set_id(refsect)
188 if copyright:
189 # Put the new "References" section before "Copyright":
190 doc.insert(copyright, refsect)
191 else:
192 # Put the new "References" section at end of doc:
193 doc.append(refsect)
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
220 def apply(self):
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
234 themselves.
237 pep_url = Headers.pep_url
239 def unknown_visit(self, node):
240 pass
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']:
247 raise nodes.SkipNode
249 def visit_tgroup(self, node):
250 self.pep_table = node['cols'] == 4
251 self.entry = 0
253 def visit_colspec(self, node):
254 self.entry += 1
255 if self.pep_table and self.entry == 2:
256 node['classes'].append('num')
258 def visit_row(self, node):
259 self.entry = 0
261 def visit_entry(self, node):
262 self.entry += 1
263 if self.pep_table and self.entry == 2 and len(node) == 1:
264 node['classes'].append('num')
265 p = node[0]
266 if isinstance(p, nodes.paragraph) and len(p) == 1:
267 text = p.astext()
268 try:
269 pep = int(text)
270 ref = (self.document.settings.pep_base_url
271 + self.pep_url % pep)
272 p[0] = nodes.reference(text, text, refuri=ref)
273 except ValueError:
274 pass
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:
294 replacement = ref[0]
295 else:
296 replacement_text = ref.astext().replace('@', '&#32;&#97;t&#32;')
297 replacement = nodes.raw('', replacement_text, format='html')
298 if pepno is None:
299 return replacement
300 else:
301 ref['refuri'] += '?subject=PEP%%20%s' % pepno
302 ref[:] = [replacement]
303 return ref
304 else:
305 return ref