summaryrefslogtreecommitdiffstats
path: root/scripts/quip2html.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/quip2html.py')
-rwxr-xr-xscripts/quip2html.py265
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))