@johnmorrisdotca/taisen-brackets 0.6.0 → 0.7.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/dist/judo.js ADDED
@@ -0,0 +1,75 @@
1
+ import { layoutBracket } from "./layout.js";
2
+ import { singleElimination } from "./seeding.js";
3
+ import { fill, voiceOf } from "./locales.js";
4
+ export { JUDO_WEIGHT_CLASSES, judoOutcome, judoSeconds, judoClock } from "./judo-model.js";
5
+ /**
6
+ * The contests of one judo weight category for any number of athletes, seeded in the standard way (the top seeds have the byes when the number is
7
+ * not a power of two). The athlete drawn first is white and the second blue (`Matchup.sides`: first white).
8
+ *
9
+ * With `repechage` (IJF Sport and Organisation Rules, 2.5.2 "Quarter-final Repechage", the system of the Olympic Games): the athletes are in two tables
10
+ * and an elimination produces two finalists. "The athletes defeated in the quarter-final will compete in two (2) repechage contests. The winners of each of
11
+ * these two (2) repechage contests will compete in bronze medal contests against the loser of the semi-final contest of the respective opposite table.
12
+ * The winners (2) of those contests are placed third." The repechage and bronze contests are `stage: "repechage"` and `stage: "bronze"` matchups, fed by
13
+ * the losers of the tree's, and are drawn under it. Needs eight athletes or more. With `direct`, there are no such contests: the two losing semi-finalists are
14
+ * the bronze medallists (`judoPodium`).
15
+ *
16
+ * Double repechage and full repechage (2.5.3 and 2.5.4) are not drawn: their pools are laid out in the rules' Appendix G charts.
17
+ */
18
+ export function judoBracket(athletes, options = {}) {
19
+ const system = options.system ?? (athletes.length >= 8 ? "repechage" : "direct");
20
+ if (system !== "repechage" && system !== "direct")
21
+ throw new RangeError(`taisen-brackets: a judo system is "repechage" or "direct", not ${JSON.stringify(system)}`);
22
+ if (athletes.length < 4)
23
+ throw new RangeError("taisen-brackets: a judo bracket needs four athletes or more (the two losing semi-finalists are bronze medallists)");
24
+ if (system === "repechage" && athletes.length < 8)
25
+ throw new RangeError("taisen-brackets: a quarter-final repechage needs eight athletes or more; use system: \"direct\" for fewer");
26
+ const prefix = options.prefix ?? "J";
27
+ const made = singleElimination(athletes, { prefix });
28
+ if (system === "direct")
29
+ return made;
30
+ const rounds = Math.max(...made.matchups.map((m) => m.round ?? 0));
31
+ const semis = made.matchups.filter((m) => m.round === rounds - 1);
32
+ const quarters = (semi) => {
33
+ const [a, b] = semi.feeds ?? [];
34
+ if (typeof a !== "string" || typeof b !== "string")
35
+ throw new Error("taisen-brackets: a semi-final of a repechage bracket needs two quarter-finals feeding it");
36
+ return [a, b];
37
+ };
38
+ const loser = (tie) => ({ tie, result: "loser" });
39
+ const extra = [];
40
+ semis.forEach((semi, k) => {
41
+ const [q1, q2] = quarters(semi);
42
+ extra.push({ id: `${prefix}rep-${k + 1}`, sides: [null, null], feeds: [loser(q1), loser(q2)], stage: "repechage" });
43
+ });
44
+ semis.forEach((semi, k) => {
45
+ // The repechage winner of one table meets the semi-final loser of the other.
46
+ const other = semis[1 - k];
47
+ extra.push({ id: `${prefix}bronze-${k + 1}`, sides: [null, null], feeds: [`${prefix}rep-${k + 1}`, loser(other.id)], stage: "bronze" });
48
+ });
49
+ return { teams: made.teams, matchups: [...made.matchups, ...extra] };
50
+ }
51
+ /** The gold, silver and two bronze medals of a bracket (matchups, input or layout), from the final, the bronze medal contests or the semi-finals. */
52
+ export function judoPodium(input) {
53
+ const layout = "ties" in input && "sides" in input ? input : layoutBracket(input);
54
+ const fin = layout.finalIndex === null ? null : layout.ties[layout.finalIndex];
55
+ const gold = fin && fin.winner !== null ? fin.sides[fin.winner] : null;
56
+ const silver = fin && fin.winner !== null ? fin.sides[(1 - fin.winner)] : null;
57
+ const bronzeTies = layout.ties.filter((t) => t.stage === "bronze");
58
+ let bronze;
59
+ if (bronzeTies.length)
60
+ bronze = bronzeTies.flatMap((t) => (t.winner === null || !t.sides[t.winner] ? [] : [t.sides[t.winner]]));
61
+ else {
62
+ const semis = layout.finalIndex === null ? [] : layout.ties.filter((t) => t.round === layout.rounds - 1 && t.stage === undefined && !t.third && layout.place[t.index] !== "o");
63
+ bronze = semis.flatMap((t) => (t.winner === null || !t.sides[(1 - t.winner)] ? [] : [t.sides[(1 - t.winner)]]));
64
+ }
65
+ return { gold, silver, bronze };
66
+ }
67
+ /** A weight category as a heading in a language: "Men -66 kg", "Women +78 kg". */
68
+ export function judoCategoryLabel(category, locale) {
69
+ if (category.sex !== "men" && category.sex !== "women")
70
+ throw new RangeError(`taisen-brackets: a judo category's sex is "men" or "women", not ${JSON.stringify(category.sex)}`);
71
+ if (!/^[-+]\d{2,3}$/.test(category.weight))
72
+ throw new RangeError(`taisen-brackets: a judo weight is "-66" or "+100", not ${JSON.stringify(category.weight)}`);
73
+ const w = voiceOf({ locale }).words;
74
+ return fill(category.weight.startsWith("+") ? w.judoWeightOver : w.judoWeightClass, { sex: category.sex === "men" ? w.judoMen : w.judoWomen, w: category.weight.slice(1) });
75
+ }
package/dist/layout.js CHANGED
@@ -17,8 +17,8 @@ export function layoutBracket(input, options = {}) {
17
17
  throw new Error(`taisen-brackets: two matchups have the id ${JSON.stringify(t.id)}`);
18
18
  byId.set(t.id, t.index);
19
19
  }
