@abreen/tada 1.6.2 → 1.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.
@@ -57,11 +57,39 @@ function parseFrontMatterPlainText(rawContent: string): RawParsedFrontMatter {
57
57
  };
58
58
  }
59
59
 
60
+ function parseFrontMatterStandard(rawContent: string): RawParsedFrontMatter {
61
+ const lines = rawContent.split(/\r?\n/);
62
+
63
+ // Caller has already verified lines[0] === '---'
64
+ for (let i = 1; i < lines.length; i++) {
65
+ if (lines[i].trimEnd() === '---') {
66
+ return {
67
+ frontMatter: lines.slice(1, i).join('\n'),
68
+ content: lines.slice(i + 1).join('\n'),
69
+ };
70
+ }
71
+ }
72
+
73
+ throw new Error(
74
+ 'Front matter starts with --- but no closing --- delimiter was found',
75
+ );
76
+ }
77
+
60
78
  export function parseFrontMatter(
61
79
  rawContent: string,
62
80
  ext: string,
63
81
  ): RawParsedFrontMatter {
64
82
  if (extensionIsMarkdown(ext) || ext === '.html') {
83
+ // Detect standard YAML front matter (first line is exactly ---,
84
+ // ignoring trailing whitespace and \r)
85
+ const firstNewline = rawContent.indexOf('\n');
86
+ const firstLine =
87
+ firstNewline === -1 ? rawContent : rawContent.slice(0, firstNewline);
88
+
89
+ if (firstLine.trimEnd() === '---') {
90
+ return parseFrontMatterStandard(rawContent);
91
+ }
92
+
65
93
  return parseFrontMatterPlainText(rawContent);
66
94
  } else {
67
95
  // unknown type, return raw
@@ -36,6 +36,22 @@ function capitalize(str: string): string {
36
36
  return str[0].toUpperCase() + str.slice(1);
37
37
  }
38
38
 
39
+ const MAX_FOOTNOTES = 35;
40
+
41
+ export function footnoteLabel(oneBasedIndex: number): string {
42
+ if (oneBasedIndex < 1 || oneBasedIndex > MAX_FOOTNOTES) {
43
+ throw new Error(
44
+ `Tada supports at most ${MAX_FOOTNOTES} footnotes per page ` +
45
+ `(9 digits + 26 capital letters), but footnote ${oneBasedIndex} ` +
46
+ `was requested. Reduce the number of footnotes on this page.`,
47
+ );
48
+ }
49
+ if (oneBasedIndex <= 9) {
50
+ return String(oneBasedIndex);
51
+ }
52
+ return String.fromCharCode('A'.charCodeAt(0) + (oneBasedIndex - 10));
53
+ }
54
+
39
55
  export function createMarkdown(
40
56
  siteVariables: SiteVariables,
41
57
  options: CreateMarkdownOptions = {},
@@ -196,24 +212,22 @@ export function createMarkdown(
196
212
  });
197
213
 
198
214
  /*
199
- * Customize markdown-it-footnote renderer
215
+ * Customize markdown-it-footnote renderer.
216
+ *
217
+ * Footnotes are labeled with single characters from Inter's ss06
218
+ * stylistic set: 1-9 for the first nine footnotes, then A-Z. The
219
+ * default <ol> numbering is suppressed via CSS and we inject a
220
+ * marker span per item so the label sequence can mix digits and
221
+ * letters.
200
222
  */
201
223
  markdown.renderer.rules.footnote_block_open = () =>
202
224
  '<div class="footnotes"><p class="title">Footnotes</p><ol>';
203
225
 
204
226
  markdown.renderer.rules.footnote_block_close = () => '</ol></div>';
205
227
 
206
- // Change appearance of reference
207
- const caption = markdown.renderer.rules.footnote_caption!;
208
- markdown.renderer.rules.footnote_caption = (
209
- tokens: Token[],
210
- idx: number,
211
- options: Options,
212
- env: unknown,
213
- self: Renderer,
214
- ) => {
215
- const str = caption(tokens, idx, options, env, self);
216
- return str.slice(1, str.length - 1);
228
+ markdown.renderer.rules.footnote_caption = (tokens: Token[], idx: number) => {
229
+ const id = tokens[idx].meta!.id as number;
230
+ return footnoteLabel(id + 1);
217
231
  };
218
232
 
219
233
  const footnoteRef = markdown.renderer.rules.footnote_ref!;
@@ -224,11 +238,26 @@ export function createMarkdown(
224
238
  env: unknown,
225
239
  self: Renderer,
226
240
  ) =>
241
+ // Prepend a non-breaking space so the reference link cannot wrap
242
+ // away from the word it follows.
243
+ '\u00a0' +
227
244
  footnoteRef(tokens, idx, options, env, self)
228
245
  .replace('<sup class="footnote-ref">', '')
229
246
  .replace('</sup>', '')
230
247
  .replace('<a href="', '<a class="footnote-ref" href="');
231
248
 
249
+ markdown.renderer.rules.footnote_open = (tokens: Token[], idx: number) => {
250
+ const id = tokens[idx].meta!.id as number;
251
+ const label = footnoteLabel(id + 1);
252
+ const refid = id + 1;
253
+ return (
254
+ `<li id="fn${refid}" class="footnote-item">` +
255
+ `<span class="footnote-marker" aria-hidden="true">${label}</span> `
256
+ );
257
+ };
258
+
259
+ markdown.renderer.rules.footnote_close = () => '</li>\n';
260
+
232
261
  const footnoteAnchor = markdown.renderer.rules.footnote_anchor!;
233
262
  markdown.renderer.rules.footnote_anchor = (
234
263
  tokens: Token[],
@@ -1,8 +1,10 @@
1
+ ---
1
2
  title: Home
2
3
  description: The home page for <%= site.title %>.
3
4
  author: alex
4
5
  published: 2026-03-20
5
6
  toolName: Tada
7
+ ---
6
8
 
7
9
  ## Welcome
8
10
 
@@ -1,9 +1,11 @@
1
+ ---
1
2
  parent: /labs/00/index.html
2
3
  parentLabel: Lab 0
3
4
  title: Counting vowels
4
5
  author: alex
5
6
  description: An example of a literate Java program that takes predefined input.
6
7
  stdin: "hello, world!\n"
8
+ ---
7
9
 
8
10
  A Java program can process input in different ways: it could open a file and
9
11
  read it; it could open a network connection and wait for data from another
@@ -1,8 +1,10 @@
1
+ ---
1
2
  parent: /labs/index.html
2
3
  parentLabel: Labs
3
4
  title: Lab 0
4
5
  author: alex
5
6
  description: An example lab page for <%= site.title %>.
7
+ ---
6
8
 
7
9
  In this lab, we'll explore the [`VowelCounter` program](./VowelCounter.java.html).
8
10
 
@@ -1,8 +1,10 @@
1
+ ---
1
2
  parent: /labs/index.html
2
3
  parentLabel: Labs
3
4
  title: Lab 1
4
5
  author: alex
5
6
  description: A lab page for <%= site.title %> demonstrating the trace feature.
7
+ ---
6
8
 
7
9
  In this lab, we'll examine memory diagrams.
8
10
 
@@ -1,6 +1,8 @@
1
+ ---
1
2
  title: Labs
2
3
  author: alex
3
4
  description: An example page for <%= site.title %>.
5
+ ---
4
6
 
5
7
  In each one-hour lab, we'll dive deeper and write code together, exploring how
6
8
  to apply the concepts we learned in lecture to solve real-world problems and
@@ -1,6 +1,8 @@
1
+ ---
1
2
  title: A generic `Pair` class
2
3
  author: alex
3
4
  description: An example of a literate Java program.
5
+ ---
4
6
 
5
7
  <!---
6
8
  You may hide code from the generated HTML page but still include it in the
@@ -1,8 +1,10 @@
1
+ ---
1
2
  parentLabel: Lectures
2
3
  parent: /lectures/index.html
3
4
  title: Lecture 1
4
5
  description: An example lecture page.
5
6
  author: alex
7
+ ---
6
8
 
7
9
  Here is a sample description of the topics covered at the first lecture.
8
10
 
@@ -1,8 +1,10 @@
1
+ ---
1
2
  parentLabel: Lectures
2
3
  parent: /lectures/index.html
3
4
  title: Lecture 2
4
5
  description: An example lecture page demonstrating partials.
5
6
  author: alex
7
+ ---
6
8
 
7
9
  Here is a page constructed from multiple partials.
8
10
 
@@ -1,6 +1,8 @@
1
+ ---
1
2
  title: Lectures
2
3
  author: alex
3
4
  description: An example page for <%= site.title %>.
5
+ ---
4
6
 
5
7
  <%= renderTimeZoneChooser() %>
6
8
 
@@ -1,18 +1,22 @@
1
+ ---
1
2
  title: Markdown Examples
2
3
  description: Examples of Markdown syntax supported by Tada.
3
4
  author: alex
4
5
  published: 2026-03-20
5
6
  toc: true
7
+ ---
6
8
 
7
9
  Markdown and HTML files in the `content/` directory
8
10
  must contain ["front matter"][front-matter] (YAML-formatted metadata).
9
11
  For example, this file's first five lines look like this:
10
12
 
11
13
  ```text
14
+ ---
12
15
  title: Markdown Examples
13
- description: Examples of Markdown syntax supported by tada.
16
+ description: Examples of Markdown syntax supported by Tada.
14
17
  toc: true
15
18
  author: alex
19
+ ---
16
20
  ```
17
21
 
18
22
  This page contains a table of contents because the front matter
@@ -182,10 +186,10 @@ footnote.[^example-footnote]
182
186
  Definition lists are supported using the same syntax as Presto:
183
187
 
184
188
  Stack
185
- : Last-in first-out collection.
189
+ : Last-in first-out collection.[^footnote-2]
186
190
 
187
191
  Queue
188
- : First-in first-out collection.
192
+ : First-in first-out collection.[^footnote-3]
189
193
 
190
194
  Binary tree
191
195
  : Hierarchical structure where each node has up to two children.
@@ -417,7 +421,15 @@ Here's a time range:
417
421
 
418
422
 
419
423
  [^example-footnote]: Footnotes render inside a numbered list at the bottom of
420
- the page.
424
+ the page.
425
+
426
+ Here's a second paragraph.
427
+
428
+ Here's a third paragraph.
429
+
430
+ [^footnote-2]: Also known as "LIFO".
431
+
432
+ [^footnote-3]: Also known as "FIFO".
421
433
 
422
434
  [front-matter]: https://www.npmjs.com/package/front-matter
423
435
  [markdown-it]: https://markdown-it.github.io/
@@ -1,7 +1,9 @@
1
+ ---
1
2
  title: Problem Sets
2
3
  author: alex
3
4
  description: An example page for <%= site.title %> which is not generated.
4
5
  skip: true
6
+ ---
5
7
 
6
8
  <p>
7
9
  For convenience or backwards compatibility, you can write standard HTML
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
package/src/_alerts.scss CHANGED
@@ -1,7 +1,7 @@
1
1
  @use 'mixins' as *;
2
2
 
3
3
  .alert {
4
- border: 3px solid var(--bg2-color);
4
+ border: 2px solid var(--bg2-color);
5
5
  position: relative;
6
6
  margin: 1rem 0;
7
7
  }
package/src/_base.scss CHANGED
@@ -92,7 +92,7 @@ code {
92
92
  font-size: var(--mono-font-size);
93
93
  background: var(--bg2-color);
94
94
  border-radius: var(--mono-border-radius);
95
- padding: 0.12em 0.25em;
95
+ padding: var(--mono-padding);
96
96
  overflow-wrap: anywhere;
97
97
  }
98
98
 
@@ -139,18 +139,16 @@ details .content {
139
139
  }
140
140
 
141
141
  @media (hover: hover) {
142
- details summary {
143
- &:hover {
144
- color: var(--fg2-color);
145
- @include link-underline;
146
- }
147
-
148
- &:active {
149
- color: var(--link-active-color);
150
- }
142
+ details summary:hover {
143
+ color: var(--fg2-color);
144
+ @include link-underline;
151
145
  }
152
146
  }
153
147
 
148
+ details summary:active {
149
+ color: var(--link-active-color);
150
+ }
151
+
154
152
  main details {
155
153
  margin-bottom: 1rem;
156
154
  border-radius: var(--border-radius);
package/src/_mixins.scss CHANGED
@@ -42,4 +42,8 @@
42
42
  -webkit-mask-position: center center;
43
43
  mask-position: center center;
44
44
  background-color: currentColor;
45
+ // The icon's visible fill is the background color, which browsers strip
46
+ // when printing by default. Preserve it so masked icons remain visible.
47
+ -webkit-print-color-adjust: exact;
48
+ print-color-adjust: exact;
45
49
  }
@@ -35,9 +35,21 @@ export default (window: Window) => {
35
35
  }
36
36
  window.addEventListener('keydown', handleWindowKeyDown);
37
37
 
38
+ function handleDetailsFocusOut(e: FocusEvent) {
39
+ if (!details.open) {
40
+ return;
41
+ }
42
+ const next = e.relatedTarget as Node | null;
43
+ if (!next || !details.contains(next)) {
44
+ close();
45
+ }
46
+ }
47
+ details.addEventListener('focusout', handleDetailsFocusOut);
48
+
38
49
  return () => {
39
50
  window.removeEventListener('keydown', handleWindowKeyDown);
40
51
  window.removeEventListener('click', handleWindowClick);
41
52
  details.removeEventListener('click', handleDetailsClick);
53
+ details.removeEventListener('focusout', handleDetailsFocusOut);
42
54
  };
43
55
  };
@@ -1,13 +1,10 @@
1
- :root {
2
- --print-font-size: 15px;
3
- }
1
+ @use '../mixins' as *;
4
2
 
5
3
  @media print {
6
4
  html,
7
- body,
8
- main,
9
- section {
5
+ body {
10
6
  background: none !important;
7
+ color: #000;
11
8
  }
12
9
 
13
10
  hr {
@@ -27,15 +24,22 @@
27
24
  margin-top: 0;
28
25
  }
29
26
 
30
- body {
31
- font-size: var(--print-font-size);
32
- color: #000;
27
+ main a:not(.heading-anchor):not(.footnote-ref):not(.line-number) {
28
+ @include link-underline;
33
29
  }
34
30
 
35
- pre {
36
- font-size: var(--print-font-size);
37
- border: none;
38
- padding: 0;
31
+ pre,
32
+ tt,
33
+ :not(pre) > code {
34
+ border: 1px solid #ccc;
35
+ }
36
+
37
+ // .alert strips the default padding from inline code (since it also
38
+ // strips the background). In print we add a border, so restore the
39
+ // padding so the border doesn't hug the text.
40
+ .alert .content tt,
41
+ .alert .content code {
42
+ padding: var(--mono-padding);
39
43
  }
40
44
 
41
45
  .footnotes {
@@ -50,18 +54,15 @@
50
54
  display: none !important;
51
55
  }
52
56
 
53
- main details {
54
- border: 2px solid #000;
55
- }
56
-
57
- main details .alert {
58
- border: none;
59
- padding: 0;
57
+ details,
58
+ .alert {
59
+ border-color: #000 !important;
60
60
  }
61
61
 
62
62
  section {
63
63
  border: none;
64
64
  padding: 0;
65
+ margin: 0;
65
66
  width: 100%;
66
67
  }
67
68
 
@@ -69,7 +70,8 @@
69
70
  display: none;
70
71
  }
71
72
 
72
- a.external::after {
73
- display: none;
73
+ .external-link-tail::after,
74
+ a.external:not(:has(.external-link-tail))::after {
75
+ content: none;
74
76
  }
75
77
  }
package/src/style.scss CHANGED
@@ -91,14 +91,34 @@ a.external:not(:has(.external-link-tail))::after {
91
91
  margin: 0;
92
92
  }
93
93
 
94
+ .footnotes ol {
95
+ list-style: none;
96
+ padding-left: 0;
97
+ }
98
+
94
99
  .footnotes li {
100
+ position: relative;
101
+ padding-left: 1.8em;
95
102
  margin-bottom: 8px;
96
103
  }
97
104
 
98
105
  a.footnote-ref {
99
- vertical-align: super;
100
- border-bottom: none;
106
+ vertical-align: text-bottom;
101
107
  font-size: 0.8em;
108
+ font-weight: 600;
109
+ }
110
+
111
+ .footnote-marker {
112
+ @include user-select(none);
113
+ position: absolute;
114
+ left: 0;
115
+ top: 0;
116
+ font-weight: 500;
117
+ }
118
+
119
+ a.footnote-ref,
120
+ .footnote-marker {
121
+ font-feature-settings: 'ss06';
102
122
  }
103
123
 
104
124
  .footnote-item {
@@ -108,7 +128,10 @@ a.footnote-ref {
108
128
  }
109
129
  }
110
130
 
111
- a.footnote-ref,
131
+ a.footnote-ref {
132
+ @include user-select(none);
133
+ }
134
+
112
135
  .footnotes .footnote-backref {
113
136
  display: inline-block;
114
137
  @include user-select(none);
@@ -61,10 +61,11 @@ body.code .toc ol a {
61
61
  .toc ol li:has(a):hover a {
62
62
  color: var(--fg2-color);
63
63
  }
64
+ }
64
65
 
65
- .toc ol li a:active {
66
- color: var(--link-active-color);
67
- }
66
+ .toc ol li:has(a):active,
67
+ .toc ol li:has(a):active a {
68
+ color: var(--link-active-color);
68
69
  }
69
70
 
70
71
  @media (min-width: 900px) {
@@ -57,6 +57,7 @@
57
57
  --mono-font-size: 0.94em;
58
58
  --mono-line-height: 1.5;
59
59
  --mono-border-radius: 0.375em;
60
+ --mono-padding: 0.12em 0.25em;
60
61
 
61
62
  // SVG icons
62
63
  // Mask icons (color-agnostic, painted via background-color: currentColor)