@abreen/tada 1.4.1 → 1.5.1

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 (103) hide show
  1. package/README.md +4 -2
  2. package/bin/tada.ts +81 -57
  3. package/build/apply-base-path-plugin.ts +22 -6
  4. package/build/code.test.ts +108 -1
  5. package/build/colors.test.ts +66 -0
  6. package/build/columns-plugin.ts +116 -0
  7. package/build/content-watch.ts +30 -21
  8. package/build/copy.ts +1 -1
  9. package/build/external-links-plugin.ts +1 -1
  10. package/build/generate-content-assets.ts +48 -6
  11. package/build/generate-favicon.ts +1 -1
  12. package/build/generate-fonts.ts +1 -1
  13. package/build/generate-katex-assets.ts +1 -1
  14. package/build/log.test.ts +7 -1
  15. package/build/log.ts +4 -1
  16. package/build/markdown-plugins.test.ts +65 -3
  17. package/build/pagefind.ts +1 -1
  18. package/build/pipeline.ts +4 -20
  19. package/build/serve.test.ts +59 -0
  20. package/build/serve.ts +29 -63
  21. package/build/templates.ts +2 -2
  22. package/build/toc-plugin.test.ts +40 -25
  23. package/build/toc-plugin.ts +9 -2
  24. package/build/{types.ts → types.d.ts} +88 -1
  25. package/build/util.ts +1 -1
  26. package/build/utils/code.ts +76 -3
  27. package/build/utils/content-files.test.ts +56 -0
  28. package/build/utils/content-files.ts +15 -11
  29. package/build/utils/file-types.test.ts +18 -0
  30. package/build/utils/file-types.ts +4 -0
  31. package/build/utils/include.test.ts +117 -0
  32. package/build/utils/include.ts +53 -0
  33. package/build/utils/jdi-runner/TraceRunner.java +416 -0
  34. package/build/utils/literate-java.ts +2 -2
  35. package/build/utils/markdown.ts +32 -14
  36. package/build/utils/paths.ts +1 -1
  37. package/build/utils/render.ts +102 -11
  38. package/build/utils/shiki-highlighter.ts +1 -1
  39. package/build/utils/trace-layout.test.ts +570 -0
  40. package/build/utils/trace-layout.ts +848 -0
  41. package/build/utils/trace-svg.test.ts +425 -0
  42. package/build/utils/trace-svg.ts +905 -0
  43. package/build/utils/trace.test.ts +169 -0
  44. package/build/utils/trace.ts +382 -0
  45. package/build/validate-internal-links-plugin.test.ts +30 -2
  46. package/build/validate-internal-links-plugin.ts +3 -3
  47. package/build/watch-reload-client.ts +4 -20
  48. package/build/watch.ts +436 -448
  49. package/init/content/index.md +10 -5
  50. package/init/content/labs/00/index.md +1 -1
  51. package/init/content/labs/01/SearchTreeDemo.java +31 -0
  52. package/init/content/labs/01/TraceDemo.java +22 -0
  53. package/init/content/labs/01/index.md +27 -0
  54. package/init/content/labs/index.md +2 -0
  55. package/init/content/lectures/01/Rectangle.java +2 -0
  56. package/init/content/lectures/01/index.md +2 -1
  57. package/init/content/lectures/01/rectangle.py +45 -0
  58. package/init/content/lectures/02/_pr1.md +5 -0
  59. package/init/content/lectures/02/index.md +11 -0
  60. package/init/content/lectures/02/subdir/_foobar.html +3 -0
  61. package/init/content/lectures/02/subdir/_pr2.md +6 -0
  62. package/init/content/lectures/index.md +3 -0
  63. package/init/content/markdown.md +30 -0
  64. package/package.json +21 -4
  65. package/src/_base.scss +13 -2
  66. package/src/_layout.scss +6 -5
  67. package/src/anchor/index.test.ts +64 -0
  68. package/src/code/index.test.ts +71 -0
  69. package/src/code/style.scss +23 -0
  70. package/src/code.scss +5 -3
  71. package/src/header/_base.scss +48 -9
  72. package/src/header/index.test.ts +89 -0
  73. package/src/header/index.ts +1 -1
  74. package/src/header/style.scss +1 -5
  75. package/src/index.ts +18 -20
  76. package/src/literate/style.scss +15 -7
  77. package/src/navigate/eligible.test.ts +101 -0
  78. package/src/navigate/eligible.ts +62 -0
  79. package/src/navigate/index.test.ts +82 -0
  80. package/src/navigate/index.ts +365 -0
  81. package/src/navigate/lifecycle.test.ts +26 -0
  82. package/src/navigate/lifecycle.ts +65 -0
  83. package/src/navigate/style.scss +61 -0
  84. package/src/print/index.test.ts +71 -0
  85. package/src/question/index.test.ts +82 -0
  86. package/src/question/style.scss +12 -0
  87. package/src/search/index.ts +38 -14
  88. package/src/search/style.scss +23 -14
  89. package/src/style.scss +25 -12
  90. package/src/timezone/index.test.ts +301 -1
  91. package/src/timezone/index.ts +68 -56
  92. package/src/toc/index.test.ts +237 -0
  93. package/src/toc/index.ts +29 -69
  94. package/src/toc/style.scss +17 -0
  95. package/src/top/index.test.ts +46 -0
  96. package/src/top/index.ts +2 -2
  97. package/src/trace/index.test.ts +554 -0
  98. package/src/trace/index.ts +258 -0
  99. package/src/trace/style.scss +210 -0
  100. package/src/trace/types.d.ts +47 -0
  101. package/src/util.test.ts +135 -0
  102. package/templates/_top.html +23 -22
  103. package/types/jsdom.d.ts +9 -0
@@ -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][github].
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') %>
@@ -7,3 +7,5 @@ to apply the concepts we learned in lecture to solve real-world problems and
7
7
  write full-length programs.
8
8
 
9
9
  0. [`VowelCounter` and user input](/labs/00/index.html)
10
+
11
+ 1. [Memory diagrams](/labs/01/index.html)
@@ -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,5 @@
1
+ ## Problem 1 # The first example problem
2
+
3
+ Here's the `_pr1.md` partial. From this partial we can use the page-level
4
+ front matter fields from the page that includes us. The page that called
5
+ `include()` has a title of: <%= page.title %>
@@ -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') %>
@@ -0,0 +1,3 @@
1
+ <p>Here's an HTML partial in a subdirectory.</p>
2
+
3
+ <p>Here's the title of the page including this partial: <%= page.title %></p>
@@ -0,0 +1,6 @@
1
+ ## Problem 2 # The second example problem
2
+
3
+ Here's the `_pr2.md` partial. If we use `include()` here, the path is resolved
4
+ relative to this file, **not** the page or partial that included us.
5
+
6
+ <%= include('_foobar.html') %>
@@ -23,3 +23,6 @@ and other relevant information.
23
23
 
24
24
  [Lecture 1](/lectures/01/index.html)
25
25
  : Introduction, computation, models of a computer
26
+
27
+ [Lecture 2](/lectures/02/index.html)
28
+ : Algorithms
@@ -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.4.1",
3
+ "version": "1.5.1",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -24,13 +24,16 @@
24
24
  "tsconfig.json"
25
25
  ],
26
26
  "scripts": {
27
- "prepare": "husky && bun run scripts/check-deps.ts",
27
+ "prepare": "bunx 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
- --focus-border-width: 3px;
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: max-content auto;
173
- gap: 1rem;
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
+ });
@@ -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);