20
- // A play-in game is outside the tree; so are a third-place match and a round-robin game.
21
- const playIns = ties.filter((t) => matchups[t.index].stage === "play-in").map((t) => t.index);
20
+ // A play-in game is outside the tree, and so are a judo repechage and bronze medal contest (a stage's ties are fed by the tree's losers); so are a third-place match and a round-robin game.
21
+ const playIns = ties.filter((t) => matchups[t.index].stage !== undefined).map((t) => t.index);
22
22
  const isPlayIn = new Set(playIns);
23
23
  const thirds = ties.filter((t) => t.third && !isPlayIn.has(t.index)).map((t) => t.index);
24
24
  const robin = ties.filter((t) => !t.third && !isPlayIn.has(t.index) && matchups[t.index].round === 0).map((t) => t.index);
package/dist/layouts.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { abbrOf } from "./matchup.js";
2
+ import { sideScore } from "./judo-text.js";
2
3
  import { drawMark } from "./marks.js";
3
4
  import { emWidth, esc, fit, num } from "./text.js";
4
5
  const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
5
6
  /** The width of a text set in bold at a size: the package's generous estimate, so that a text stays inside its place in any font. */
6
7
  const bw = (text, size) => emWidth(text) * size;
7
- const scoreOf = (e, k) => (e.scoresOn && e.tie.result.score ? e.v.number(e.tie.result.score[k]) : "");
8
+ const scoreOf = (e, k) => (e.scoresOn ? sideScore(e.tie, k, e.v) : "");
8
9
  const pensOf = (e, k) => (e.scoresOn && e.tie.result.pens ? `(${e.v.number(e.tie.result.pens[k])})` : "");
9
10
  /** A small pill with a seed tag, its left edge at `x`, centred on `cy`. */
