@johnmorrisdotca/taisen-brackets 0.2.1 → 0.4.0
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 +37 -1
- package/NOTICE.md +4 -2
- package/README.md +161 -11
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/locales.js +30 -0
- package/dist/marks.d.ts +29 -1
- package/dist/marks.js +94 -1
- package/dist/presets-entry.d.ts +1 -1
- package/dist/presets-entry.js +1 -1
- package/dist/presets.d.ts +7 -0
- package/dist/presets.js +15 -2
- package/dist/render.d.ts +1 -1
- package/dist/render.js +243 -78
- package/dist/road-entry.d.ts +2 -0
- package/dist/road-entry.js +1 -0
- package/dist/road.d.ts +28 -0
- package/dist/road.draw.d.ts +4 -0
- package/dist/road.draw.js +256 -0
- package/dist/road.js +58 -0
- package/dist/road.model.d.ts +63 -0
- package/dist/road.model.js +312 -0
- package/dist/road.types.d.ts +115 -0
- package/dist/road.types.js +1 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +46 -0
- package/dist/theme.d.ts +21 -2
- package/dist/theme.js +36 -11
- package/dist/types.d.ts +98 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -2
package/dist/marks.js
CHANGED
|
@@ -24,6 +24,42 @@ export function cleanSvg(markup) {
|
|
|
24
24
|
.replace(/(\s(?:xlink:)?href\s*=\s*)(["'])\s*javascript:[^"']*\2/gi, "$1$2#$2");
|
|
25
25
|
return { viewBox: box, inner };
|
|
26
26
|
}
|
|
27
|
+
const EMBLEM_ID = /^[a-z][a-z0-9-]*:[a-z0-9][a-z0-9-]*(?:\/(?:metal|line|color))?$/;
|
|
28
|
+
/** Whether a `trophy` value names an emblem (`"trophy:hockey-cup"`, `"medal:gold/line"`) rather than being inline SVG or an image address. */
|
|
29
|
+
export function isEmblemId(value) {
|
|
30
|
+
return EMBLEM_ID.test(value.trim());
|
|
31
|
+
}
|
|
32
|
+
/** Whether `drawBracket` has something to draw for this `trophy` and `emblems`: an emblem id needs a renderer; SVG and addresses do not. */
|
|
33
|
+
export function hasTrophy(o) {
|
|
34
|
+
const t = o.trophy?.trim();
|
|
35
|
+
if (!t)
|
|
36
|
+
return false;
|
|
37
|
+
return isEmblemId(t) ? typeof o.emblems === "function" : true;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The emblem a renderer makes for an id, cleaned like any inline SVG and with its `<title>` removed (the drawing says what it shows in its own
|
|
41
|
+
* words), or `null` when the renderer throws or returns something that is not an `<svg>`. The look decides the ink: `light` and `dark` are
|
|
42
|
+
* forced, `poster` is dark, and `auto` and `none` are `inherit`, which the drawing's stylesheet colours from `--tb-ink`.
|
|
43
|
+
*/
|
|
44
|
+
export function drawEmblem(id, size, o) {
|
|
45
|
+
if (typeof o.emblems !== "function")
|
|
46
|
+
return null;
|
|
47
|
+
const look = o.theme ?? "auto";
|
|
48
|
+
const request = { style: o.emblemStyle ?? "metal", size, theme: look === "light" ? "light" : look === "dark" || look === "poster" ? "dark" : "inherit" };
|
|
49
|
+
if (o.emblemTint)
|
|
50
|
+
request.tint = o.emblemTint;
|
|
51
|
+
let markup;
|
|
52
|
+
try {
|
|
53
|
+
markup = o.emblems(id.trim(), request);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (typeof markup !== "string")
|
|
59
|
+
return null;
|
|
60
|
+
const svg = cleanSvg(markup);
|
|
61
|
+
return svg ? { viewBox: svg.viewBox, inner: svg.inner.replace(/<title\b[\s\S]*?<\/title\s*>/gi, "") } : null;
|
|
62
|
+
}
|
|
27
63
|
/** Readable text on a team colour: white on a dark one, near-black on a light one; `null` when the colour is not a hex one this can read. */
|
|
28
64
|
function inkOn(color) {
|
|
29
65
|
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim())?.[1];
|
|
@@ -42,6 +78,44 @@ export function safeUrl(url) {
|
|
|
42
78
|
return true;
|
|
43
79
|
return !/^[a-z][a-z0-9+.-]*:/i.test(u) && !u.startsWith("//") ? u.length > 0 : false;
|
|
44
80
|
}
|
|
81
|
+
/** Whether an address may be a link in a drawing: http(s), a relative path or a `#` fragment. `javascript:`, `data:` and every other scheme are refused. */
|
|
82
|
+
export function safeHref(url) {
|
|
83
|
+
const u = url.trim();
|
|
84
|
+
// eslint-disable-next-line no-control-regex
|
|
85
|
+
if (u.length === 0 || /[\u0000-\u001f\u007f]/.test(u))
|
|
86
|
+
return false;
|
|
87
|
+
if (/^https?:\/\//i.test(u))
|
|
88
|
+
return true;
|
|
89
|
+
return !/^[a-z][a-z0-9+.-]*:/i.test(u) && !u.startsWith("//");
|
|
90
|
+
}
|
|
91
|
+
/** The flag a renderer makes for a code, cleaned like any inline SVG, or `null` when there is no renderer, it has no flag for the code, it throws, or what it returns is not an `<svg>`. */
|
|
92
|
+
function flagOf(code, size, ctx) {
|
|
93
|
+
if (typeof ctx.flags !== "function" || typeof code !== "string" || code.trim() === "")
|
|
94
|
+
return null;
|
|
95
|
+
const shape = ctx.shape === "square" ? "square" : "round";
|
|
96
|
+
const key = `${code.trim()}|${shape}|${Math.round(size)}`;
|
|
97
|
+
const cache = ctx.flagCache;
|
|
98
|
+
if (cache?.has(key))
|
|
99
|
+
return cache.get(key) ?? null;
|
|
100
|
+
let made = null;
|
|
101
|
+
try {
|
|
102
|
+
const markup = ctx.flags(code.trim(), { shape, size: Math.round(size), theme: ctx.theme ?? "inherit" });
|
|
103
|
+
if (typeof markup === "string")
|
|
104
|
+
made = cleanSvg(markup);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
made = null;
|
|
108
|
+
}
|
|
109
|
+
cache?.set(key, made);
|
|
110
|
+
return made;
|
|
111
|
+
}
|
|
112
|
+
/** The `<defs>` content the marks drawn with a context need: a circular clip for each radius, and each inline mark or flag once as a `<symbol>`. Written after the marks are drawn. */
|
|
113
|
+
export function markDefs(ctx) {
|
|
114
|
+
return [
|
|
115
|
+
...[...ctx.radii].sort((a, b) => a - b).map((r) => `<clipPath id="${clipId(ctx.prefix, r)}"><circle r="${num(r)}"/></clipPath>`),
|
|
116
|
+
...[...ctx.symbols.values()].map((s) => `<symbol id="${s.id}" viewBox="${esc(s.viewBox)}" preserveAspectRatio="xMidYMid slice">${s.inner}</symbol>`),
|
|
117
|
+
].join("");
|
|
118
|
+
}
|
|
45
119
|
/** The id of the circular clip for a radius. */
|
|
46
120
|
export const clipId = (prefix, r) => `${prefix}-c${String(num(r)).replace(".", "_")}`;
|
|
47
121
|
/**
|
|
@@ -58,7 +132,26 @@ export function drawMark(team, cx, cy, size, ctx, abbrText) {
|
|
|
58
132
|
ctx.radii.add(r);
|
|
59
133
|
const wrapOpen = clip ? `<g class="tb-mark" transform="translate(${num(cx)} ${num(cy)})" clip-path="url(#${clipId(ctx.prefix, r)})">` : `<g class="tb-mark">`;
|
|
60
134
|
const ox = clip ? -r : x, oy = clip ? -r : y;
|
|
61
|
-
const
|
|
135
|
+
const road = ctx.look === "road";
|
|
136
|
+
const rw = Math.max(r * 0.09, 0.5);
|
|
137
|
+
const hair = num(Math.max(rw * 0.3, 0.3));
|
|
138
|
+
const ring = road
|
|
139
|
+
? clip
|
|
140
|
+
? `<circle class="tb-ring tb-ring-r" cx="${num(cx)}" cy="${num(cy)}" r="${num(r - rw / 2)}" stroke-width="${num(rw)}"/><circle class="tb-edge" cx="${num(cx)}" cy="${num(cy)}" r="${num(r)}" stroke-width="${hair}"/>`
|
|
141
|
+
: `<rect class="tb-ring tb-ring-r" x="${num(x + rw / 2)}" y="${num(y + rw / 2)}" width="${num(size - rw)}" height="${num(size - rw)}" rx="2" stroke-width="${num(rw)}"/><rect class="tb-edge" x="${num(x)}" y="${num(y)}" width="${num(size)}" height="${num(size)}" rx="2" stroke-width="${hair}"/>`
|
|
142
|
+
: clip
|
|
143
|
+
? `<circle class="tb-ring" cx="${num(cx)}" cy="${num(cy)}" r="${num(r)}"/>`
|
|
144
|
+
: `<rect class="tb-ring" x="${num(x)}" y="${num(y)}" width="${num(size)}" height="${num(size)}" rx="2"/>`;
|
|
145
|
+
const flag = flagOf(team.flag, size, ctx);
|
|
146
|
+
if (flag) {
|
|
147
|
+
const key = `flag:${flag.viewBox}:${flag.inner}`;
|
|
148
|
+
let symbol = ctx.symbols.get(key);
|
|
149
|
+
if (!symbol) {
|
|
150
|
+
symbol = { id: `${ctx.prefix}-m${ctx.symbols.size + 1}`, ...flag };
|
|
151
|
+
ctx.symbols.set(key, symbol);
|
|
152
|
+
}
|
|
153
|
+
return `${wrapOpen}<use href="#${symbol.id}" x="${num(ox)}" y="${num(oy)}" width="${num(size)}" height="${num(size)}"/></g>${ring}`;
|
|
154
|
+
}
|
|
62
155
|
const mark = team.mark?.trim();
|
|
63
156
|
if (mark) {
|
|
64
157
|
if (mark.startsWith("<")) {
|
package/dist/presets-entry.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { playoffFormat, playoffEras, LEAGUES } from "./presets.ts";
|
|
1
|
+
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.ts";
|
|
2
2
|
export type { League, PlayoffFormat, PlayoffRound } from "./presets.ts";
|
|
3
3
|
export { roundName, roundNamesFor } from "./rounds.ts";
|
package/dist/presets-entry.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { playoffFormat, playoffEras, LEAGUES } from "./presets.js";
|
|
1
|
+
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.js";
|
|
2
2
|
export { roundName, roundNamesFor } from "./rounds.js";
|
package/dist/presets.d.ts
CHANGED
|
@@ -29,9 +29,16 @@ export interface PlayoffFormat {
|
|
|
29
29
|
/** The names of the two halves, left first, or null where the bracket is not split in two. */
|
|
30
30
|
groups: readonly [string, string] | null;
|
|
31
31
|
note?: string;
|
|
32
|
+
/** The league's default emblem id (`LEAGUE_EMBLEMS`), for `trophy` with an `emblems` renderer. */
|
|
33
|
+
emblem: string;
|
|
32
34
|
/** The English Wikipedia articles this entry was checked against (CC BY-SA 4.0; only facts are used). */
|
|
33
35
|
sources: readonly string[];
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The emblem each league's champion gets by default: an id of `@johnmorrisdotca/dougu-sports` (generic forms, no league's own mark). Pass it as
|
|
39
|
+
* `trophy` together with an `emblems` renderer; with no renderer it draws nothing. A cup for MLS because the MLS Cup is one.
|
|
40
|
+
*/
|
|
41
|
+
export declare const LEAGUE_EMBLEMS: Readonly<Record<League, string>>;
|
|
35
42
|
/** The leagues that have a format here. */
|
|
36
43
|
export declare const LEAGUES: readonly League[];
|
|
37
44
|
/**
|
package/dist/presets.js
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* Wikipedia articles in its `sources` (2026-10-13); where a season did something of its own (a strike, a pandemic) it has
|
|
5
5
|
* its own entry. The year is the year the playoffs are played in (the NHL's and the NBA's 2024 are the 2024 playoffs, of the 2023-24 season), except for the NFL, whose year is its season's: 2024 is the season played in 2024, with playoffs in January and February 2025.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* The emblem each league's champion gets by default: an id of `@johnmorrisdotca/dougu-sports` (generic forms, no league's own mark). Pass it as
|
|
9
|
+
* `trophy` together with an `emblems` renderer; with no renderer it draws nothing. A cup for MLS because the MLS Cup is one.
|
|
10
|
+
*/
|
|
11
|
+
export const LEAGUE_EMBLEMS = Object.freeze({
|
|
12
|
+
nhl: "trophy:hockey-cup",
|
|
13
|
+
nba: "trophy:basketball",
|
|
14
|
+
mlb: "trophy:baseball",
|
|
15
|
+
nfl: "trophy:football",
|
|
16
|
+
cfl: "trophy:canadian-bowl",
|
|
17
|
+
mls: "trophy:cup",
|
|
18
|
+
worldcup: "trophy:soccer-globe",
|
|
19
|
+
});
|
|
7
20
|
/** The address of English Wikipedia articles, by title. */
|
|
8
21
|
const wiki = (...titles) => titles.map((t) => `https://en.wikipedia.org/wiki/${t.replace(/ /g, "_")}`);
|
|
9
22
|
const r = (name, bestOf = 7) => ({ name, bestOf });
|
|
@@ -76,9 +89,9 @@ export function playoffFormat(league, year) {
|
|
|
76
89
|
const { years: _years, except: _except, ...rest } = hit;
|
|
77
90
|
void _years;
|
|
78
91
|
void _except;
|
|
79
|
-
return { ...rest, year };
|
|
92
|
+
return { ...rest, year, emblem: LEAGUE_EMBLEMS[league] };
|
|
80
93
|
}
|
|
81
94
|
/** Every era of a league, oldest first. */
|
|
82
95
|
export function playoffEras(league) {
|
|
83
|
-
return ERAS[league].map(({ years: _y, except: _e, ...rest }) => { void _y; void _e; return { ...rest, year: rest.from }; });
|
|
96
|
+
return ERAS[league].map(({ years: _y, except: _e, ...rest }) => { void _y; void _e; return { ...rest, year: rest.from, emblem: LEAGUE_EMBLEMS[league] }; });
|
|
84
97
|
}
|
package/dist/render.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BracketInput, BracketLayout, DrawOptions, Matchup, Size, SizeName } from "./types.ts";
|
|
2
2
|
import { type Voice } from "./locales.ts";
|
|
3
|
-
/** The sizes by name. A phone's drawing is 390 wide and as tall as the bracket needs. */
|
|
3
|
+
/** The sizes by name. A phone's drawing is 390 wide and as tall as the bracket needs; a card is 1080 by 1350, a tall post. */
|
|
4
4
|
export declare const SIZES: Readonly<Record<SizeName, Size>>;
|
|
5
5
|
/** The layout with each team's name in the language: what the drawing, the table and the description are written from. */
|
|
6
6
|
export declare function localizedLayout(layout: BracketLayout, v: Voice): BracketLayout;
|