@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
package/src/util.ts ADDED
@@ -0,0 +1,122 @@
1
+ export function debounce(fn: Function, time: number) {
2
+ let timer: number;
3
+ return (...args: any[]) => {
4
+ window.clearTimeout(timer);
5
+ timer = window.setTimeout(() => {
6
+ fn.apply(null, args);
7
+ }, time);
8
+ };
9
+ }
10
+
11
+ export function removeClass(el: HTMLElement, className: string) {
12
+ el.classList.remove(className);
13
+ if (!el.className) {
14
+ el.removeAttribute('class');
15
+ }
16
+ }
17
+
18
+ export function getElement(
19
+ parent: Document | Element,
20
+ selector: string,
21
+ ): HTMLElement {
22
+ const el = parent.querySelector(selector);
23
+ if (!el) {
24
+ throw new Error(`no element matching "${selector}"`);
25
+ }
26
+ return el as HTMLElement;
27
+ }
28
+
29
+ export function applyBasePath(subPath: string): string {
30
+ if (!subPath.startsWith('/')) {
31
+ throw new Error('invalid internal path, must start with "/": ' + subPath);
32
+ }
33
+
34
+ let path = window.siteVariables.basePath || '/';
35
+ if (path.endsWith('/')) {
36
+ path = path.slice(0, -1);
37
+ }
38
+ return path + subPath;
39
+ }
40
+
41
+ export function scheduleTask(fn: () => void) {
42
+ if (typeof window.requestIdleCallback === 'function') {
43
+ window.requestIdleCallback(fn);
44
+ } else {
45
+ return setTimeout(fn, 0);
46
+ }
47
+ }
48
+
49
+ export function formatDuration(ms: number): string {
50
+ const sign = ms < 0 ? '-' : '';
51
+ const absMs = Math.abs(ms);
52
+
53
+ const roundTo = (val: number, decimals: number) => {
54
+ const factor = Math.pow(10, decimals);
55
+ return Math.round(val * factor) / factor;
56
+ };
57
+
58
+ const formatSecondsFromMs = (msVal: number): string => {
59
+ const sec = msVal / 1000;
60
+ // Ensure fixed width 8 for seconds:
61
+ // <10s => 1 + '.' + 5 + 's' = 8
62
+ // >=10s => 2 + '.' + 4 + 's' = 8
63
+ let decimals = sec < 10 ? 5 : 4;
64
+ let rounded = roundTo(sec, decimals);
65
+
66
+ // If rounding reaches 60, switch to minutes
67
+ if (rounded >= 60) {
68
+ return formatMinutesFromMs(msVal);
69
+ }
70
+
71
+ // If value was <10 but rounding crossed into >=10, drop one decimal to keep width 8
72
+ if (decimals === 5 && rounded >= 10) {
73
+ decimals = 4;
74
+ rounded = roundTo(sec, decimals);
75
+ if (rounded >= 60) {
76
+ return formatMinutesFromMs(msVal);
77
+ }
78
+ }
79
+
80
+ return `${rounded.toFixed(decimals)}s`;
81
+ };
82
+
83
+ const formatMinutesFromMs = (msVal: number): string => {
84
+ const totalSec = msVal / 1000;
85
+ let minutes = Math.floor(totalSec / 60);
86
+ const rawSeconds = totalSec - minutes * 60;
87
+
88
+ let decimals: number;
89
+ if (minutes < 10) {
90
+ decimals = rawSeconds < 10 ? 3 : 2;
91
+ } else {
92
+ decimals = rawSeconds < 10 ? 2 : 1;
93
+ }
94
+
95
+ let seconds = roundTo(rawSeconds, decimals);
96
+
97
+ if (seconds >= 60) {
98
+ minutes += 1;
99
+ // after carry, seconds is 0 and decimals depend on new minutes value
100
+ decimals = minutes < 10 ? 3 : 2;
101
+ seconds = 0;
102
+ }
103
+
104
+ return `${minutes}m${seconds.toFixed(decimals)}s`;
105
+ };
106
+
107
+ if (absMs < 1000) {
108
+ // 0.0000ms - 999.99ms
109
+ let decimals = absMs < 10 ? 4 : absMs < 100 ? 3 : 2;
110
+ let val = roundTo(absMs, decimals);
111
+ if (val >= 1000) {
112
+ return sign + formatSecondsFromMs(absMs);
113
+ }
114
+ return sign + `${val.toFixed(decimals)}ms`;
115
+ } else if (absMs < 60000) {
116
+ // 0.0000s - 59.9999s (variable decimals based on magnitude)
117
+ return sign + formatSecondsFromMs(absMs);
118
+ } else {
119
+ // Minutes + seconds with precision based on minute/second magnitude
120
+ return sign + formatMinutesFromMs(absMs);
121
+ }
122
+ }
@@ -0,0 +1,27 @@
1
+ <aside class="author">
2
+ Written by
3
+ <% if (page.author.url) { %>
4
+ <a href="<%= applyBasePath(page.author.url) %>">
5
+ <% } %>
6
+ <span class="text"><%= page.author.name %></span>
7
+ <% if (page.author.avatar) { %>
8
+ <img
9
+ width="32"
10
+ height="32"
11
+ src="<%= applyBasePath(page.author.avatar) %>"
12
+ alt="Picture of <%= page.author.name %>"
13
+ class="avatar"
14
+ />
15
+ <% } else { %>
16
+ <img
17
+ width="32"
18
+ height="32"
19
+ src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><rect width='32' height='32'/></svg>"
20
+ alt=""
21
+ class="avatar"
22
+ />
23
+ <% } %>
24
+ <% if (page.author.url) { %>
25
+ </a>
26
+ <% } %>
27
+ </aside>
@@ -0,0 +1,3 @@
1
+ </div>
2
+ </body>
3
+ </html>
@@ -0,0 +1 @@
1
+ <a class="button" href="<%= page.codeFilePath %>" download="<%= page.downloadName %>" data-pagefind-ignore>Download</a>
@@ -0,0 +1,19 @@
1
+ <div class="title-and-info" data-pagefind-body>
2
+ <% if (page.parent) { %>
3
+ <a class="breadcrumb" href="<%= applyBasePath(page.parent) %>" data-pagefind-ignore><%= page.parentLabel %></a>
4
+ <% } %>
5
+ <h1 data-pagefind-weight="10"><%= page.titleHtml %></h1>
6
+
7
+ <div class="info" data-pagefind-ignore>
8
+ <% if (page.published) { %>
9
+ <div class="publish-date">
10
+ <time datetime="<%= isoDate(page.published) %>">
11
+ <%= readableDate(page.published) %>
12
+ </time>
13
+ </div>
14
+ <% } %>
15
+ <% if (page.author) { %>
16
+ <%= render('_author.html') %>
17
+ <% } %>
18
+ </div>
19
+ </div>
@@ -0,0 +1,18 @@
1
+ <nav>
2
+ <% json('nav.json').forEach(function(section) { %>
3
+ <div>
4
+ <p><%= section.title %></p>
5
+ <ul>
6
+ <% section.links.forEach(function(link) { %>
7
+ <li>
8
+ <a
9
+ class="<%= cx({disabled: link.disabled, external: link.external}) %>"
10
+ <% if (!link.disabled) { %>href="<%= link.internal ? applyBasePath(link.internal) : link.external %>"<% } %>
11
+ >
12
+ <%= link.text %></a>
13
+ </li>
14
+ <% }) %>
15
+ </ul>
16
+ </div>
17
+ <% }) %>
18
+ </nav>
@@ -0,0 +1,97 @@
1
+ :root {
2
+ --theme-hue: <%= themeHue %>deg;
3
+ --theme-saturation: <%= themeSaturation %>%;
4
+ --theme-lightness: <%= themeLightness %>%;
5
+ --theme-color: hsl(
6
+ var(--theme-hue) var(--theme-saturation) var(--theme-lightness)
7
+ );
8
+ --theme-color-text: hsl(
9
+ var(--theme-hue) clamp(30%, var(--theme-saturation), 70%)
10
+ clamp(25%, var(--theme-lightness), 42%)
11
+ );
12
+
13
+ // Background & foreground color tint
14
+ --tint-hue: <%= tintHue %>deg;
15
+ --tint-amount: <%= tintAmount / 100 %>;
16
+
17
+ --link-active-color: #000;
18
+
19
+ --warning-bg-color: hsl(36 95% 70%);
20
+ --note-bg-color: hsl(209 95% 75%);
21
+
22
+ // Main foreground color (the color of most text)
23
+ --fg-color: hsl(var(--tint-hue) calc(8% * var(--tint-amount)) 8%);
24
+ --fg-color-translucent: hsl(
25
+ var(--tint-hue) calc(70% * var(--tint-amount)) 8% / 33%
26
+ );
27
+
28
+ // Secondary foreground color (lighter)
29
+ --fg2-color: hsl(var(--tint-hue) calc(6% * var(--tint-amount)) 45%);
30
+
31
+ // Main background color (the color of the page background)
32
+ --bg-color: hsl(var(--tint-hue) calc(5% * var(--tint-amount)) 97%);
33
+ --bg-color-translucent: hsl(
34
+ var(--tint-hue) calc(30% * var(--tint-amount)) 95% / 66%
35
+ );
36
+
37
+ // Secondary background color (lighter)
38
+ --bg2-color: hsl(var(--tint-hue) calc(12% * var(--tint-amount)) 92%);
39
+ --bg2-color-translucent: hsl(
40
+ var(--tint-hue) calc(12% * var(--tint-amount)) 92% / 66%
41
+ );
42
+
43
+ --box-shadow: 0 2px 9px
44
+ hsl(var(--tint-hue) calc(85% * var(--tint-amount)) 10% / 25%);
45
+ --backdrop-filter: blur(10px);
46
+
47
+ --font:
48
+ 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
49
+ Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
50
+ --heading-font: var(--font);
51
+ --mono-font: 'Google Sans Code', Consolas, Courier, monospace;
52
+
53
+ --font-size: 16px;
54
+ --font-size-smaller: 15px;
55
+ --font-size-small: 13px;
56
+
57
+ --font-weight: 400;
58
+ --line-height: 1.7;
59
+
60
+ // Use em units to specify a size relative to normal body text
61
+ --mono-font-size: 0.94em;
62
+ --mono-line-height: 1.5;
63
+ --mono-border-radius: 0.375em;
64
+
65
+ color-scheme: light dark;
66
+ }
67
+
68
+ @media (prefers-color-scheme: dark) {
69
+ :root {
70
+ --theme-color-text: hsl(
71
+ var(--theme-hue) clamp(30%, var(--theme-saturation), 70%)
72
+ clamp(50%, calc(var(--theme-lightness) + 15%), 80%)
73
+ );
74
+
75
+ --fg-color: hsl(var(--tint-hue) calc(10% * var(--tint-amount)) 92%);
76
+ --fg-color-translucent: hsl(
77
+ var(--tint-hue) calc(85% * var(--tint-amount)) 90% / 33%
78
+ );
79
+ --fg2-color: hsl(var(--tint-hue) calc(6% * var(--tint-amount)) 60%);
80
+ --bg-color: hsl(var(--tint-hue) calc(3% * var(--tint-amount)) 5%);
81
+ --bg-color-translucent: hsl(
82
+ var(--tint-hue) calc(15% * var(--tint-amount)) 10% / 66%
83
+ );
84
+ --bg2-color: hsl(var(--tint-hue) calc(12% * var(--tint-amount)) 12%);
85
+ --bg2-color-translucent: hsl(
86
+ var(--tint-hue) calc(12% * var(--tint-amount)) 12% / 66%
87
+ );
88
+
89
+ --box-shadow: 0 3px 8px
90
+ hsl(var(--tint-hue) calc(33% * var(--tint-amount)) 8% / 70%);
91
+
92
+ --link-active-color: #fff;
93
+
94
+ --warning-bg-color: hsl(36 77% 27%);
95
+ --note-bg-color: hsl(209 90% 25%);
96
+ }
97
+ }
@@ -0,0 +1,87 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <% if (site.features.favicon) { %>
5
+ <link rel="icon" href="<%= applyBasePath('/favicon.ico') %>" sizes="any" />
6
+ <link rel="icon" type="image/svg+xml" href="<%= applyBasePath('/favicon.svg') %>" />
7
+ <link rel="icon" type="image/png" sizes="512x512" href="<%= applyBasePath('/favicon-512.png') %>" />
8
+ <link rel="icon" type="image/png" sizes="256x256" href="<%= applyBasePath('/favicon-256.png') %>" />
9
+ <link rel="icon" type="image/png" sizes="192x192" href="<%= applyBasePath('/favicon-192.png') %>" />
10
+ <link rel="icon" type="image/png" sizes="128x128" href="<%= applyBasePath('/favicon-128.png') %>" />
11
+ <link rel="icon" type="image/png" sizes="64x64" href="<%= applyBasePath('/favicon-64.png') %>" />
12
+ <link rel="icon" type="image/png" sizes="48x48" href="<%= applyBasePath('/favicon-48.png') %>" />
13
+ <link rel="icon" type="image/png" sizes="32x32" href="<%= applyBasePath('/favicon-32.png') %>" />
14
+ <link rel="icon" type="image/png" sizes="16x16" href="<%= applyBasePath('/favicon-16.png') %>" />
15
+ <link rel="apple-touch-icon" sizes="512x512" href="<%= applyBasePath('/favicon-512.png') %>" />
16
+ <link rel="apple-touch-icon" sizes="256x256" href="<%= applyBasePath('/favicon-256.png') %>" />
17
+ <link rel="manifest" href="<%= applyBasePath('/manifest.json') %>" />
18
+ <meta name="apple-mobile-web-app-title" content="<%= site.symbol %>">
19
+ <meta name="mobile-web-app-capable" content="yes">
20
+ <% } %>
21
+ <meta charset="UTF-8" />
22
+ <meta http-equiv="cache-control" content="no-cache" />
23
+ <meta http-equiv="expires" content="0" />
24
+ <meta http-equiv="pragma" content="no-cache" />
25
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
26
+ <% if (page.description) { %>
27
+ <meta name="description" content="<%= page.description %>" />
28
+ <% } %>
29
+ <meta property="og:title" content="<%= page.title %>" />
30
+ <% if (page.author) { %>
31
+ <meta name="author" content="<%= page.author.name %>" />
32
+ <meta property="og:author" content="<%= page.author.name %>" />
33
+ <% } %>
34
+ <title><%= page.title %><%= site.titlePostfix %></title>
35
+ <script type="speculationrules">
36
+ {
37
+ "prerender": [{
38
+ "where": {
39
+ "href_matches": "<%= applyBasePath('/') %>*"
40
+ },
41
+ "eagerness": "moderate"
42
+ }]
43
+ }
44
+ </script>
45
+ <noscript><style>
46
+ .question-a-body:not([data-revealed]) > * { color: unset; background-color: unset; }
47
+ .question-a-body:not([data-revealed]) > * * { color: unset; background-color: unset; }
48
+ .question-a-body::after { display: none; }
49
+ .question-a-body { cursor: unset; }
50
+ </style></noscript>
51
+ </head>
52
+
53
+ <body class="<%= page.template %><% if (page.tocHtml) { %> toc-is-active<% } %>">
54
+ <header data-pagefind-ignore>
55
+ <details>
56
+ <summary aria-label="Toggle site navigation">
57
+ <span class="logo" aria-hidden="true"><%= site.symbol %></span>
58
+ <span class="site-title" aria-hidden="true"><%= site.title %></span>
59
+ <div id="to-top-container" class="to-top-container"></div>
60
+ <% if (site.features.search) { %>
61
+ <div class="search-controls">
62
+ <input type="search" class="search quick-search"
63
+ autocomplete="off" name="quick-search"
64
+ placeholder="Search"
65
+ role="combobox"
66
+ aria-haspopup="listbox"
67
+ aria-autocomplete="list"
68
+ aria-expanded="false"
69
+ aria-controls="quick-search-results"
70
+ disabled />
71
+ <script>document.currentScript.previousElementSibling.disabled=false;</script>
72
+ <% if (site.features.search) { %>
73
+ <div class="results-container" role="region"
74
+ aria-label="Search results"
75
+ aria-live="polite" aria-hidden="true" inert>
76
+ <div class="results" style="display:none"></div>
77
+ </div>
78
+ <% } %>
79
+ </div>
80
+ <% } %>
81
+ </summary>
82
+ <div class="content">
83
+ <%= render('_nav.html') %>
84
+ </div>
85
+ </details>
86
+ </header>
87
+ <div class="container">
@@ -0,0 +1,13 @@
1
+ {
2
+ "type": "object",
3
+ "additionalProperties": {
4
+ "type": "object",
5
+ "required": ["name", "avatar"],
6
+ "properties": {
7
+ "name": { "type": "string" },
8
+ "avatar": { "type": "string", "pattern": "^\\/[-a-zA-Z0-9_./#]*$" },
9
+ "url": { "type": "string", "pattern": "^\\/[-a-zA-Z0-9_./#]*$" }
10
+ },
11
+ "additionalProperties": false
12
+ }
13
+ }
@@ -0,0 +1,31 @@
1
+ <%= render('_top.html') %>
2
+
3
+ <div class="main-content">
4
+ <main class="body" data-pagefind-body>
5
+ <div class="file-header">
6
+ <h1 data-pagefind-weight="10"><%= page.titleHtml %></h1>
7
+ <%= render('_download.html') %>
8
+ </div>
9
+ <div class="code-body">
10
+ <%= content %>
11
+ </div>
12
+ <div class="code-scrollbar" aria-hidden="true">
13
+ <div></div>
14
+ </div>
15
+ <% if (page.tocItems && page.tocItems.length > 0) { %>
16
+ <div class="code-search-index" aria-hidden="true" inert>
17
+ <% page.tocItems.forEach(function(item) { %>
18
+ <section>
19
+ <h2 id="L<%= item.line %>"><%= item.name %></h2>
20
+ <p><%= item.label %> defined at line <%= item.line %>.</p>
21
+ </section>
22
+ <% }); %>
23
+ </div>
24
+ <% } %>
25
+ </main>
26
+ <% if (page.tocHtml) { %>
27
+ <nav class="toc"><%= page.tocHtml %></nav>
28
+ <% } %>
29
+ </div>
30
+
31
+ <%= render('_bottom.html') %>
@@ -0,0 +1,13 @@
1
+ <%= render('_top.html') %>
2
+
3
+ <div class="main-content">
4
+ <%= render('_heading.html') %>
5
+ <main class="body" data-pagefind-body>
6
+ <%= content %>
7
+ </main>
8
+ <% if (page.toc && page.tocHtml) { %>
9
+ <nav class="toc"><%= page.tocHtml %></nav>
10
+ <% } %>
11
+ </div>
12
+
13
+ <%= render('_bottom.html') %>
@@ -0,0 +1,16 @@
1
+ <%= render('_top.html') %>
2
+
3
+ <div class="main-content">
4
+ <%= render('_heading.html') %>
5
+ <main class="body" data-pagefind-body>
6
+ <div class="file-header">
7
+ <code><%= page.downloadName %></code><%= render('_download.html') %>
8
+ </div>
9
+ <%= content %>
10
+ </main>
11
+ <% if (page.toc && page.tocHtml) { %>
12
+ <nav class="toc"><%= page.tocHtml %></nav>
13
+ <% } %>
14
+ </div>
15
+
16
+ <%= render('_bottom.html') %>
@@ -0,0 +1,27 @@
1
+ {
2
+ "type": "array",
3
+ "items": {
4
+ "type": "object",
5
+ "required": ["title", "links"],
6
+ "properties": {
7
+ "title": { "type": "string" },
8
+ "links": {
9
+ "type": "array",
10
+ "items": {
11
+ "type": "object",
12
+ "required": ["text"],
13
+ "properties": {
14
+ "text": { "type": "string" },
15
+ "internal": { "type": "string", "pattern": "^\\/.*$" },
16
+ "external": { "type": "string", "pattern": "^https?:\\/\\/.*$" },
17
+ "disabled": { "type": "boolean" },
18
+ "keywords": { "type": "array", "items": { "type": "string" } }
19
+ },
20
+ "oneOf": [{ "required": ["internal"] }, { "required": ["external"] }],
21
+ "additionalProperties": false
22
+ }
23
+ }
24
+ },
25
+ "additionalProperties": false
26
+ }
27
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "@tsconfig/node23/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ES6",
5
+ "lib": ["es2024", "dom"],
6
+ "module": "esnext",
7
+ "moduleResolution": "bundler",
8
+ "outDir": "./dist/",
9
+ "resolveJsonModule": true,
10
+ "strict": true,
11
+ "noUnusedLocals": true
12
+ },
13
+ "include": ["src", "types"],
14
+ "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"]
15
+ }
package/types/dev.ts ADDED
@@ -0,0 +1,3 @@
1
+ interface Window {
2
+ IS_DEV: boolean | undefined;
3
+ }
@@ -0,0 +1 @@
1
+ declare module '*.scss';
@@ -0,0 +1,16 @@
1
+ interface TimezoneDef {
2
+ value: string;
3
+ label: string;
4
+ abbreviation: string;
5
+ offsetMinutes?: number;
6
+ }
7
+
8
+ interface Window {
9
+ siteVariables: {
10
+ base: string;
11
+ basePath: string;
12
+ titlePostfix?: string;
13
+ defaultTimeZone: string;
14
+ timezones: TimezoneDef[];
15
+ };
16
+ }
@@ -0,0 +1,78 @@
1
+ const { createApplyBasePath } = require('./utils/paths');
2
+ const { makeLogger } = require('./log');
3
+
4
+ const log = makeLogger(__filename);
5
+
6
+ module.exports = function externalLinks(md, siteVariables, options = {}) {
7
+ const applyBasePath = createApplyBasePath(siteVariables);
8
+ const rewriteCodeLinks = siteVariables.features?.code !== false;
9
+
10
+ function rewriteInternalHref(href) {
11
+ const match = href.match(/^([^?#]*)(.*)$/);
12
+ const pathname = match ? match[1] : href;
13
+ const suffix = match ? match[2] : '';
14
+ let modifiedPath = pathname;
15
+
16
+ if (rewriteCodeLinks) {
17
+ // Rewrite code file links to .html links
18
+ for (const ext of Object.keys(siteVariables.codeLanguages)) {
19
+ if (modifiedPath.endsWith(`.${ext}`)) {
20
+ modifiedPath = modifiedPath.replace(
21
+ new RegExp(`\\.${ext}$`),
22
+ '.html',
23
+ );
24
+ break;
25
+ }
26
+ }
27
+ }
28
+
29
+ return modifiedPath + suffix;
30
+ }
31
+
32
+ function checkAndApplyBasePath(token) {
33
+ if (token.type === 'link_open') {
34
+ const href = token.attrGet('href');
35
+
36
+ if (href.startsWith('/')) {
37
+ const modifiedHref = rewriteInternalHref(href);
38
+ const afterApply = applyBasePath(modifiedHref);
39
+ log.debug`Applying base path: ${href} -> ${afterApply}`;
40
+ token.attrSet('href', afterApply);
41
+ } else if (
42
+ !href.startsWith('#') &&
43
+ !href.startsWith('//') &&
44
+ !/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href)
45
+ ) {
46
+ // Relative link: only rewrite code extensions, don't apply base path
47
+ const modifiedHref = rewriteInternalHref(href);
48
+ if (modifiedHref !== href) {
49
+ log.debug`Rewriting internal link: ${href} -> ${modifiedHref}`;
50
+ token.attrSet('href', modifiedHref);
51
+ }
52
+ }
53
+ } else if (token.type === 'image') {
54
+ const src = token.attrGet('src');
55
+ if (src && src.startsWith('/')) {
56
+ const afterApply = applyBasePath(src);
57
+ log.debug`Applying base path to image: ${src} -> ${afterApply}`;
58
+ token.attrSet('src', afterApply);
59
+ }
60
+ } else if (token.type === 'html_block' || token.type === 'html_inline') {
61
+ token.content = token.content.replace(
62
+ /(<img\b[^>]*\bsrc=")(\/)([^"]*")/g,
63
+ (match, prefix, slash, rest) => {
64
+ const src = slash + rest.slice(0, -1);
65
+ const afterApply = applyBasePath(src);
66
+ log.debug`Applying base path to img tag src: ${src} -> ${afterApply}`;
67
+ return prefix + afterApply + '"';
68
+ },
69
+ );
70
+ }
71
+
72
+ token.children?.map(checkAndApplyBasePath);
73
+ }
74
+
75
+ md.core.ruler.push('apply_base_path', state => {
76
+ state.tokens.map(checkAndApplyBasePath);
77
+ });
78
+ };