10
11
  function tagPill(text, x, cy, size) {
@@ -260,7 +261,7 @@ export function seriesLineWidth(tie, v, size, scoresOn) {
260
261
  const score = scoresOn && tie.result.score ? fillScore(tie, v) : v.words.vs;
261
262
  return bw(code(0), size) + bw(code(1), size) + bw(score, size) + 4 * size * 0.6 + 8 * 2 + 2 * bw("00", size * 0.72);
262
263
  }
263
- const fillScore = (tie, v) => (tie.result.score ? v.words.scoreText.replace("{a}", v.number(tie.result.score[0])).replace("{b}", v.number(tie.result.score[1])) : "");
264
+ const fillScore = (tie, v, swap = false) => (tie.result.score ? v.words.scoreText.replace("{a}", sideScore(tie, swap ? 1 : 0, v)).replace("{b}", sideScore(tie, swap ? 0 : 1, v)) : "");
264
265
  /** One line a series: "(1) BOS 4–0 MIA (8)", the winner in bold and the loser quiet. Where the box is too narrow for a line, two compact rows. */
265
266
  function series(e) {
266
267
  const { x, y, w, h, tie, v } = e;
@@ -274,7 +275,7 @@ function series(e) {
274
275
  const [a, b] = e.order;
275
276
  const cy = y + h / 2;
276
277
  const score = e.scoresOn && tie.result.score ? fillScore(tie, v) : v.words.vs;
277
- const scoreOrdered = e.scoresOn && tie.result.score && a === 1 ? v.words.scoreText.replace("{a}", v.number(tie.result.score[1])).replace("{b}", v.number(tie.result.score[0])) : score;
278
+ const scoreOrdered = e.scoresOn && tie.result.score && a === 1 ? fillScore(tie, v, true) : score;
278
279
  const sw = bw(scoreOrdered, size);
279
280
  const cx = x + w / 2;
280
281
  const ss = Math.max(size * 0.72, 7.5);
package/dist/locales.js CHANGED
@@ -21,6 +21,11 @@ const EN = {
21
21
  roadFinal: "The final: {a} {score} {b} ({how}).", roadChampion: "{name} won the final.", roadToPlay: "The final is still to be played.",
22
22
  vs: "vs", at: "AT", groupName: "Group {n}", colPlayed: "P", colWon: "W", colDrawn: "D", colLost: "L", colDiff: "GD", colPoints: "Pts",
23
23
  playIn: "Play-in", playInWinner: "Winner {a}/{b}", playInLoser: "Loser {a}/{b}", seedSlot: "Seed {n}", winnerOf: "Winner of {name}", loserOf: "Loser of {name}",
24
+ judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
25
+ judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "decision", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
26
+ judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}: {n} shido", judoShidoShort: "shido {a}–{b}",
27
+ judoRepechage: "Repechage", judoBronze: "Bronze medal contest", judoRepechageBronze: "Repechage and bronze medal contests", judoWhite: "white", judoBlue: "blue",
28
+ judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Men", judoWomen: "Women",
24
29
  };
25
30
  const FR = {
26
31
  tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
@@ -38,6 +43,11 @@ const FR = {
38
43
  roadFinal: "La finale : {a} {score} {b} ({how}).", roadChampion: "{name} a remporté la finale.", roadToPlay: "La finale reste à jouer.",
39
44
  vs: "vs", at: "À", groupName: "Groupe {n}", colPlayed: "PJ", colWon: "V", colDrawn: "N", colLost: "D", colDiff: "+/-", colPoints: "Pts",
40
45
  playIn: "Play-in", playInWinner: "Gagnant {a}/{b}", playInLoser: "Perdant {a}/{b}", seedSlot: "N°\u00a0{n}", winnerOf: "Gagnant du match {name}", loserOf: "Perdant du match {name}",
46
+ judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
47
+ judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "décision", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
48
+ judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}\u00a0: {n}\u00a0shido", judoShidoShort: "shido {a}–{b}",
49
+ judoRepechage: "Repêchage", judoBronze: "Combat pour la médaille de bronze", judoRepechageBronze: "Repêchages et combats pour la médaille de bronze", judoWhite: "blanc", judoBlue: "bleu",
50
+ judoWeightClass: "{sex} -{w}\u00a0kg", judoWeightOver: "{sex} +{w}\u00a0kg", judoMen: "Hommes", judoWomen: "Femmes",
41
51
  };
42
52
  const JA = {
43
53
  tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
@@ -55,6 +65,11 @@ const JA = {
55
65
  roadFinal: "決勝は{a} {score} {b}({how})でした。", roadChampion: "{name}が優勝しました。", roadToPlay: "決勝はまだ行われていません。",
56
66
  vs: "対", at: "@", groupName: "グループ{n}", colPlayed: "試合", colWon: "勝", colDrawn: "分", colLost: "敗", colDiff: "得失差", colPoints: "勝点",
57
67
  playIn: "プレイイン", playInWinner: "{a}/{b}の勝者", playInLoser: "{a}/{b}の敗者", seedSlot: "第{n}シード", winnerOf: "{name}の勝者", loserOf: "{name}の敗者",
68
+ judoIppon: "一", judoWazaAri: "技", judoYuko: "有", judoIpponLong: "一本", judoWazaAriLong: "技あり", judoYukoLong: "有効",
69
+ judoMethodIppon: "一本", judoMethodAwasete: "合わせ技一本", judoMethodScore: "優勢勝ち", judoMethodHansoku: "反則勝ち", judoMethodFusen: "不戦勝", judoMethodKiken: "棄権勝ち",
70
+ judoGolden: "GS", judoGoldenLong: "ゴールデンスコア", judoTime: "{t}", judoShidoOf: "{name}:指導{n}", judoShidoShort: "指導{a}–{b}",
71
+ judoRepechage: "敗者復活戦", judoBronze: "銅メダル決定戦", judoRepechageBronze: "敗者復活戦と銅メダル決定戦", judoWhite: "白", judoBlue: "青",
72
+ judoWeightClass: "{sex}{w}kg級", judoWeightOver: "{sex}{w}kg超級", judoMen: "男子", judoWomen: "女子",
58
73
  };
59
74
  const ES = {
60
75
  tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
@@ -72,6 +87,11 @@ const ES = {
72
87
  roadFinal: "La final: {a} {score} {b} ({how}).", roadChampion: "{name} ganó la final.", roadToPlay: "La final aún no se ha jugado.",
73
88
  vs: "vs.", at: "EN", groupName: "Grupo {n}", colPlayed: "PJ", colWon: "G", colDrawn: "E", colLost: "P", colDiff: "DG", colPoints: "Pts",
74
89
  playIn: "Play-in", playInWinner: "Ganador {a}/{b}", playInLoser: "Perdedor {a}/{b}", seedSlot: "Puesto {n}", winnerOf: "Ganador de {name}", loserOf: "Perdedor de {name}",
90
+ judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
91
+ judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "decisión", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
92
+ judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}: {n} shido", judoShidoShort: "shido {a}–{b}",
93
+ judoRepechage: "Repesca", judoBronze: "Combate por la medalla de bronce", judoRepechageBronze: "Repesca y combates por la medalla de bronce", judoWhite: "blanco", judoBlue: "azul",
94
+ judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Hombres", judoWomen: "Mujeres",
75
95
  };
76
96
  const DE = {
77
97
  tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
@@ -89,6 +109,11 @@ const DE = {
89
109
  roadFinal: "Das Finale: {a} {score} {b} ({how}).", roadChampion: "{name} gewann das Finale.", roadToPlay: "Das Finale steht noch aus.",
90
110
  vs: "vs.", at: "BEI", groupName: "Gruppe {n}", colPlayed: "Sp", colWon: "S", colDrawn: "U", colLost: "N", colDiff: "Diff.", colPoints: "Pkt",
91
111
  playIn: "Play-in", playInWinner: "Sieger {a}/{b}", playInLoser: "Verlierer {a}/{b}", seedSlot: "Platz {n}", winnerOf: "Sieger von {name}", loserOf: "Verlierer von {name}",
112
+ judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "Ippon", judoWazaAriLong: "Waza-ari", judoYukoLong: "Yuko",
113
+ judoMethodIppon: "Ippon", judoMethodAwasete: "Waza-ari awasete Ippon", judoMethodScore: "Punktsieg", judoMethodHansoku: "Hansoku-make", judoMethodFusen: "Fusen-gachi", judoMethodKiken: "Kiken-gachi",
114
+ judoGolden: "GS", judoGoldenLong: "Golden Score", judoTime: "{t}", judoShidoOf: "{name}: {n} Shido", judoShidoShort: "Shido {a}–{b}",
115
+ judoRepechage: "Hoffnungsrunde", judoBronze: "Kampf um Bronze", judoRepechageBronze: "Hoffnungsrunde und Kämpfe um Bronze", judoWhite: "weiß", judoBlue: "blau",
116
+ judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Herren", judoWomen: "Damen",
92
117
  };
93
118
  /**
94
119
  * The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
package/dist/matchup.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { judoHasScore, judoOutcome } from "./judo-model.js";
2
+ import { judoNotes, sideScoreLong } from "./judo-text.js";
1
3
  import { abbreviate } from "./text.js";
2
4
  import { fill } from "./locales.js";
3
5
  /** A bracket's teams: the list given, plus a team made from every id that a matchup names and the list does not. */
@@ -77,18 +79,31 @@ export function resolveMatchup(m, index, teams) {
77
79
  if (ids[0] !== null && ids[0] === ids[1])
78
80
  throw new Error(`taisen-brackets: matchup ${index} has ${JSON.stringify(ids[0])} on both sides`);
79
81
  const kind = kindOf(m);
80
- const { result, winner: settled } = settle(m, kind);
81
- let winner = settled;
82
+ let named;
82
83
  if (m.winner !== undefined) {
83
84
  if (m.winner === null)
84
- winner = null;
85
+ named = null;
85
86
  else if (ids[0] === m.winner)
86
- winner = 0;
87
+ named = 0;
87
88
  else if (ids[1] === m.winner)
88
- winner = 1;
89
+ named = 1;
89
90
  else
90
91
  throw new Error(`taisen-brackets: matchup ${index}'s winner ${JSON.stringify(m.winner)} is not one of its sides`);
91
92
  }
93
+ let judo;
94
+ let result;
95
+ let winner;
96
+ if (m.judo) {
97
+ judo = judoOutcome(m.judo, named ?? null);
98
+ if (judo.winner !== null && named !== undefined && named !== null && named !== judo.winner)
99
+ throw new RangeError(`taisen-brackets: matchup ${index}'s judo scores make side ${judo.winner} the winner, but its winner is side ${named}`);
100
+ winner = judo.winner;
101
+ result = { kind: "game", score: judoHasScore(judo) || winner !== null ? judo.points : null, played: judoHasScore(judo) || winner !== null ? 1 : 0, bestOf: null, pens: null, extraTime: false, replays: 0, games: [judo.points], decided: winner !== null };
102
+ }
103
+ else
104
+ ({ result, winner } = settle(m, kind));
105
+ if (named !== undefined)
106
+ winner = named;
92
107
  const sides = ids.map((id) => (id === null ? null : teams.get(id) ?? null));
93
108
  return {
94
109
  index,
@@ -102,6 +117,8 @@ export function resolveMatchup(m, index, teams) {
102
117
  third: !!m.third,
103
118
  walkover: !!m.walkover,
104
119
  note: m.note ?? null,
120
+ ...(judo ? { judo } : {}),
121
+ ...(m.stage ? { stage: m.stage } : {}),
105
122
  ...(m.seeds ? { seeds: m.seeds } : {}),
106
123
  ...(m.home !== undefined ? { home: m.home } : {}),
107
124
  };
@@ -112,7 +129,16 @@ export const abbrOf = (team, v) => (team ? team.abbr ?? abbreviate(v.team(team))
112
129
  export function noteParts(tie, v) {
113
130
  const r = tie.result;
114
131
  const w = v.words;
115
- const out = [];
132
+ const out = tie.judo ? judoNotes(tie, v, false) : [];
133
+ if (tie.stage === "repechage" || tie.stage === "bronze")
134
+ out.unshift(tie.stage === "repechage" ? w.judoRepechage : w.judoBronze);
135
+ if (tie.judo) {
136
+ if (tie.walkover)
137
+ out.push(w.walkover);
138
+ if (tie.note)
139
+ out.push(tie.note);
140
+ return out;
141
+ }
116
142
  if (r.kind === "series") {
117
143
  if (r.decided && r.played > 0)
118
144
  out.push(fill(w.inGames, { n: v.number(r.played) }));
@@ -139,7 +165,14 @@ export function noteParts(tie, v) {
139
165
  export function noteLong(tie, v) {
140
166
  const r = tie.result;
141
167
  const w = v.words;
142
- const out = [];
168
+ const out = tie.judo ? judoNotes(tie, v, true) : [];
169
+ if (tie.judo) {
170
+ if (tie.walkover)
171
+ out.push(w.walkover);
172
+ if (tie.note)
173
+ out.push(tie.note);
174
+ return out;
175
+ }
143
176
  if (r.kind === "series") {
144
177
  if (r.decided && r.played > 0)
145
178
  out.push(fill(w.inGames, { n: v.number(r.played) }));
@@ -169,7 +202,7 @@ export function describeTie(tie, v) {
169
202
  const [a, b] = tie.sides;
170
203
  const r = tie.result;
171
204
  let text = r.score
172
- ? fill(w.tieScore, { a: name(a), b: name(b), sa: v.number(r.score[0]), sb: v.number(r.score[1]) })
205
+ ? fill(w.tieScore, { a: name(a), b: name(b), sa: sideScoreLong(tie, 0, v), sb: sideScoreLong(tie, 1, v) })
173
206
  : fill(w.tieVs, { a: name(a), b: name(b) });
174
207
  const notes = noteLong(tie, v);
175
208
  if (notes.length)
package/dist/playin.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { fill } from "./locales.js";
2
2
  import { abbrOf, describeTie } from "./matchup.js";
3
+ import { sideScore } from "./judo-text.js";
3
4
  import { drawMark } from "./marks.js";
4
5
  import { bold, headTile, headingTexts, titleHeight } from "./furniture.js";
5
6
  import { esc, fit, num } from "./text.js";
@@ -147,7 +148,7 @@ export function drawPlayIn(layout, o, v, size, ctx, road, linked) {
147
148
  const team = t.sides[k];
148
149
  const ry = rect.y + k * rowH;
149
150
  const cy = ry + rowH / 2;
150
- const score = o.scores !== false && t.result.score ? v.number(t.result.score[k]) : "";
151
+ const score = o.scores !== false ? sideScore(t, k, v) : "";
151
152
  const cls = t.winner === null ? "tb-pending" : t.winner === k ? "tb-won" : "tb-lost";
152
153
  const items = [];
153
154
  const slotX = rect.x + rect.w - 5 - 26;
package/dist/render.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { layoutBracket } from "./layout.js";
2
2
  import { abbrOf, describeTie, noteParts } from "./matchup.js";
3
+ import { shidoCards, sideScore } from "./judo-text.js";
3
4
  import { cleanSvg, drawEmblem, drawMark, hasTrophy as hasTrophyOf, isEmblemId, markDefs, safeHref, safeUrl } from "./marks.js";
4
5
  import { roundLabel } from "./rounds.js";
5
6
  import { styleFor } from "./theme.js";
@@ -115,7 +116,7 @@ export function drawBracket(input, options = {}) {
115
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";
116
117
  const road = options.look === "road";
117
118
  const hasBg = typeof options.background === "string" || (typeof options.background === "object" && options.background !== null);
118
- 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]), 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 = 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
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() };
120
121
  const path = pathOf(layout, options.highlightPath);
121
122
  const linked = { count: 0 };
@@ -124,10 +125,10 @@ export function drawBracket(input, options = {}) {
124
125
  // The group tables and the play-in are not the tree: they plan and draw their own parts, and share the frame (the style, the background, the title) with it.
125
126
  const own = kind === "groups" ? drawGroups(layout, options, v, size, ctx, road, linked) : kind === "playin" ? drawPlayIn(layout, options, v, size, ctx, road, linked) : null;
126
127
  if (own)
127
- return frame({ prefix, v, options, theme, road, kind, title, desc: own.desc ?? desc, width: own.width, height: own.height, followed: false, links: [], parts: [...own.body, ...own.texts], linkCount: linked.count, ctx });
128
+ return frame({ prefix, v, options, theme, road, kind, title, desc: own.desc ?? desc, width: own.width, height: own.height, followed: false, links: [], parts: [...own.body, ...own.texts], linkCount: linked.count, ctx, judo: layout.ties.some((t) => t.judo !== undefined) });
128
129
  const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx, road, path, kind);
129
130
  const drawn = plan.boxes.map((b) => drawBox(b, plan, options, v, ctx, linked, layout.place[b.tie.index])); // draws the marks, so before the defs are written
130
- return frame({ prefix, v, options, theme, road, kind, title, desc, width: plan.width, height: plan.height, followed: path.size > 0, links: plan.links, parts: [...drawn, ...plan.texts, ...plan.extras], linkCount: linked.count, ctx });
131
+ return frame({ prefix, v, options, theme, road, kind, title, desc, width: plan.width, height: plan.height, followed: path.size > 0, links: plan.links, parts: [...drawn, ...plan.texts, ...plan.extras], linkCount: linked.count, ctx, judo: layout.ties.some((t) => t.judo !== undefined) });
131
132
  }
132
133
  /** The drawing's frame: the root `<svg>` with its title, its style, the defs the marks wrote and the background, round the parts a layout drew. */
133
134
  function frame(f) {
@@ -135,7 +136,7 @@ function frame(f) {
135
136
  const picture = resolveBackground(options.background, theme, f.width, f.height, prefix);
136
137
  const defs = markDefs(ctx) + (picture?.defs ?? "");
137
138
  const scope = scopeOf(options, prefix);
138
- const style = theme === "none" || options.style === false ? "" : `<style>${styleFor(`.${scope}`, theme, { road, links: f.linkCount > 0, follow: f.followed, bgAuto: picture?.auto, layout: f.kind !== "tree" || options.headers === true }, picture?.inks)}</style>`;
139
+ const style = theme === "none" || options.style === false ? "" : `<style>${styleFor(`.${scope}`, theme, { road, links: f.linkCount > 0, follow: f.followed, bgAuto: picture?.auto, layout: f.kind !== "tree" || options.headers === true, judo: f.judo }, picture?.inks)}</style>`;
139
140
  const bg = options.background === false ? "" : `<rect class="tb-bg" width="${num(f.width)}" height="${num(f.height)}"${theme === "none" ? ' fill="none"' : ""}/>${picture?.body ?? ""}`;
140
141
  return [
141
142
  `<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(v.locale.code)}" class="taisen ${prefix}${scope !== prefix ? ` ${scope}` : ""}${road ? " tb-look-road" : ""}${f.followed ? " tb-follow" : ""}${options.className ? ` ${esc(options.className)}` : ""}" viewBox="0 0 ${num(f.width)} ${num(f.height)}" width="${num(f.width)}" height="${num(f.height)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${prefix}-t ${prefix}-d">`,
@@ -168,7 +169,7 @@ export function bracketStyle(options) {
168
169
  if (theme === "none")
169
170
  return "";
170
171
  const picture = resolveBackground(options.background, theme, 100, 100, "x");
171
- return styleFor(`.${scope}`, theme, { road: options.look === "road", links: typeof options.href === "function", follow: options.highlightPath !== undefined, bgAuto: picture?.auto, layout: (options.layout ?? "tree") !== "tree" || options.headers === true }, picture?.inks);
172
+ return styleFor(`.${scope}`, theme, { road: options.look === "road", links: typeof options.href === "function", follow: options.highlightPath !== undefined, bgAuto: picture?.auto, layout: (options.layout ?? "tree") !== "tree" || options.headers === true, judo: options.judo === true }, picture?.inks);
172
173
  }
173
174
  /** The groups' names with the east first, for the series card: the names the teams carry, ordered so that a name with "east" comes before one with "west". */
174
175
  function eastFirst(input) {
@@ -181,6 +182,8 @@ function eastFirst(input) {
181
182
  }
182
183
  // ---------------------------------------------------------------------------------------------- the plan
183
184
  function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
185
+ // The ties kept apart below the tree are a little taller in the road look when any of them has a note to write (a judo contest's method and time), so that the note has its line.
186
+ const OH = road && [...layout.other, ...layout.robin, ...layout.playIn].some((i) => noteParts(layout.ties[i], v).length > 0) ? 66 : OTHER_H;
184
187
  // The layouts that draw each tie their own way take the tree's places and lines, and fill the boxes themselves.
185
188
  const V = VARIANTS.includes(kind) ? kind : null;
186
189
  const pad = vertical ? 14 : 22;
@@ -238,7 +241,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
238
241
  const top = pad + titleH + (hasGroups ? groupRoom : 0) + labelRoom;
239
242
  others = othersOf(layout);
240
243
  ow = clamp(D * 1.1, 120, 200);
241
- extraH0 = othersHeight(others.length, W0, pad, ow, OTHER_H) + thirdBlock;
244
+ extraH0 = othersHeight(others.length, W0, pad, ow, OH) + thirdBlock;
242
245
  const bodyH = size.height === null ? rows * 60 : size.height - top - pad - extraH0;
243
246
  rowStep = bodyH / rows;
244
247
  S = clamp(rowStep - (road ? 7 : 9), 24, road ? 132 : 104);
@@ -264,6 +267,8 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
264
267
  const tagW = tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 8))) + 8 : 0;
265
268
  // The road look's marks and scores are large: they are scaled down together until a row fits its box, before a name is dropped for its abbreviation.
266
269
  const anyNotes = layout.ties.some((t) => noteParts(t, v).length > 0);
270
+ // The widest score a row may carry: two digits, or a judo contest's marks ("W2 Y3").
271
+ const probe = layout.ties.reduce((best, t) => [0, 1].reduce((b, k) => { const x = sideScore(t, k, v); return wide(x, 10) > wide(b, 10) ? x : b; }, best), "00");
267
272
  // The road look's boxes of marks alone carry each score under its mark, unless marks alone were asked for.
268
273
  const markScores = road && o.detail !== "mark" && (o.scores ?? true) && layout.ties.some((t) => t.result.score !== null);
269
274
  const fitOf = (w, h, named) => {
@@ -273,7 +278,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
273
278
  const anyPens = layout.ties.some((t) => t.result.pens !== null);
274
279
  const extra = (anyBye ? wide(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? wide("(00)", Math.max(score * 0.55, 8)) + 5 : 0);
275
280
  const fixed = clamp(rowH * 0.16, 5, 12) + clamp(rowH * 0.18, 5, 10) + 8 + 10 + (tagW > 0 ? tagW + 3 : 0);
276
- const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide("00", score) + extra;
281
+ const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide(probe, score) + extra;
277
282
  return Math.min(1, (w - fixed) / varying);
278
283
  };
279
284
  let fitNow = 1;
@@ -284,7 +289,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
284
289
  const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
285
290
  const anyPens = layout.ties.some((t) => t.result.pens !== null);
286
291
  const extra = (anyBye ? boldWidth(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? boldWidth("(00)", Math.max(font - 2, 8)) + 3 : 0);
287
- const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth("00", score) + extra + 14;
292
+ const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth(probe, score) + extra + 14;
288
293
  const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
289
294
  return named === "mark" && o.markShape === "none" ? "compact" : named;
290
295
  };
@@ -389,7 +394,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
389
394
  dl = pick(wl, hl);
390
395
  const strays = othersOf(layout);
391
396
  const ows = Math.min(availW, 200);
392
- const strayH = othersHeight(strays.length, W0, pad, ows, OTHER_H);
397
+ const strayH = othersHeight(strays.length, W0, pad, ows, OH);
393
398
  const labelH = showLabels ? 22 : 8;
394
399
  const content = (champ ? champH + 14 : 0) + labelH + hl + (withThird ? 8 + 18 + hl + 8 : 0) + strayH;
395
400
  const top0 = pad + titleH;
@@ -410,7 +415,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
410
415
  y += 8 + 18 + hl + 8;
411
416
  }
412
417
  if (strays.length > 0)
413
- othersBlock(layout, strays, y + 6, W0, pad, ows, OTHER_H, v, road, boxes, texts);
418
+ othersBlock(layout, strays, y + 6, W0, pad, ows, OH, v, road, boxes, texts);
414
419
  texts.push(...headings(v, o, W0, pad, poster));
415
420
  return finish({ width: W0, height, boxes, links: [], texts, extras, detail: dl, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
416
421
  }
@@ -518,7 +523,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
518
523
  }
519
524
  if (bottomThird)
520
525
  thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, road, boxes, texts);
521
- othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OTHER_H, v, road, boxes, texts);
526
+ othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OH, v, road, boxes, texts);
522
527
  texts.push(...headings(v, o, width, pad, poster));
523
528
  return finish({ width, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
524
529
  }
@@ -531,7 +536,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
531
536
  const bandCount = halves ? 2 * bandsPerSide : nc + (champ ? 1 : 0);
532
537
  others = othersOf(layout);
533
538
  ow = Math.min(W0 - 2 * pad, 150);
534
- const othersV = othersHeight(others.length, W0, pad, ow, OTHER_H);
539
+ const othersV = othersHeight(others.length, W0, pad, ow, OH);
535
540
  // The height the page needs for boxes `h` high with `gap` between the rounds, and what it keeps for the final's band and the third-place match.
536
541
  const needOf = (h, gap) => {
537
542
  const finalHere = V === "columns" ? Math.max(h, finalH) : detail === "mark" ? Math.max(h, 52) : h;
@@ -664,7 +669,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
664
669
  }
665
670
  if (bottomThird)
666
671
  thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, road ? Math.max(boxW, 200) : 200), thirdH2, v, road, boxes, texts);
667
- othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OTHER_H, v, road, boxes, texts);
672
+ othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OH, v, road, boxes, texts);
668
673
  texts.push(...headings(v, o, W0, pad, poster));
669
674
  return finish({ width: W0, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
670
675
  }
@@ -868,7 +873,10 @@ function othersBlock(layout, list, y0, width, pad, ow, oh, v, road, boxes, texts
868
873
  return;
869
874
  const cols = Math.max(1, Math.floor((width - 2 * pad + 10) / (ow + 10)));
870
875
  const x0 = pad + (width - 2 * pad - (Math.min(cols, list.length) * (ow + 10) - 10)) / 2;
871
- texts.push(label(v, road, v.words.other, width / 2, y0 + 12, "middle"));
876
+ const stages = new Set(list.map((i) => layout.ties[i].stage ?? "other"));
877
+ const w = v.words;
878
+ const heading = stages.size === 1 && stages.has("repechage") ? w.judoRepechage : stages.size === 1 && stages.has("bronze") ? w.judoBronze : [...stages].every((x) => x === "repechage" || x === "bronze") ? w.judoRepechageBronze : w.other;
879
+ texts.push(label(v, road, heading, width / 2, y0 + 12, "middle"));
872
880
  list.forEach((i, n) => {
873
881
  const rect = { x: x0 + (n % cols) * (ow + 10), y: y0 + 24 + Math.floor(n / cols) * (oh + 10), w: ow, h: oh };
874
882
  boxes.push({ rect, tie: layout.ties[i], order: [0, 1], bye: null, byes: [], final: false, champion: false });
@@ -932,7 +940,7 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
932
940
  const withScore = plan.markScores && scoresOn && r.score !== null;
933
941
  const tags = [0, 1].map((k) => (plan.tags ? tagOf(tie, k) : ""));
934
942
  const tagH = tags.some((t) => t !== "") ? 12 : 0;
935
- const scoreOf = (k) => (withScore && r.score ? v.number(r.score[k]) : "");
943
+ const scoreOf = (k) => (withScore ? sideScore(tie, k, v) : "");
936
944
  const pensOf = (k) => (withScore && r.pens ? `(${v.number(r.pens[k])})` : "");
937
945
  const hasPens = withScore && r.pens !== null;
938
946
  const lay = (m) => {
@@ -1028,7 +1036,13 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
1028
1036
  rowParts.push(`<rect class="tb-tagbox" x="${num(tx)}" y="${num(cy - (tf + 4) / 2)}" width="${num(tw)}" height="${num(tf + 4)}" rx="${num((tf + 4) / 2)}"/><text class="tb-tag" x="${num(tx + tw / 2)}" y="${num(cy + tf * 0.35)}" font-size="${num(tf)}" text-anchor="middle">${esc(tag)}</text>`);
1029
1037
  tx += tw + 3;
1030
1038
  }
1031
- const score = scoresOn && r.score ? v.number(r.score[k]) : "";
1039
+ const score = scoresOn ? sideScore(tie, k, v) : "";
1040
+ // A judo contest: a bar at the row's edge in the colour of the side (the first is white, the second blue) and its shido cards beside the score.
1041
+ if (tie.judo)
1042
+ rowParts.push(`<rect class="${k === 0 ? "tb-jw" : "tb-jb"}" x="${num(x + 1.5)}" y="${num(cy - rowH * 0.3)}" width="3" height="${num(rowH * 0.6)}" rx="1.5"/>`);
1043
+ const cards = tie.judo && scoresOn ? shidoCards(tie, k) : [];
1044
+ const cardSize = Math.max(5, Math.min(9, rowH * 0.3));
1045
+ const cardsW = cards.length ? cards.length * (cardSize * 0.72 + 2) + 4 : 0;
1032
1046
  const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
1033
1047
  const bye = p.byes.includes(k);
1034
1048
  const right = x + w - padX;
@@ -1040,7 +1054,8 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
1040
1054
  const byeFits = bye && right - tx - (scoreW + pensW) - (mw(v.words.bye, small2) + 6) - 6 >= mw(`${seed}${nm}`, font);
1041
1055
  const byeText = byeFits ? v.words.bye : "";
1042
1056
  const byeW = byeText ? mw(byeText, small2) : 0;
1043
- const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
1057
+ const cardsOn = cardsW > 0 && right - tx - (scoreW + pensW) - cardsW - 6 >= 36;
1058
+ const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6 - (cardsOn ? cardsW : 0);
1044
1059
  // A name that does not fit is set a little smaller before it is cut.
1045
1060
  let nameFont = font;
1046
1061
  while (nameFont > 9.5 && mw(`${seed}${nm}`, nameFont) > avail)
@@ -1049,6 +1064,10 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
1049
1064
  rowParts.push(`<text class="tb-name${team ? "" : " tb-muted"}" x="${num(tx)}" y="${num(cy + nameFont * 0.35)}" font-size="${num(nameFont)}">${esc(text)}</text>`);
1050
1065
  if (score)
1051
1066
  rowParts.push(`<text class="tb-score" x="${num(right)}" y="${num(cy + scoreFont * 0.35)}" font-size="${num(scoreFont)}" text-anchor="end" font-variant-numeric="tabular-nums">${esc(score)}</text>`);
1067
+ if (cardsOn) {
1068
+ const cw = cardSize * 0.72;
1069
+ cards.forEach((c, n) => rowParts.push(`<rect class="${c === "r" ? "tb-sh-r" : "tb-sh-y"}" x="${num(right - scoreW - pensW - cardsW + 2 + n * (cw + 2))}" y="${num(cy - cardSize / 2)}" width="${num(cw)}" height="${num(cardSize)}" rx="1"/>`));
1070
+ }
1052
1071
  if (pensText)
1053
1072
  rowParts.push(`<text class="tb-muted" x="${num(right - scoreW - (road ? 4 : 3))}" y="${num(cy + small * 0.35)}" font-size="${num(small)}" text-anchor="end">${esc(pensText)}</text>`);
1054
1073
  if (byeText)
@@ -1059,7 +1078,7 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
1059
1078
  if (noteH) {
1060
1079
  const f = road ? 9 : 9.5;
1061
1080
  const line = road ? v.upper(notes.join(" · ")) : notes.join(" · ");
1062
- parts.push(`<text class="tb-muted${road ? " tb-note" : ""}" x="${num(x + w / 2)}" y="${num(y + h - (road ? 5 : 4))}" font-size="${f}"${road ? ' font-weight="700" letter-spacing=".05em"' : ""} text-anchor="middle">${esc(fit(line, f * (road ? 1.1 : 1), w - 8))}</text>`);
1081
+ parts.push(`<text class="tb-muted${road ? " tb-note" : ""}" x="${num(x + w / 2)}" y="${num(y + h - (road ? 5 : 4))}" font-size="${f}"${road ? ' font-weight="700" letter-spacing=".05em"' : ""} text-anchor="middle">${esc(fit(line, f * (road ? 1.1 : 1), w - 8, boldWidth))}</text>`);
1063
1082
  }
1064
1083
  }
1065
1084
  const extra = tieClassOf(o, tie);
package/dist/table.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { layoutBracket } from "./layout.js";
2
2
  import { noteLong } from "./matchup.js";
3
+ import { sideScore } from "./judo-text.js";
3
4
  import { fill, voiceOf } from "./locales.js";
4
5
  import { localizedLayout } from "./render.js";
5
6
  import { roundLabel } from "./rounds.js";
@@ -23,12 +24,12 @@ export function bracketTable(input, options = {}) {
23
24
  const text = team ? esc(team.name) : esc(w.tbd);
24
25
  return t.winner === k ? `<strong>${text}</strong>` : text;
25
26
  };
26
- const score = (t) => (t.result.score ? fill(w.scoreText, { a: v.number(t.result.score[0]), b: v.number(t.result.score[1]) }) : "");
27
+ const score = (t) => (t.result.score ? fill(w.scoreText, { a: sideScore(t, 0, v), b: sideScore(t, 1, v) }) : "");
27
28
  // Where a side comes from, in words, when the tie says so: "Loser of A v B".
28
29
  const named = (i) => fill(w.tieVs, { a: layout.ties[i].sides[0]?.name ?? w.tbd, b: layout.ties[i].sides[1]?.name ?? w.tbd });
29
30
  const origins = (t) => (t.from ?? []).flatMap((s) => (s ? [fill(s.result === "loser" ? w.loserOf : w.winnerOf, { name: named(s.tie) })] : []));
30
31
  const rows = order.map((t) => {
31
- const roundText = t.third ? w.thirdPlace : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
32
+ const roundText = t.third ? w.thirdPlace : t.stage === "repechage" ? w.judoRepechage : t.stage === "bronze" ? w.judoBronze : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
32
33
  const date = dated ? `<td>${esc(v.date(t.date) ?? "")}</td>` : "";
33
34
  return `<tr><th scope="row">${esc(roundText)}</th>${date}<td>${name(t, 0)}</td><td>${esc(score(t))}</td><td>${name(t, 1)}</td><td>${esc([...noteLong(t, v), ...origins(t)].join(w.noteJoin))}</td></tr>`;
34
35
  });
package/dist/theme.d.ts CHANGED
@@ -36,7 +36,16 @@ export interface StyleParts {
36
36
  bgAuto?: boolean;
37
37
  /** The rules of the layouts of their own (slots, shields, conference colours, header tiles, arrows, group cards). */
38
38
  layout?: boolean;
39
+ /** A judo bracket: the white and blue side bars and the shido cards (`--tb-judo-white`, `--tb-judo-blue`, `--tb-shido`, `--tb-hansoku`). */
40
+ judo?: boolean;
39
41
  }
42
+ /** The colours of a judo contest: the white and the blue judogi, the yellow shido card and the red hansoku-make card. */
43
+ export declare const JUDO_COLORS: Readonly<{
44
+ white: "#ffffff";
45
+ blue: "#1f4fa8";
46
+ shido: "#f0c000";
47
+ hansoku: "#d3302a";
48
+ }>;
40
49
  /** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
41
50
  export declare const CONFERENCE_COLORS: readonly [string, string];
42
51
  /** The colours a background says to draw in (`backgroundSvg`'s `ink`): what `styleFor` takes in place of a look's own palette. */
package/dist/theme.js CHANGED
@@ -10,6 +10,8 @@ export const ROAD_THEMES = Object.freeze({
10
10
  poster: { ring: "#f6f1e3", edge: "rgba(0,0,0,.5)", panel: "#1b2766", spine: "#d9b24c" },
11
11
  });
12
12
  const FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Hiragino Sans", "Yu Gothic UI", "Noto Sans CJK JP", Meiryo, sans-serif';
13
+ /** The colours of a judo contest: the white and the blue judogi, the yellow shido card and the red hansoku-make card. */
14
+ export const JUDO_COLORS = Object.freeze({ white: "#ffffff", blue: "#1f4fa8", shido: "#f0c000", hansoku: "#d3302a" });
13
15
  /** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
14
16
  export const CONFERENCE_COLORS = Object.freeze(["#2457c5", "#b3261e"]);
15
17
  /** A palette and a road palette from a background's ink. */
@@ -59,6 +61,9 @@ function rules(p, r, poster, withFont, parts, side) {
59
61
  const [ca, cb] = CONFERENCE_COLORS;
60
62
  list.push([[".tb-slot"], `fill:${v("plain")};fill-opacity:.55;stroke:${v("edge")};stroke-width:1`], [[".tb-won .tb-slot"], `fill-opacity:.9;stroke:${v("win")};stroke-width:1.6`], [[".tb-lost .tb-slot"], "fill-opacity:.35"], [[".tb-emptybar"], `fill:${v("plain")};opacity:.8`], [[".tb-seed"], "font-variant-numeric:tabular-nums"], [[".tb-seedn"], `fill:${v("muted")};opacity:.85`], [[".tb-shield"], `fill:none;stroke:${v("edge")};stroke-width:1.4`], [[".tb-won .tb-shield"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-shield.tb-empty"], `fill:${v("plain")};fill-opacity:.5`], [[".tb-vs", ".tb-code"], `fill:${v("muted")};font-weight:700`], [[".tb-c0 .tb-box"], `stroke:var(--tb-conf-a, ${ca});stroke-width:1.6`], [[".tb-c1 .tb-box"], `stroke:var(--tb-conf-b, ${cb});stroke-width:1.6`], [[".tb-atag"], `fill:${v("ink")}`], [[".tb-atag-t"], `fill:${v("bg")}`], [[".tb-c0 .tb-atag"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-c1 .tb-atag"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-c0 .tb-atag-t", ".tb-c1 .tb-atag-t"], "fill:#fff"], [[".tb-head"], `fill:${rv("panel", "panel")}`], [[".tb-head.tb-c0"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-head.tb-c1"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-head-t"], `fill:${v("ink")};font-weight:800`], [[".tb-head.tb-c0 + .tb-head-t", ".tb-head.tb-c1 + .tb-head-t"], "fill:#fff"], [[".tb-arrow"], `stroke:${v("line")};stroke-width:1.6;fill:none;stroke-linecap:round;stroke-linejoin:round`], [[".tb-arrow-loser"], "stroke-dasharray:5 4"], [[".tb-arrowhead"], `fill:${v("line")};stroke:none`], [[".tb-gcard"], `fill:${v("box")};stroke:${v("edge")};stroke-width:1`], [[".tb-gtile"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-gtile-t"], "fill:#fff;font-weight:800"], [[".tb-ghead"], `fill:${v("muted")};font-weight:700`], [[".tb-gcell"], `fill:${v("ink")};font-variant-numeric:tabular-nums`], [[".tb-gcell-p"], "font-weight:800"], [[".tb-gname"], `fill:${v("ink")};font-weight:700`], [[".tb-qual"], `fill:${v("win")};fill-opacity:.16`], [[".tb-gsep"], `stroke:${v("edge")};stroke-width:1;stroke-opacity:.6`]);
61
63
  }
64
+ if (parts.judo === true) {
65
+ list.push([[".tb-jw"], `fill:var(--tb-judo-white, ${JUDO_COLORS.white});stroke:${v("edge")};stroke-width:.8`], [[".tb-jb"], `fill:var(--tb-judo-blue, ${JUDO_COLORS.blue});stroke:${v("edge")};stroke-width:.8`], [[".tb-sh-y"], `fill:var(--tb-shido, ${JUDO_COLORS.shido});stroke:rgba(0,0,0,.35);stroke-width:.6`], [[".tb-sh-r"], `fill:var(--tb-hansoku, ${JUDO_COLORS.hansoku});stroke:rgba(0,0,0,.35);stroke-width:.6`]);
66
+ }
62
67
  if (parts.follow === true) {
63
68
  list.push([[".tb-tie"], "opacity:.4"], [[".tb-tie.tb-on-path"], "opacity:1"], [[".tb-on-path .tb-box"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-line"], "stroke-opacity:.25"], [[".tb-line.tb-path"], `stroke:${v("win")};stroke-width:4.5;stroke-opacity:1;stroke-linecap:round;stroke-dasharray:.1 9`]);
64
69
  }