"""Defines HTMLPrinter, a BasePrinter subclass for a single-page HTML results file.""" # Copyright (c) 2018-2019 Collabora, Ltd. # # SPDX-License-Identifier: Apache-2.0 # # Author(s): Ryan Pavlik import html import re from collections import namedtuple from .base_printer import BasePrinter, getColumn from .shared import (MessageContext, MessageType, generateInclude, getHighlightedRange) # Bootstrap styles (for constructing CSS class names) associated with MessageType values. MESSAGE_TYPE_STYLES = { MessageType.ERROR: 'danger', MessageType.WARNING: 'warning', MessageType.NOTE: 'secondary' } # HTML Entity for a little emoji-icon associated with MessageType values. MESSAGE_TYPE_ICONS = { MessageType.ERROR: '⊗', # makeIcon('times-circle'), MessageType.WARNING: '⚠', # makeIcon('exclamation-triangle'), MessageType.NOTE: 'ℹ' # makeIcon('info-circle') } LINK_ICON = '🔗' # link icon class HTMLPrinter(BasePrinter): """Implementation of BasePrinter for generating diagnostic reports in HTML format. Generates a single file containing neatly-formatted messages. The HTML file loads Bootstrap 4 as well as 'prism' syntax highlighting from CDN. """ def __init__(self, filename): """Construct by opening the file.""" self.f = open(filename, 'w', encoding='utf-8') self.f.write(""" check_spec_links results

check_spec_links.py Scan Results

""") # self.filenameTransformer = re.compile(r'[^\w]+') self.fileRange = {} self.fileLines = {} self.backLink = namedtuple( 'BackLink', ['lineNum', 'col', 'end_col', 'target', 'tooltip', 'message_type']) self.fileBackLinks = {} self.nextAnchor = 0 super().__init__() def close(self): """Write the tail end of the file and close it.""" self.f.write("""
""") self.f.close() ### # Output methods: these all write to the HTML file. def outputResults(self, checker, broken_links=True, missing_includes=False): """Output the full results of a checker run. Includes the diagnostics, broken links (if desired), missing includes (if desired), and excerpts of all files with diagnostics. """ self.output(checker) self.outputBrokenAndMissing( checker, broken_links=broken_links, missing_includes=missing_includes) self.f.write("""

Excerpts of referenced files

""") for fn in self.fileRange: self.outputFileExcerpt(fn) self.f.write('
\n') def outputChecker(self, checker): """Output the contents of a MacroChecker object. Starts and ends the accordion populated by outputCheckerFile(). """ self.f.write( '

Per-File Warnings and Errors

\n') self.f.write('
\n') super(HTMLPrinter, self).outputChecker(checker) self.f.write("""
\n""") def outputCheckerFile(self, fileChecker): """Output the contents of a MacroCheckerFile object. Stashes the lines of the file for later excerpts, and outputs any diagnostics in an accordion card. """ # Save lines for later self.fileLines[fileChecker.filename] = fileChecker.lines if not fileChecker.numDiagnostics(): return self.f.write("""
""".format(id=self.makeIdentifierFromFilename(fileChecker.filename), relativefn=html.escape(self.getRelativeFilename(fileChecker.filename)))) self.f.write('
') warnings = fileChecker.numMessagesOfType(MessageType.WARNING) if warnings > 0: self.f.write(""" {icon} {num} warnings""".format(num=warnings, icon=MESSAGE_TYPE_ICONS[MessageType.WARNING])) self.f.write('
\n
') errors = fileChecker.numMessagesOfType(MessageType.ERROR) if errors > 0: self.f.write(""" {icon} {num} errors""".format(num=errors, icon=MESSAGE_TYPE_ICONS[MessageType.ERROR])) self.f.write("""
""".format(id=self.makeIdentifierFromFilename(fileChecker.filename))) super(HTMLPrinter, self).outputCheckerFile(fileChecker) self.f.write("""
""") def outputMessage(self, msg): """Output a Message.""" anchor = self.getUniqueAnchor() self.recordUsage(msg.context, linkBackTarget=anchor, linkBackTooltip='{}: {} [...]'.format( msg.message_type, msg.message[0]), linkBackType=msg.message_type) self.f.write("""
{icon} {t} Line {lineNum}, Column {col} (-{arg})

""".format( anchor=anchor, icon=MESSAGE_TYPE_ICONS[msg.message_type], style=MESSAGE_TYPE_STYLES[msg.message_type], t=self.formatBrief(msg.message_type), lineNum=msg.context.lineNum, col=getColumn(msg.context), arg=msg.message_id.enable_arg())) self.f.write(self.formatContext(msg.context)) self.f.write('
') for line in msg.message: self.f.write(html.escape(line)) self.f.write('
\n') self.f.write('

\n') if msg.see_also: self.f.write('

See also:

    \n') for see in msg.see_also: if isinstance(see, MessageContext): self.f.write( '
  • {}
  • \n'.format(self.formatContext(see))) self.recordUsage(see, linkBackTarget=anchor, linkBackType=MessageType.NOTE, linkBackTooltip='see-also associated with {} at {}'.format(msg.message_type, self.formatContextBrief(see))) else: self.f.write('
  • {}
  • \n'.format(self.formatBrief(see))) self.f.write('
') if msg.replacement is not None: self.f.write( '
Hover the highlight text to view suggested replacement.
') if msg.fix is not None: self.f.write( '
Note: Auto-fix available.
') if msg.script_location: self.f.write( '

Message originated at {}

'.format(msg.script_location)) self.f.write('
'.format(
            msg.context.lineNum))
        highlightStart, highlightEnd = getHighlightedRange(msg.context)
        self.f.write(html.escape(msg.context.line[:highlightStart]))
        self.f.write(
            '')
        self.f.write(html.escape(
            msg.context.line[highlightStart:highlightEnd]))
        self.f.write('')
        self.f.write(html.escape(msg.context.line[highlightEnd:]))
        self.f.write('
') def outputBrokenLinks(self, checker, broken): """Output a table of broken links. Called by self.outputBrokenAndMissing() if requested. """ self.f.write("""

