diff options
Diffstat (limited to 'scripts/quip2html.py')
-rwxr-xr-x | scripts/quip2html.py | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/scripts/quip2html.py b/scripts/quip2html.py new file mode 100755 index 0000000..544b472 --- /dev/null +++ b/scripts/quip2html.py @@ -0,0 +1,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)) |