@abreen/tada 1.4.1 → 1.5.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/README.md +4 -2
- package/bin/tada.ts +81 -57
- package/build/apply-base-path-plugin.ts +22 -6
- package/build/code.test.ts +108 -1
- package/build/colors.test.ts +66 -0
- package/build/columns-plugin.ts +116 -0
- package/build/content-watch.ts +30 -21
- package/build/copy.ts +1 -1
- package/build/external-links-plugin.ts +1 -1
- package/build/generate-content-assets.ts +46 -4
- package/build/generate-favicon.ts +1 -1
- package/build/generate-fonts.ts +1 -1
- package/build/generate-katex-assets.ts +1 -1
- package/build/log.test.ts +7 -1
- package/build/log.ts +4 -1
- package/build/markdown-plugins.test.ts +65 -3
- package/build/pagefind.ts +1 -1
- package/build/pipeline.ts +4 -20
- package/build/serve.test.ts +59 -0
- package/build/serve.ts +29 -63
- package/build/templates.ts +2 -2
- package/build/toc-plugin.test.ts +40 -25
- package/build/toc-plugin.ts +9 -2
- package/build/{types.ts → types.d.ts} +87 -1
- package/build/util.ts +1 -1
- package/build/utils/code.ts +76 -3
- package/build/utils/content-files.test.ts +56 -0
- package/build/utils/content-files.ts +15 -11
- package/build/utils/file-types.test.ts +18 -0
- package/build/utils/file-types.ts +4 -0
- package/build/utils/include.test.ts +117 -0
- package/build/utils/include.ts +53 -0
- package/build/utils/jdi-runner/TraceRunner.java +416 -0
- package/build/utils/literate-java.ts +2 -2
- package/build/utils/markdown.ts +32 -14
- package/build/utils/paths.ts +1 -1
- package/build/utils/render.ts +97 -11
- package/build/utils/shiki-highlighter.ts +1 -1
- package/build/utils/trace-layout.test.ts +570 -0
- package/build/utils/trace-layout.ts +848 -0
- package/build/utils/trace-svg.test.ts +425 -0
- package/build/utils/trace-svg.ts +905 -0
- package/build/utils/trace.test.ts +169 -0
- package/build/utils/trace.ts +334 -0
- package/build/validate-internal-links-plugin.test.ts +30 -2
- package/build/validate-internal-links-plugin.ts +3 -3
- package/build/watch-reload-client.ts +4 -20
- package/build/watch.ts +436 -448
- package/init/content/index.md +10 -5
- package/init/content/labs/00/index.md +1 -1
- package/init/content/labs/01/SearchTreeDemo.java +31 -0
- package/init/content/labs/01/TraceDemo.java +22 -0
- package/init/content/labs/01/index.md +27 -0
- package/init/content/labs/index.md +2 -0
- package/init/content/lectures/01/Rectangle.java +2 -0
- package/init/content/lectures/01/index.md +2 -1
- package/init/content/lectures/01/rectangle.py +45 -0
- package/init/content/lectures/02/_pr1.md +5 -0
- package/init/content/lectures/02/index.md +11 -0
- package/init/content/lectures/02/subdir/_foobar.html +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +6 -0
- package/init/content/lectures/index.md +3 -0
- package/init/content/markdown.md +30 -0
- package/package.json +20 -3
- package/src/_base.scss +13 -2
- package/src/_layout.scss +6 -5
- package/src/anchor/index.test.ts +64 -0
- package/src/code/index.test.ts +71 -0
- package/src/code/style.scss +23 -0
- package/src/code.scss +5 -3
- package/src/header/_base.scss +48 -9
- package/src/header/index.test.ts +89 -0
- package/src/header/index.ts +1 -1
- package/src/header/style.scss +1 -5
- package/src/index.ts +18 -20
- package/src/literate/style.scss +15 -7
- package/src/navigate/eligible.test.ts +101 -0
- package/src/navigate/eligible.ts +62 -0
- package/src/navigate/index.test.ts +82 -0
- package/src/navigate/index.ts +365 -0
- package/src/navigate/lifecycle.test.ts +26 -0
- package/src/navigate/lifecycle.ts +65 -0
- package/src/navigate/style.scss +61 -0
- package/src/print/index.test.ts +71 -0
- package/src/question/index.test.ts +82 -0
- package/src/question/style.scss +12 -0
- package/src/search/index.ts +38 -14
- package/src/search/style.scss +23 -14
- package/src/style.scss +25 -12
- package/src/timezone/index.test.ts +301 -1
- package/src/timezone/index.ts +68 -56
- package/src/toc/index.test.ts +237 -0
- package/src/toc/index.ts +29 -69
- package/src/toc/style.scss +17 -0
- package/src/top/index.test.ts +46 -0
- package/src/top/index.ts +2 -2
- package/src/trace/index.test.ts +528 -0
- package/src/trace/index.ts +250 -0
- package/src/trace/style.scss +210 -0
- package/src/trace/types.d.ts +47 -0
- package/src/util.test.ts +135 -0
- package/templates/_top.html +23 -22
- package/types/jsdom.d.ts +9 -0
package/init/content/index.md
CHANGED
|
@@ -18,9 +18,14 @@ if you won't use the `author` front matter field.
|
|
|
18
18
|
See the [Markdown examples page](/markdown.html) for syntax examples.
|
|
19
19
|
|
|
20
20
|
!!! note
|
|
21
|
-
For more documentation, see the [GitHub page]
|
|
21
|
+
For more documentation, see the [GitHub page](https://github.com/abreen/Tada).
|
|
22
|
+
<% if (vars.testCoveragePercent) { %>
|
|
23
|
+
- <%= vars.testCoveragePercent %>% of <%= page.toolName %>'s code is covered by
|
|
24
|
+
automated tests (unit tests, [Playwright](https://playwright.dev/) browser
|
|
25
|
+
tests, and functional, black-box tests which exercise functionality using the
|
|
26
|
+
CLI). See [the test coverage metrics](<%= vars.testCoverageUrl %>).
|
|
27
|
+
- <%= page.toolName %>'s features and behavior are
|
|
28
|
+
[documented in Markdown](https://github.com/abreen/Tada/tree/main/spec) for
|
|
29
|
+
easy reading by people and LLMs.
|
|
30
|
+
<% } %>
|
|
22
31
|
!!!
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
[github]: https://github.com/abreen/Tada
|
|
@@ -4,7 +4,7 @@ title: Lab 0
|
|
|
4
4
|
author: alex
|
|
5
5
|
description: An example lab page for <%= site.title %>.
|
|
6
6
|
|
|
7
|
-
In this lab, we'll explore the [`VowelCounter` program](./VowelCounter.html).
|
|
7
|
+
In this lab, we'll explore the [`VowelCounter` program](./VowelCounter.java.html).
|
|
8
8
|
|
|
9
9
|
## Review questions
|
|
10
10
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
public class SearchTreeDemo {
|
|
2
|
+
static class Node {
|
|
3
|
+
String data;
|
|
4
|
+
Node left;
|
|
5
|
+
Node right;
|
|
6
|
+
Node parent; // @trace-ignore
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static Node insert(Node node, Node parent, String val) {
|
|
10
|
+
if (node == null) {
|
|
11
|
+
Node n = new Node();
|
|
12
|
+
n.data = val;
|
|
13
|
+
n.parent = parent;
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
if (val.compareTo(node.data) < 0)
|
|
17
|
+
node.left = insert(node.left, node, val);
|
|
18
|
+
else
|
|
19
|
+
node.right = insert(node.right, node, val);
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static void main(String[] args) {
|
|
24
|
+
Node root = null;
|
|
25
|
+
root = insert(root, null, "fig");
|
|
26
|
+
root = insert(root, null, "cherry");
|
|
27
|
+
root = insert(root, null, "kiwi");
|
|
28
|
+
root = insert(root, null, "apple");
|
|
29
|
+
root = insert(root, null, "date");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
public class TraceDemo {
|
|
2
|
+
private static int sum(int n) {
|
|
3
|
+
if (n <= 0) {
|
|
4
|
+
return 0;
|
|
5
|
+
}
|
|
6
|
+
return n + sum(n - 1);
|
|
7
|
+
}
|
|
8
|
+
public static void main(String[] args) {
|
|
9
|
+
int[] arr = new int[7];
|
|
10
|
+
for (int i = 0; i < arr.length; i++) {
|
|
11
|
+
int n = (int)Math.pow(2, i);
|
|
12
|
+
arr[i] = n;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
int total = sum(4);
|
|
16
|
+
|
|
17
|
+
String str = "hello";
|
|
18
|
+
for (int i = 0; i < str.length(); i++) {
|
|
19
|
+
System.out.println(str.charAt(i));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
parent: /labs/index.html
|
|
2
|
+
parentLabel: Labs
|
|
3
|
+
title: Lab 1
|
|
4
|
+
author: alex
|
|
5
|
+
description: A lab page for <%= site.title %> demonstrating the trace feature.
|
|
6
|
+
|
|
7
|
+
In this lab, we'll examine memory diagrams.
|
|
8
|
+
|
|
9
|
+
## A simple trace
|
|
10
|
+
|
|
11
|
+
A simple program with an array, recursion, and a string.
|
|
12
|
+
|
|
13
|
+
<%= renderTrace('TraceDemo.java') %>
|
|
14
|
+
|
|
15
|
+
## Binary search tree
|
|
16
|
+
|
|
17
|
+
Building a binary search tree with parent references.
|
|
18
|
+
|
|
19
|
+
!!! note
|
|
20
|
+
The `@trace-ignore` hint (used in a comment next to the `parent` field,
|
|
21
|
+
see [`SearchTreeDemo.java`](./SearchTreeDemo.java)) allows
|
|
22
|
+
[d3-flextree](https://www.npmjs.com/package/d3-flextree) to visually arrange
|
|
23
|
+
the nodes as a standard binary tree. Without the hint, the parent references
|
|
24
|
+
confuse the algorithm and lead to a more awkward layout.
|
|
25
|
+
!!!
|
|
26
|
+
|
|
27
|
+
<%= renderTrace('SearchTreeDemo.java') %>
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
///
|
|
3
3
|
/// Objects of this class may be mutated (changed after construction)
|
|
4
4
|
/// by scaling using a _scale factor_ (a floating-point number between 0 and 1).
|
|
5
|
+
///
|
|
6
|
+
/// See [`rectangle.py`](./rectangle.py) for a Python version of this class.
|
|
5
7
|
public class Rectangle {
|
|
6
8
|
private int width;
|
|
7
9
|
private int height;
|
|
@@ -23,7 +23,8 @@ you should see a result for page 2 of `lecture1.pdf`.
|
|
|
23
23
|
|
|
24
24
|
* [`Rectangle.java`](./Rectangle.java) (an example of a code page)
|
|
25
25
|
* [`demo.py`](./demo.py) (another example of a code page)
|
|
26
|
-
* [`Pair.java`](./Pair.java) (an example literate Java page)
|
|
26
|
+
* [`Pair.java`](./Pair.java.html) (an example literate Java page)
|
|
27
|
+
- [A direct link](./Pair.java) to the source code
|
|
27
28
|
|
|
28
29
|
!!! note
|
|
29
30
|
Syntax highlighting is controlled by the `codeLanguages` configuration object
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Rectangle:
|
|
2
|
+
"""A rectangle with integer side lengths.
|
|
3
|
+
|
|
4
|
+
Objects may be mutated by scaling using a scale factor
|
|
5
|
+
(a float between 0 and 1).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
def __init__(self, width: int, height: int):
|
|
9
|
+
self._set_width(width)
|
|
10
|
+
self._set_height(height)
|
|
11
|
+
|
|
12
|
+
def _set_width(self, new_width: int):
|
|
13
|
+
if new_width <= 0:
|
|
14
|
+
raise ValueError("width must be positive")
|
|
15
|
+
self._width = new_width
|
|
16
|
+
|
|
17
|
+
def _set_height(self, new_height: int):
|
|
18
|
+
if new_height <= 0:
|
|
19
|
+
raise ValueError("height must be positive")
|
|
20
|
+
self._height = new_height
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def width(self) -> int:
|
|
24
|
+
return self._width
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def height(self) -> int:
|
|
28
|
+
return self._height
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def area(self) -> int:
|
|
32
|
+
return self.width * self.height
|
|
33
|
+
|
|
34
|
+
def scale_horizontally(self, scale_factor: float):
|
|
35
|
+
self._check_scale_factor(scale_factor)
|
|
36
|
+
self._set_width(round(self.width * scale_factor))
|
|
37
|
+
|
|
38
|
+
def scale_vertically(self, scale_factor: float):
|
|
39
|
+
self._check_scale_factor(scale_factor)
|
|
40
|
+
self._set_height(round(self.height * scale_factor))
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _check_scale_factor(scale_factor: float):
|
|
44
|
+
if scale_factor < 0 or scale_factor > 1:
|
|
45
|
+
raise ValueError("scale factor must be between zero and one")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
parentLabel: Lectures
|
|
2
|
+
parent: /lectures/index.html
|
|
3
|
+
title: Lecture 2
|
|
4
|
+
description: An example lecture page demonstrating partials.
|
|
5
|
+
author: alex
|
|
6
|
+
|
|
7
|
+
Here is a page constructed from multiple partials.
|
|
8
|
+
|
|
9
|
+
<%= include('_pr1.md') %>
|
|
10
|
+
|
|
11
|
+
<%= include('subdir/_pr2.md') %>
|
package/init/content/markdown.md
CHANGED
|
@@ -307,6 +307,7 @@ Here are the steps to follow when you are ready to submit your work for
|
|
|
307
307
|
this problem set.
|
|
308
308
|
|
|
309
309
|
...
|
|
310
|
+
|
|
310
311
|
:::
|
|
311
312
|
```
|
|
312
313
|
|
|
@@ -328,6 +329,35 @@ Email the files you changed to the course staff.
|
|
|
328
329
|
|
|
329
330
|
:::
|
|
330
331
|
|
|
332
|
+
### Two-column layout
|
|
333
|
+
|
|
334
|
+
Use
|
|
335
|
+
|
|
336
|
+
```markdown
|
|
337
|
+
+++
|
|
338
|
+
Content in the first column (on the left)...
|
|
339
|
+
+++
|
|
340
|
+
Content in the second column (on the right)...
|
|
341
|
+
+++
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
to arrange Markdown content in two columns:
|
|
345
|
+
|
|
346
|
+
+++
|
|
347
|
+
Here is a paragraph in the first column. It can contain any Markdown you want,
|
|
348
|
+
including lists:
|
|
349
|
+
1. One
|
|
350
|
+
2. Two
|
|
351
|
+
3. Three
|
|
352
|
+
+++
|
|
353
|
+
Here's a paragraph in the second column. We'll use a code block on this side:
|
|
354
|
+
```java
|
|
355
|
+
static boolean isOdd(int n) {
|
|
356
|
+
return n % 2 == 1;
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
+++
|
|
360
|
+
|
|
331
361
|
---
|
|
332
362
|
|
|
333
363
|
## Additional features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abreen/tada",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A static site generator",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,11 +26,14 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"prepare": "husky && bun run scripts/check-deps.ts",
|
|
28
28
|
"prepack": "bun run scripts/prepack.ts",
|
|
29
|
-
"init-example": "bun run bin/tada.ts init example --no-interactive",
|
|
30
29
|
"lint": "eslint src/ build/ bin/",
|
|
31
30
|
"typecheck": "tsc --noEmit",
|
|
32
31
|
"test:functional": "python3 -m pip install -q -r functional_tests/requirements.txt && python3 -m pytest functional_tests/ -v -n auto --durations=10",
|
|
33
|
-
"test:all": "bun test && bun run test:functional"
|
|
32
|
+
"test:all": "bun test && bun run test:playwright && bun run test:functional",
|
|
33
|
+
"test:coverage": "rm -rf coverage/unit coverage/functional && bun test --coverage && TADA_COVERAGE=1 python3 -m pip install -q -r functional_tests/requirements.txt && TADA_COVERAGE=1 python3 -m pytest functional_tests/ -v -n auto --durations=10 && bun run scripts/coverage-report.ts",
|
|
34
|
+
"test:playwright": "bunx playwright test",
|
|
35
|
+
"init-example": "bun run bin/tada.ts init example --no-interactive",
|
|
36
|
+
"iew": "if [ -d example ]; then cd example; else bun run init-example && cd example; fi && ../bin/tada.ts watch"
|
|
34
37
|
},
|
|
35
38
|
"engines": {
|
|
36
39
|
"bun": ">=1.0.0"
|
|
@@ -44,6 +47,8 @@
|
|
|
44
47
|
"ajv": "^8.17.1",
|
|
45
48
|
"chokidar": "^4.0.0",
|
|
46
49
|
"culori": "^4.0.2",
|
|
50
|
+
"d3-flextree": "^2.1.2",
|
|
51
|
+
"d3-hierarchy": "^3.1.2",
|
|
47
52
|
"fontkit": "^2.0.4",
|
|
48
53
|
"front-matter": "^4.0.2",
|
|
49
54
|
"java-parser": "^3.0.1",
|
|
@@ -71,14 +76,26 @@
|
|
|
71
76
|
},
|
|
72
77
|
"devDependencies": {
|
|
73
78
|
"@eslint/js": "^10.0.1",
|
|
79
|
+
"@playwright/test": "^1.59.1",
|
|
80
|
+
"@saintedlama/lcov-parse": "^1.0.1",
|
|
74
81
|
"@types/bun": "^1.3.10",
|
|
75
82
|
"@types/culori": "^4.0.1",
|
|
83
|
+
"@types/d3-flextree": "^2.1.4",
|
|
84
|
+
"@types/d3-hierarchy": "^3.1.7",
|
|
76
85
|
"@types/fontkit": "^2.0.8",
|
|
86
|
+
"@types/istanbul-lib-coverage": "^2.0.6",
|
|
87
|
+
"@types/istanbul-lib-instrument": "^1.7.8",
|
|
88
|
+
"@types/istanbul-lib-report": "^3.0.3",
|
|
89
|
+
"@types/istanbul-reports": "^3.0.4",
|
|
77
90
|
"@types/lodash": "^4.17.24",
|
|
78
91
|
"@types/markdown-it-container": "^4.0.0",
|
|
79
92
|
"@types/ws": "^8.18.1",
|
|
80
93
|
"eslint": "^10.0.3",
|
|
81
94
|
"husky": "^9.1.7",
|
|
95
|
+
"istanbul-lib-coverage": "^3.2.2",
|
|
96
|
+
"istanbul-lib-instrument": "^6.0.3",
|
|
97
|
+
"istanbul-lib-report": "^3.0.1",
|
|
98
|
+
"istanbul-reports": "^3.2.0",
|
|
82
99
|
"lint-staged": "^16.4.0",
|
|
83
100
|
"prettier": "^3.6.2",
|
|
84
101
|
"prettier-plugin-curly": "^0.4.1",
|
package/src/_base.scss
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
--border-radius: 12px;
|
|
8
8
|
--control-border-width: 2px;
|
|
9
9
|
--label-border-width: 2px;
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
--table-padding: 12px;
|
|
12
12
|
--details-padding: 12px;
|
|
13
13
|
--section-padding: 64px;
|
|
@@ -104,7 +104,9 @@ pre {
|
|
|
104
104
|
background: var(--bg2-color);
|
|
105
105
|
border-radius: var(--mono-border-radius);
|
|
106
106
|
line-height: var(--mono-line-height);
|
|
107
|
-
overflow: auto;
|
|
107
|
+
overflow-x: auto;
|
|
108
|
+
overflow-y: hidden;
|
|
109
|
+
overscroll-behavior-x: none;
|
|
108
110
|
padding: var(--gap);
|
|
109
111
|
|
|
110
112
|
code {
|
|
@@ -126,6 +128,15 @@ details summary {
|
|
|
126
128
|
padding: var(--details-padding);
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
main details summary:focus {
|
|
132
|
+
outline: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main details:focus-within {
|
|
136
|
+
outline: 2px solid var(--theme-color);
|
|
137
|
+
outline-offset: 1px;
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
details .content {
|
|
130
141
|
padding: 0 var(--details-padding) var(--details-padding)
|
|
131
142
|
var(--details-padding);
|
package/src/_layout.scss
CHANGED
|
@@ -45,6 +45,10 @@ body.default .title-and-info {
|
|
|
45
45
|
border-bottom: 1px solid var(--fg2-color);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
body.default main > *:first-child {
|
|
49
|
+
margin-top: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
body.default main table,
|
|
49
53
|
body.literate main table {
|
|
50
54
|
width: 100%;
|
|
@@ -169,9 +173,6 @@ body.literate main table {
|
|
|
169
173
|
|
|
170
174
|
.columns {
|
|
171
175
|
display: grid;
|
|
172
|
-
grid-template-columns:
|
|
173
|
-
gap:
|
|
174
|
-
&.split {
|
|
175
|
-
grid-template-columns: repeat(auto-fit, minmax(300px, auto));
|
|
176
|
-
}
|
|
176
|
+
grid-template-columns: 1fr 1fr;
|
|
177
|
+
gap: var(--gap);
|
|
177
178
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
import mount from './index';
|
|
4
|
+
|
|
5
|
+
function create(html: string) {
|
|
6
|
+
const dom = new JSDOM(`<body>${html}</body>`);
|
|
7
|
+
return dom.window;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('anchor', () => {
|
|
11
|
+
test('wraps heading content in an anchor link', () => {
|
|
12
|
+
const win = create('<h2 id="intro">Introduction</h2>');
|
|
13
|
+
mount(win);
|
|
14
|
+
|
|
15
|
+
const h2 = win.document.querySelector('h2')!;
|
|
16
|
+
const link = h2.querySelector('a.heading-anchor')!;
|
|
17
|
+
expect(link).not.toBeNull();
|
|
18
|
+
expect(link.getAttribute('href')).toBe('#intro');
|
|
19
|
+
expect(link.textContent).toBe('Introduction');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('sets title on anchor link', () => {
|
|
23
|
+
const win = create('<h3 id="setup">Setup</h3>');
|
|
24
|
+
mount(win);
|
|
25
|
+
|
|
26
|
+
const link = win.document.querySelector('a.heading-anchor')!;
|
|
27
|
+
expect(link.getAttribute('title')).toBe('Link to this heading');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('skips headings without an id', () => {
|
|
31
|
+
const win = create('<h2>No ID</h2>');
|
|
32
|
+
mount(win);
|
|
33
|
+
|
|
34
|
+
const link = win.document.querySelector('a.heading-anchor');
|
|
35
|
+
expect(link).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('handles multiple headings', () => {
|
|
39
|
+
const win = create('<h1 id="a">A</h1><h2 id="b">B</h2><h3>C</h3>');
|
|
40
|
+
mount(win);
|
|
41
|
+
|
|
42
|
+
const links = win.document.querySelectorAll('a.heading-anchor');
|
|
43
|
+
expect(links.length).toBe(2);
|
|
44
|
+
expect(links[0].getAttribute('href')).toBe('#a');
|
|
45
|
+
expect(links[1].getAttribute('href')).toBe('#b');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('preserves child elements inside heading', () => {
|
|
49
|
+
const win = create('<h2 id="x"><em>Bold</em> text</h2>');
|
|
50
|
+
mount(win);
|
|
51
|
+
|
|
52
|
+
const link = win.document.querySelector('a.heading-anchor')!;
|
|
53
|
+
expect(link.querySelector('em')).not.toBeNull();
|
|
54
|
+
expect(link.textContent).toBe('Bold text');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('does nothing when no headings exist', () => {
|
|
58
|
+
const win = create('<p>No headings here</p>');
|
|
59
|
+
mount(win);
|
|
60
|
+
|
|
61
|
+
const links = win.document.querySelectorAll('a.heading-anchor');
|
|
62
|
+
expect(links.length).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
import mount from './index';
|
|
4
|
+
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
// jsdom does not provide ResizeObserver
|
|
7
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
8
|
+
(globalThis as Record<string, unknown>).ResizeObserver = class {
|
|
9
|
+
observe() {}
|
|
10
|
+
unobserve() {}
|
|
11
|
+
disconnect() {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function create(html: string) {
|
|
17
|
+
const dom = new JSDOM(`<body class="code">${html}</body>`);
|
|
18
|
+
return dom.window;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('code', () => {
|
|
22
|
+
test('returns early when body does not have code class', async () => {
|
|
23
|
+
const dom = new JSDOM('<body><div>No code</div></body>');
|
|
24
|
+
await mount(dom.window);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('returns early when no code-body or scrollbar', async () => {
|
|
28
|
+
const win = create('<div>Code page with no code body</div>');
|
|
29
|
+
await mount(win);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('registers a copy event listener on code page', async () => {
|
|
33
|
+
const win = create(
|
|
34
|
+
'<div class="code-body"><div class="code-row"><code>hello</code></div></div>' +
|
|
35
|
+
'<div class="code-scrollbar"><div></div></div>',
|
|
36
|
+
);
|
|
37
|
+
await mount(win);
|
|
38
|
+
|
|
39
|
+
// Dispatch a copy event to exercise the listener (jsdom has limited
|
|
40
|
+
// ClipboardEvent support, so the handler exits early at getSelection)
|
|
41
|
+
const event = new win.Event('copy', { bubbles: true });
|
|
42
|
+
win.document.dispatchEvent(event);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('sets up scrollbar syncing between code-body and scrollbar', async () => {
|
|
46
|
+
const win = create(
|
|
47
|
+
'<div class="code-body"><div class="code-row"><code>x</code></div></div>' +
|
|
48
|
+
'<div class="code-scrollbar"><div></div></div>',
|
|
49
|
+
);
|
|
50
|
+
await mount(win);
|
|
51
|
+
|
|
52
|
+
const codeBody = win.document.querySelector('.code-body') as HTMLElement;
|
|
53
|
+
const scrollbar = win.document.querySelector(
|
|
54
|
+
'.code-scrollbar',
|
|
55
|
+
) as HTMLElement;
|
|
56
|
+
expect(codeBody).not.toBeNull();
|
|
57
|
+
expect(scrollbar).not.toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('handles download link when present', async () => {
|
|
61
|
+
const win = create(
|
|
62
|
+
'<div class="file-header"><a download="test.py" href="/test.py">Download</a></div>' +
|
|
63
|
+
'<div class="code-body"><div class="code-row"><code>x</code></div></div>' +
|
|
64
|
+
'<div class="code-scrollbar"><div></div></div>',
|
|
65
|
+
);
|
|
66
|
+
await mount(win);
|
|
67
|
+
|
|
68
|
+
const link = win.document.querySelector('a[download]');
|
|
69
|
+
expect(link).not.toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
package/src/code/style.scss
CHANGED
|
@@ -10,6 +10,7 @@ body.code .file-header {
|
|
|
10
10
|
body.code .code-body {
|
|
11
11
|
background: var(--bg2-color);
|
|
12
12
|
border-radius: var(--border-radius);
|
|
13
|
+
clip-path: inset(0 round var(--border-radius));
|
|
13
14
|
display: grid;
|
|
14
15
|
grid-template-columns: minmax(100%, max-content);
|
|
15
16
|
overflow-x: auto;
|
|
@@ -38,6 +39,24 @@ body.code .code-body > pre {
|
|
|
38
39
|
margin: 0;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
body.code .code-body > pre:first-child {
|
|
43
|
+
*:first-child {
|
|
44
|
+
.line-number,
|
|
45
|
+
code {
|
|
46
|
+
padding-top: 0.5rem;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
body.code .code-body > pre:last-child {
|
|
52
|
+
*:last-child {
|
|
53
|
+
.line-number,
|
|
54
|
+
code {
|
|
55
|
+
padding-bottom: 0.5rem;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
body.code pre {
|
|
42
61
|
display: block;
|
|
43
62
|
padding: 0;
|
|
@@ -69,6 +88,10 @@ body.code .code-prose {
|
|
|
69
88
|
padding: var(--gap) var(--gap) var(--gap)
|
|
70
89
|
calc(var(--prose-indent, 0ch) - 0.2em);
|
|
71
90
|
|
|
91
|
+
*:first-of-type {
|
|
92
|
+
margin-top: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
*:last-of-type {
|
|
73
96
|
margin-bottom: 0;
|
|
74
97
|
}
|
package/src/code.scss
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
@use 'mixins' as *;
|
|
2
2
|
|
|
3
|
-
body.code
|
|
4
|
-
body.literate {
|
|
3
|
+
body.code {
|
|
5
4
|
--code-gutter-width: 4rem;
|
|
6
5
|
}
|
|
7
6
|
|
|
@@ -28,7 +27,6 @@ body.literate {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
.line-number {
|
|
31
|
-
cursor: pointer;
|
|
32
30
|
display: block;
|
|
33
31
|
padding: 0 0.75em 0 0.75em;
|
|
34
32
|
text-align: right;
|
|
@@ -42,6 +40,10 @@ body.literate {
|
|
|
42
40
|
background: var(--bg2-color);
|
|
43
41
|
border-right: 1px solid var(--fg2-color);
|
|
44
42
|
margin-right: var(--gap);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
a.line-number {
|
|
46
|
+
cursor: pointer;
|
|
45
47
|
|
|
46
48
|
&:hover {
|
|
47
49
|
color: var(--fg-color);
|