All files / src/figures tooltip.ts

100% Statements 25/25
88.88% Branches 8/9
100% Functions 4/4
100% Lines 25/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84                                                                        2x 2x 2x 2x 2x 2x 2x                 2x 104x 104x     2x 13x 13x                 21x 21x 14x 14x 14x   14x     2x 21x 21x 21x 21x   21x  
/**
 * What the pointer says over a chart, as the markup the chart library parses.
 *
 * The seam where this application writes HTML, and it exists because the
 * library does the writing: ECharts sets a tooltip's content with `innerHTML`
 * (`TooltipHTMLContent.setContent`), so the string a formatter returns is parsed
 * as markup on every hover. Everywhere else a string reaches the page as text —
 * `ui/dom.ts`'s `el()` sets `textContent` — and here that rule has to be kept
 * by hand instead, by the xy figures' formatter (`figures/option.ts`) and the
 * matrix's (`figures/matrixOption.ts`) alike.
 *
 * ## Every piece of text is escaped, by one function
 *
 * What a tooltip names comes out of the file being read. A cis/trans square's
 * ticks are the sequences' own names (`analysis/run/cistrans.ts`), a track
 * correlation's are the tracks', and a curve is named after the sequence or the
 * track it was measured on — so a sequence called `<img src=x onerror=…>` ran
 * its script the moment the pointer crossed its row. Nothing is interpolated
 * into the markup here except through {@link escapeHtml}: the names, the
 * numbers, and a series' colour too, which a workspace file carries
 * (`figures/style.ts`) and which is therefore as foreign as a name. One
 * function, so there is one place to get it right and one place to look.
 *
 * ## And no style attribute, which is why every chart has a formatter
 *
 * The library's own tooltip — what a chart with no formatter shows — escapes
 * its text correctly, and sets every margin, size and colour in it through
 * `style=` attributes. A Content-Security-Policy without `'unsafe-inline'`
 * refuses each of those (§11, §15): one pass of the pointer over P(s) raised
 * seven hundred violations and left a box of bare text, its colour swatches
 * gone. So the markup here carries classes, which `ui/styles.css` styles, and a
 * series' swatch is an SVG circle whose colour is a `fill` — a presentation
 * attribute, which no `style-src` governs. The box around it is the library's
 * and is styled through the CSSOM, which the policy does not govern either.
 */
 
const ENTITIES: Readonly<Record<string, string>> = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#39;',
}
 
/**
 * Text, made safe to stand between two tags or inside a quoted attribute.
 *
 * All five characters, not the two that open a tag: a value interpolated into
 * an attribute — the swatch's `fill` — is closed by a quote, and one escape
 * that is right in both places is the one nobody has to choose between.
 */
export function escapeHtml(text: string): string {
  return text.replace(/[&<>"']/g, (char) => ENTITIES[char] ?? char)
}
 
/** The line a tooltip opens with: the place, or the series, it is about. */
export function tipHead(text: string): string {
  return `<div class="figure-tip-head">${escapeHtml(text)}</div>`
}
 
/**
 * A series' colour, as a swatch the policy lets through.
 *
 * Absent rather than guessed where the library hands over no string: a
 * gradient arrives as an object, and a summary row — a box's median — is about
 * no one series' colour.
 */
function swatch(colour: unknown): string {
  if (typeof colour !== 'string' || colour === '') return ''
  return (
    '<svg class="figure-tip-swatch" viewBox="0 0 10 10" aria-hidden="true">' +
    `<circle cx="5" cy="5" r="5" fill="${escapeHtml(colour)}"/></svg>`
  )
}
 
/** One row: a swatch where there is a colour, what it is, and how much. */
export function tipRow(colour: unknown, name: string, value: string): string {
  return (
    `<div class="figure-tip-row">${swatch(colour)}` +
    `<span class="figure-tip-name">${escapeHtml(name)}</span>` +
    `<span class="figure-tip-value">${escapeHtml(value)}</span></div>`
  )
}