Vulkan-Docs/scripts/spec_tools/html_printer.py
Jon Leech 476e3f422d Change log for March 18, 2019 Vulkan 1.1.104 spec update:
* Update release number to 104.

Public Issues:

  * Remove the incorrect line from "`Initial`" to "`Invalid`" state in the
    <<commandbuffer-lifecycle-diagram, Lifecycle of a command buffer>>
    diagram (public issue 881).
  * Add Fuchsia platform to <<boilerplate-wsi-header-table, Window System
    Extensions and Headers>> table (public pull request 933).
  * Change the type of
    slink:VkBufferDeviceAddressCreateInfoEXT::pname:deviceAddress from
    basetype:VkDeviceSize to basetype:VkDeviceAddress. These are both
    typedefs of code:uint64_t, so it is an ABI-compatible change (public
    issue 934).

Internal Issues:

  * Remove generated header files and update the CI tests to build a copy of
    the headers for use by the hpp-generate / hpp-compile CI stages. Targets
    to generate the headers will not be removed, but keeping these generated
    files in the repository increased the frequency of conflicts between
    branches when merging to master (internal issue 745).
  * Reword "`undefined: behavior if *action*" to "`must: not do *action*`"
    in the places the old terminology was used, and add a new
    <<writing-undefined, Describing Undefined Behavior>> section of the
    style guide to explain how to write such language in the future
    (internal issue 1579).
  * Move almost all Python scripts into the toplevel `scripts/` directory.
    Apply extensive internal edits to clean up and simplify the scripts, and
    try to follow PEP8 guidelines. Generalize the scripts with the use of a
    Conventions object controlling many aspects of output generation, to
    enable their use in other Khronos projects with similar requirements.
    Autogenerate extension interface refpages (these are experimental and
    may be retired going forward).

New Extensions:

  * `VK_AMD_display_native_hdr`
  * `VK_EXT_full_screen_exclusive` (internal issue 1439)
  * `VK_EXT_host_query_reset`
  * `VK_EXT_pipeline_creation_feedback` (internal issue 1560)
  * `VK_KHR_surface_protected_capabilities` (internal issue 1520)
2019-03-17 06:05:46 -07:00

455 lines
21 KiB
Python

