@johnmorrisdotca/taisen-brackets 0.17.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/family-backgrounds.d.ts +2 -2
- package/dist/family-backgrounds.js +7 -2
- package/dist/judo-board.js +2 -2
- package/dist/judo-sheets.js +2 -2
- package/dist/render.js +2 -2
- package/dist/road.js +2 -2
- package/dist/text.d.ts +5 -0
- package/dist/text.js +10 -0
- package/dist/types.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.17.2] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
Security fix.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **The background's `id` option is made safe.** `backgroundSvg` (and the `background` option of the drawings that use it, which already passed a safe id) wrote the `id` it was given as it came into element ids and `url(#...)`, so a hostile `id` (`x"><script>…`) could break out of the attribute when `backgroundSvg` is called directly. It now keeps only letters, digits, `_` and `-` (every other character becomes `-`) and puts `bg-` in front of an id that does not start with a letter. An id that is already safe is unchanged. The shared module `family-backgrounds` is now version 1.0.1; `test/family-backgrounds.test.ts` draws every preset with eight hostile ids.
|
|
16
|
+
|
|
17
|
+
## [0.17.1] - 2026-10-20
|
|
18
|
+
|
|
19
|
+
Security fix.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **The `id` option is made safe in every drawing that takes one.** The judo drawings (`judoScoreboard`, `judoContestCard`, `judoMatQueue`, `judoRoadPanel`, and `drawBracket` with judo ties; 0.7.0 to 0.17.0) wrote the caller's `id` as it came into the `<style>` text, a class name and element ids, so a hostile `id` (`x"><script>…`, or `a}</style>…`) could break out of the style block or the attribute and put markup into the SVG. `drawBracket` and `roadToFinal` did the same. They now keep only letters, digits, `_` and `-` (every other character becomes `-`) and put the drawing's own prefix in front of an id that does not start with a letter, as the 0.15.0 boards already did. An id made of letters, digits, `_` and `-` that starts with a letter is unchanged, so most pages see no difference. `test/hostile-id.test.ts` draws every drawing that takes an `id` with six hostile ids.
|
|
24
|
+
|
|
9
25
|
## [0.17.0] - 2026-10-20
|
|
10
26
|
|
|
11
27
|
Broadcast boards, step 3: the medal table.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const FAMILY_BACKGROUNDS_VERSION = "1.0.
|
|
1
|
+
export declare const FAMILY_BACKGROUNDS_VERSION = "1.0.1";
|
|
2
2
|
/** The presets, by name. The five that end in a playing surface read as one: pitch (soccer), ice (hockey), hardwood (basketball), infield (baseball) and turf (football). */
|
|
3
3
|
export declare const BACKGROUND_PRESETS: readonly ["midnight", "crimson", "royal", "emerald", "pitch", "ice", "hardwood", "infield", "turf", "ball-panels", "swirl", "waves", "glow", "floodlights", "confetti", "carbon", "aurora", "sunburst", "chevrons", "halftone", "argyle", "contour", "stardust", "mesh", "spotlight"];
|
|
4
4
|
export type BackgroundPresetName = (typeof BACKGROUND_PRESETS)[number];
|
|
@@ -15,7 +15,7 @@ export interface BackgroundSvgOptions {
|
|
|
15
15
|
intensity?: number;
|
|
16
16
|
/** Moves the scattered parts (confetti, stars, scratches, contours). Default 1. */
|
|
17
17
|
seed?: number;
|
|
18
|
-
/** The prefix of the ids the background writes. Default: a hash of its options. */
|
|
18
|
+
/** The prefix of the ids the background writes. Default: a hash of its options. Only letters, digits, `_` and `-` are kept (every other character becomes `-`), and `bg-` goes in front of one that does not start with a letter; a safe id is used as written. */
|
|
19
19
|
id?: string;
|
|
20
20
|
}
|
|
21
21
|
/** The colours to draw over a background. Every text colour is checked against `dominant`, the colours the background mostly is, for a contrast of 4.5 to 1 (3 to 1 for `muted`). */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// backgroundSvg("midnight", { width: 1080, height: 1350, theme: "dark" }) -> { defs, body, ink }
|
|
9
9
|
//
|
|
10
10
|
// `defs` goes inside the drawing's `<defs>`, `body` right after a plain rectangle of `ink.base`, and `ink` says which colours to draw text, lines, boxes and marks in.
|
|
11
|
-
export const FAMILY_BACKGROUNDS_VERSION = "1.0.
|
|
11
|
+
export const FAMILY_BACKGROUNDS_VERSION = "1.0.1";
|
|
12
12
|
/** The presets, by name. The five that end in a playing surface read as one: pitch (soccer), ice (hockey), hardwood (basketball), infield (baseball) and turf (football). */
|
|
13
13
|
export const BACKGROUND_PRESETS = [
|
|
14
14
|
"midnight", "crimson", "royal", "emerald", "pitch", "ice", "hardwood", "infield", "turf", "ball-panels", "swirl", "waves", "glow",
|
|
@@ -58,6 +58,11 @@ function seeded(seed) {
|
|
|
58
58
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
/** The caller's id made safe to write into an `id` attribute and a `url(#...)`: letters, digits, `_` and `-`, starting with a letter. An id that is already safe is unchanged. */
|
|
62
|
+
function safeId(id) {
|
|
63
|
+
const clean = id.replace(/[^\w-]/g, "-");
|
|
64
|
+
return /^[A-Za-z]/.test(clean) ? clean : `bg-${clean}`;
|
|
65
|
+
}
|
|
61
66
|
/** A small stable hash of a string, as base 36. */
|
|
62
67
|
function hashOf(text) {
|
|
63
68
|
let h = 0x811c9dc5;
|
|
@@ -437,7 +442,7 @@ export function backgroundSvg(preset, options) {
|
|
|
437
442
|
palette[1] = toHex(mix(parseColor(given[0]), dark ? BLACK : WHITE, 0.35));
|
|
438
443
|
const intensity = clamp(options.intensity ?? 0.6, 0, 1);
|
|
439
444
|
const k = intensity / 0.6;
|
|
440
|
-
const id = options.id
|
|
445
|
+
const id = options.id != null ? safeId(String(options.id)) : `bg${hashOf(JSON.stringify([preset, w, h, dark, given, options.angle, intensity, options.seed]))}`;
|
|
441
446
|
const angle = options.angle ?? def.angle;
|
|
442
447
|
const ground = { w, h, c: palette, k, rnd: seeded(options.seed ?? 1), id, dark, long: Math.max(w, h), short: Math.min(w, h) };
|
|
443
448
|
const made = def.draw(ground);
|
package/dist/judo-board.js
CHANGED
|
@@ -3,7 +3,7 @@ import { judoMethodWord, judoSideLong } from "./judo-text.js";
|
|
|
3
3
|
import { fill, voiceOf } from "./locales.js";
|
|
4
4
|
import { drawMark, markDefs } from "./marks.js";
|
|
5
5
|
import { JUDO_COLORS } from "./theme.js";
|
|
6
|
-
import { capitals, emWidth, esc, fit, fitLines, hash, num } from "./text.js";
|
|
6
|
+
import { capitals, emWidth, esc, fit, fitLines, hash, num, safeId } from "./text.js";
|
|
7
7
|
/** The sizes: a TV's 1920 by 1080, a tablet on its side, 1280 by 800, and a tablet held upright, 800 by 1280. */
|
|
8
8
|
export const JUDO_BOARD_SIZES = Object.freeze({
|
|
9
9
|
tv: { width: 1920, height: 1080 },
|
|
@@ -148,7 +148,7 @@ export function judoScoreboard(contest, options = {}) {
|
|
|
148
148
|
const osa = contest.osaekomi;
|
|
149
149
|
if (osa && (!(osa.side === 0 || osa.side === 1) || !Number.isFinite(osa.seconds) || osa.seconds < 0))
|
|
150
150
|
throw new RangeError("taisen-brackets: osaekomi is { side: 0 | 1, seconds } with seconds from 0");
|
|
151
|
-
const prefix = options.id ?? `jb-${hash(JSON.stringify([contest, options.size, options.style, theme, options.swap, options.coaches, v.locale.code, options.flags ? 1 : 0, options.flagHref ? 1 : 0, options.title]))}`;
|
|
151
|
+
const prefix = safeId(options.id, "jb") ?? `jb-${hash(JSON.stringify([contest, options.size, options.style, theme, options.swap, options.coaches, v.locale.code, options.flags ? 1 : 0, options.flagHref ? 1 : 0, options.title]))}`;
|
|
152
152
|
const ctx = { radii: new Set(), symbols: new Map(), prefix, shape: "round", theme: "inherit", flagCache: new Map(), ...(options.flags ? { flags: options.flags } : {}), ...(options.flagHref ? { flagHref: options.flagHref } : {}) };
|
|
153
153
|
const landscape = W >= H;
|
|
154
154
|
const unit = Math.min(W, H);
|
package/dist/judo-sheets.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BLUE_PANEL, DARK, FONT, JUDO_BOARD_SIZES, LIGHT, WHITE_PANEL, bw, clampN, fitSize, rulesFor } from "./judo-board.js";
|
|
2
2
|
import { fill, voiceOf } from "./locales.js";
|
|
3
3
|
import { drawMark, markDefs } from "./marks.js";
|
|
4
|
-
import { emWidth, esc, fit, fitLines, hash, num } from "./text.js";
|
|
4
|
+
import { emWidth, esc, fit, fitLines, hash, num, safeId } from "./text.js";
|
|
5
5
|
/** The IJF-style mat colours, in turn: red, green, yellow, purple, orange, teal. Each reads with the ink the board picks for it. */
|
|
6
6
|
export const JUDO_MAT_COLORS = Object.freeze(["#d2232a", "#1e9c4c", "#e7b800", "#7a3fb4", "#e8741a", "#12877f"]);
|
|
7
7
|
/** White or near-black, whichever reads better on a hex colour. */
|
|
@@ -47,7 +47,7 @@ export function frameOf(options, seed) {
|
|
|
47
47
|
const theme = options.theme ?? "auto";
|
|
48
48
|
if (theme !== "auto" && theme !== "light" && theme !== "dark")
|
|
49
49
|
throw new RangeError(`taisen-brackets: a theme is "auto", "light" or "dark", not ${JSON.stringify(theme)}`);
|
|
50
|
-
const scope = options.id ?? `jb-${hash(JSON.stringify([seed, options.size, theme, options.swap, v.locale.code, options.flags ? 1 : 0, options.flagHref ? 1 : 0, options.title]))}`;
|
|
50
|
+
const scope = safeId(options.id, "jb") ?? `jb-${hash(JSON.stringify([seed, options.size, theme, options.swap, v.locale.code, options.flags ? 1 : 0, options.flagHref ? 1 : 0, options.title]))}`;
|
|
51
51
|
const ctx = { radii: new Set(), symbols: new Map(), prefix: scope, shape: "round", theme: "inherit", flagCache: new Map(), ...(options.flags ? { flags: options.flags } : {}), ...(options.flagHref ? { flagHref: options.flagHref } : {}) };
|
|
52
52
|
return { W: size.width, H: size.height, v, scope, theme, ctx, options };
|
|
53
53
|
}
|
package/dist/render.js
CHANGED
|
@@ -5,7 +5,7 @@ import { cleanSvg, drawEmblem, drawMark, hasTrophy as hasTrophyOf, isEmblemId, m
|
|
|
5
5
|
import { roundLabel } from "./rounds.js";
|
|
6
6
|
import { styleFor } from "./theme.js";
|
|
7
7
|
import { resolveBackground } from "./background.js";
|
|
8
|
-
import { emWidth, esc, fit, hash, num } from "./text.js";
|
|
8
|
+
import { emWidth, esc, fit, hash, num, safeId } from "./text.js";
|
|
9
9
|
import { fill, voiceOf } from "./locales.js";
|
|
10
10
|
import { paintVariant, seriesLineWidth } from "./layouts.js";
|
|
11
11
|
import { drawGroups } from "./groups.js";
|
|
@@ -116,7 +116,7 @@ export function drawBracket(input, options = {}) {
|
|
|
116
116
|
const orientation = options.orientation && options.orientation !== "auto" ? options.orientation : kind === "series" || sizeName === "phone" || sizeName === "card" || (size.height !== null && size.width < size.height * 0.8) ? "vertical" : "horizontal";
|
|
117
117
|
const road = options.look === "road";
|
|
118
118
|
const hasBg = typeof options.background === "string" || (typeof options.background === "object" && options.background !== null);
|
|
119
|
-
const prefix = options.id ?? `tb-${hash(JSON.stringify([v.locale.code, layout.teams.map((t) => t.name), layout.ties.map((t) => [t.id, t.sides.map((s) => s?.id ?? null), t.winner, t.result.score, t.round, ...(t.judo ? [t.judo.method, t.judo.goldenScore, t.judo.seconds, t.judo.sides] : [])]), options.size, orientation, options.detail, theme, options.title, options.subtitle, options.seeds, options.scores, options.champion, options.markShape, options.roundLabels, options.thirdPlace, options.trophy ? 1 : 0, road ? 1 : 0, options.highlightPath ?? null, options.flags ? 1 : 0, ...(hasBg ? [options.background, options.vs, options.codes, options.seeds] : []), ...(options.flagHref ? ["h"] : []), ...(kind !== "tree" ? [kind, options.at, options.standings, options.qualify] : []), ...(options.headers ? ["head"] : [])]))}`;
|
|
119
|
+
const prefix = safeId(options.id, "tb") ?? `tb-${hash(JSON.stringify([v.locale.code, layout.teams.map((t) => t.name), layout.ties.map((t) => [t.id, t.sides.map((s) => s?.id ?? null), t.winner, t.result.score, t.round, ...(t.judo ? [t.judo.method, t.judo.goldenScore, t.judo.seconds, t.judo.sides] : [])]), options.size, orientation, options.detail, theme, options.title, options.subtitle, options.seeds, options.scores, options.champion, options.markShape, options.roundLabels, options.thirdPlace, options.trophy ? 1 : 0, road ? 1 : 0, options.highlightPath ?? null, options.flags ? 1 : 0, ...(hasBg ? [options.background, options.vs, options.codes, options.seeds] : []), ...(options.flagHref ? ["h"] : []), ...(kind !== "tree" ? [kind, options.at, options.standings, options.qualify] : []), ...(options.headers ? ["head"] : [])]))}`;
|
|
120
120
|
const ctx = { radii: new Set(), symbols: new Map(), prefix, shape: options.markShape ?? "round", look: road ? "road" : "classic", flags: options.flags, flagHref: options.flagHref, theme: theme === "light" ? "light" : theme === "dark" || theme === "poster" ? "dark" : "inherit", flagCache: new Map(), defs: new Map() };
|
|
121
121
|
const path = pathOf(layout, options.highlightPath);
|
|
122
122
|
const linked = { count: 0 };
|
package/dist/road.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { SIZES } from "./render.js";
|
|
6
6
|
import { drawRoad } from "./road.draw.js";
|
|
7
7
|
import { nameOf, readModel, summaryOf, tableOf, totalsOf } from "./road.model.js";
|
|
8
|
-
import { hash } from "./text.js";
|
|
8
|
+
import { hash, safeId } from "./text.js";
|
|
9
9
|
const check = (o) => {
|
|
10
10
|
if (o.size !== undefined && !Object.prototype.hasOwnProperty.call(SIZES, o.size))
|
|
11
11
|
throw new RangeError(`Unknown size "${String(o.size)}": use one of ${Object.keys(SIZES).join(", ")}`);
|
|
@@ -44,7 +44,7 @@ export function roadToFinal(input, options = {}) {
|
|
|
44
44
|
const merged = { ...options, ...extra };
|
|
45
45
|
check(merged);
|
|
46
46
|
const m = readModel(input, merged);
|
|
47
|
-
const prefix = merged.id ?? `tb-${hash(JSON.stringify([m.voice.locale.code, m.finalists.map((t) => t.name), [...m.path[0].values()].map((v) => [v.round, v.mine, v.theirs]), [...m.path[1].values()].map((v) => [v.round, v.mine, v.theirs]), m.rounds, merged.size, merged.theme, merged.title, merged.subtitle, merged.final ?? null, merged.endnote, merged.winner, merged.codes, merged.markShape, ...(typeof merged.background === "string" || typeof merged.background === "object" ? [merged.background] : [])]))}`;
|
|
47
|
+
const prefix = safeId(merged.id, "tb") ?? `tb-${hash(JSON.stringify([m.voice.locale.code, m.finalists.map((t) => t.name), [...m.path[0].values()].map((v) => [v.round, v.mine, v.theirs]), [...m.path[1].values()].map((v) => [v.round, v.mine, v.theirs]), m.rounds, merged.size, merged.theme, merged.title, merged.subtitle, merged.final ?? null, merged.endnote, merged.winner, merged.codes, merged.markShape, ...(typeof merged.background === "string" || typeof merged.background === "object" ? [merged.background] : [])]))}`;
|
|
48
48
|
return drawRoad(m, merged, prefix);
|
|
49
49
|
};
|
|
50
50
|
return {
|
package/dist/text.d.ts
CHANGED
|
@@ -27,3 +27,8 @@ export interface FittedLines {
|
|
|
27
27
|
export declare function fitLines(name: string, maxWidth: number, maxHeight: number, maxSize: number): FittedLines;
|
|
28
28
|
/** A number written without float noise: at most two decimals, no trailing zeros. */
|
|
29
29
|
export declare function num(n: number): string;
|
|
30
|
+
/**
|
|
31
|
+
* A caller's `id` option made safe to use as a class name, an element id and a CSS selector: only letters, digits, `_` and `-` stay (every other character becomes `-`),
|
|
32
|
+
* and an id that does not start with a letter gets `fallback` in front (an id may not start with a digit, and a CSS selector may not either). Left out, it is `undefined`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function safeId(id: unknown, fallback: string): string | undefined;
|
package/dist/text.js
CHANGED
|
@@ -106,3 +106,13 @@ export function num(n) {
|
|
|
106
106
|
const r = Math.round(n * 100) / 100;
|
|
107
107
|
return Object.is(r, -0) ? "0" : String(r);
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* A caller's `id` option made safe to use as a class name, an element id and a CSS selector: only letters, digits, `_` and `-` stay (every other character becomes `-`),
|
|
111
|
+
* and an id that does not start with a letter gets `fallback` in front (an id may not start with a digit, and a CSS selector may not either). Left out, it is `undefined`.
|
|
112
|
+
*/
|
|
113
|
+
export function safeId(id, fallback) {
|
|
114
|
+
if (id === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
const clean = String(id).replace(/[^\w-]/g, "-");
|
|
117
|
+
return /^[A-Za-z]/.test(clean) ? clean : `${fallback}-${clean}`;
|
|
118
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -636,7 +636,7 @@ export interface DrawOptions extends LayoutOptions {
|
|
|
636
636
|
words?: Partial<Words>;
|
|
637
637
|
/** The name to draw for a team in this language: `undefined` keeps `Team.names[locale]` or `Team.name`. `countryName(team.region, locale)` makes a country's name from Intl, with no list of your own. */
|
|
638
638
|
teamName?: (team: Team, locale: Locale) => string | undefined;
|
|
639
|
-
/** Prefix for the drawing's ids. Default: a hash of the bracket and the options. */
|
|
639
|
+
/** Prefix for the drawing's ids. Default: a hash of the bracket and the options. Only letters, digits, `_` and `-` are kept (any other character becomes `-`), and an id that does not start with a letter gets the drawing's own prefix in front. */
|
|
640
640
|
id?: string;
|
|
641
641
|
/** Extra class names for the root `<svg>`. */
|
|
642
642
|
className?: string;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.17.
|
|
1
|
+
export declare const VERSION = "0.17.2";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.17.
|
|
1
|
+
export const VERSION = "0.17.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"description": "Taisen Brackets 対戦, knockout brackets as SVG: from a list of ties (games, best-of series, two legs, replays, byes, a third-place match) it works out the rounds and halves and draws the bracket with the halves meeting at the final, or left to right, wide, square, as a tall card or for a phone, in a classic or a road look (large round flags, big scores, a team's dotted path, a link on every box), in light, dark or a gold-on-navy poster look, in eight layouts (the tree, rows, shields, a score-update card, tall columns, the NBA's play-in, a series card and group tables), on one of twenty-five poster backgrounds (the playing surfaces of five sports among them) or your own, with each team's logo or round flag, the champion in the centre, and an accessible table; and the road to the final of either finalist. NHL, NBA, MLB, NFL, CFL, MLS and World Cup formats by era, in English, French, Japanese, Spanish and German. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bracket",
|