| 1 | #!/usr/bin/env python2
 | 
| 2 | from __future__ import print_function
 | 
| 3 | """fmt_check.py
 | 
| 4 | 
 | 
| 5 | Check that the output HTML obeys the following rules:
 | 
| 6 | 
 | 
| 7 |  - No orphaned backticks '`' should be part of a `inline code block`
 | 
| 8 |    (ie. any backticks not in a <code> block is treated as an error)
 | 
| 9 | """
 | 
| 10 | 
 | 
| 11 | import HTMLParser
 | 
| 12 | import sys
 | 
| 13 | 
 | 
| 14 | from doctools.util import log
 | 
| 15 | 
 | 
| 16 | 
 | 
| 17 | class TagAwareHTMLParser(HTMLParser.HTMLParser):
 | 
| 18 | 
 | 
| 19 |     def __init__(self, file):
 | 
| 20 |         HTMLParser.HTMLParser.__init__(self)
 | 
| 21 |         self.tag_stack = []
 | 
| 22 |         self.file = file
 | 
| 23 | 
 | 
| 24 |     def location_str(self):
 | 
| 25 |         line, col = self.getpos()
 | 
| 26 |         return '%s:%d:%d' % (self.file, line, col)
 | 
| 27 | 
 | 
| 28 |     def handle_starttag(self, tag, _attrs):
 | 
| 29 |         # Skip self-closing elements
 | 
| 30 |         if tag in ('meta', 'img'):
 | 
| 31 |             return
 | 
| 32 | 
 | 
| 33 |         self.tag_stack.append(tag)
 | 
| 34 | 
 | 
| 35 |     def handle_endtag(self, tag):
 | 
| 36 |         popped = self.tag_stack.pop()
 | 
| 37 |         if tag != popped:
 | 
| 38 |             print('%s [WARN] Mismatched tag!' % self.location_str(),
 | 
| 39 |                   'Expected </%s> but got </%s>' % (popped, tag))
 | 
| 40 | 
 | 
| 41 | 
 | 
| 42 | class CheckBackticks(TagAwareHTMLParser):
 | 
| 43 | 
 | 
| 44 |     def __init__(self, file):
 | 
| 45 |         TagAwareHTMLParser.__init__(self, file)
 | 
| 46 |         self.has_error = False
 | 
| 47 | 
 | 
| 48 |     def handle_data(self, text):
 | 
| 49 |         # Ignore eg, <code> tags
 | 
| 50 |         if len(self.tag_stack) and (self.tag_stack[-1]
 | 
| 51 |                                     not in ("p", "h1", "h2", "h3", "a")):
 | 
| 52 |             return
 | 
| 53 | 
 | 
| 54 |         idx = text.find('`')
 | 
| 55 |         if idx == -1:
 | 
| 56 |             return
 | 
| 57 | 
 | 
| 58 |         print('%s [ERROR] Found stray backtick %r' %
 | 
| 59 |               (self.location_str(), text))
 | 
| 60 | 
 | 
| 61 |         self.has_error = True
 | 
| 62 | 
 | 
| 63 | 
 | 
| 64 | def FormatCheck(filename):
 | 
| 65 |     backticks = CheckBackticks(filename)
 | 
| 66 |     with open(filename, "r") as f:
 | 
| 67 |         backticks.feed(f.read())
 | 
| 68 | 
 | 
| 69 |     return backticks.has_error
 | 
| 70 | 
 | 
| 71 | 
 | 
| 72 | def main(argv):
 | 
| 73 |     any_error = False
 | 
| 74 |     for path in argv[1:]:
 | 
| 75 |         if not path.endswith('.html'):
 | 
| 76 |             raise RuntimeError('Expected %r to be a .html file' % path)
 | 
| 77 | 
 | 
| 78 |         this_error = FormatCheck(path)
 | 
| 79 |         any_error = any_error or this_error
 | 
| 80 |         log("%s %s" % ("ER" if this_error else "OK", path))
 | 
| 81 | 
 | 
| 82 |     if any_error:
 | 
| 83 |         raise RuntimeError("Formatting errors found")
 | 
| 84 | 
 | 
| 85 | 
 | 
| 86 | if __name__ == '__main__':
 | 
| 87 |     try:
 | 
| 88 |         main(sys.argv)
 | 
| 89 |     except RuntimeError as e:
 | 
| 90 |         print('FATAL: %s' % e, file=sys.stderr)
 | 
| 91 |         sys.exit(1)
 |