1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
|
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2017 The Qt Company Ltd.
# Contact: http://www.qt.io/licensing/
#
# You may use this file under the terms of the CC0 license.
# See the file LICENSE.CC0 from this package for details.
"""Qt's Utilitarian Improvement Process (QUIP) processing
Adapted from the python project's similar code for PEPs.
"""
from __future__ import absolute_import, with_statement
from __future__ import print_function
__docformat__ = 'reStructuredText'
import os, sys, subprocess
from docutils import core, nodes, utils, languages, DataError
from docutils.readers import standalone
from docutils.transforms import Transform, frontmatter, parts
from docutils.parsers import rst
from docutils.writers import html4css1
class QuipHeaders(Transform):
"""Refines the table displaying RFC 2822 headers
Checks all mandatory headers are present, inserts Version and
Last-Modified (in that order, after Title).
FIXME: Version should probably be called Commit, as it's a
commit-id, not any sort of sequential version number, which is
what the name "Version" is apt to conjure up in the reader's mind.
"""
default_priority = 360
@staticmethod
def raw_node(text):
return nodes.raw('', text, format='html')
@staticmethod
def field_name(text):
text = '-'.join(x.capitalize() for x in text.split('-'))
return nodes.field_name('', text, format='html')
@staticmethod
def field_body(text):
return nodes.field_body('', nodes.paragraph('', text), format='html')
@classmethod
def mask_email(cls, ref):
"""
Mask the email address in `ref` and return a replacement node.
`ref` is returned unchanged if it contains no email address.
For email addresses such as 'user@host', mask the address as 'user
at host' (text) to thwart simple email address harvesters.
"""
if ref.hasattr('refuri') and ref['refuri'].startswith('mailto:'):
return cls.raw_node(ref.astext().replace('@', ' at '))
return ref
# Populated by main():
git_data = {}
def apply(self):
# QUIP 1 says these should be in this order - not checked.
required = ['quip', 'title', 'author', 'status', 'type', 'created',
'post-history',
]
insertor = None # index after which to insert git_data entries
for index, field in enumerate(self.document[0]):
name = field[0].astext().lower()
body = field[1]
if name in required:
required.remove(name)
# else: optional fields - could check whether recognized
if name == 'title' and insertor is None:
insertor = index
if len(body) > 1:
raise DataError(
'QUIP header field body contains several elements:\n%s'
% field.pformat(level=1))
elif name in self.git_data:
sys.stderr.write('Over-writing %s header content: %s\n'
% (field[0].astext(), body.astext()))
body.replace_self(self.field_body(self.git_data[name]))
del self.git_data[name]
if name == 'version': # appears before last-modified
insertor = index
elif len(body): # NB: body is a Node, true even if empty
para, = body
if not isinstance(para, nodes.paragraph):
raise DataError(
'QUIP header field body may only contain a single paragraph:\n%s'
% field.pformat(level=1))
elif name == 'quip':
try:
int(body.astext()) # evaluate to check it *is* an int ...
except (ValueError, TypeError):
raise DataError(
'QUIP header QUIP has non-numeric value: ' + body.astext())
elif name == 'author':
for node in para:
if isinstance(node, nodes.reference):
node.replace_self(self.mask_email(node))
# Check for missing mandatory field
if required:
sys.stderr.write('Missing mandatory header(s): ' + ', '.join(required) + '\n')
# raise DataError() instead, once post-history is fixed in all QUIPs
# Insert any git_data fields not replaced on the way:
for key in reversed(sorted(self.git_data)):
value = self.git_data[key]
field = self.document[0][insertor].deepcopy()
insertor += 1
field[0].replace_self(self.field_name(key))
field[1].replace_self(self.field_body(value))
self.document[0].insert(insertor, field)
class QuipContents(Transform):
"""
Insert an empty table of contents topic and a transform placeholder into
the document after the RFC 2822 header.
"""
default_priority = 380
def apply(self):
language = languages.get_language(self.document.settings.language_code,
self.document.reporter)
name = language.labels['contents']
title = nodes.title('', name)
topic = nodes.topic('', title, classes=['contents'])
name = nodes.fully_normalize_name(name)
if not self.document.has_name(name):
topic['names'].append(name)
self.document.note_implicit_target(topic)
pending = nodes.pending(parts.Contents)
topic += pending
self.document.insert(1, topic)
self.document.note_pending(pending)
class QuipReader(standalone.Reader):
# Based on PEP reader: docutils.readers.pep
inliner_class = rst.states.Inliner
def get_transforms(self):
transforms = standalone.Reader.get_transforms(self)
# We have QUIP-specific frontmatter handling.
transforms.remove(frontmatter.DocTitle)
transforms.remove(frontmatter.SectionSubTitle)
transforms.remove(frontmatter.DocInfo)
transforms.extend([QuipHeaders, QuipContents])
return transforms
def __init__(self, parser=None, parser_name=None):
"""`parser` should be ``None``."""
if parser is None:
parser = rst.Parser(rfc2822=True, inliner=self.inliner_class())
standalone.Reader.__init__(self, parser, '')
class QuipWriter(html4css1.Writer):
# Assumed to be run from the build directory
cwd_file = os.path.join(os.getcwd(), 'dummy')
script_dir = os.path.dirname(__file__)
default_template = 'template.html'
default_stylesheet = utils.relative_path(
cwd_file, os.path.join(script_dir, '../docutils/quip.css'))
# Can we arrange to just use /usr/share/docutils/writers/pep_html/pep.css instead ?
del cwd_file, script_dir
# Used by base-class somehow ?
settings_default_overrides = {
'stylesheet_path': default_stylesheet, # => subs['stylesheet'], below
'template': default_template,
}
del default_template, default_stylesheet
relative_path_settings = ('template',)
def interpolation_dict(self):
"""Set up the substitions in our template
The template can contain tokens of form %(name)s which get
expanded to subs[name] expressed as a string, for various
names. The base-class's version of this provides 'stylesheet'
as one key of the resulting dictionary, populated from the
settings_default_overrides['stylesheet_path'] above.
"""
subs = html4css1.Writer.interpolation_dict(self)
settings = self.document.settings
# Presently unused:
# subs['qthome'] = 'http://www.qt.io'
# subs['quiphome'] = 'http://quips.qt.io'
# subs['quipindex'] = 'http://quips.qt.io'
index = self.document.first_child_matching_class(nodes.field_list)
header = self.document[index]
self.quipnum = header[0][1].astext()
subs['quip'] = self.quipnum
try:
subs['quipnum'] = '%04i' % int(self.quipnum)
except ValueError:
subs['quipnum'] = self.quipnum
self.title = header[1][1].astext()
subs['title'] = 'QUIP %s | %s' % (self.quipnum, self.title)
subs['body'] = ''.join(self.body_pre_docinfo + self.docinfo + self.body)
return subs
def assemble_parts(self):
html4css1.Writer.assemble_parts(self)
self.parts['title'] = [self.title]
self.parts['quipnum'] = self.quipnum
class GitRunner(object):
def __init__(self, filename, stderr):
path, self.base = os.path.split(filename)
self.path = os.path.abspath(path)
self.stderr = stderr
def run(self, args):
proc = subprocess.Popen(['git'] + args,
stderr=self.stderr, stdout=subprocess.PIPE,
cwd=self.path, universal_newlines=True)
line = proc.stdout.readline()
proc.wait()
return line
def main(filename, source, output, stderr):
git = GitRunner(filename, stderr)
data = QuipHeaders.git_data
data['version'] = git.run(['rev-list', 'HEAD', '-1', '--', git.base]).strip()
# Could use %aI for author (ISO) date, rather than commit date (%c):
data['last-modified'] = git.run(['show', '-s', '--pretty=format:%cI',
data['version']]).split('T')[0]
try:
output.write(core.publish_string(
source=source.read(),
reader=QuipReader(),
parser_name='restructuredtext',
writer_name='html4css1',
writer=QuipWriter(),
# Allow Docutils traceback if there's an exception:
settings_overrides={'traceback': True, 'halt_level': 2}))
except DataError as what:
stderr.write('\n'.join(what.args) + '\nin %s\n' % filename)
return 1
return 0
if __name__ == '__main__':
if len(sys.argv) < 2 or sys.argv[1] == '-':
here = os.path.split(sys.argv[0])[0]
sys.exit(main(os.path.join(here, 'gen-quip-0000.py'),
sys.stdin, sys.stdout, sys.stderr))
else:
with open(sys.argv[1]) as fd:
sys.exit(main(sys.argv[1], fd, sys.stdout, sys.stderr))
|