@abreen/tada 1.0.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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/bin/tada.js +361 -0
  4. package/config/authors.json +1 -0
  5. package/config/nav.json +28 -0
  6. package/content/index.md +19 -0
  7. package/content/lectures/01/Pair.java.md +296 -0
  8. package/content/lectures/01/Rectangle.java +80 -0
  9. package/content/lectures/01/demo.py +9 -0
  10. package/content/lectures/01/index.md +39 -0
  11. package/content/lectures/01/lecture1.pdf +0 -0
  12. package/content/lectures/index.md +25 -0
  13. package/content/markdown.md +379 -0
  14. package/content/problem_sets/index.md +6 -0
  15. package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
  16. package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
  17. package/fonts/google-sans-code/LICENSE.txt +93 -0
  18. package/fonts/inter/InterVariable-Italic.ttf +0 -0
  19. package/fonts/inter/InterVariable.ttf +0 -0
  20. package/fonts/inter/LICENSE.txt +92 -0
  21. package/package.json +70 -0
  22. package/public/avatars/alex.jpg +0 -0
  23. package/public/test.txt +1 -0
  24. package/src/_mixins.scss +4 -0
  25. package/src/anchor/README.md +6 -0
  26. package/src/anchor/index.ts +34 -0
  27. package/src/anchor/style.scss +48 -0
  28. package/src/code/README.md +5 -0
  29. package/src/code/index.ts +113 -0
  30. package/src/code/style.scss +101 -0
  31. package/src/code.scss +54 -0
  32. package/src/header/README.md +8 -0
  33. package/src/header/index.ts +43 -0
  34. package/src/header/style.scss +228 -0
  35. package/src/index.ts +73 -0
  36. package/src/layout.scss +144 -0
  37. package/src/literate/style.scss +60 -0
  38. package/src/print/README.md +4 -0
  39. package/src/print/index.ts +32 -0
  40. package/src/print/style.scss +82 -0
  41. package/src/question/README.md +3 -0
  42. package/src/question/index.ts +25 -0
  43. package/src/question/style.scss +116 -0
  44. package/src/search/README.md +6 -0
  45. package/src/search/index.ts +574 -0
  46. package/src/search/style.scss +217 -0
  47. package/src/style.scss +815 -0
  48. package/src/timezone/index.test.ts +100 -0
  49. package/src/timezone/index.ts +298 -0
  50. package/src/timezone/style.scss +16 -0
  51. package/src/timezone/timezones.json +58 -0
  52. package/src/toc/README.md +3 -0
  53. package/src/toc/index.ts +322 -0
  54. package/src/toc/style.scss +203 -0
  55. package/src/top/README.md +4 -0
  56. package/src/top/index.ts +75 -0
  57. package/src/util.ts +122 -0
  58. package/templates/_author.html +27 -0
  59. package/templates/_bottom.html +3 -0
  60. package/templates/_download.html +1 -0
  61. package/templates/_heading.html +19 -0
  62. package/templates/_nav.html +18 -0
  63. package/templates/_theme.scss +97 -0
  64. package/templates/_top.html +87 -0
  65. package/templates/authors.schema.json +13 -0
  66. package/templates/code.html +31 -0
  67. package/templates/default.html +13 -0
  68. package/templates/literate.html +16 -0
  69. package/templates/nav.schema.json +27 -0
  70. package/tsconfig.json +15 -0
  71. package/types/dev.ts +3 -0
  72. package/types/sass.d.ts +1 -0
  73. package/types/site-variables.d.ts +16 -0
  74. package/webpack/apply-base-path-plugin.js +78 -0
  75. package/webpack/build-state.js +97 -0
  76. package/webpack/code.test.js +162 -0
  77. package/webpack/colors.js +15 -0
  78. package/webpack/config.base.js +147 -0
  79. package/webpack/config.dev.js +23 -0
  80. package/webpack/config.prod.js +32 -0
  81. package/webpack/content-watch-plugin.js +153 -0
  82. package/webpack/deflist-id-plugin.js +62 -0
  83. package/webpack/external-links-plugin.js +37 -0
  84. package/webpack/features.js +5 -0
  85. package/webpack/flair.json +1 -0
  86. package/webpack/generate-content-assets-plugin.js +308 -0
  87. package/webpack/generate-favicon-plugin.js +198 -0
  88. package/webpack/generate-fonts-plugin.js +69 -0
  89. package/webpack/generate-manifest-plugin.js +116 -0
  90. package/webpack/globals.js +74 -0
  91. package/webpack/heading-subtitle-plugin.js +80 -0
  92. package/webpack/json-schema.js +19 -0
  93. package/webpack/log.js +143 -0
  94. package/webpack/markdown-plugins.test.js +203 -0
  95. package/webpack/pagefind-plugin.js +379 -0
  96. package/webpack/pagefind-plugin.test.js +131 -0
  97. package/webpack/pdf-text.js +163 -0
  98. package/webpack/print-flair-plugin.js +22 -0
  99. package/webpack/reachability.js +273 -0
  100. package/webpack/reachability.test.js +80 -0
  101. package/webpack/serve.js +104 -0
  102. package/webpack/site-variables.js +53 -0
  103. package/webpack/site.schema.json +67 -0
  104. package/webpack/templates.js +128 -0
  105. package/webpack/text-to-id.js +8 -0
  106. package/webpack/toc-plugin.js +167 -0
  107. package/webpack/util.js +49 -0
  108. package/webpack/utils/code.js +439 -0
  109. package/webpack/utils/content-files.js +147 -0
  110. package/webpack/utils/define-plugin.js +20 -0
  111. package/webpack/utils/file-types.js +26 -0
  112. package/webpack/utils/front-matter.js +57 -0
  113. package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
  114. package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
  115. package/webpack/utils/literate-java.js +153 -0
  116. package/webpack/utils/markdown.js +244 -0
  117. package/webpack/utils/parse-hsl.js +8 -0
  118. package/webpack/utils/paths.js +58 -0
  119. package/webpack/utils/render.js +466 -0
  120. package/webpack/utils/shiki-highlighter.js +26 -0
  121. package/webpack/validate-internal-links-plugin.js +155 -0
  122. package/webpack/watch-reachability-state.js +273 -0
  123. package/webpack/watch-reachability-state.test.js +198 -0
  124. package/webpack/watch-reload-client.js +54 -0
  125. package/webpack/watch.js +166 -0