Missing Referenced API Includes

Items here have been referenced by a linking macro, so these are all broken links in the spec!

""") for entity_name, uses in sorted(broken.items()): category = checker.findEntity(entity_name).category anchor = self.getUniqueAnchor() asciidocAnchor = '[[{}]]'.format(entity_name) include = generateInclude(dir_traverse='../../generated/', generated_type='api', category=category, entity=entity_name) self.f.write(""" """) self.f.write("""
Add line to include this file or add this macro instead Links to this entity
{} {}
    """.format(anchor, include, asciidocAnchor)) for context in uses: self.f.write( '
  • {}
  • '.format(self.formatContext(context, MessageType.NOTE))) self.recordUsage( context, linkBackTooltip='Link broken in spec: {} not seen'.format( include), linkBackTarget=anchor, linkBackType=MessageType.NOTE) self.f.write("""
""") def outputMissingIncludes(self, checker, missing): """Output a table of missing includes. Called by self.outputBrokenAndMissing() if requested. """ self.f.write("""

Missing Unreferenced API Includes

These items are expected to be generated in the spec build process, but aren't included. However, as they also are not referenced by any linking macros, they aren't broken links - at worst they are undocumented entities, at best they are errors in check_spec_links.py logic computing which entities get generated files.

""") for entity in sorted(missing): fn = checker.findEntity(entity).filename anchor = '[[{}]]'.format(entity) self.f.write(""" """.format(filename=fn, anchor=anchor)) self.f.write("""
Add line to include this file or add this macro instead
{filename} {anchor}
""") def outputFileExcerpt(self, filename): """Output a card containing an excerpt of a file, sufficient to show locations of all diagnostics plus some context. Called by self.outputResults(). """ self.f.write("""
""".format(id=self.makeIdentifierFromFilename(filename), fn=self.getRelativeFilename(filename))) lines = self.fileLines[filename] r = self.fileRange[filename] self.f.write("""
""".format(
            id=self.makeIdentifierFromFilename(filename),
            start=r.start))
        for lineNum, line in enumerate(
                lines[(r.start - 1):(r.stop - 1)], r.start):
            # self.f.write(line)
            lineLinks = [x for x in self.fileBackLinks[filename]
                         if x.lineNum == lineNum]
            for col, char in enumerate(line):
                colLinks = (x for x in lineLinks if x.col == col)
                for link in colLinks:
                    # TODO right now the syntax highlighting is interfering with the link! so the link-generation is commented out,
                    # only generating the emoji icon.

                    # self.f.write('{icon}'.format(
                    # target=link.target, title=html.escape(link.tooltip),
                    # icon=MESSAGE_TYPE_ICONS[link.message_type]))
                    self.f.write(MESSAGE_TYPE_ICONS[link.message_type])
                    self.f.write('Cross reference: {t} {title}'.format(
                        title=html.escape(link.tooltip, False), t=link.message_type))

                    # self.f.write('')

                # Write the actual character
                self.f.write(html.escape(char))
            self.f.write('\n')

        self.f.write('
') self.f.write('
\n') self.f.write('
\n') def outputFallback(self, obj): """Output some text in a general way.""" self.f.write(obj) ### # Format method: return a string. def formatContext(self, context, message_type=None): """Format a message context in a verbose way.""" if message_type is None: icon = LINK_ICON else: icon = MESSAGE_TYPE_ICONS[message_type] return 'In context: {icon}{relative}:{lineNum}:{col}'.format( href=self.getAnchorLinkForContext(context), icon=icon, # id=self.makeIdentifierFromFilename(context.filename), relative=self.getRelativeFilename(context.filename), lineNum=context.lineNum, col=getColumn(context)) ### # Internal methods: not mandated by parent class. def recordUsage(self, context, linkBackTooltip=None, linkBackTarget=None, linkBackType=MessageType.NOTE): """Internally record a 'usage' of something. Increases the range of lines that are included in the excerpts, and records back-links if appropriate. """ BEFORE_CONTEXT = 6 AFTER_CONTEXT = 3 # Clamp because we need accurate start line number to make line number # display right start = max(1, context.lineNum - BEFORE_CONTEXT) stop = context.lineNum + AFTER_CONTEXT + 1 if context.filename not in self.fileRange: self.fileRange[context.filename] = range(start, stop) self.fileBackLinks[context.filename] = [] else: oldRange = self.fileRange[context.filename] self.fileRange[context.filename] = range( min(start, oldRange.start), max(stop, oldRange.stop)) if linkBackTarget is not None: start_col, end_col = getHighlightedRange(context) self.fileBackLinks[context.filename].append(self.backLink( lineNum=context.lineNum, col=start_col, end_col=end_col, target=linkBackTarget, tooltip=linkBackTooltip, message_type=linkBackType)) def makeIdentifierFromFilename(self, fn): """Compute an acceptable HTML anchor name from a filename.""" return self.filenameTransformer.sub('_', self.getRelativeFilename(fn)) def getAnchorLinkForContext(self, context): """Compute the anchor link to the excerpt for a MessageContext.""" return '#excerpt-{}.{}'.format( self.makeIdentifierFromFilename(context.filename), context.lineNum) def getUniqueAnchor(self): """Create and return a new unique string usable as a link anchor.""" anchor = 'anchor-{}'.format(self.nextAnchor) self.nextAnchor += 1 return anchor