@abreen/tada 1.6.4 → 1.7.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 (51) hide show
  1. package/bin/tada.ts +4 -11
  2. package/build/apply-base-path-plugin.ts +11 -19
  3. package/build/colors.ts +19 -22
  4. package/build/content-watch.ts +3 -16
  5. package/build/copy.ts +42 -20
  6. package/build/deflist-id-plugin.ts +3 -16
  7. package/build/features.ts +3 -6
  8. package/build/generate-content-assets.ts +9 -5
  9. package/build/generate-favicon.ts +5 -15
  10. package/build/generate-web-app-manifest.ts +19 -57
  11. package/build/globals.ts +19 -24
  12. package/build/log.ts +30 -46
  13. package/build/pagefind.ts +2 -2
  14. package/build/reachability.ts +1 -1
  15. package/build/serve.ts +1 -5
  16. package/build/site-variables.ts +2 -2
  17. package/build/templates.ts +2 -2
  18. package/build/text-to-id.ts +6 -0
  19. package/build/types.d.ts +1 -7
  20. package/build/utils/code.ts +3 -7
  21. package/build/utils/front-matter.ts +28 -0
  22. package/build/utils/link.ts +8 -0
  23. package/build/utils/literate-java.ts +42 -33
  24. package/build/utils/markdown.ts +13 -17
  25. package/build/utils/paths.ts +3 -11
  26. package/build/utils/render.ts +25 -34
  27. package/build/utils/trace-layout.ts +308 -246
  28. package/build/utils/trace-svg.ts +1 -6
  29. package/build/utils/trace.ts +22 -47
  30. package/build/validate-internal-links-plugin.ts +2 -9
  31. package/build/watch-reload-client.ts +4 -6
  32. package/build/watch.ts +67 -116
  33. package/init/content/index.md +2 -0
  34. package/init/content/labs/00/VowelCounter.java.md +2 -0
  35. package/init/content/labs/00/index.md +2 -0
  36. package/init/content/labs/01/index.md +2 -0
  37. package/init/content/labs/index.md +2 -0
  38. package/init/content/lectures/01/Pair.java.md +2 -0
  39. package/init/content/lectures/01/index.md +2 -0
  40. package/init/content/lectures/02/index.md +2 -0
  41. package/init/content/lectures/index.md +2 -0
  42. package/init/content/markdown.md +5 -1
  43. package/init/content/problem_sets/index.html +2 -0
  44. package/package.json +2 -2
  45. package/src/_alerts.scss +1 -1
  46. package/src/_base.scss +9 -10
  47. package/src/_mixins.scss +4 -0
  48. package/src/literate/style.scss +1 -0
  49. package/src/print/style.scss +37 -20
  50. package/src/toc/style.scss +4 -3
  51. package/templates/_theme.scss +1 -1
package/build/log.ts CHANGED
@@ -48,64 +48,58 @@ export function makeLogger(name: string, logLevel: string = 'info'): Logger {
48
48
  logLevel = ENV_LOG_LEVEL;
49
49
  }
50
50
 