"""Defines HTMLPrinter, a BasePrinter subclass for a single-page HTML results file."""
# Copyright (c) 2018-2019 Collabora, Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Author(s): Ryan Pavlik <ryan.pavlik@collabora.com>
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: '&#x2297;', # makeIcon('times-circle'),
MessageType.WARNING: '&#9888;', # makeIcon('exclamation-triangle'),
MessageType.NOTE: '&#x2139;' # makeIcon('info-circle')
}
LINK_ICON = '&#128279;' # 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("""<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/themes/prism.min.css" integrity="sha256-N1K43s+8twRa+tzzoF3V8EgssdDiZ6kd9r8Rfgg8kZU=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/plugins/line-numbers/prism-line-numbers.min.css" integrity="sha256-Afz2ZJtXw+OuaPX10lZHY7fN1+FuTE/KdCs+j7WZTGc=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/plugins/line-highlight/prism-line-highlight.min.css" integrity="sha256-FFGTaA49ZxFi2oUiWjxtTBqoda+t1Uw8GffYkdt9aco=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<style>
pre {
overflow-x: scroll;
white-space: nowrap;
}
</style>
<title>check_spec_links results</title>
</head>
<body>
<div class="container">
<h1><code>check_spec_links.py</code> Scan Results</h1>
""")
#
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
def close(self):
"""Write the tail end of the file and close it."""
self.f.write("""
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/prism.min.js" integrity="sha256-jc6y1s/Y+F+78EgCT/lI2lyU7ys+PFYrRSJ6q8/R8+o=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/plugins/keep-markup/prism-keep-markup.min.js" integrity="sha256-mP5i3m+wTxxOYkH+zXnKIG5oJhXLIPQYoiicCV1LpkM=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/components/prism-asciidoc.min.js" integrity="sha256-NHPE1p3VBIdXkmfbkf/S0hMA6b4Ar4TAAUlR+Rlogoc=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/plugins/line-numbers/prism-line-numbers.min.js" integrity="sha256-JfF9MVfGdRUxzT4pecjOZq6B+F5EylLQLwcQNg+6+Qk=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/plugins/line-highlight/prism-line-highlight.min.js" integrity="sha256-DEl9ZQE+lseY13oqm2+mlUr+sVI18LG813P+kzzIm8o=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.slim.min.js" integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/esm/popper.min.js" integrity="sha256-T0gPN+ySsI9ixTd/1ciLl2gjdLJLfECKvkQjJn98lOs=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
function autoExpand() {
var hash = window.location.hash;
if (!hash) {
return;
}
$(hash).parents().filter('.collapse').collapse('show');
}
window.addEventListener('hashchange', autoExpand);
$(document).ready(autoExpand);
$('.accordion').on('shown.bs.collapse', function(e) {
e.target.parentNode.scrollIntoView();
})
})
</script>
</body></html>
""")
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)
if broken_links:
broken = checker.getBrokenLinks()
if len(broken) > 0:
self.outputBrokenLinks(checker, broken)
if missing_includes:
missing = checker.getMissingUnreferencedApiIncludes()
if len(missing) > 0:
self.outputMissingIncludes(checker, missing)
self.f.write("""
<div class="container">
<h2>Excerpts of referenced files</h2>""")
for fn in self.fileRange:
self.outputFileExcerpt(fn)
self.f.write('</div><!-- .container -->\n')
def outputChecker(self, checker):
"""Output the contents of a MacroChecker object.
Starts and ends the accordion populated by outputCheckerFile().
"""
self.f.write(
'<div class="container"><h2>Per-File Warnings and Errors</h2>\n')
self.f.write('<div class="accordion" id="fileAccordion">\n')
super(HTMLPrinter, self).outputChecker(checker)
self.f.write("""</div><!-- #fileAccordion -->
</div><!-- .container -->\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("""
<div class="card">
<div class="card-header" id="{id}-file-heading">
<div class="row">
<div class="col">
<button data-target="#collapse-{id}" class="btn btn-link btn-primary mb-0 collapsed" type="button" data-toggle="collapse" aria-expanded="false" aria-controls="collapse-{id}">
{relativefn}
</button>
</div>
""".format(id=self.makeIdentifierFromFilename(fileChecker.filename), relativefn=html.escape(self.getRelativeFilename(fileChecker.filename))))
self.f.write('<div class="col-1">')
warnings = fileChecker.numMessagesOfType(MessageType.WARNING)
if warnings > 0:
self.f.write("""<span class="badge badge-warning" data-toggle="tooltip" title="{num} warnings in this file">
{icon}
{num}<span class="sr-only"> warnings</span></span>""".format(num=warnings, icon=MESSAGE_TYPE_ICONS[MessageType.WARNING]))
self.f.write('</div>\n<div class="col-1">')
errors = fileChecker.numMessagesOfType(MessageType.ERROR)
if errors > 0:
self.f.write("""<span class="badge badge-danger" data-toggle="tooltip" title="{num} errors in this file">
{icon}
{num}<span class="sr-only"> errors</span></span>""".format(num=errors, icon=MESSAGE_TYPE_ICONS[MessageType.ERROR]))
self.f.write("""
</div><!-- .col-1 -->
</div><!-- .row -->
</div><!-- .card-header -->
<div id="collapse-{id}" class="collapse" aria-labelledby="{id}-file-heading" data-parent="#fileAccordion">
<div class="card-body">
""".format(
linkicon=LINK_ICON,
id=self.makeIdentifierFromFilename(fileChecker.filename), fn=html.escape(fileChecker.filename)))
super(HTMLPrinter, self).outputCheckerFile(fileChecker)
self.f.write("""
</div><!-- .card-body -->
</div><!-- .collapse -->
</div><!-- .card -->
<!-- ..................................... -->
""".format(id=self.makeIdentifierFromFilename(fileChecker.filename)))
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("""
<div class="card">
<div class="card-body">
<h5 class="card-header bg bg-{style}" id="{anchor}">{icon} {t} Line {lineNum}, Column {col} (-{arg})</h5>
<p class="card-text">
""".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('<br/>')
for line in msg.message:
self.f.write(html.escape(line))
self.f.write('<br />\n')
self.f.write('</p>\n')
if msg.see_also is not None and len(msg.see_also) != 0:
self.f.write('<p>See also:</p><ul>\n')
for see in msg.see_also:
if isinstance(see, MessageContext):
self.f.write(
'<li>{}</li>\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('<li>{}</li>\n'.format(self.formatBrief(see)))
self.f.write('</ul>')
if msg.replacement is not None:
self.f.write(
'<div class="alert alert-primary">Hover the highlight text to view suggested replacement.</div>')
if msg.fix is not None:
self.f.write(
'<div class="alert alert-info">Note: Auto-fix available.</div>')
if msg.script_location:
self.f.write(
'<p>Message originated at <code>{}</code></p>'.format(msg.script_location))
self.f.write('<pre class="line-numbers language-asciidoc" data-start="{}"><code>'.format(
msg.context.lineNum))
highlightStart, highlightEnd = getHighlightedRange(msg.context)
self.f.write(html.escape(msg.context.line[:highlightStart]))
self.f.write(
'<span class="border border-{}"'.format(MESSAGE_TYPE_STYLES[msg.message_type]))
if msg.replacement is not None:
self.f.write(
' data-toggle="tooltip" title="{}"'.format(msg.replacement))
self.f.write('>')
self.f.write(html.escape(
msg.context.line[highlightStart:highlightEnd]))
self.f.write('</span>')
self.f.write(html.escape(msg.context.line[highlightEnd:]))
self.f.write('</code></pre></div></div>')
def outputBrokenLinks(self, checker, broken):
"""Output a table of broken links.
Called by self.outputResults().
"""
self.f.write("""
<div class="container">
<h2>Missing Referenced API Includes</h2>
<p>Items here have been referenced by a linking macro, so these are all broken links in the spec!</p>
<table class="table table-striped">
<thead>
<th scope="col">Add line to include this file</th>
<th scope="col">or add this macro instead</th>
<th scope="col">Links to this entity</th></thead>
""")
for entity in sorted(broken):
uses = broken[entity]
category = checker.findEntity(entity).category
anchor = self.getUniqueAnchor()
asciidocAnchor = '[[{}]]'.format(entity)
include = generateInclude(dir_traverse='../../generated/',
generated_type='api',
category=category,
entity=entity)
self.f.write("""
<tr id={}>
<td><code class="text-dark language-asciidoc">{}</code></td>
<td><code class="text-dark">{}</code></td>
<td><ul class="list-inline">
""".format(anchor, include, asciidocAnchor))
for context in uses:
self.f.write(
'<li class="list-inline-item">{}</li>'.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("""</ul></td></tr>""")
self.f.write("""</table></div>""")
def outputMissingIncludes(self, checker, missing):
"""Output a table of missing includes.
Called by self.outputResults().
"""
self.f.write("""
<div class="container">
<h2>Missing Unreferenced API Includes</h2>
<p>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 <code>check_spec_links.py</code> logic computing which entities get generated files.</p>
<table class="table table-striped">
<thead>
<th scope="col">Add line to include this file</th>
<th scope="col">or add this macro instead</th>
""")
for entity in sorted(missing):
fn = checker.findEntity(entity).filename
anchor = '[[{}]]'.format(entity)
self.f.write("""
<tr>
<td><code class="text-dark">{filename}</code></td>
<td><code class="text-dark">{anchor}</code></td>
""".format(filename=fn, anchor=anchor))
self.f.write("""</table></div>""")
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("""<div class="card">
<div class="card-header" id="heading-{id}"><h5 class="mb-0">
<button class="btn btn-link" type="button">
{fn}
</button></h5></div><!-- #heading-{id} -->
<div class="card-body">
""".format(id=self.makeIdentifierFromFilename(filename), fn=self.getRelativeFilename(filename)))
lines = self.fileLines[filename]
r = self.fileRange[filename]
self.f.write("""<pre class="line-numbers language-asciidoc line-highlight" id="excerpt-{id}" data-start="{start}"><code>""".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('<a href="#{target}" title="{title}" data-toggle="tooltip" data-container="body">{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('<span class="sr-only">Cross reference: {t} {title}</span>'.format(
title=html.escape(link.tooltip, False), t=link.message_type))
# self.f.write('</a>')
# Write the actual character
self.f.write(html.escape(char))
self.f.write('\n')
self.f.write('</code></pre>')
self.f.write('</div><!-- .card-body -->\n')
self.f.write('</div><!-- .card -->\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: <a href="{href}">{icon}{relative}:{lineNum}:{col}</a>'.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