@jarenjs/mermaid 0.46.5 → 0.49.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.
@@ -0,0 +1,269 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Gantt layout: a resolved schedule → a deterministic
4
+ * `PositionedDiagram` scene. Pure, host-free and vnode-free, so the
5
+ * geometry can be asserted without rendering anything.
6
+ *
7
+ * The time axis is not this file's invention: the domain comes from the
8
+ * AST, the tick positions come from `@jarenjs/core/dates` — either the
9
+ * document's own `tickInterval` through `timeTicksEvery`, or the shared
10
+ * step ladder through `axisTicksTime`, the same one the chart component
11
+ * draws its time axis with — and the tick labels come from the
12
+ * document's `axisFormat` compiled once here. A second tick ladder in a
13
+ * renderer would be a second set of positions.
14
+ *
15
+ * Two decisions are made here rather than in the renderer, because both
16
+ * are geometry:
17
+ *
18
+ * - **Tick labels thin out.** `tickInterval 1day` over two years is 730
19
+ * ticks; the marks all stay, and labels are kept at the largest
20
+ * stride that leaves them from overlapping. The scene says which.
21
+ * - **Row names elide.** A name wider than the label column is cut with
22
+ * an ellipsis at layout time, so the measured width and the drawn text
23
+ * are the same string.
24
+ */
25
+
26
+ import { textWidth } from '@jarenjs/view/helpers';
27
+ import {
28
+ compileDateFormat, axisTicksTime, timeTicksEvery, partsFromEpoch,
29
+ } from '@jarenjs/core/dates';
30
+ import { coord as round } from '../utils.js';
31
+ import { strftimeToLdml } from '../parser/gantt-grammar.js';
32
+ import { createExcluder, DAY_MS } from '../gantt-calendar.js';
33
+
34
+ const FONT_SIZE = 13;
35
+ const MARGIN = 12;
36
+ const TITLE_H = 30;
37
+ const AXIS_H = 26;
38
+ const ROW_H = 24;
39
+ const BAR_H = 15;
40
+ const SECTION_H = 22;
41
+ const LABEL_PAD = 10;
42
+ const LABEL_MAX = 220;
43
+ const PLOT_W = 640;
44
+ const TICK_LABEL_GAP = 12;
45
+ const MILESTONE_R = 7;
46
+ const MIN_BAR_W = 2;
47
+ const LINK_INSET = 6;
48
+
49
+ /**
50
+ * @param {any} ast gantt AST (`parser/gantt.js`)
51
+ * @param {{ dateNames?: import('@jarenjs/core/dates').DateNames }} [options]
52
+ * @returns {any} PositionedDiagram
53
+ */
54
+ export function layoutGantt(ast, options = {}) {
55
+ const rules = ast.rules;
56
+ const title = ast.meta.title ?? null;
57
+
58
+ // --- the label column
59
+ let labelW = 0;
60
+ for (const section of ast.sections)
61
+ for (const task of section.tasks)
62
+ labelW = Math.max(labelW, textWidth(task.name, FONT_SIZE));
63
+ labelW = Math.min(LABEL_MAX, Math.ceil(labelW)) + 2 * LABEL_PAD;
64
+
65
+ const plotX = MARGIN + labelW;
66
+ const plotW = PLOT_W;
67
+ const width = plotX + plotW + MARGIN;
68
+
69
+ // --- the shared time domain
70
+ const domain = ast.domain;
71
+ const span = domain.end - domain.start || DAY_MS;
72
+ const xOf = (at) => round(plotX + ((at - domain.start) / span) * plotW);
73
+
74
+ // --- rows and sections, top to bottom
75
+ const top = (title === null ? 0 : TITLE_H) + AXIS_H + MARGIN;
76
+ const sections = [];
77
+ const rows = [];
78
+ /** @type {Map<string, any>} */
79
+ const byId = new Map();
80
+ let y = top;
81
+ let index = 0;
82
+ for (let si = 0; si < ast.sections.length; si++) {
83
+ const section = ast.sections[si];
84
+ const named = section.name !== null && section.name !== '';
85
+ const sectionTop = y;
86
+ if (named) y += SECTION_H;
87
+ for (const task of section.tasks) {
88
+ const milestone = task.flags.includes('milestone');
89
+ const barW = milestone ? 0 : Math.max(MIN_BAR_W, xOf(task.end) - xOf(task.start));
90
+ // a task far shorter than a pixel still has to be visible, and the
91
+ // minimum width would otherwise push the last bar of a long
92
+ // schedule past the right edge of its own plot
93
+ const barX = milestone
94
+ ? xOf((task.start + task.end) / 2)
95
+ : Math.min(xOf(task.start), plotX + plotW - barW);
96
+ const row = {
97
+ index,
98
+ id: task.id,
99
+ name: elide(task.name, labelW - 2 * LABEL_PAD),
100
+ line: task.line,
101
+ start: task.start,
102
+ end: task.end,
103
+ milestone,
104
+ flags: task.flags,
105
+ after: task.after,
106
+ y,
107
+ h: ROW_H,
108
+ labelX: MARGIN + LABEL_PAD,
109
+ labelY: round(y + ROW_H / 2),
110
+ x: round(barX),
111
+ w: round(barW),
112
+ barY: round(y + (ROW_H - BAR_H) / 2),
113
+ barH: BAR_H,
114
+ r: MILESTONE_R,
115
+ classes: classesFor(task.flags),
116
+ };
117
+ rows.push(row);
118
+ byId.set(task.id, row);
119
+ y += ROW_H;
120
+ index++;
121
+ }
122
+ sections.push({
123
+ name: named ? section.name : null,
124
+ index: si,
125
+ y: sectionTop,
126
+ h: y - sectionTop,
127
+ labelY: round(sectionTop + SECTION_H / 2),
128
+ band: si % 2 === 1,
129
+ });
130
+ }
131
+ const plotBottom = Math.max(y, top);
132
+ const height = plotBottom + MARGIN;
133
+
134
+ // --- ticks, positions from the core kernel and labels from axisFormat
135
+ const ticks = planTicks(domain, rules, xOf, options.dateNames);
136
+ // --- excluded days, as shaded bands clipped to the domain
137
+ const bands = planBands(domain, rules, xOf);
138
+ // --- dependency anchors
139
+ const links = planLinks(rows, byId);
140
+
141
+ return {
142
+ kind: 'gantt',
143
+ width,
144
+ height,
145
+ fontSize: FONT_SIZE,
146
+ title,
147
+ domain,
148
+ plot: { x: plotX, y: top, w: plotW, h: plotBottom - top },
149
+ axisY: (title === null ? 0 : TITLE_H) + AXIS_H,
150
+ titleY: title === null ? 0 : 22,
151
+ labelWidth: labelW,
152
+ ticks,
153
+ bands,
154
+ sections,
155
+ rows,
156
+ links,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Tick positions and the labels that survive collision.
162
+ * @param {{ start: number, end: number }} domain
163
+ * @param {any} rules
164
+ * @param {(at: number) => number} xOf
165
+ * @param {any} dateNames
166
+ * @returns {any[]}
167
+ */
168
+ function planTicks(domain, rules, xOf, dateNames) {
169
+ const at = rules.tick === null
170
+ ? axisTicksTime(domain.start, domain.end, Math.max(2, Math.round(PLOT_W / 110)))
171
+ : timeTicksEvery(domain.start, domain.end, rules.tick.unit, rules.tick.amount,
172
+ { weekStart: rules.weekStart });
173
+
174
+ const adapted = strftimeToLdml(rules.axisFormat);
175
+ // the parser already refused an unsupported specifier, so this compiles
176
+ const label = compileDateFormat(adapted.value, dateNames);
177
+ const parts = at.map((ms) => label(partsFromEpoch(ms)));
178
+
179
+ // keep every `stride`-th label, the smallest stride that fits
180
+ let widest = 0;
181
+ for (const text of parts) widest = Math.max(widest, textWidth(text, FONT_SIZE - 1));
182
+ const need = widest + TICK_LABEL_GAP;
183
+ const step = at.length > 1 ? Math.abs(xOf(at[1]) - xOf(at[0])) : need;
184
+ const stride = step >= need ? 1 : Math.max(1, Math.ceil(need / Math.max(step, 0.5)));
185
+
186
+ return at.map((ms, i) => ({
187
+ at: ms,
188
+ x: xOf(ms),
189
+ label: i % stride === 0 ? parts[i] : null,
190
+ }));
191
+ }
192
+
193
+ /**
194
+ * The excluded days, as bands over the domain. Rebuilt from the AST's
195
+ * published rules rather than carried through it, because a working
196
+ * calendar is a closure and the AST is plain JSON.
197
+ * @param {{ start: number, end: number }} domain
198
+ * @param {any} rules
199
+ * @param {(at: number) => number} xOf
200
+ * @returns {any[]}
201
+ */
202
+ function planBands(domain, rules, xOf) {
203
+ const excluder = createExcluder(rules.excludes, rules.weekendStart);
204
+ if (!excluder.any) return [];
205
+ const fromDay = Math.floor(domain.start / DAY_MS);
206
+ const toDay = Math.ceil(domain.end / DAY_MS);
207
+ // a domain wider than a few years shades nothing: the bands would be
208
+ // narrower than a hairline and the answer is the axis, not stripes
209
+ if (toDay - fromDay > 1200) return [];
210
+ return excluder.intervalsIn(fromDay, toDay).map((span) => {
211
+ const start = Math.max(span.start, domain.start);
212
+ const end = Math.min(span.end, domain.end);
213
+ return { start, end, x: xOf(start), w: round(Math.max(0, xOf(end) - xOf(start))) };
214
+ }).filter((band) => band.w > 0);
215
+ }
216
+
217
+ /**
218
+ * One orthogonal connector per `after` dependency: out of the
219
+ * predecessor's right edge, across, and into this row's left edge.
220
+ * @param {any[]} rows
221
+ * @param {Map<string, any>} byId
222
+ * @returns {any[]}
223
+ */
224
+ function planLinks(rows, byId) {
225
+ const links = [];
226
+ for (const row of rows) {
227
+ for (const id of row.after) {
228
+ const from = byId.get(id);
229
+ if (from === undefined || from.index >= row.index) continue;
230
+ const x1 = round(from.milestone ? from.x + from.r : from.x + from.w);
231
+ const y1 = round(from.y + from.h / 2);
232
+ const x2 = round(row.milestone ? row.x - row.r : row.x);
233
+ const y2 = round(row.y + row.h / 2);
234
+ if (Math.abs(x2 - x1) < 1) {
235
+ // the successor starts where its predecessor ended: a straight
236
+ // drop reads as "immediately after", a hook reads as a delay
237
+ links.push({ from: id, to: row.id, points: [[x1, y1], [x1, y2]] });
238
+ continue;
239
+ }
240
+ const mid = round(Math.max(x1 + LINK_INSET, x2 - LINK_INSET));
241
+ links.push({ from: id, to: row.id, points: [[x1, y1], [mid, y1], [mid, y2], [x2, y2]] });
242
+ }
243
+ }
244
+ return links;
245
+ }
246
+
247
+ /**
248
+ * @param {string[]} flags
249
+ * @returns {string}
250
+ */
251
+ function classesFor(flags) {
252
+ let out = 'mm-gantt-task';
253
+ for (const flag of flags) out += ` mm-gantt-${flag}`;
254
+ return out;
255
+ }
256
+
257
+ /**
258
+ * Cut a name to the label column, measuring the string that will
259
+ * actually be drawn.
260
+ * @param {string} text
261
+ * @param {number} max
262
+ * @returns {string}
263
+ */
264
+ function elide(text, max) {
265
+ if (textWidth(text, FONT_SIZE) <= max) return text;
266
+ let cut = text.length;
267
+ while (cut > 1 && textWidth(text.slice(0, cut) + '…', FONT_SIZE) > max) cut--;
268
+ return text.slice(0, cut) + '…';
269
+ }
@@ -0,0 +1,298 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The three Gantt token grammars, adapted to the core date
4
+ * kernel. Mermaid's Gantt header speaks **three different pattern
5
+ * languages** and none of them is Unicode LDML:
6
+ *
7
+ * - `dateFormat` is dayjs' `customParseFormat` vocabulary (moment's
8
+ * spelling): `YYYY-MM-DD`, where `YYYY` is the calendar year;
9
+ * - `axisFormat` is d3-time-format's strftime vocabulary: `%Y-%m-%d`;
10
+ * - `tickInterval` and a task's duration are two small regular
11
+ * grammars of their own (`1week`, `3d`).
12
+ *
13
+ * Each gets its own tokenizer here, and each token is mapped
14
+ * individually onto the LDML token `compileDateFormat`/`compileDateParser`
15
+ * understand. Handing `YYYY` straight to the core compiler would be a
16
+ * silent lie — LDML's `YYYY` is the *week-numbering* year, which is
17
+ * moment's most reported footgun and the reason `@jarenjs/core/dates`
18
+ * refuses that spelling in the first place.
19
+ *
20
+ * Every function here returns `{ value, error }` rather than throwing:
21
+ * the caller owns the source line, and a `JM` error without one is not
22
+ * worth much.
23
+ *
24
+ * The vendored conformance table for all of this — which tokens the
25
+ * shipped Mermaid version documents, which of them this engine
26
+ * implements and why the rest are refused — is
27
+ * `test/mermaid/fixtures/gantt-grammar.json`, pinned against this file
28
+ * by `test/mermaid/gantt.test.js`.
29
+ */
30
+
31
+ /** Mermaid's own default when a diagram declares no `dateFormat`. */
32
+ export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
33
+
34
+ /** Mermaid's own default when a diagram declares no `axisFormat`. */
35
+ export const DEFAULT_AXIS_FORMAT = '%Y-%m-%d';
36
+
37
+ /** dayjs token → the LDML token that reads and writes the same field. */
38
+ const MOMENT_TO_LDML = Object.freeze({
39
+ YYYY: 'yyyy', YY: 'yy', Y: 'y',
40
+ MMMM: 'MMMM', MMM: 'MMM', MM: 'MM', M: 'M',
41
+ DD: 'dd', D: 'd',
42
+ HH: 'HH', H: 'H', hh: 'hh', h: 'h',
43
+ mm: 'mm', m: 'm', ss: 'ss', s: 's',
44
+ SSS: 'SSS', S: 'S',
45
+ A: 'a', a: 'a',
46
+ ZZ: 'XX', Z: 'XXX',
47
+ });
48
+
49
+ /** Documented dayjs tokens this engine will not read, and why. */
50
+ const MOMENT_REFUSED = Object.freeze({
51
+ SS: 'the fraction tokens are tenths (S) and milliseconds (SSS); there is no hundredths field',
52
+ Q: 'a quarter is derived from a month, not a field of a date',
53
+ Do: 'the ordinal suffix is locale text and this engine ships no ordinal data',
54
+ DDDD: 'the day of the year is derived from a date, not a field of one',
55
+ DDD: 'the day of the year is derived from a date, not a field of one',
56
+ X: 'an epoch second is a whole value, not a calendar field',
57
+ x: 'an epoch millisecond is a whole value, not a calendar field',
58
+ ww: 'the ISO week number is derived from a date, not a field of one',
59
+ w: 'the ISO week number is derived from a date, not a field of one',
60
+ });
61
+
62
+ /** strftime specifier → the LDML fragment that writes the same field. */
63
+ const STRFTIME_TO_LDML = Object.freeze({
64
+ Y: 'yyyy', y: 'yy', m: 'MM', d: 'dd',
65
+ H: 'HH', I: 'hh', M: 'mm', S: 'ss', L: 'SSS', j: 'DDD',
66
+ a: 'EEE', A: 'EEEE', b: 'MMM', B: 'MMMM', p: 'a',
67
+ Z: 'XX',
68
+ x: "MM'/'dd'/'yyyy", X: "HH':'mm':'ss",
69
+ '%': "'%'",
70
+ });
71
+
72
+ /** Documented strftime specifiers this engine will not write, and why. */
73
+ const STRFTIME_REFUSED = Object.freeze({
74
+ e: 'a space-padded day is a token the formatter does not have',
75
+ c: 'it expands to %e, which is unsupported',
76
+ U: 'd3 counts weeks from the first Sunday; the core week number is ISO, so the two disagree',
77
+ W: 'd3 counts weeks from the first Monday; the core week number is ISO, so the two disagree',
78
+ w: 'd3 numbers Sunday 0; the core weekday number is ISO, where 1 is Monday',
79
+ f: 'microseconds are below the millisecond this suite measures time in',
80
+ g: 'the week-based year is a second year field, and the core parts record has one',
81
+ G: 'the week-based year is a second year field, and the core parts record has one',
82
+ q: 'a quarter on a time axis is a label, not a tick this engine plans',
83
+ Q: 'an epoch is a whole value, not a calendar field',
84
+ s: 'an epoch is a whole value, not a calendar field',
85
+ u: 'the ISO weekday number is a tick label this engine does not plan',
86
+ V: 'the ISO week number is a tick label this engine does not plan',
87
+ });
88
+
89
+ /** The LDML tokens that need a `names` provider before they compile. */
90
+ const NEEDS_NAMES = new Set(['MMM', 'MMMM', 'EEE', 'EEEE', 'a']);
91
+
92
+ const MAX_MOMENT_TOKEN = 4;
93
+
94
+ /**
95
+ * @typedef {{ value: any, error: string | null }} Adapted
96
+ */
97
+
98
+ /** @param {string} message @returns {Adapted} */
99
+ function bad(message) {
100
+ return { value: null, error: message };
101
+ }
102
+
103
+ /** @param {any} value @returns {Adapted} */
104
+ function ok(value) {
105
+ return { value, error: null };
106
+ }
107
+
108
+ /**
109
+ * Quote a run of literal text so `compileDateFormat` cannot read a
110
+ * letter inside it as a token.
111
+ * @param {string} text
112
+ * @returns {string}
113
+ */
114
+ function quoted(text) {
115
+ return text === '' ? '' : `'${text.replace(/'/g, "''")}'`;
116
+ }
117
+
118
+ /**
119
+ * Adapt a Mermaid `dateFormat` to an LDML pattern.
120
+ * @param {string} pattern - a dayjs `customParseFormat` pattern
121
+ * @returns {Adapted} `value` is the LDML pattern
122
+ */
123
+ export function momentToLdml(pattern) {
124
+ let out = '';
125
+ let literal = '';
126
+ for (let i = 0; i < pattern.length;) {
127
+ const ch = pattern[i];
128
+ if (ch === '[') { // dayjs' bracket escape
129
+ const end = pattern.indexOf(']', i + 1);
130
+ if (end < 0)
131
+ return bad(`unterminated '[' escape in dateFormat '${pattern}'`);
132
+ literal += pattern.slice(i + 1, end);
133
+ i = end + 1;
134
+ continue;
135
+ }
136
+ let matched = null;
137
+ for (let len = MAX_MOMENT_TOKEN; len >= 1; len--) {
138
+ const candidate = pattern.slice(i, i + len);
139
+ if (candidate.length !== len) continue;
140
+ if (MOMENT_TO_LDML[candidate] !== undefined || MOMENT_REFUSED[candidate] !== undefined) {
141
+ matched = candidate;
142
+ break;
143
+ }
144
+ }
145
+ if (matched === null) {
146
+ literal += ch;
147
+ i += 1;
148
+ continue;
149
+ }
150
+ if (MOMENT_REFUSED[matched] !== undefined) {
151
+ return bad(`dateFormat token '${matched}' is not supported: ${MOMENT_REFUSED[matched]}`);
152
+ }
153
+ out += quoted(literal);
154
+ literal = '';
155
+ out += MOMENT_TO_LDML[matched];
156
+ i += matched.length;
157
+ }
158
+ out += quoted(literal);
159
+ return ok(out);
160
+ }
161
+
162
+ /**
163
+ * Adapt a Mermaid `axisFormat` to an LDML pattern. This is a separate
164
+ * grammar from `dateFormat` on purpose: `%m` is a month and `m` is a
165
+ * minute, so one table serving both would silently mis-read half of
166
+ * every axis.
167
+ * @param {string} pattern - a d3-time-format (strftime) pattern
168
+ * @returns {Adapted} `value` is the LDML pattern
169
+ */
170
+ export function strftimeToLdml(pattern) {
171
+ let out = '';
172
+ let literal = '';
173
+ for (let i = 0; i < pattern.length;) {
174
+ if (pattern[i] !== '%') {
175
+ literal += pattern[i];
176
+ i += 1;
177
+ continue;
178
+ }
179
+ const spec = pattern[i + 1];
180
+ if (spec === undefined)
181
+ return bad(`axisFormat '${pattern}' ends in a bare '%'`);
182
+ if (STRFTIME_REFUSED[spec] !== undefined)
183
+ return bad(`axisFormat specifier '%${spec}' is not supported: ${STRFTIME_REFUSED[spec]}`);
184
+ const ldml = STRFTIME_TO_LDML[spec];
185
+ if (ldml === undefined)
186
+ return bad(`axisFormat specifier '%${spec}' is not a d3-time-format directive`);
187
+ out += quoted(literal);
188
+ literal = '';
189
+ out += ldml;
190
+ i += 2;
191
+ }
192
+ out += quoted(literal);
193
+ return ok(out);
194
+ }
195
+
196
+ /**
197
+ * Whether an LDML pattern uses a token that needs locale names, and
198
+ * which token that is. Used to turn `compileDateFormat`'s TypeError into
199
+ * a `JM` error that names the Mermaid spelling and the way out.
200
+ * @param {string} ldml
201
+ * @returns {string | null}
202
+ */
203
+ export function ldmlNeedsNames(ldml) {
204
+ let inQuote = false;
205
+ for (let i = 0; i < ldml.length;) {
206
+ if (ldml[i] === "'") {
207
+ if (ldml[i + 1] === "'") { i += 2; continue; }
208
+ inQuote = !inQuote;
209
+ i += 1;
210
+ continue;
211
+ }
212
+ if (inQuote) { i += 1; continue; }
213
+ for (let len = 4; len >= 1; len--) {
214
+ const candidate = ldml.slice(i, i + len);
215
+ if (candidate.length === len && NEEDS_NAMES.has(candidate))
216
+ return candidate;
217
+ }
218
+ i += 1;
219
+ }
220
+ return null;
221
+ }
222
+
223
+ // `tickInterval` is one regular grammar in the shipped renderer, and
224
+ // this is that regular expression. It is a module constant: a Gantt
225
+ // header is read once per parse, but building a pattern per parse is
226
+ // the habit this codebase does not have.
227
+ const TICK_INTERVAL = /^([1-9]\d*)(millisecond|second|minute|hour|day|week|month)$/;
228
+
229
+ /**
230
+ * Parse a `tickInterval` directive.
231
+ * @param {string} text
232
+ * @returns {Adapted} `value` is `{ amount, unit }`, the unit a core
233
+ * `DATE_UNITS` member
234
+ */
235
+ export function parseTickInterval(text) {
236
+ const match = TICK_INTERVAL.exec(text.trim());
237
+ if (match === null) {
238
+ return bad(`tickInterval '${text}' is not a whole count followed by`
239
+ + ' millisecond, second, minute, hour, day, week or month');
240
+ }
241
+ return ok({ amount: Number(match[1]), unit: match[2] });
242
+ }
243
+
244
+ // The task duration grammar, likewise straight from the shipped chunk.
245
+ const DURATION = /^(\d+(?:\.\d+)?)([Mdhmswy]|ms)$/;
246
+
247
+ /** Mermaid's one-letter duration units → core calendar units. */
248
+ const DURATION_UNITS = Object.freeze({
249
+ ms: 'millisecond', s: 'second', m: 'minute', h: 'hour',
250
+ d: 'day', w: 'week', M: 'month', y: 'year',
251
+ });
252
+
253
+ /**
254
+ * Parse a task duration (`3d`, `1.5w`, `2M`).
255
+ * @param {string} text
256
+ * @returns {Adapted} `value` is `{ amount, unit }`, or `null` with no
257
+ * error when the text is simply not a duration (the caller then tries
258
+ * to read it as a date)
259
+ */
260
+ export function parseTaskDuration(text) {
261
+ const match = DURATION.exec(text.trim());
262
+ if (match === null)
263
+ return { value: null, error: null };
264
+ return ok({ amount: Number(match[1]), unit: DURATION_UNITS[match[2]] });
265
+ }
266
+
267
+ /** ISO weekday numbers, 1 is Monday — the numbering `isoWeekdayFromDays` uses. */
268
+ export const ISO_WEEKDAYS = Object.freeze({
269
+ monday: 1, tuesday: 2, wednesday: 3, thursday: 4,
270
+ friday: 5, saturday: 6, sunday: 7,
271
+ });
272
+
273
+ /**
274
+ * Parse a `weekday` directive: which day a whole-week tick starts on.
275
+ * @param {string} text
276
+ * @returns {Adapted} `value` is an ISO weekday, 1 to 7
277
+ */
278
+ export function parseWeekday(text) {
279
+ const day = ISO_WEEKDAYS[text.trim().toLowerCase()];
280
+ if (day === undefined)
281
+ return bad(`weekday '${text}' is not a day name (monday … sunday)`);
282
+ return ok(day);
283
+ }
284
+
285
+ /**
286
+ * Parse a `weekend` directive: the first of the two days
287
+ * `excludes weekends` removes.
288
+ * @param {string} text
289
+ * @returns {Adapted} `value` is an ISO weekday, 5 (friday) or 6 (saturday)
290
+ */
291
+ export function parseWeekend(text) {
292
+ const name = text.trim().toLowerCase();
293
+ if (name !== 'friday' && name !== 'saturday')
294
+ return bad(`weekend '${text}' is neither friday nor saturday`);
295
+ return ok(ISO_WEEKDAYS[name]);
296
+ }
297
+
298
+ export { MOMENT_TO_LDML, MOMENT_REFUSED, STRFTIME_TO_LDML, STRFTIME_REFUSED };