@abreen/tada 1.17.0 → 1.18.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 (107) hide show
  1. package/README.md +41 -7
  2. package/bin/tada.ts +23 -9
  3. package/bin/validators.ts +34 -1
  4. package/build/branding.ts +41 -0
  5. package/build/build-manifest.ts +10 -5
  6. package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
  7. package/build/build-validation.ts +176 -0
  8. package/build/bundle.ts +75 -65
  9. package/build/columns-plugin.ts +27 -1
  10. package/build/config-loader.ts +3 -3
  11. package/build/custom-fonts.ts +8 -5
  12. package/build/deflist-id-plugin.ts +2 -2
  13. package/build/generate-favicon.ts +7 -6
  14. package/build/generate-fonts.ts +8 -15
  15. package/build/generate-katex-assets.ts +19 -17
  16. package/build/generate-web-app-manifest.ts +3 -11
  17. package/build/lodash-template.ts +14 -0
  18. package/build/output-publication.ts +238 -0
  19. package/build/pagefind.ts +49 -21
  20. package/build/pdf-text.ts +41 -1
  21. package/build/pipeline.ts +95 -101
  22. package/build/site-assets.ts +46 -0
  23. package/build/site-build.ts +267 -0
  24. package/build/source-model.ts +223 -229
  25. package/build/source-records.ts +69 -81
  26. package/build/template-globals.ts +3 -0
  27. package/build/templates.ts +12 -8
  28. package/build/toc-plugin.ts +21 -21
  29. package/build/types.d.ts +38 -38
  30. package/build/util.ts +0 -4
  31. package/build/utils/code.ts +51 -117
  32. package/build/utils/final-html.ts +171 -47
  33. package/build/utils/jdi-runner/LiterateRunner.java +9 -1
  34. package/build/utils/literate-java.ts +11 -7
  35. package/build/utils/markdown-partials.ts +4 -3
  36. package/build/utils/markdown.ts +6 -17
  37. package/build/utils/paths.ts +3 -0
  38. package/build/utils/plain-text.ts +18 -0
  39. package/build/utils/render.ts +197 -154
  40. package/build/utils/shiki-highlighter.ts +11 -0
  41. package/build/utils/source-template.ts +2 -2
  42. package/build/utils/trace-core.ts +8 -83
  43. package/build/utils/trace.ts +38 -95
  44. package/build/validate-config-links.ts +39 -6
  45. package/build/watch/build-incremental.ts +46 -103
  46. package/build/watch/compiler.ts +158 -37
  47. package/build/watch/engine.ts +207 -162
  48. package/build/watch/index.ts +9 -2
  49. package/build/watch/planner.ts +88 -144
  50. package/build/watch/runtime.ts +21 -34
  51. package/build/watch/types.d.ts +32 -64
  52. package/package.json +4 -5
  53. package/schema/site.schema.json +18 -2
  54. package/src/_content.scss +447 -0
  55. package/src/_fonts.scss +90 -0
  56. package/src/_layout.scss +50 -35
  57. package/src/appearance-picker/{style.scss → _index.scss} +1 -1
  58. package/src/code/{style.scss → _index.scss} +61 -0
  59. package/src/header/{_base.scss → _index.scss} +98 -6
  60. package/src/index.ts +1 -17
  61. package/src/navigate/{style.scss → _index.scss} +20 -0
  62. package/src/navigate/eligible.ts +0 -40
  63. package/src/navigate/index.ts +50 -5
  64. package/src/navigate/lifecycle.ts +19 -2
  65. package/src/navigate/runtime.ts +20 -4
  66. package/src/page-update/index.ts +33 -20
  67. package/src/print/{style.scss → _index.scss} +3 -1
  68. package/src/search/index.ts +30 -2
  69. package/src/slides/{style.scss → _index.scss} +5 -0
  70. package/src/style.scss +24 -536
  71. package/src/timezone/index.ts +29 -3
  72. package/src/timezone/time-format.ts +4 -0
  73. package/src/toc/index.ts +6 -5
  74. package/src/trace/index.ts +11 -1
  75. package/templates/_author.html +2 -2
  76. package/templates/_download.html +1 -1
  77. package/templates/_footer.html +1 -1
  78. package/templates/_heading.html +3 -3
  79. package/templates/_nav.html +2 -2
  80. package/templates/_page-bottom.html +6 -0
  81. package/templates/_top.html +18 -14
  82. package/templates/code.html +22 -21
  83. package/templates/default.html +12 -11
  84. package/templates/literate.html +6 -5
  85. package/build/copy.ts +0 -82
  86. package/build/generate-content-assets.ts +0 -121
  87. package/build/utils/content-files.ts +0 -170
  88. package/build/watch/assets.ts +0 -103
  89. package/build/watch/build-full.ts +0 -97
  90. package/build/watch/build-helpers.ts +0 -66
  91. package/build/watch/build-result.ts +0 -19
  92. package/build/watch/fs-commit.ts +0 -153
  93. package/build/watch/mutations.ts +0 -73
  94. package/build/watch/snapshot.ts +0 -201
  95. package/build/watch/validation.ts +0 -50
  96. package/src/code.scss +0 -60
  97. package/src/critical.scss +0 -5
  98. package/src/header/style.scss +0 -91
  99. /package/src/{literate.scss → _literate.scss} +0 -0
  100. /package/src/{material-symbols.scss → _material-symbols.scss} +0 -0
  101. /package/src/anchor/{style.scss → _index.scss} +0 -0
  102. /package/src/page-update/{style.scss → _index.scss} +0 -0
  103. /package/src/question/{style.scss → _index.scss} +0 -0
  104. /package/src/search/{style.scss → _index.scss} +0 -0
  105. /package/src/timezone/{style.scss → _index.scss} +0 -0
  106. /package/src/toc/{style.scss → _index.scss} +0 -0
  107. /package/src/trace/{style.scss → _index.scss} +0 -0
