@johnmorrisdotca/taisen-brackets 0.17.0 → 0.17.1
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 +8 -0
- 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,14 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.17.1] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
Security fix.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
|
|
9
17
|
## [0.17.0] - 2026-10-20
|
|
10
18
|
|
|
11
19
|
Broadcast boards, step 3: the medal table.
|
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.1";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.17.
|
|
1
|
+
export const VERSION = "0.17.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
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",
|