WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Owner, verbatim (2026-08-30), about the reinforcing report:
|
||||
# "最大的,最上面的部分是target跟doner。其余的部分都跟在后面,小一点,并且做成
|
||||
# 可以折叠的样子。"
|
||||
# "这里面其实有大量的我不在乎的信息。除非有啥问题我感觉我不会读。这种东西都
|
||||
# 给做成默认折叠。"
|
||||
"""Reports that fold: a headline you read, and detail that opens only when it
|
||||
has a problem in it. No Revit here, and nothing report-specific either.
|
||||
|
||||
THE RULE THIS IMPLEMENTS, AND WHOSE IT IS
|
||||
------------------------------------------
|
||||
The owner, 2026-08-30, about the reinforcing report:
|
||||
|
||||
"The biggest, top-most part is the target and the donor. Everything
|
||||
else follows after, smaller, and made foldable."
|
||||
"There is actually a large amount of information in here that I do not
|
||||
care about. Unless something is wrong I don't think I will read it.
|
||||
Make this kind of thing default-folded."
|
||||
|
||||
Which is a stronger statement than a layout preference, and it is written down
|
||||
here rather than in one button because it generalises to every report this
|
||||
tool prints:
|
||||
|
||||
* THE ANSWER IS THE HEADLINE. Biggest, first, never folded.
|
||||
* EVERYTHING ELSE STARTS CLOSED. He will not read it and does not want to
|
||||
see it.
|
||||
* A SECTION THAT CARRIES A PROBLEM OPENS ITSELF. A check that did not run, a
|
||||
caveat that actually bites, a gap somebody has to act on. The reader's
|
||||
contract becomes: **if nothing below the headline is open, nothing needs
|
||||
me.** That contract is the whole value, and it is worth more than any
|
||||
individual section being visible -- which is why `problem` is a SENTENCE
|
||||
naming what is wrong, not a boolean: a section that opens has to say why
|
||||
it opened.
|
||||
* FOLDED IS NOT GONE. Every closed section still prints a one-line header
|
||||
saying what is inside it and how much. Nothing is hidden; things are
|
||||
folded. A caveat that has been removed from the page is a caveat the tool
|
||||
has decided the reader does not need, and this project has already paid
|
||||
for one of those.
|
||||
|
||||
WHAT THE OUTPUT PANEL ACTUALLY IS, AND WHY THE HTML LOOKS DATED
|
||||
----------------------------------------------------------------
|
||||
pyRevit's output window is a `System.Windows.Forms.WebBrowser`
|
||||
(`pyrevit/output/__init__.py`, line 106) -- the IE engine hosted in a WinForms
|
||||
control, NOT a modern browser and not the environment the panellift HTML
|
||||
report renders in. A WinForms WebBrowser runs in IE7 document mode unless the
|
||||
host process has a `FEATURE_BROWSER_EMULATION` registry entry, and pyRevit
|
||||
sets none. So:
|
||||
|
||||
* `<details>` / `<summary>` DO NOT EXIST here. They are HTML5. This is the
|
||||
trap, because they are the obvious answer and they fail silently -- an
|
||||
unknown element renders as an inline span and the "collapsed" content is
|
||||
simply always visible.
|
||||
* `addEventListener`, `classList`, `querySelector` are all out.
|
||||
* `document.getElementById(id).style.display` and an inline `onclick`
|
||||
attribute are IE5-era DOM and work in every mode. That is what this uses.
|
||||
|
||||
JavaScript definitely runs in the panel: pyRevit's own charts feature injects
|
||||
Chart.js into it (`coreutils/charts.py`). So the toggle should work. But it
|
||||
has NOT been exercised inside Revit by the agent that wrote it -- there was no
|
||||
bridge available -- so the failure is designed for rather than hoped against:
|
||||
|
||||
* A PROBLEM SECTION IS NEVER FOLDED IN THE FIRST PLACE. It renders open, as
|
||||
plain HTML, with no dependence on the script running at all. If the toggle
|
||||
is dead, every section that matters is still on the page.
|
||||
* A folded section that will not open loses nothing either, as long as the
|
||||
caller writes the full text out as well -- `text(sections, show_all=True)`
|
||||
is that, and `reinforcereport` hands the button a path to say so in the
|
||||
footer.
|
||||
|
||||
THE BLOCK SHAPE
|
||||
---------------
|
||||
A section holds `('md', text)` and `('table', columns, rows)` tuples, the same
|
||||
two the reports already produce. Markdown, not HTML, because the same sections
|
||||
have to render as plain text at a command line -- `tools/reinforce_offline.py`
|
||||
has no panel -- and a report that became HTML-only would have to be written
|
||||
twice and would drift.
|
||||
|
||||
IronPython 2.7: no f-strings, no pathlib, no dataclasses.
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
MD = 'md'
|
||||
TABLE = 'table'
|
||||
|
||||
|
||||
class Section(object):
|
||||
"""One foldable part of a report.
|
||||
|
||||
`key` an id, unique on the page. Used for the toggle's element id.
|
||||
`title` the one-line header. ALWAYS shown, open or closed.
|
||||
`summary` what is inside, in numbers, shown on the header when closed.
|
||||
A header that says only "details" tells the reader nothing
|
||||
about whether to open it.
|
||||
`blocks` ('md', text) / ('table', columns, rows).
|
||||
`problem` None for routine. A SENTENCE when something in here needs the
|
||||
reader -- it is printed on the header and it forces the
|
||||
section open.
|
||||
`headline` True for the part that is the answer: printed big, first, and
|
||||
never given a fold at all.
|
||||
"""
|
||||
|
||||
def __init__(self, key, title, summary=None, blocks=None, problem=None,
|
||||
headline=False):
|
||||
self.key = key
|
||||
self.title = title
|
||||
self.summary = summary
|
||||
self.blocks = list(blocks or [])
|
||||
self.problem = problem or None
|
||||
self.headline = bool(headline)
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
"""Open when it is the answer, or when it has a problem in it."""
|
||||
return self.headline or bool(self.problem)
|
||||
|
||||
def add(self, block):
|
||||
self.blocks.append(block)
|
||||
return self
|
||||
|
||||
def md(self, text):
|
||||
return self.add((MD, text))
|
||||
|
||||
def table(self, columns, rows):
|
||||
return self.add((TABLE, columns, rows))
|
||||
|
||||
def __repr__(self):
|
||||
return '<Section {0} {1}>'.format(
|
||||
self.key, 'OPEN' if self.is_open else 'folded')
|
||||
|
||||
|
||||
def sections_with_problems(sections):
|
||||
"""The ones that opened themselves, in order. What the reader must see."""
|
||||
return [s for s in sections if s.problem]
|
||||
|
||||
|
||||
def problem_count(sections):
|
||||
return len(sections_with_problems(sections))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ as blocks
|
||||
|
||||
def blocks(sections, show_all=False):
|
||||
"""Every section flattened to blocks, folding the routine ones.
|
||||
|
||||
A folded section becomes ONE line -- its title, its summary and nothing
|
||||
else. `show_all` prints everything, which is what a file written for the
|
||||
record wants and what `--all` gives at a command line.
|
||||
"""
|
||||
out = []
|
||||
for section in sections or []:
|
||||
if section.headline:
|
||||
out.extend(section.blocks)
|
||||
continue
|
||||
if section.is_open or show_all:
|
||||
out.append((MD, _open_header(section)))
|
||||
out.extend(section.blocks)
|
||||
else:
|
||||
out.append((MD, _folded_header(section)))
|
||||
return out
|
||||
|
||||
|
||||
def _open_header(section):
|
||||
head = '## ' + _text(section.title)
|
||||
if section.problem:
|
||||
head += ' — ' + _text(section.problem)
|
||||
return head
|
||||
|
||||
|
||||
def _folded_header(section):
|
||||
bits = ['**' + _text(section.title) + '**']
|
||||
if section.summary:
|
||||
bits.append(_text(section.summary))
|
||||
return '- ' + _text(' — ').join(bits) + ' _(folded)_'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- as text
|
||||
|
||||
def _text(value):
|
||||
"""`value` as the interpreter's own text type -- never through `str()`.
|
||||
|
||||
THE TEXT REPORT LOST THE OWNER'S OWN WORDS THROUGH THIS PATH, measured
|
||||
2026-08-31 in the sibling renderer: `last-ticketqc.txt` was written with
|
||||
every CJK character of the job file's mute sentences removed -- the ASCII
|
||||
fragments and the gaps between them survived -- while the same sentences
|
||||
rendered correctly in the panel and in `qc-run.json`. A file promised as
|
||||
the complete record that silently drops half a sentence is worse than one
|
||||
that fails to write.
|
||||
|
||||
`vibe.compat` is the project's one answer to this class: it returns the
|
||||
engine's text type, decoding bytes as UTF-8, and the caller encodes once
|
||||
on the way to disk. Imported lazily so this module keeps the standard-
|
||||
library-only shape it has at import time.
|
||||
"""
|
||||
from vibe import compat
|
||||
return compat.as_text('' if value is None else value)
|
||||
|
||||
|
||||
def text(sections, show_all=False, width=78):
|
||||
"""The whole report as plain text. What a terminal and a log file want."""
|
||||
lines = []
|
||||
for block in blocks(sections, show_all=show_all):
|
||||
lines.append(_text(as_text(block, width=width)))
|
||||
return _text('\n').join(lines)
|
||||
|
||||
|
||||
def as_text(block, width=78):
|
||||
if block[0] == TABLE:
|
||||
return table_text(block[1], block[2])
|
||||
return md_text(block[1], width=width)
|
||||
|
||||
|
||||
def md_text(md, width=78):
|
||||
"""Markdown as these reports write it -> plain text.
|
||||
|
||||
Nothing is reworded. Entities become ASCII (the extension is IronPython
|
||||
2.7, where a non-ASCII source literal is a byte string, so the reports
|
||||
write `—` rather than the character), emphasis marks are dropped,
|
||||
and headings get a rule under them.
|
||||
|
||||
NOR IS ANYTHING DROPPED. Every line goes out through `_fill`, and
|
||||
`_flat` coerces with `_text` on the way in -- read both before
|
||||
putting a bare `textwrap` call or a `str()` back here.
|
||||
"""
|
||||
body = (md.replace('—', '--').replace(' ', ' ')
|
||||
.replace('†', '+').replace('&', '&'))
|
||||
if body.startswith('# '):
|
||||
title = _flat(body[2:])
|
||||
return '\n' + '=' * width + '\n' + title + '\n' + '=' * width
|
||||
if body.startswith('## '):
|
||||
title = _flat(body[3:])
|
||||
return '\n' + title + '\n' + '-' * min(width, len(title))
|
||||
if body.strip() == '---':
|
||||
return '-' * width
|
||||
quoted = body.startswith('> ')
|
||||
if quoted:
|
||||
body = body[2:]
|
||||
listed = body.startswith('- ')
|
||||
prefix = ' ! ' if quoted else ' '
|
||||
# A folded section's header is a list item, and its continuation lines are
|
||||
# indented past the dash so a two-line header cannot read as two headers.
|
||||
tail = ' ' * (len(prefix) + (2 if listed else 0))
|
||||
# NOTHING LONG IS EVER BROKEN. A file path or a piece mark split across
|
||||
# two lines is not a wrapped sentence, it is an unusable value: the reader
|
||||
# copies these into Revit's Select by ID or into a shell. Better a line
|
||||
# that runs past the margin than one that has to be reassembled.
|
||||
return _fill(_flat(body), width, prefix, tail)
|
||||
|
||||
|
||||
def _fill(body, width, initial_indent, subsequent_indent):
|
||||
"""`textwrap.fill`, with its whitespace TRANSLATE step taken out of
|
||||
the path -- and the reason is invisible on the interpreter the suite
|
||||
runs on.
|
||||
|
||||
`textwrap` munges whitespace by calling `text.translate(<a 256-entry
|
||||
table>)` on anything it takes for a `str`. On CPython that table is
|
||||
applied to a byte string, where every byte has an entry; the engine
|
||||
this extension ships to is IronPython 2.7, whose `str` holds .NET
|
||||
UTF-16 text, so A CHARACTER ABOVE U+00FF HAS NO ENTRY IN THE TABLE AT
|
||||
ALL -- and the report came back missing exactly those characters and
|
||||
nothing else. Measured on the sibling renderer, 2026-08-31.
|
||||
|
||||
Everything reaching here has already been flattened by `_flat`, so
|
||||
there is no whitespace left to munge and `textwrap` is told not to
|
||||
try. Nothing about the wrapping changes.
|
||||
`ticketpage._wrap_text` is the same cure at the other renderer; the
|
||||
two are one measured defect, not two style preferences.
|
||||
|
||||
NOTHING LONG IS EVER BROKEN, unchanged: a file path or a piece mark
|
||||
split across two lines is not a wrapped sentence, it is an unusable
|
||||
value -- the reader copies these into Revit or into a shell.
|
||||
"""
|
||||
return textwrap.fill(_text(body), width=width,
|
||||
initial_indent=initial_indent,
|
||||
subsequent_indent=subsequent_indent,
|
||||
break_long_words=False, break_on_hyphens=False,
|
||||
expand_tabs=False, replace_whitespace=False)
|
||||
|
||||
|
||||
def _flat(body):
|
||||
return (' '.join(_text(body).split())
|
||||
.replace('**', '').replace('`', '').replace('__', ''))
|
||||
|
||||
|
||||
def table_text(columns, rows):
|
||||
"""A fixed-width table. Nothing is truncated: every cell is a measurement
|
||||
or a piece mark, and a silently cut mark is a wrong answer that looks like
|
||||
a right one.
|
||||
|
||||
The column widths are counted in CHARACTERS, so a cell of CJK -- two
|
||||
columns wide in a terminal font and one character long -- leaves its
|
||||
column looking ragged. That is cosmetic and it is the only thing this
|
||||
gets wrong about such a cell: nothing is dropped, cut or reordered.
|
||||
Straightening it needs east-asian-width tables; nobody has asked."""
|
||||
if not rows:
|
||||
return ' (nothing)'
|
||||
grid = [[_cell(c) for c in columns]]
|
||||
grid += [[_cell(c) for c in row] for row in rows]
|
||||
width = [max(len(row[i]) for row in grid) for i in range(len(columns))]
|
||||
out = [' ' + ' '.join(c.ljust(width[i]) for i, c in enumerate(grid[0])),
|
||||
' ' + ' '.join('-' * w for w in width)]
|
||||
for row in grid[1:]:
|
||||
out.append(' ' + ' '.join(c.ljust(width[i])
|
||||
for i, c in enumerate(row)))
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
def _cell(value):
|
||||
# `_text`, not `str`: a cell can hold a mark, a note, or a sentence
|
||||
# somebody typed. This is the text path's OTHER coercion boundary --
|
||||
# `md_text` itself has none, its only exposure was the wrap.
|
||||
return (_text(value).replace('—', '--').replace('†', '+')
|
||||
.replace(' ', ' ').replace('&', '&'))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- as HTML
|
||||
#
|
||||
# Everything below is written for an IE-engine WebBrowser control. See the
|
||||
# module docstring: no HTML5 elements, no modern DOM, inline handlers only.
|
||||
|
||||
STYLE = """
|
||||
.wt-fold { margin: 10px 0 0 0; }
|
||||
.wt-head { background: #eef1f5; border-left: 4px solid #9fb0c4;
|
||||
padding: 5px 8px; cursor: pointer; font-size: 8pt; color: #445; }
|
||||
.wt-head-open { background: #f9f2f4; border-left: 4px solid #c7254e;
|
||||
color: #c7254e; cursor: pointer; padding: 5px 8px;
|
||||
font-size: 8pt; }
|
||||
.wt-body { padding: 2px 8px 8px 14px; font-size: 8pt; }
|
||||
.wt-why { font-weight: bold; }
|
||||
.wt-hint { color: #889; font-size: 7pt; }
|
||||
"""
|
||||
|
||||
# The toggle. `document.getElementById` and `style.display` are IE5-era DOM
|
||||
# and work in every document mode a WinForms WebBrowser can be in; nothing
|
||||
# here uses addEventListener, classList or querySelector, and nothing uses
|
||||
# <details>, which does not exist in this engine at all.
|
||||
SCRIPT = """
|
||||
function wtFold(id) {
|
||||
var el = document.getElementById(id);
|
||||
var hd = document.getElementById(id + '_h');
|
||||
if (!el) { return; }
|
||||
if (el.style.display == 'none') {
|
||||
el.style.display = 'block';
|
||||
if (hd) { hd.innerHTML = hd.innerHTML.replace('[+]', '[-]'); }
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
if (hd) { hd.innerHTML = hd.innerHTML.replace('[-]', '[+]'); }
|
||||
}
|
||||
}
|
||||
function wtClip(id) {
|
||||
var el = document.getElementById(id);
|
||||
var hd = document.getElementById(id + '_h');
|
||||
if (!el) { return; }
|
||||
if (el.style.maxHeight == 'none') {
|
||||
el.style.maxHeight = el.getAttribute('wtcap');
|
||||
if (hd) { hd.innerHTML = hd.innerHTML.replace('[-]', '[+]'); }
|
||||
} else {
|
||||
el.style.maxHeight = 'none';
|
||||
if (hd) { hd.innerHTML = hd.innerHTML.replace('[+]', '[-]'); }
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# `wtClip` IS `wtFold`'S SIBLING, NOT A SECOND MECHANISM.
|
||||
#
|
||||
# Same conventions, deliberately: an inline `onclick`, an element id, a
|
||||
# header at `id + '_h'`, and the same `[+]`/`[-]` marker swap. It exists
|
||||
# because the two folds hide differently and one function cannot do both:
|
||||
#
|
||||
# wtFold toggles `display`, for a block that is ABSENT when closed. Used
|
||||
# by every section of a folding report.
|
||||
# wtClip toggles `max-height` against the element's own `wtcap`
|
||||
# attribute, for a block that is WHOLLY PRESENT and merely too
|
||||
# tall -- the checklist's note cells (`ticketpage.note_cell_html`).
|
||||
# Its text stays in the document when collapsed, which is exactly
|
||||
# why it is not `display: none`: a reader can still select and
|
||||
# copy the part he cannot see, and a browser's own find still
|
||||
# reaches it.
|
||||
#
|
||||
# Both are IE5-era DOM. `style.maxHeight` and `getAttribute` are as old as
|
||||
# `style.display`, and nothing here uses addEventListener, classList,
|
||||
# querySelector or `<details>`.
|
||||
|
||||
|
||||
def html(sections, md_to_html=None, headline=False):
|
||||
"""The folding half of the report as one HTML string.
|
||||
|
||||
`headline` sections are skipped by default: the button prints those
|
||||
through pyRevit's own `print_md` / `print_table`, because that is what can
|
||||
linkify an element id and what the reader is used to reading. Pass
|
||||
`headline=True` to render everything.
|
||||
|
||||
`md_to_html` converts one markdown block. The default handles exactly the
|
||||
vocabulary these reports use; a caller with a real markdown library (the
|
||||
button has `pyrevit.coreutils.markdown`) can pass that instead.
|
||||
"""
|
||||
md_to_html = md_to_html or md_html
|
||||
out = []
|
||||
for section in sections or []:
|
||||
if section.headline and not headline:
|
||||
continue
|
||||
out.append(_section_html(section, md_to_html))
|
||||
# ONE LINE, NO NEWLINE ANYWHERE IN IT. pyRevit's `print_html` is a
|
||||
# `print(..., end='')` into the panel's stream, and every newline in that
|
||||
# stream starts a new output entry -- which would cut a `<div>` in half
|
||||
# and leave a fold's body outside its own container. So joined with
|
||||
# nothing, here and inside each section.
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def _section_html(section, md_to_html):
|
||||
body = []
|
||||
for block in section.blocks:
|
||||
if block[0] == TABLE:
|
||||
body.append(table_html(block[1], block[2]))
|
||||
else:
|
||||
body.append(md_to_html(block[1]))
|
||||
shown = section.is_open
|
||||
mark = '[-]' if shown else '[+]'
|
||||
# Escaped, then the report's own entities put back -- exactly as
|
||||
# `_cell_html` and `md_html` do. A header may carry `—` like any
|
||||
# other report string; without the restore it would reach the panel as
|
||||
# the literal source text.
|
||||
head = [mark, '<b>' + _entities(_escape(section.title)) + '</b>']
|
||||
if section.problem:
|
||||
head.append('<span class="wt-why">— ' +
|
||||
_entities(_escape(section.problem)) + '</span>')
|
||||
elif section.summary:
|
||||
head.append('— ' + _entities(_escape(section.summary)))
|
||||
if not shown:
|
||||
head.append('<span class="wt-hint">(click to open)</span>')
|
||||
return (
|
||||
'<div class="wt-fold">'
|
||||
'<div id="{0}_h" class="{1}" onclick="wtFold(\'{0}\')">{2}</div>'
|
||||
'<div id="{0}" class="wt-body" style="display:{3}">{4}</div>'
|
||||
'</div>').format(
|
||||
_id(section.key),
|
||||
'wt-head-open' if section.problem else 'wt-head',
|
||||
' '.join(head),
|
||||
'block' if shown else 'none',
|
||||
''.join(body))
|
||||
|
||||
|
||||
def _id(key):
|
||||
return 'wt_' + re.sub(r'[^0-9A-Za-z_]', '_', str(key))
|
||||
|
||||
|
||||
def table_html(columns, rows):
|
||||
if not rows:
|
||||
return '<p><i>(nothing)</i></p>'
|
||||
out = ['<table><tr>']
|
||||
for column in columns:
|
||||
out.append('<th>' + _escape(str(column)) + '</th>')
|
||||
out.append('</tr>')
|
||||
for row in rows:
|
||||
out.append('<tr>')
|
||||
for cell in row:
|
||||
out.append('<td>' + _cell_html(cell) + '</td>')
|
||||
out.append('</tr>')
|
||||
out.append('</table>')
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def _cell_html(value):
|
||||
# Cells already carry the report's own entities (`—`, `†`)
|
||||
# and its marks. They are escaped and the entities put back, so a mark
|
||||
# containing an ampersand cannot break the row and a dagger still prints.
|
||||
return _entities(_escape(str(value)))
|
||||
|
||||
|
||||
# The report's whole markdown vocabulary, and nothing else: it is a fixed set
|
||||
# written by one module, not arbitrary user text, so a 30-line converter is
|
||||
# honest here where a general one would be a dependency.
|
||||
_BOLD = re.compile(r'\*\*(.+?)\*\*')
|
||||
_CODE = re.compile(r'`(.+?)`')
|
||||
_ITALIC = re.compile(r'(?<![A-Za-z0-9])_(.+?)_(?![A-Za-z0-9])')
|
||||
|
||||
|
||||
def md_html(md):
|
||||
"""One markdown block -> HTML, for the panel."""
|
||||
text_ = md or ''
|
||||
if text_.strip() == '---':
|
||||
return '<hr/>'
|
||||
tag, body = 'p', text_
|
||||
if body.startswith('### '):
|
||||
tag, body = 'h4', body[4:]
|
||||
elif body.startswith('## '):
|
||||
tag, body = 'h3', body[3:]
|
||||
elif body.startswith('# '):
|
||||
tag, body = 'h2', body[2:]
|
||||
quoted = body.startswith('> ')
|
||||
if quoted:
|
||||
body = body[2:]
|
||||
body = _escape(body)
|
||||
body = _BOLD.sub(r'<b>\1</b>', body)
|
||||
body = _CODE.sub(r'<code>\1</code>', body)
|
||||
body = _ITALIC.sub(r'<i>\1</i>', body)
|
||||
body = _entities(body)
|
||||
if quoted:
|
||||
return ('<div style="border-left:3px solid #9fb0c4;padding-left:8px">'
|
||||
'<' + tag + '>' + body + '</' + tag + '></div>')
|
||||
return '<' + tag + '>' + body + '</' + tag + '>'
|
||||
|
||||
|
||||
def _escape(body):
|
||||
return (body.replace('&', '&').replace('<', '<')
|
||||
.replace('>', '>'))
|
||||
|
||||
|
||||
def _entities(body):
|
||||
"""Put back the handful of entities the reports write on purpose.
|
||||
|
||||
They are escaped along with everything else on the way in -- that is the
|
||||
point of escaping -- and then restored by name here, so the reports keep
|
||||
writing `—` (ASCII source, IronPython 2.7 safe) and the panel keeps
|
||||
printing an em dash.
|
||||
"""
|
||||
for name in ('mdash', 'dagger', 'nbsp', 'ndash', 'hellip'):
|
||||
body = body.replace('&' + name + ';', '&' + name + ';')
|
||||
return body
|
||||
Reference in New Issue
Block a user