51
- if (!name) {
52
- name = '';
53
- } else {
54
- // Allow for passing import.meta.url (file:// URL) or a plain path
51
+ if (name) {
55
52
  if (name.startsWith('file://')) {
56
53
  name = new URL(name).pathname;
57
54
  }
58
55
  name = path.basename(name, path.extname(name));
59
56
  }
60
57
 
58
+ function getArgs(
59
+ level: string,
60
+ strings: TemplateStringsArray | string | string[],
61
+ args: unknown[],
62
+ labelFn: (strings: TemplateStringsArray, ...args: unknown[]) => string,
63
+ ): string[] {
64
+ return [
65
+ labelFn` ${level} ` + ' ' + (level === 'debug' ? name + ' ' : ''),
66
+ format(strings, ...args),
67
+ ];
68
+ }
69
+
61
70
  const logger: Logger = {
62
71
  minLogLevel: logLevel,
63
- /** Don't log if the level is < minLogLevel */
64
72
  setMinLogLevel(minLogLevel: string) {
65
73
  this.minLogLevel = minLogLevel;
66
74
  },
67
- getArgs(
68
- level: string,
69
- strings: TemplateStringsArray | string | string[],
70
- args: unknown[],
71
- labelFn: (strings: TemplateStringsArray, ...args: unknown[]) => string,
72
- ): string[] {
73
- const params: string[] = [];
74
- params.push(
75
- labelFn` ${level} ` + ' ' + (level === 'debug' ? name + ' ' : ''),
76
- );
77
- params.push(format(strings, ...args));
78
- return params;
79
- },
80
75
  debug(strings: TemplateStringsArray, ...args: unknown[]) {
81
76
  if (shouldLog(this.minLogLevel, 'debug')) {
82
- print(this.getArgs('debug', strings, args, L), 'stderr');
77
+ print(getArgs('debug', strings, args, L), 'stderr');
83
78
  }
84
79
  },
85
80
  info(strings: TemplateStringsArray, ...args: unknown[]) {
86
81
  if (shouldLog(this.minLogLevel, 'info')) {
87
- print(this.getArgs('info', strings, args, Li));
82
+ print(getArgs('info', strings, args, Li));
88
83
  }
89
84
  },
90
85
  warn(strings: TemplateStringsArray, ...args: unknown[]) {
91
86
  if (shouldLog(this.minLogLevel, 'warn')) {
92
- print(this.getArgs('warn', strings, args, Yi));
87
+ print(getArgs('warn', strings, args, Yi));
93
88
  }
94
89
  },
95
90
  error(strings: TemplateStringsArray, ...args: unknown[]) {
96
91
  if (shouldLog(this.minLogLevel, 'error')) {
97
- print(this.getArgs('error', strings, args, Ri));
92
+ print(getArgs('error', strings, args, Ri));
98
93
  }
99
94
  },
100
95
  event(strings: TemplateStringsArray, ...args: unknown[]) {
101
- print(this.getArgs('event', strings, args, Gi));
96
+ print(getArgs('event', strings, args, Gi));
102
97
  },
103
98
  followup(strings: string[]) {
104
99
  print(strings);
105
100
  },
106
101
  };
107
102
 
108
- logger.setMinLogLevel(logLevel);
109
103
  return logger;
110
104
  }
111
105
 