@@ -0,0 +1,100 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { detectPeriodStyle, to12Hour, normalizeHM } from './index';
3
+
4
+ describe('detectPeriodStyle', () => {
5
+ test('detects uppercase "PM"', () => {
6
+ expect(detectPeriodStyle('5:40 PM')).toEqual(['AM', 'PM']);
7
+ });
8
+
9
+ test('detects uppercase "AM"', () => {
10
+ expect(detectPeriodStyle('10:00 AM')).toEqual(['AM', 'PM']);
11
+ });
12
+
13
+ test('detects lowercase dotted "p.m."', () => {
14
+ expect(detectPeriodStyle('5:40 p.m.')).toEqual(['a.m.', 'p.m.']);
15
+ });
16
+
17
+ test('detects lowercase dotted "a.m."', () => {
18
+ expect(detectPeriodStyle('9:15 a.m.')).toEqual(['a.m.', 'p.m.']);
19
+ });
20
+
21
+ test('detects lowercase "pm"', () => {
22
+ expect(detectPeriodStyle('5:40 pm')).toEqual(['am', 'pm']);
23
+ });
24
+
25
+ test('detects lowercase "am"', () => {
26
+ expect(detectPeriodStyle('8:00 am')).toEqual(['am', 'pm']);
27
+ });
28
+
29
+ test('detects uppercase dotted "P.M."', () => {
30
+ expect(detectPeriodStyle('5:40 P.M.')).toEqual(['A.M.', 'P.M.']);
31
+ });
32
+
33
+ test('detects uppercase dotted "A.M."', () => {
34
+ expect(detectPeriodStyle('7:30 A.M.')).toEqual(['A.M.', 'P.M.']);
35
+ });
36
+
37
+ test('returns default for text with no AM/PM', () => {
38
+ expect(detectPeriodStyle('no time here')).toBeNull();
39
+ });
40
+
41
+ test('returns default for empty string', () => {
42
+ expect(detectPeriodStyle('')).toBeNull();
43
+ });
44
+ });
45
+
46
+ describe('to12Hour', () => {
47
+ test('formats afternoon time with default style', () => {
48
+ expect(to12Hour(17, 40)).toBe('5:40 p.m.');
49
+ });
50
+
51
+ test('formats morning time with default style', () => {
52
+ expect(to12Hour(9, 5)).toBe('9:05 a.m.');
53
+ });
54
+
55
+ test('formats with uppercase style', () => {
56
+ expect(to12Hour(17, 40, ['AM', 'PM'])).toBe('5:40 PM');
57
+ });
58
+
59
+ test('formats with lowercase style', () => {
60
+ expect(to12Hour(0, 0, ['am', 'pm'])).toBe('12:00 am');
61
+ });
62
+
63
+ test('noon is p.m.', () => {
64
+ expect(to12Hour(12, 0)).toBe('12:00 p.m.');
65
+ });
66
+
67
+ test('formats 12:30 p.m.', () => {
68
+ expect(to12Hour(12, 30)).toBe('12:30 p.m.');
69
+ });
70
+
71
+ test('midnight is a.m.', () => {
72
+ expect(to12Hour(0, 0)).toBe('12:00 a.m.');
73
+ });
74
+
75
+ test('formats 23:59', () => {
76
+ expect(to12Hour(23, 59)).toBe('11:59 p.m.');
77
+ });
78
+ });
79
+
80
+ describe('normalizeHM', () => {
81
+ test('normalizes minutes exceeding a day', () => {
82
+ expect(normalizeHM(1500)).toEqual([1, 0]);
83
+ });
84
+
85
+ test('normalizes negative minutes', () => {
86
+ expect(normalizeHM(-60)).toEqual([23, 0]);
87
+ });
88
+
89
+ test('handles zero', () => {
90
+ expect(normalizeHM(0)).toEqual([0, 0]);
91
+ });
92
+
93
+ test('handles exact day boundary', () => {
94
+ expect(normalizeHM(1440)).toEqual([0, 0]);
95
+ });
96
+
97
+ test('handles normal value', () => {
98
+ expect(normalizeHM(750)).toEqual([12, 30]);
99
+ });
100
+ });
@@ -0,0 +1,298 @@
1
+ const STORAGE_KEY = 'timezoneSelection';
2
+
3
+ type PeriodStyle = [am: string, pm: string];
4
+
5
+ const DEFAULT_PERIOD_STYLE: PeriodStyle = ['a.m.', 'p.m.'];
6
+
7
+ const PERIOD_PATTERNS: { pattern: RegExp; style: PeriodStyle }[] = [
8
+ { pattern: /[AP]\.M\./, style: ['A.M.', 'P.M.'] },
9
+ { pattern: /[AP]M/, style: ['AM', 'PM'] },
10
+ { pattern: /[ap]\.m\./, style: ['a.m.', 'p.m.'] },
11
+ { pattern: /[ap]m/, style: ['am', 'pm'] },
12
+ ];
13
+
14
+ export function detectPeriodStyle(text: string): PeriodStyle | null {
15
+ for (const { pattern, style } of PERIOD_PATTERNS) {
16
+ if (pattern.test(text)) {
17
+ return style;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function pad(n: number | string) {
24
+ return String(n).padStart(2, '0');
25
+ }
26
+
27
+ export function to12Hour(
28
+ h: number,
29
+ m: number,
30
+ style: PeriodStyle | null = DEFAULT_PERIOD_STYLE,
31
+ ) {
32
+ const hour12 = ((h + 11) % 12) + 1;
33
+ if (!style) {
34
+ return `${hour12}:${pad(m)}`;
35
+ }
36
+ const period = h >= 12 ? style[1] : style[0];
37
+ return `${hour12}:${pad(m)} ${period}`;
38
+ }
39
+
40
+ export function normalizeHM(totalMinutes: number) {
41
+ const mins = ((totalMinutes % 1440) + 1440) % 1440;
42
+ const h = Math.floor(mins / 60);
43
+ const m = mins % 60;
44
+ return [h, m] as const;
45
+ }
46
+
47
+ function parseHHMM(hhmm: string) {
48
+ const [h, m] = hhmm.split(':');
49
+ if (h == null || m == null) {
50
+ return NaN;
51
+ }
52
+ return Number(h) * 60 + Number(m);
53
+ }
54
+
55
+ // Compute UTC offset (in minutes) for a time zone at given date (DST-aware)
56
+ function getOffsetMinutes(tz: string, date: Date): number {
57
+ const dtf = new Intl.DateTimeFormat('en-US', {
58
+ timeZone: tz,
59
+ hour12: false,
60
+ year: 'numeric',
61
+ month: '2-digit',
62
+ day: '2-digit',
63
+ hour: '2-digit',
64
+ minute: '2-digit',
65
+ second: '2-digit',
66
+ });
67
+ const parts = dtf
68
+ .formatToParts(date)
69
+ .reduce<Record<string, string>>((acc, p) => {
70
+ if (p.type !== 'literal') {
71
+ acc[p.type] = p.value;
72
+ }
73
+ return acc;
74
+ }, {});
75
+ const utcTs = Date.UTC(
76
+ Number(parts.year),
77
+ Number(parts.month) - 1,
78
+ Number(parts.day),
79
+ Number(parts.hour),
80
+ Number(parts.minute),
81
+ Number(parts.second),
82
+ );
83
+ // Difference (local - UTC) in minutes
84
+ return (utcTs - date.getTime()) / 60000;
85
+ }
86
+
87
+ // window.siteVariables.timezones is replaced by DefinePlugin at build time.
88
+ // The typeof guard prevents a ReferenceError in test environments where
89
+ // window is not defined.
90
+ const TIMEZONES: TimezoneDef[] =
91
+ typeof window !== 'undefined' ? window.siteVariables.timezones : [];
92
+
93
+ function getDefaultTimezone() {
94
+ const value = window.siteVariables.defaultTimeZone;
95
+ return TIMEZONES.find(tz => tz.value === value)!;
96
+ }
97
+
98
+ function computeOffsets(baseDate: Date) {
99
+ TIMEZONES.forEach(tz => {
100
+ tz.offsetMinutes = getOffsetMinutes(tz.value, baseDate);
101
+ });
102
+ }
103
+
104
+ function init(element: HTMLDataListElement, selectedTz: string) {
105
+ const beforeText = window.document.createTextNode('Times shown in ');
106
+ element.parentElement?.insertBefore(beforeText, element);
107
+
108
+ TIMEZONES.forEach(tz => {
109
+ const opt = document.createElement('option');
110
+ opt.value = tz.value;
111
+ if (tz.value === 'UTC') {
112
+ opt.textContent = tz.label;
113
+ } else {
114
+ opt.textContent = `${tz.label} (${tz.abbreviation})`;
115
+ }
116
+ if (tz.value === selectedTz) {
117
+ opt.selected = true;
118
+ }
119
+ element.appendChild(opt);
120
+ });
121
+
122
+ element.removeAttribute('hidden');
123
+ }
124
+
125
+ function dominantPeriodStyle(
126
+ styles: Map<HTMLTimeElement, PeriodStyle | null>,
127
+ ): PeriodStyle {
128
+ const counts = new Map<PeriodStyle, number>();
129
+ for (const style of styles.values()) {
130
+ if (style !== null) {
131
+ counts.set(style, (counts.get(style) ?? 0) + 1);
132
+ }
133
+ }
134
+ let best: PeriodStyle = DEFAULT_PERIOD_STYLE;
135
+ let bestCount = 0;
136
+ for (const [style, count] of counts) {
137
+ if (count > bestCount) {
138
+ best = style;
139
+ bestCount = count;
140
+ }
141
+ }
142
+ return best;
143
+ }
144
+
145
+ export default (window: Window) => {
146
+ const defaultTz = getDefaultTimezone();
147
+ const resetTitle = `Reset time zone to ${defaultTz.abbreviation} (default)`;
148
+
149
+ // determine initial time zone (from storage or default)
150
+ let initialTz = defaultTz.value;
151
+ try {
152
+ const stored = window.localStorage.getItem(STORAGE_KEY);
153
+ if (stored && TIMEZONES.some(t => t.value === stored)) {
154
+ initialTz = stored;
155
+ }
156
+ } catch {
157
+ // ignored
158
+ }
159
+
160
+ const now = new Date();
161
+ computeOffsets(now);
162
+ const defaultOffset = defaultTz.offsetMinutes ?? 0;
163
+
164
+ // Snapshot the original AM/PM style from each <time> element's text
165
+ const periodStyles = new Map<HTMLTimeElement, PeriodStyle | null>();
166
+ Array.from(window.document.querySelectorAll('time[datetime]'))
167
+ .filter((el): el is HTMLTimeElement => el instanceof HTMLTimeElement)
168
+ .forEach(el => {
169
+ periodStyles.set(el, detectPeriodStyle(el.textContent ?? ''));
170
+ });
171
+ const pagePeriodStyle = dominantPeriodStyle(periodStyles);
172
+
173
+ function updateTimes(targetTz: string) {
174
+ const target = TIMEZONES.find(t => t.value === targetTz) || defaultTz;
175
+ const targetOffset = target.offsetMinutes ?? 0;
176
+ const deltaMinutes = targetOffset - defaultOffset;
177
+
178
+ Array.from(window.document.querySelectorAll('time[datetime]'))
179
+ .filter(el => el instanceof HTMLTimeElement)
180
+ .forEach(el => {
181
+ const datetime = el.getAttribute('datetime');
182
+ if (!datetime) {
183
+ return;
184
+ }
185
+
186
+ const isDefault = target.value === defaultTz.value;
187
+
188
+ const baseMinutes = parseHHMM(datetime);
189
+ if (isNaN(baseMinutes)) {
190
+ return;
191
+ }
192
+
193
+ // raw (can be < 0 or > 1439)
194
+ const rawMinutes = baseMinutes + deltaMinutes;
195
+ const dayShift =
196
+ Math.floor(rawMinutes / 1440) - (rawMinutes < 0 ? 1 : 0);
197
+ // Normalize after computing dayShift
198
+ const [h, m] = normalizeHM(rawMinutes);
199
+
200
+ let suffix = '';
201
+ if (!isDefault) {
202
+ if (dayShift === 1) {
203
+ suffix = ' <span class="next-prev-day">(next day)</span>';
204
+ } else if (dayShift === -1) {
205
+ suffix = ' <span class="next-prev-day">(prev. day)</span>';
206
+ }
207
+ }
208
+
209
+ const originalStyle = periodStyles.get(el) ?? null;
210
+ const originalIsPm = Math.floor(baseMinutes / 60) >= 12;
211
+ const periodChanged = originalIsPm !== h >= 12 || dayShift !== 0;
212
+ const style =
213
+ originalStyle === null && periodChanged
214
+ ? pagePeriodStyle
215
+ : originalStyle;
216
+ el.innerHTML = to12Hour(h, m, style) + suffix;
217
+
218
+ if (isDefault) {
219
+ el.classList.remove('is-modified');
220
+ el.title = '';
221
+ } else {
222
+ el.classList.add('is-modified');
223
+ el.title = `${to12Hour(...normalizeHM(baseMinutes), style ?? pagePeriodStyle)} ${defaultTz.abbreviation}`;
224
+ }
225
+ });
226
+ }
227
+
228
+ window.document
229
+ .querySelectorAll('select.time-zone')
230
+ .forEach(el => init(el as HTMLSelectElement, initialTz));
231
+
232
+ const choosers: Array<{ sync: (val: string) => void }> = [];
233
+
234
+ const syncAll = (val: string) => {
235
+ choosers.forEach(({ sync }) => sync(val));
236
+ };
237
+
238
+ window.document.querySelectorAll('select.time-zone').forEach(sel => {
239
+ const selectEl = sel as HTMLSelectElement;
240
+
241
+ const resetBtn = window.document.createElement('button');
242
+ resetBtn.type = 'button';
243
+ resetBtn.innerHTML =
244
+ '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">' +
245
+ '<line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>' +
246
+ '<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>' +
247
+ '</svg>';
248
+ resetBtn.className = 'icon-button';
249
+
250
+ resetBtn.setAttribute('aria-label', resetTitle);
251
+ resetBtn.title = resetTitle;
252
+
253
+ const wrapper = window.document.createElement('div');
254
+ wrapper.className = 'timezone-wrapper';
255
+ selectEl.insertAdjacentElement('beforebegin', wrapper);
256
+ wrapper.appendChild(selectEl);
257
+ wrapper.appendChild(resetBtn);
258
+
259
+ const sync = (val: string) => {
260
+ selectEl.value = val;
261
+ const isDefault = val === defaultTz.value;
262
+ resetBtn.style.opacity = isDefault ? '0' : '1';
263
+ resetBtn.style.pointerEvents = isDefault ? 'none' : '';
264
+ };
265
+
266
+ choosers.push({ sync });
267
+
268
+ selectEl.addEventListener('change', () => {
269
+ const val = selectEl.value;
270
+ try {
271
+ if (val === defaultTz.value) {
272
+ window.localStorage.removeItem(STORAGE_KEY);
273
+ } else {
274
+ window.localStorage.setItem(STORAGE_KEY, val);
275
+ }
276
+ } catch {
277
+ // ignore storage errors
278
+ }
279
+ syncAll(val);
280
+ updateTimes(val);
281
+ });
282
+
283
+ resetBtn.addEventListener('click', () => {
284
+ try {
285
+ window.localStorage.removeItem(STORAGE_KEY);
286
+ } catch {
287
+ // ignore
288
+ }
289
+ syncAll(defaultTz.value);
290
+ updateTimes(defaultTz.value);
291
+ });
292
+ });
293
+
294
+ // Initial render uses stored or default time zone
295
+ syncAll(initialTz);
296
+ updateTimes(initialTz);
297
+ return () => {};
298
+ };
@@ -0,0 +1,16 @@
1
+ time[datetime] {
2
+ &.is-modified {
3
+ text-decoration: var(--fg2-color) underline dotted;
4
+ }
5
+ }
6
+
7
+ span.next-prev-day {
8
+ color: var(--fg2-color);
9
+ font-style: italic;
10
+ }
11
+
12
+ .timezone-wrapper {
13
+ display: inline-flex;
14
+ align-items: center;
15
+ gap: 4px;
16
+ }
@@ -0,0 +1,58 @@
1
+ [
2
+ { "value": "Pacific/Honolulu", "label": "Hawaii", "abbreviation": "HT" },
3
+ { "value": "America/Anchorage", "label": "Alaska", "abbreviation": "AKT" },
4
+ {
5
+ "value": "America/Los_Angeles",
6
+ "label": "US Pacific",
7
+ "abbreviation": "PT"
8
+ },
9
+ { "value": "America/Denver", "label": "US Mountain", "abbreviation": "MT" },
10
+ { "value": "America/Chicago", "label": "US Central", "abbreviation": "CT" },
11
+ {
12
+ "value": "America/Mexico_City",
13
+ "label": "Mexico City",
14
+ "abbreviation": "CST"
15
+ },
16
+ { "value": "America/New_York", "label": "US Eastern", "abbreviation": "ET" },
17
+ { "value": "America/Halifax", "label": "Atlantic", "abbreviation": "AT" },
18
+ {
19
+ "value": "America/St_Johns",
20
+ "label": "Newfoundland",
21
+ "abbreviation": "NT"
22
+ },
23
+ { "value": "America/Sao_Paulo", "label": "São Paulo", "abbreviation": "BRT" },
24
+ {
25
+ "value": "America/Argentina/Buenos_Aires",
26
+ "label": "Buenos Aires",
27
+ "abbreviation": "ART"
28
+ },
29
+ { "value": "UTC", "label": "UTC", "abbreviation": "UTC" },
30
+ { "value": "Europe/London", "label": "London", "abbreviation": "GMT" },
31
+ { "value": "Europe/Paris", "label": "Paris", "abbreviation": "CET" },
32
+ { "value": "Africa/Lagos", "label": "Lagos", "abbreviation": "WAT" },
33
+ { "value": "Africa/Cairo", "label": "Cairo", "abbreviation": "EET" },
34
+ {
35
+ "value": "Africa/Johannesburg",
36
+ "label": "Johannesburg",
37
+ "abbreviation": "SAST"
38
+ },
39
+ { "value": "Europe/Moscow", "label": "Moscow", "abbreviation": "MSK" },
40
+ { "value": "Africa/Nairobi", "label": "Nairobi", "abbreviation": "EAT" },
41
+ { "value": "Asia/Tehran", "label": "Tehran", "abbreviation": "IRST" },
42
+ { "value": "Asia/Dubai", "label": "Dubai", "abbreviation": "GST" },
43
+ { "value": "Asia/Karachi", "label": "Karachi", "abbreviation": "PKT" },
44
+ { "value": "Asia/Kolkata", "label": "India", "abbreviation": "IST" },
45
+ { "value": "Asia/Dhaka", "label": "Dhaka", "abbreviation": "BST" },
46
+ { "value": "Asia/Jakarta", "label": "Jakarta", "abbreviation": "WIB" },
47
+ {
48
+ "value": "Asia/Ho_Chi_Minh",
49
+ "label": "Ho Chi Minh City",
50
+ "abbreviation": "ICT"
51
+ },
52
+ { "value": "Asia/Shanghai", "label": "Shanghai", "abbreviation": "CST" },
53
+ { "value": "Asia/Singapore", "label": "Singapore", "abbreviation": "SGT" },
54
+ { "value": "Asia/Tokyo", "label": "Tokyo", "abbreviation": "JST" },
55
+ { "value": "Asia/Seoul", "label": "Seoul", "abbreviation": "KST" },
56
+ { "value": "Australia/Sydney", "label": "Sydney", "abbreviation": "AEST" },
57
+ { "value": "Pacific/Auckland", "label": "Auckland", "abbreviation": "NZST" }
58
+ ]
@@ -0,0 +1,3 @@
1
+ # The `toc` component
2
+
3
+ A floating, dynamically updating table of contents.