package/build/bundle.ts CHANGED
@@ -1,14 +1,8 @@
1
1
  import fs from 'fs';
2
- import os from 'os';
3
2
  import path from 'path';
4
- import _ from 'lodash';
3
+ import { compileTemplate } from './lodash-template';
5
4
  import * as sass from 'sass';
6
- import {
7
- getPackageDir,
8
- getProjectDir,
9
- getDistDir,
10
- toPosix,
11
- } from './utils/paths';
5
+ import { getPackageDir, toPosix } from './utils/paths';
12
6
  import {
13
7
  deriveTheme,
14
8
  deriveLinkHue,
@@ -40,7 +34,7 @@ interface CoverageGlobal {
40
34
  __tadaCoverage?: CoverageHooks;
41
35
  }
42
36
 
43
- export function getBundleNaming(): string {
37
+ function getBundleNaming(): string {
44
38
  const version = pkg.version.replace(/[^a-zA-Z0-9.-]/g, '-');
45
39
  return `[name].bundle.tada-${version}.[ext]`;
46
40
  }
@@ -53,6 +47,7 @@ function formatCssNumber(value: number, precision = 4): string {
53
47
  return value.toFixed(precision).replace(/\.?0+$/, '');
54
48
  }
55
49
 
50
+ // Returns the rendered SCSS source of the theme module (`config/theme`)
56
51
  function renderThemeScss(siteVariables: SiteVariables): string {
57
52
  const templatePath = path.join(getPackageDir(), 'templates/_theme.scss');
58
53
  const template = fs.readFileSync(templatePath, 'utf-8');
@@ -74,7 +69,7 @@ function renderThemeScss(siteVariables: SiteVariables): string {
74
69
  siteVariables.fontOverrides,
75
70
  );
76
71
 
77
- const renderedTemplate = _.template(template)({
72
+ const renderedTemplate = compileTemplate(template)({
78
73
  ...theme,
79
74
  tintHue,
80
75
  tintAmount,
@@ -98,12 +93,7 @@ function renderThemeScss(siteVariables: SiteVariables): string {
98
93
  .replace('/* TADA_CUSTOM_FONT_TUNING */', customFontTuning)
99
94
  .replace('/* TADA_MATERIAL_SYMBOL_VARIABLES */', materialSymbolVariables);
100
95
 
101
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
102
- const configDir = path.join(tmpDir, 'config');
103
- fs.mkdirSync(configDir);
104
- fs.writeFileSync(path.join(configDir, '_theme.scss'), rendered);
105
-
106
- return tmpDir;
96
+ return rendered;
107
97
  }
108
98
 
109
99
  function createDefine(
@@ -119,14 +109,31 @@ function createDefine(
119
109
  };
120
110
  }
121
111
 
122
- function createScssPlugin(themeDir: string) {
112
+ const THEME_MODULE_URL = 'config/theme';
113
+ const THEME_CANONICAL_URL = 'tada:config/theme';
114
+
115
+ // Serves the rendered theme from memory to `@use 'config/theme'`
116
+ function createThemeImporter(themeScss: string): sass.Importer<'sync'> {
117
+ return {
118
+ canonicalize(url) {
119
+ return url === THEME_MODULE_URL ? new URL(THEME_CANONICAL_URL) : null;
120
+ },
121
+ load(canonicalUrl) {
122
+ if (canonicalUrl.href !== THEME_CANONICAL_URL) {
123
+ return null;
124
+ }
125
+ return { contents: themeScss, syntax: 'scss' };
126
+ },
127
+ };
128
+ }
129
+
130
+ function createScssPlugin(themeScss: string) {
131
+ const importers = [createThemeImporter(themeScss)];
123
132
  return {
124
133
  name: 'scss',
125
134
  setup(build: PluginBuilder) {
126
135
  build.onLoad({ filter: /\.scss$/ }, args => {
127
- const result = sass.compile(args.path, {
128
- loadPaths: [themeDir, getProjectDir()],
129
- });
136
+ const result = sass.compile(args.path, { importers });
130
137
  return { contents: result.css, loader: 'css' as const };
131
138
  });
132
139
  },
@@ -139,55 +146,58 @@ function getCoverageBundlePlugin(): BunBuildPlugin | null {
139
146
  );
140
147
  }
141
148
 
142
- export async function bundle(
149
+ async function buildToMemory(
150
+ options: Parameters<typeof Bun.build>[0],
151
+ ): Promise<Map<string, string>> {
152
+ const result = await Bun.build(options);
153
+ if (!result.success) {
154
+ const messages = result.logs
155
+ .filter(log => log.level === 'error')
156
+ .map(log => log.message || String(log));
157
+ throw new Error(`Bundle failed:\n${messages.join('\n')}`);
158
+ }
159
+ const outputs = new Map<string, string>();
160
+ for (const output of result.outputs) {
161
+ outputs.set(
162
+ path.posix.normalize(toPosix(output.path)),
163
+ await output.text(),
164
+ );
165
+ }
166
+ return outputs;
167
+ }
168
+
169
+ /** Bundles Tada's client CSS and JavaScript, keyed by output path */
170
+ export function bundle(
143
171
  siteVariables: SiteVariables,
144
- { mode = 'development', distDir }: { mode?: string; distDir?: string } = {},
145
- ): Promise<string[]> {
146
- const packageDir = getPackageDir();
147
- const resolvedDistDir = distDir ?? getDistDir();
172
+ { mode = 'development' }: { mode?: string } = {},
173
+ ): Promise<Map<string, string>> {
148
174
  const isDev = mode === 'development';
175
+ const plugins = [createScssPlugin(renderThemeScss(siteVariables))];
176
+ const coverageBundlePlugin = getCoverageBundlePlugin();
177
+ if (coverageBundlePlugin) {
178
+ plugins.push(coverageBundlePlugin);
179
+ }
149
180
 
150
- const entrypoints = [
151
- path.resolve(packageDir, 'src/index.ts'),
152
- path.resolve(packageDir, 'src/critical.scss'),
153
- ];
154
-
155
- const themeDir = renderThemeScss(siteVariables);
156
-
157
- try {
158
- const plugins = [createScssPlugin(themeDir)];
159
- const coverageBundlePlugin = getCoverageBundlePlugin();
160
- if (coverageBundlePlugin) {
161
- plugins.push(coverageBundlePlugin);
162
- }
163
-
164
- const result = await Bun.build({
165
- entrypoints,
166
- outdir: resolvedDistDir,
167
- naming: getBundleNaming(),
168
- minify: mode === 'production',
169
- sourcemap: isDev ? 'inline' : 'none',
170
- define: createDefine(siteVariables, isDev),
171
- external: ['*.woff2'],
172
- plugins,
173
- });
174
-
175
- if (!result.success) {
176
- const messages = result.logs
177
- .filter(log => log.level === 'error')
178
- .map(log => log.message || String(log));
179
- throw new Error(`Bundle failed:\n${messages.join('\n')}`);
180
- }
181
-
182
- // Return the output filenames for asset tag injection
183
- const assetFiles = result.outputs.map(output =>
184
- toPosix(path.relative(resolvedDistDir, output.path)),
185
- );
181
+ return buildToMemory({
182
+ entrypoints: [path.resolve(getPackageDir(), 'src/index.ts')],
183
+ naming: getBundleNaming(),
184
+ minify: mode === 'production',
185
+ sourcemap: isDev ? 'inline' : 'none',
186
+ define: createDefine(siteVariables, isDev),
187
+ external: ['*.woff2'],
188
+ plugins,
189
+ });
190
+ }
186
191
 
187
- return assetFiles;
188
- } finally {
189
- fs.rmSync(themeDir, { recursive: true, force: true });
190
- }
192
+ /** Bundles the watch-mode reload client, keyed by output path */
193
+ export function bundleReloadClient(): Promise<Map<string, string>> {
194
+ return buildToMemory({
195
+ entrypoints: [
196
+ path.resolve(getPackageDir(), 'build/watch-reload-client.ts'),
197
+ ],
198
+ naming: getBundleNaming(),
199
+ sourcemap: 'inline',
200
+ });
191
201
  }
192
202
 
193
203
  export { renderThemeScss };
@@ -1,9 +1,14 @@
1
- import type MarkdownIt from 'markdown-it';
1
+ import MarkdownIt from 'markdown-it';
2
+ import type Token from 'markdown-it/lib/token.mjs';
2
3
  import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs';
3
4
 
4
5
  const MARKER_CHAR = 0x2b; // '+'
5
6
  const MIN_MARKERS = 3;
6
7
 
8
+ // A separate block parser avoids invoking column rules recursively or mutating
9
+ // the document's tokens and plugin environment while locating literal code.
10
+ const codeContextParser = new MarkdownIt('commonmark');
11
+
7
12
  function isColumnsFence(state: StateBlock, line: number): boolean {
8
13
  const start = state.bMarks[line] + state.tShift[line];
9
14
  const max = state.eMarks[line];
@@ -46,7 +51,28 @@ function columnsRule(
46
51
  let separatorLine = -1;
47
52
  let closeLine = -1;
48
53
 
54
+ // Parse block context, including lists and blockquotes, before looking for
55
+ // delimiters. Token maps keep code fences and their contents literal.
56
+ const contextTokens: Token[] = [];
57
+ codeContextParser.block.parse(
58
+ state.getLines(startLine + 1, endLine, state.blkIndent, false),
59
+ codeContextParser,
60
+ {},
61
+ contextTokens,
62
+ );
63
+ const codeLines = new Set<number>();
64
+ for (const token of contextTokens) {
65
+ if ((token.type === 'fence' || token.type === 'code_block') && token.map) {
66
+ for (let line = token.map[0]; line < token.map[1]; line++) {
67
+ codeLines.add(startLine + 1 + line);
68
+ }
69
+ }
70
+ }
71
+
49
72
  for (let line = startLine + 1; line < endLine; line++) {
73
+ if (codeLines.has(line)) {
74
+ continue;
75
+ }
50
76
  if (!isColumnsFence(state, line)) {
51
77
  continue;
52
78
  }
@@ -1,5 +1,5 @@
1
1
  import fs from 'fs';
2
- import _ from 'lodash';
2
+ import { compileTemplate } from './lodash-template';
3
3
  import {
4
4
  resolveProjectConfigFile,
5
5
  resolveSiteConfigFile,
@@ -44,7 +44,7 @@ function renderConfigTemplateString(
44
44
  fileName: string,
45
45
  ): string {
46
46
  try {
47
- return _.template(value)(context);
47
+ return compileTemplate(value)(context);
48
48
  } catch (error) {
49
49
  throw new Error(
50
50
  `${fileName}: Lodash template error in config: ${getTemplateErrorMessage(error)}`,
@@ -61,7 +61,7 @@ function evaluateConfigTemplateExpression(
61
61
  let captured: unknown;
62
62
 
63
63
  try {
64
- _.template(`<% __capture(( ${expression} )); %>`)({
64
+ compileTemplate(`<% __capture(( ${expression} )); %>`)({
65
65
  ...context,
66
66
  __capture(value: unknown) {
67
67
  captured = value;
@@ -41,14 +41,17 @@ interface CustomFontValidationOptions {
41
41
  }
42
42
 
43
43
  export function isValidPublicWoff2Path(filePath: string): boolean {
44
+ return isValidPublicAssetPath(filePath) && filePath.endsWith('.woff2');
45
+ }
46
+
47
+ export function isValidPublicAssetPath(filePath: string): boolean {
44
48
  if (
45
49
  filePath.length === 0 ||
46
50
  filePath.startsWith('/') ||
47
51
  /^[A-Za-z]:/.test(filePath) ||
48
52
  filePath.includes('\\') ||
49
53
  filePath.includes('?') ||
50
- filePath.includes('#') ||
51
- !filePath.endsWith('.woff2')
54
+ filePath.includes('#')
52
55
  ) {
53
56
  return false;
54
57
  }
@@ -160,7 +163,7 @@ export function renderCustomFontTuningScss(
160
163
  serifTuning?.lineHeight !== undefined
161
164
  ) {
162
165
  rules.push(
163
- ` ${rootSelector} main.body > :is(footer, .appearance-pickers, .slides-header, .file-header) {\n font-size: var(--font-size);\n line-height: var(--line-height);\n }`,
166
+ ` ${rootSelector} main.body .page-bottom, ${rootSelector} main.body > .page-content > :is(.slides-header, .file-header) {\n font-size: var(--font-size);\n line-height: var(--line-height);\n }`,
164
167
  );
165
168
  }
166
169
 
@@ -206,7 +209,7 @@ export function renderCustomFontTuningScss(
206
209
  export function getSerifFontStack(
207
210
  fontOverrides: FontOverrides | undefined,
208
211
  ): string {
209
- const custom = fontOverrides?.serif
212
+ const custom = fontOverrides?.serif?.regular
210
213
  ? `'${CUSTOM_FONT_FAMILY_DEFINITIONS.serif.cssFamily}', `
211
214
  : '';
212
215
  return `${custom}'Source Serif 4', 'Times New Roman', 'Times', serif`;
@@ -215,7 +218,7 @@ export function getSerifFontStack(
215
218
  export function getSerifMonoFontStack(
216
219
  fontOverrides: FontOverrides | undefined,
217
220
  ): string {
218
- const custom = fontOverrides?.serifMono
221
+ const custom = fontOverrides?.serifMono?.regular
219
222
  ? `'${CUSTOM_FONT_FAMILY_DEFINITIONS.serifMono.cssFamily}', `
220
223
  : '';
221
224
  return `${custom}'Courier Prime', 'Courier New', 'Courier', monospace`;
@@ -24,11 +24,11 @@ export default function deflistIdPlugin(md: MarkdownIt): void {
24
24
  break;
25
25
  }
26
26
 
27
- if (t.type === 'text') {
27
+ if (t.type === 'text' || t.type === 'code_inline') {
28
28
  termText += t.content;
29
29
  } else if (t.type === 'inline' && t.children) {
30
30
  for (const child of t.children) {
31
- if (child.type === 'text') {
31
+ if (child.type === 'text' || child.type === 'code_inline') {
32
32
  termText += child.content;
33
33
  }
34
34
  }
@@ -1,4 +1,3 @@
1
- import fs from 'fs';
2
1
  import path from 'path';
3
2
  import * as fontkit from 'fontkit';
4
3
  import sharp from 'sharp';
@@ -108,10 +107,11 @@ function createFaviconSvg(
108
107
  </svg>`;
109
108
  }
110
109
 
110
+ /** Generated favicon files, keyed by output path */
111
111
  export async function generateFavicons(
112
112
  siteVariables: SiteVariables,
113
- distDir: string,
114
- ): Promise<void> {
113
+ ): Promise<Map<string, string | Uint8Array>> {
114
+ const outputs = new Map<string, string | Uint8Array>();
115
115
  const color = siteVariables.faviconColor!;
116
116
  const symbol = siteVariables.faviconSymbol!;
117
117
  const fontWeight = siteVariables.faviconFontWeight || 700;
@@ -132,7 +132,7 @@ export async function generateFavicons(
132
132
  font,
133
133
  fontWeight,
134
134
  );
135
- fs.writeFileSync(path.join(distDir, `${filenameBase}.svg`), svgMarkup);
135
+ outputs.set(`${filenameBase}.svg`, svgMarkup);
136
136
 
137
137
  const pngBuffers = await Promise.all(
138
138
  FAVICON_SIZES.map(async size => {
@@ -145,7 +145,7 @@ export async function generateFavicons(
145
145
  fontWeight,
146
146
  );
147
147
  const buf = await sharp(Buffer.from(svgForSize)).png().toBuffer();
148
- fs.writeFileSync(path.join(distDir, `${filenameBase}-${size}.png`), buf);
148
+ outputs.set(`${filenameBase}-${size}.png`, buf);
149
149
  return { size, buf };
150
150
  }),
151
151
  );
@@ -156,5 +156,6 @@ export async function generateFavicons(
156
156
  .sort((a, b) => a.size - b.size)
157
157
  .map(x => x.buf),
158
158
  );
159
- fs.writeFileSync(path.join(distDir, `${filenameBase}.ico`), icoBuffer);
159
+ outputs.set(`${filenameBase}.ico`, icoBuffer);
160
+ return outputs;
160
161
  }
@@ -1,10 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { getPackageDir } from './utils/paths';
4
- import { makeLogger } from './log';
5
- import type { SiteVariables } from './types';
4
+ import type { CopiedFile, SiteVariables } from './types';
6
5
 
7
- const log = makeLogger(import.meta.url);
8
6
  const FONTS_DIR = path.join(getPackageDir(), 'fonts');
9
7
 
10
8
  export const DEFAULT_FONT_PRELOAD_FILES = {
@@ -49,26 +47,21 @@ export function getDefaultFontPreloadFiles(
49
47
  ];
50
48
  }
51
49
 
52
- export function copyFonts(distDir: string): void {
53
- log.info`Copying fonts`;
54
-
50
+ /** Bundled browser fonts, as copy references keyed by output path */
51
+ export function getFontOutputs(): Map<string, CopiedFile> {
52
+ const outputs = new Map<string, CopiedFile>();
55
53
  for (const family of fs.readdirSync(FONTS_DIR)) {
56
54
  const woff2Dir = path.join(FONTS_DIR, family, 'woff2');
57
55
  if (!fs.existsSync(woff2Dir) || !fs.statSync(woff2Dir).isDirectory()) {
58
56
  continue;
59
57
  }
60
-
61
- const outFamilyDir = path.join(distDir, family);
62
- fs.mkdirSync(outFamilyDir, { recursive: true });
63
-
64
58
  for (const file of fs.readdirSync(woff2Dir)) {
65
59
  if (file.endsWith('.woff2')) {
66
- fs.copyFileSync(
67
- path.join(woff2Dir, file),
68
- path.join(outFamilyDir, file),
69
- );
70
- log.debug`Copied ${family}/${file}`;
60
+ outputs.set(`${family}/${file}`, {
61
+ copyFrom: path.join(woff2Dir, file),
62
+ });
71
63
  }
72
64
  }
73
65
  }
66
+ return outputs;
74
67
  }
@@ -1,35 +1,37 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import * as sass from 'sass';
4
- import { makeLogger } from './log';
4
+ import type { OutputContent } from './types';
5
5
 
6
- const log = makeLogger(import.meta.url);
7
-
8
- export function copyKatexAssets(distDir: string): void {
9
- log.info`Copying KaTeX assets`;
10
-
11
- const outDir = path.join(distDir, 'katex');
12
- const outFontsDir = path.join(outDir, 'fonts');
13
- fs.mkdirSync(outFontsDir, { recursive: true });
6
+ let katexCss: string | undefined;
14
7
 
8
+ function compileKatexCss(): string {
15
9
  // Compile KaTeX SCSS with woff2-only font references
16
10
  const katexScssDir = path.dirname(
17
11
  require.resolve('katex/src/styles/katex.scss'),
18
12
  );
19
- const result = sass.compileString(
13
+ return sass.compileString(
20
14
  `@use 'katex' with ($use-woff2: true, $use-woff: false, $use-ttf: false, $font-folder: 'fonts');`,
21
15
  { loadPaths: [katexScssDir], style: 'compressed' },
22
- );
23
- fs.writeFileSync(path.join(outDir, 'katex.min.css'), result.css);
16
+ ).css;
17
+ }
24
18
 
25
- // Copy woff2 fonts
26
- const katexDistDir = path.dirname(
27
- require.resolve('katex/dist/katex.min.css'),
19
+ /** The KaTeX stylesheet and its woff2 fonts, keyed by output path */
20
+ export function getKatexOutputs(): Map<string, OutputContent> {
21
+ katexCss ??= compileKatexCss();
22
+ const outputs = new Map<string, OutputContent>([
23
+ ['katex/katex.min.css', katexCss],
24
+ ]);
25
+ const fontsDir = path.join(
26
+ path.dirname(require.resolve('katex/dist/katex.min.css')),
27
+ 'fonts',
28
28
  );
29
- const fontsDir = path.join(katexDistDir, 'fonts');
30
29
  for (const file of fs.readdirSync(fontsDir)) {
31
30
  if (file.endsWith('.woff2')) {
32
- fs.copyFileSync(path.join(fontsDir, file), path.join(outFontsDir, file));
31
+ outputs.set(`katex/fonts/${file}`, {
32
+ copyFrom: path.join(fontsDir, file),
33
+ });
33
34
  }
34
35
  }
36
+ return outputs;
35
37
  }
@@ -1,5 +1,3 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
1
  import { createApplyBasePath } from './util';
4
2
  import { FAVICON_SIZES } from './generate-favicon';
5
3
  import type { SiteVariables } from './types';
@@ -57,13 +55,7 @@ function createManifest(siteVariables: SiteVariables): WebAppManifest {
57
55
  };
58
56
  }
59
57
 
60
- export function generateWebAppManifest(
61
- siteVariables: SiteVariables,
62
- distDir: string,
63
- ): void {
64
- const manifest = createManifest(siteVariables);
65
- fs.writeFileSync(
66
- path.join(distDir, 'manifest.json'),
67
- JSON.stringify(manifest),
68
- );
58
+ /** The web app manifest (`manifest.json`) for generated favicons */
59
+ export function createWebAppManifest(siteVariables: SiteVariables): string {
60
+ return JSON.stringify(createManifest(siteVariables));
69
61
  }
@@ -0,0 +1,14 @@
1
+ import _ from 'lodash';
2
+
3
+ /**
4
+ * Compiles a Lodash template that only recognizes `<% %>`, `<%= %>`, and
5
+ * `<%- %>` delimiters.
6
+ *
7
+ * Lodash also treats ES template literal syntax (`${expr}`) as interpolation,
8
+ * but only while `interpolate` is its built-in regex object. Passing a separate
9
+ * regex literal with the same pattern disables that, so `${...}` in Markdown,
10
+ * code samples, and source files stays literal text.
11
+ */
12
+ export function compileTemplate(source: string): _.TemplateExecutor {
13
+ return _.template(source, { interpolate: /<%=([\s\S]+?)%>/g });
14
+ }