summaryrefslogtreecommitdiffstats
path: root/scripts/quip2html.py
blob: 544b47225c384234ecc54a5d89e0a862b7529ec7 (plain)
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
#!/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):
        self.path, self.base = os.path.split(filename)
        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))