@@ -118,7 +112,7 @@ function format(
118
112
  try {
119
113
  return String.raw(strings as TemplateStringsArray, ...args.map(toString));
120
114
  } catch {
121
- // fallback to safe join
115
+ return '';
122
116
  }
123
117
  } else {
124
118
  if (Array.isArray(strings)) {
@@ -129,35 +123,25 @@ function format(
129
123
 
130
124
  return args.map(toString).join(' ');
131
125
  }
132
-
133
- return '';
134
126
  }
135
127
 
136
128
  function toString(item: unknown): string {
137
- if (item === undefined) {
138
- return 'undefined';
139
- }
140
- if (item === null) {
141
- return 'null';
129
+ if (item == null) {
130
+ return String(item);
142
131
  }
143
132
  if (typeof item === 'string') {
144
133
  return item;
145
134
  }
146
-
147
- try {
148
- if (typeof item === 'object') {
149
- return inspect(item, {
150
- compact: true,
151
- depth: 2,
152
- breakLength: 80,
153
- maxStringLength: 250,
154
- colors: true,
155
- });
156
- }
157
- throw new Error('not an object');
158
- } catch {
159
- return String(item);
135
+ if (typeof item === 'object') {
136
+ return inspect(item, {
137
+ compact: true,
138
+ depth: 2,
139
+ breakLength: 80,
140
+ maxStringLength: 250,
141
+ colors: true,
142
+ });
160
143
  }
144
+ return String(item);
161
145
  }
162
146
 
163
147
  export function getFlair(): string {
package/build/pagefind.ts CHANGED
@@ -101,7 +101,7 @@ function collectIndexTargets(
101
101
  htmlAssetsByPath,
102
102
  knownPdfPaths: new Set(pdfSourceByOutputPath.keys()),
103
103
  rootPath: 'index.html',
104
- basePath: siteVariables?.basePath || '/',
104
+ basePath: siteVariables.basePath || '/',
105
105
  });
106
106
  }
107
107
 
@@ -264,7 +264,7 @@ export class WatchPagefindRunner {
264
264
  private htmlCacheByAssetPath: Map<string, string>;
265
265
 
266
266
  constructor(siteVariables: SiteVariables) {
267
- this.siteVariables = siteVariables || {};
267
+ this.siteVariables = siteVariables;
268
268
  this.watchRunInProgress = false;
269
269
  this.watchRunQueued = false;
270
270
  this.distPath = null;
@@ -24,7 +24,7 @@ function normalizeUrlPath(pathname: string): string {
24
24
 
25
25
  function stripBasePath(pathname: string, basePath: string): string {
26
26
  const normalizedPath = normalizeUrlPath(pathname);
27
- const normalizedBasePath = normalizeUrlPath(basePath || '/');
27
+ const normalizedBasePath = normalizeUrlPath(basePath);
28
28
 
29
29
  if (normalizedBasePath === '/') {
30
30
  return normalizedPath;
package/build/serve.ts CHANGED
@@ -29,10 +29,6 @@ export function resolvePathname(
29
29
  return null;
30
30
  }
31
31
 
32
- if (stat.isDirectory()) {
33
- return null;
34
- }
35
-
36
32
  if (!stat.isFile()) {
37
33
  return null;
38
34
  }
@@ -81,7 +77,7 @@ export function startServer(options: StartServerOptions): void {
81
77
 
82
78
  log.info`Dev server: ${B`http://localhost:${server.port}/index.html`}`;
83
79
  if (onReady) {
84
- onReady(server.port as number);
80
+ onReady(server.port!);
85
81
  }
86
82
  } catch (err) {
87
83
  log.error`Failed to start server on port ${port}: ${err}`;
@@ -39,12 +39,12 @@ function getSiteVariables(env: string): SiteVariables {
39
39
  }
40
40
 
41
41
  // Derive faviconColor from themeColor if not explicitly set
42
- if (variables.themeColor && !variables.faviconColor) {
42
+ if (!variables.faviconColor) {
43
43
  variables.faviconColor = variables.themeColor;
44
44
  }
45
45
 
46
46
  // Derive titlePostfix from title if not explicitly set
47
- if (variables.title && !variables.titlePostfix) {
47
+ if (!variables.titlePostfix) {
48
48
  variables.titlePostfix = ` - ${variables.title}`;
49
49
  }
50
50
 
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import _ from 'lodash';
4
4
  import { compile as compileJsonSchema, doValidation } from './json-schema';
5
5
  import { makeLogger } from './log';
6
- import { getPackageDir, getConfigDir } from './utils/paths';
6
+ import { getPackageDir, getProjectDir } from './utils/paths';
7
7
  import type { ValidateFunction } from 'ajv';
8
8
  import type { Logger, SiteVariables } from './types';
9
9
 
@@ -35,7 +35,7 @@ function getHtmlTemplatesDir(): string {
35
35
  }
36
36
 
37
37
  function getJsonDataDir(): string {
38
- return getConfigDir();
38
+ return getProjectDir();
39
39
  }
40
40
 
41
41
  export function json(fileName: string): unknown {
@@ -6,3 +6,9 @@ export default function textToId(value: unknown): string {
6
6
  .replace(/\s+/g, '-')
7
7
  .replace(/[^\w-]/g, '');
8
8
  }
9
+
10
+ export function deduplicateId(used: Map<string, number>, id: string): string {
11
+ const count = (used.get(id) ?? 0) + 1;
12
+ used.set(id, count);
13
+ return count === 1 ? id : `${id}-${count}`;
14
+ }
package/build/types.d.ts CHANGED
@@ -11,7 +11,7 @@ export interface SiteVariables {
11
11
  faviconFontWeight?: number;
12
12
  internalDomains?: string[];
13
13
  defaultTimeZone: string;
14
- features?: FeatureConfig;
14
+ features: FeatureConfig;
15
15
  codeLanguages?: Record<string, string>;
16
16
  tintHue?: number;
17
17
  tintAmount?: number;
@@ -57,12 +57,6 @@ export interface ContentRenderResult {
57
57
  export interface Logger {
58
58
  minLogLevel: string;
59
59
  setMinLogLevel(level: string): void;
60
- getArgs(
61
- level: string,
62
- strings: TemplateStringsArray | string | string[],
63
- args: unknown[],
64
- colorFn: (strings: TemplateStringsArray, ...args: unknown[]) => string,
65
- ): string[];
66
60
  debug(strings: TemplateStringsArray, ...args: unknown[]): void;
67
61
  info(strings: TemplateStringsArray, ...args: unknown[]): void;
68
62
  warn(strings: TemplateStringsArray, ...args: unknown[]): void;
@@ -7,6 +7,7 @@ import { getHighlighter } from './shiki-highlighter';
7
7
  import externalLinksPlugin from '../external-links-plugin';
8
8
  import applyBasePathPlugin from '../apply-base-path-plugin';
9
9
  import { createApplyBasePath } from './paths';
10
+ import { splitLines } from './literate-java';
10
11
  import type { JavaTocEntry, SiteVariables } from '../types';
11
12
 
12
13
  interface CstNode {
@@ -73,9 +74,7 @@ export function rewriteProseLinks(
73
74
 
74
75
  function rewriteHref(href: string): string {
75
76
  // Separate pathname from query/fragment
76
- const match = href.match(/^([^?#]*)(.*)$/);
77
- const pathname = match ? match[1] : href;
78
- const suffix = match ? match[2] : '';
77
+ const [, pathname, suffix] = href.match(/^([^?#]*)(.*)$/)!;
79
78
 
80
79
  // Leave external, protocol-relative, and anchor-only links unchanged
81
80
  if (
@@ -509,10 +508,7 @@ export function renderCodeWithComments(
509
508
  pageDirPath?: string,
510
509
  ): string {
511
510
  const md = createCodeMarkdown(siteVariables);
512
- const lines = sourceCode.split('\n');
513
- if (lines[lines.length - 1] === '') {
514
- lines.pop();
515
- }
511
+ const lines = splitLines(sourceCode);
516
512
 
517
513
  // Group lines into segments
518
514
  const segments: CodeSegment[] = [];
@@ -57,11 +57,39 @@ function parseFrontMatterPlainText(rawContent: string): RawParsedFrontMatter {
57
57
  };
58
58
  }
59
59
 
60
+ function parseFrontMatterStandard(rawContent: string): RawParsedFrontMatter {
61
+ const lines = rawContent.split(/\r?\n/);
62
+
63
+ // Caller has already verified lines[0] === '---'
64
+ for (let i = 1; i < lines.length; i++) {
65
+ if (lines[i].trimEnd() === '---') {
66
+ return {
67
+ frontMatter: lines.slice(1, i).join('\n'),
68
+ content: lines.slice(i + 1).join('\n'),
69
+ };
70
+ }
71
+ }
72
+
73
+ throw new Error(
74
+ 'Front matter starts with --- but no closing --- delimiter was found',
75
+ );
76
+ }
77
+
60
78
  export function parseFrontMatter(
61
79
  rawContent: string,
62
80
  ext: string,
63
81
  ): RawParsedFrontMatter {
64
82
  if (extensionIsMarkdown(ext) || ext === '.html') {
83
+ // Detect standard YAML front matter (first line is exactly ---,
84
+ // ignoring trailing whitespace and \r)
85
+ const firstNewline = rawContent.indexOf('\n');
86
+ const firstLine =
87
+ firstNewline === -1 ? rawContent : rawContent.slice(0, firstNewline);
88
+
89
+ if (firstLine.trimEnd() === '---') {
90
+ return parseFrontMatterStandard(rawContent);
91
+ }
92
+
65
93
  return parseFrontMatterPlainText(rawContent);
66
94
  } else {
67
95
  // unknown type, return raw
@@ -0,0 +1,8 @@
1
+ const SCHEME_RE = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
2
+
3
+ export function isInternalLink(href: string | undefined): boolean {
4
+ if (!href || href.startsWith('#') || href.startsWith('//')) {
5
+ return false;
6
+ }
7
+ return !SCHEME_RE.test(href);
8
+ }
@@ -14,6 +14,37 @@ import type {
14
14
 
15
15
  const log = makeLogger(import.meta.url);
16
16
 
17
+ export function runJavac(
18
+ args: string[],
19
+ opts: { cwd?: string; tempDir: string; label: string },
20
+ ): void {
21
+ try {
22
+ execFileSync('javac', args, {
23
+ cwd: opts.cwd,
24
+ timeout: 30000,
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ });
27
+ } catch (err: unknown) {
28
+ const execErr = err as { stderr?: Buffer; message: string };
29
+ const stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
30
+ fs.rmSync(opts.tempDir, { recursive: true, force: true });
31
+ throw new Error(`${opts.label}:\n${stderr}`, { cause: err });
32
+ }
33
+ }
34
+
35
+ export function getExecError(err: unknown): string {
36
+ const execErr = err as { stderr?: Buffer; message: string };
37
+ return execErr.stderr ? execErr.stderr.toString() : execErr.message;
38
+ }
39
+
40
+ export function splitLines(source: string): string[] {
41
+ const lines = source.split('\n');
42
+ if (lines.length > 0 && lines[lines.length - 1] === '') {
43
+ lines.pop();
44
+ }
45
+ return lines;
46
+ }
47
+
17
48
  let runnerClassDir: string | null = null;
18
49
 
19
50
  const MAIN_PATTERN = /\bvoid\s+main\s*\(/m;
@@ -95,20 +126,11 @@ export function compileJavaSource(
95
126
 
96
127
  log.debug`Compiling ${className}.java (${javaSource.split('\n').length} lines) in ${tempDir}`;
97
128
 
98
- try {
99
- execFileSync('javac', [`${className}.java`], {
100
- cwd: tempDir,
101
- timeout: 30000,
102
- stdio: ['pipe', 'pipe', 'pipe'],
103
- });
104
- } catch (err: unknown) {
105
- const execErr = err as { stderr?: Buffer; message: string };
106
- const stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
107
- fs.rmSync(tempDir, { recursive: true, force: true });
108
- throw new Error(`Compilation failed for ${className}.java:\n${stderr}`, {
109
- cause: err,
110
- });
111
- }
129
+ runJavac([`${className}.java`], {
130
+ cwd: tempDir,
131
+ tempDir,
132
+ label: `Compilation failed for ${className}.java`,
133
+ });
112
134
 
113
135
  return tempDir;
114
136
  }
@@ -127,19 +149,10 @@ function ensureRunnerCompiled(): string {
127
149
 
128
150
  log.debug`Compiling LiterateRunner.java into ${tempDir}`;
129
151
 
130
- try {
131
- execFileSync('javac', ['-d', tempDir, sourceFile], {
132
- timeout: 30000,
133
- stdio: ['pipe', 'pipe', 'pipe'],
134
- });
135
- } catch (err: unknown) {
136
- const execErr = err as { stderr?: Buffer; message: string };
137
- const stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
138
- fs.rmSync(tempDir, { recursive: true, force: true });
139
- throw new Error(`Failed to compile LiterateRunner.java:\n${stderr}`, {
140
- cause: err,
141
- });
142
- }
152
+ runJavac(['-d', tempDir, sourceFile], {
153
+ tempDir,
154
+ label: 'Failed to compile LiterateRunner.java',
155
+ });
143
156
 
144
157
  runnerClassDir = tempDir;
145
158
  return tempDir;
@@ -174,12 +187,8 @@ export function executeLiterateJava(
174
187
  log.debug`LiterateRunner returned ${entries.length} output entries`;
175
188
  return entries;
176
189
  } catch (err: unknown) {
177
- const execErr = err as {
178
- stderr?: Buffer | string;
179
- stdout?: Buffer | string;
180
- message: string;
181
- };
182
- const stderr = execErr.stderr ? execErr.stderr.toString() : '';
190
+ const execErr = err as { stdout?: Buffer | string; message: string };
191
+ const stderr = getExecError(err);
183
192
  const stdout = execErr.stdout ? execErr.stdout.toString() : '';
184
193
  throw new Error(
185
194
  `Execution failed for ${className}:\n${stderr || stdout || execErr.message}`,
@@ -7,7 +7,7 @@ import markdownItAnchor from 'markdown-it-anchor';
7
7
  import markdownItFootnote from 'markdown-it-footnote';
8
8
  import markdownItDeflist from 'markdown-it-deflist';
9
9
  import markdownItContainer from 'markdown-it-container';
10
- import textToId from '../text-to-id';
10
+ import textToId, { deduplicateId } from '../text-to-id';
11
11
  import { getHighlighter } from './shiki-highlighter';
12
12
  import headingSubtitlePlugin from '../heading-subtitle-plugin';
13
13
  import deflistIdPlugin from '../deflist-id-plugin';
@@ -21,6 +21,10 @@ import katex from 'katex';
21
21
  import renderA11yString from 'katex/contrib/render-a11y-string';
22
22
  import type { SiteVariables } from '../types';
23
23
 
24
+ const DETAILS_PATTERN = /^details\s+(.*)$/;
25
+ const ALERT_PATTERN = /^(note|warning)(?:\s+"(.+)"|\s+(.+))?$/;
26
+ const QUESTION_PATTERN = /^question\s+(.+)$/;
27
+
24
28
  interface CreateMarkdownOptions {
25
29
  validatorOptions?: Record<string, unknown>;
26
30
  literateJavaOutputPaths?: Set<string>;
@@ -29,11 +33,7 @@ interface CreateMarkdownOptions {
29
33
  }
30
34
 
31
35
  function capitalize(str: string): string {
32
- if (str.length < 2) {
33
- return str;
34
- }
35
-
36
- return str[0].toUpperCase() + str.slice(1);
36
+ return str.charAt(0).toUpperCase() + str.slice(1);
37
37
  }
38
38
 
39
39
  const MAX_FOOTNOTES = 35;
@@ -81,11 +81,11 @@ export function createMarkdown(
81
81
  .use(markdownItContainer, 'details', {
82
82
  marker: '<',
83
83
  validate: function (params: string) {
84
- return !!params.trim().match(/^details\s+(.*)$/);
84
+ return DETAILS_PATTERN.test(params.trim());
85
85
  },
86
86
 
87
87
  render: function (tokens: Token[], idx: number) {
88
- const m = tokens[idx].info.trim().match(/^details\s+(.*)$/);
88
+ const m = tokens[idx].info.trim().match(DETAILS_PATTERN);
89
89
 
90
90
  if (tokens[idx].nesting === 1) {
91
91
  return (
@@ -144,7 +144,7 @@ export function createMarkdown(
144
144
  markdown.use(markdownItContainer, 'alert', {
145
145
  marker: '!',
146
146
  validate: function (params: string) {
147
- return !!params.trim().match(/^(note|warning)(?:\s+"(.+)"|\s+(.+))?$/);
147
+ return ALERT_PATTERN.test(params.trim());
148
148
  },
149
149
  render: function (
150
150
  tokens: Token[],
@@ -152,9 +152,7 @@ export function createMarkdown(
152
152
  _options: unknown,
153
153
  env: Record<string, unknown>,
154
154
  ) {
155
- const matches = tokens[idx].info
156
- .trim()
157
- .match(/^(note|warning)(?:\s+"(.+)"|\s+(.+))?$/);
155
+ const matches = tokens[idx].info.trim().match(ALERT_PATTERN);
158
156
 
159
157
  if (tokens[idx].nesting === 1) {
160
158
  const classNames = ['alert'];
@@ -168,9 +166,7 @@ export function createMarkdown(
168
166
  ? markdown.utils.escapeHtml(curlyQuote(title))
169
167
  : capitalize(type || '');
170
168
  const baseId = textToId(title || type || '');
171
- const count = (usedIds.get(baseId) ?? 0) + 1;
172
- usedIds.set(baseId, count);
173
- const titleId = count === 1 ? baseId : `${baseId}-${count}`;
169
+ const titleId = deduplicateId(usedIds, baseId);
174
170
 
175
171
  if (!env.alertIds) {
176
172
  env.alertIds = [];
@@ -190,10 +186,10 @@ export function createMarkdown(
190
186
  markdown.use(markdownItContainer, 'question', {
191
187
  marker: '?',
192
188
  validate: function (params: string) {
193
- return !!params.trim().match(/^question\s+(.+)$/);
189
+ return QUESTION_PATTERN.test(params.trim());
194
190
  },
195
191
  render: function (tokens: Token[], idx: number) {
196
- const m = tokens[idx].info.trim().match(/^question\s+(.+)$/);
192
+ const m = tokens[idx].info.trim().match(QUESTION_PATTERN);
197
193
  if (tokens[idx].nesting === 1) {
198
194
  const question = markdown.renderInline(m![1]);
199
195
  return (
@@ -25,23 +25,15 @@ export function getPublicDir(): string {
25
25
  return path.resolve(getProjectDir(), 'public');
26
26
  }
27
27
 
28
- export function getConfigDir(): string {
29
- return getProjectDir();
30
- }
31
-
32
28
  export function createApplyBasePath(
33
29
  siteVariables: SiteVariables,
34
30
  ): (subPath: string) => string {
31
+ const base = (siteVariables.basePath || '/').replace(/\/$/, '');
35
32
  return function applyBasePath(subPath: string): string {
36
33
  if (!subPath.startsWith('/')) {
37
34
  throw new Error('invalid internal path, must start with "/": ' + subPath);
38
35
  }
39
-
40
- let path = siteVariables.basePath || '/';
41
- if (path.endsWith('/')) {
42
- path = path.slice(0, -1);
43
- }
44
- return path + subPath;
36
+ return base + subPath;
45
37
  };
46
38
  }
47
39
 
@@ -62,7 +54,7 @@ export function toUrlPath(p: string): string {
62
54
 
63
55
  export function normalizeOutputPath(outputPath: string): string {
64
56
  const normalized = path.posix.normalize(toPosix(outputPath));
65
- if (normalized === '.' || normalized === '') {
57
+ if (normalized === '.') {
66
58
  return '/';
67
59
  }
68
60
  return normalized.startsWith('/') ? normalized : `/${normalized}`;
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import _ from 'lodash';
4
+ import type MarkdownIt from 'markdown-it';
4
5
  import { stripHtml } from 'string-strip-html';
5
6
  import { isFeatureEnabled } from '../features';
6
7
  import { makeLogger } from '../log';
@@ -50,6 +51,20 @@ function isWatchMode(assetFiles: string[]): boolean {
50
51
  return assetFiles.some(f => f.includes('watch-reload-client'));
51
52
  }
52
53
 
54
+ function renderInlineField(
55
+ md: MarkdownIt,
56
+ vars: Record<string, unknown>,
57
+ field: string,
58
+ ): void {
59
+ const raw = vars[field];
60
+ if (!raw || typeof raw !== 'string') {
61
+ return;
62
+ }
63
+ const html = md.renderInline(raw);
64
+ vars[`${field}Html`] = html;
65
+ vars[field] = stripHtml(html).result;
66
+ }
67
+
53
68
  interface TemplateParametersInput {
54
69
  pageVariables: Record<string, unknown>;
55
70
  siteVariables: SiteVariables;
@@ -410,35 +425,16 @@ function renderPlainTextContent(
410
425
  subPath,
411
426
  isWatchMode,
412
427
  });
413
- const pageVariablesProcessed: Record<string, unknown> = Object.entries(
414
- pageVariables,
415
- )
416
- .map(([k, v]) => {
417
- const newValue =
418
- typeof v === 'string' ? _.template(v)(siteOnlyParams) : v;
419
- return [k, newValue] as [string, unknown];
420
- })
421
- .reduce(
422
- (acc, [k, v]) => {
423
- acc[k] = v;
424
- return acc;
425
- },
426
- {} as Record<string, unknown>,
427
- );
428
+ const pageVariablesProcessed: Record<string, unknown> = Object.fromEntries(
429
+ Object.entries(pageVariables).map(([k, v]) => [
430
+ k,
431
+ typeof v === 'string' ? _.template(v)(siteOnlyParams) : v,
432
+ ]),
433
+ );
428
434
 
429
435
  // Render title and description as inline Markdown
430
- if (pageVariablesProcessed.title) {
431
- const titleHtml = md.renderInline(pageVariablesProcessed.title as string);
432
- pageVariablesProcessed.titleHtml = titleHtml;
433
- pageVariablesProcessed.title = stripHtml(titleHtml).result;
434
- }
435
- if (pageVariablesProcessed.description) {
436
- const descriptionHtml = md.renderInline(
437
- pageVariablesProcessed.description as string,
438
- );
439
- pageVariablesProcessed.descriptionHtml = descriptionHtml;
440
- pageVariablesProcessed.description = stripHtml(descriptionHtml).result;
441
- }
436
+ renderInlineField(md, pageVariablesProcessed, 'title');
437
+ renderInlineField(md, pageVariablesProcessed, 'description');
442
438
 
443
439
  resolveAuthor(pageVariablesProcessed, filePath);
444
440
 
@@ -620,10 +616,7 @@ export function renderLiterateJavaPageAsset({
620
616
  : null;
621
617
 
622
618
  if (output) {
623
- const escapedOutput = output
624
- .replace(/&/g, '&amp;')
625
- .replace(/</g, '&lt;')
626
- .replace(/>/g, '&gt;');
619
+ const escapedOutput = md.utils.escapeHtml(output);
627
620
  return `<div class="literate-code-output">${codeHtml}<pre>${escapedOutput}</pre></div>`;
628
621
  }
629
622
 
@@ -641,9 +634,7 @@ export function renderLiterateJavaPageAsset({
641
634
  ),
642
635
  );
643
636
 
644
- const titleHtml = md.renderInline(pageVariables.title as string);
645
- pageVariables.titleHtml = titleHtml;
646
- pageVariables.title = stripHtml(titleHtml).result;
637
+ renderInlineField(md, pageVariables, 'title');
647
638
  pageVariables.template = 'literate';
648
639
  pageVariables.codeFilePath = codeFilePath;
649
640
  pageVariables.downloadName = javaFileName;