@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
@@ -0,0 +1,97 @@
1
+ const watchStateByCompiler = new WeakMap();
2
+ const buildDeltaByCompiler = new WeakMap();
3
+
4
+ function cloneSet(values) {
5
+ return new Set(values || []);
6
+ }
7
+
8
+ function getEmptyBuildDelta() {
9
+ return {
10
+ changedSourceFiles: new Set(),
11
+ changedHtmlAssetPaths: new Set(),
12
+ removedHtmlAssetPaths: new Set(),
13
+ templatesChanged: false,
14
+ };
15
+ }
16
+
17
+ function setWatchState(compiler, watchState) {
18
+ watchStateByCompiler.set(compiler, {
19
+ contentFiles: cloneSet(watchState?.contentFiles),
20
+ buildContentFiles: cloneSet(watchState?.buildContentFiles),
21
+ changedContentFiles: cloneSet(watchState?.changedContentFiles),
22
+ templatesChanged: Boolean(watchState?.templatesChanged),
23
+ structureChanged: Boolean(watchState?.structureChanged),
24
+ });
25
+ }
26
+
27
+ function getWatchState(compiler) {
28
+ const state = watchStateByCompiler.get(compiler);
29
+ if (!state) {
30
+ return null;
31
+ }
32
+
33
+ return {
34
+ contentFiles: cloneSet(state.contentFiles),
35
+ buildContentFiles: cloneSet(state.buildContentFiles),
36
+ changedContentFiles: cloneSet(state.changedContentFiles),
37
+ templatesChanged: state.templatesChanged,
38
+ structureChanged: state.structureChanged,
39
+ };
40
+ }
41
+
42
+ function setBuildDelta(compiler, buildDelta) {
43
+ buildDeltaByCompiler.set(compiler, {
44
+ changedSourceFiles: cloneSet(buildDelta?.changedSourceFiles),
45
+ changedHtmlAssetPaths: cloneSet(buildDelta?.changedHtmlAssetPaths),
46
+ removedHtmlAssetPaths: cloneSet(buildDelta?.removedHtmlAssetPaths),
47
+ templatesChanged: Boolean(buildDelta?.templatesChanged),
48
+ });
49
+ }
50
+
51
+ function mergeIntoSet(target, values) {
52
+ if (!values) {
53
+ return;
54
+ }
55
+
56
+ for (const value of values) {
57
+ target.add(value);
58
+ }
59
+ }
60
+
61
+ function updateBuildDelta(compiler, buildDelta) {
62
+ const current = getBuildDelta(compiler);
63
+ mergeIntoSet(current.changedSourceFiles, buildDelta?.changedSourceFiles);
64
+ mergeIntoSet(
65
+ current.changedHtmlAssetPaths,
66
+ buildDelta?.changedHtmlAssetPaths,
67
+ );
68
+ mergeIntoSet(
69
+ current.removedHtmlAssetPaths,
70
+ buildDelta?.removedHtmlAssetPaths,
71
+ );
72
+ current.templatesChanged =
73
+ current.templatesChanged || Boolean(buildDelta?.templatesChanged);
74
+ buildDeltaByCompiler.set(compiler, current);
75
+ }
76
+
77
+ function getBuildDelta(compiler) {
78
+ const delta = buildDeltaByCompiler.get(compiler);
79
+ if (!delta) {
80
+ return getEmptyBuildDelta();
81
+ }
82
+
83
+ return {
84
+ changedSourceFiles: cloneSet(delta.changedSourceFiles),
85
+ changedHtmlAssetPaths: cloneSet(delta.changedHtmlAssetPaths),
86
+ removedHtmlAssetPaths: cloneSet(delta.removedHtmlAssetPaths),
87
+ templatesChanged: delta.templatesChanged,
88
+ };
89
+ }
90
+
91
+ module.exports = {
92
+ getBuildDelta,
93
+ getWatchState,
94
+ setBuildDelta,
95
+ setWatchState,
96
+ updateBuildDelta,
97
+ };
@@ -0,0 +1,162 @@
1
+ const { describe, expect, test, beforeAll } = require('bun:test');
2
+ const { initHighlighter } = require('./utils/shiki-highlighter');
3
+ const {
4
+ renderCodeWithComments,
5
+ extractJavaMethodToc,
6
+ } = require('./utils/code');
7
+
8
+ beforeAll(async () => {
9
+ await initHighlighter(['java', 'text', 'plaintext']);
10
+ });
11
+
12
+ describe('extractJavaMethodToc', () => {
13
+ test('returns methods from a regular class', () => {
14
+ const toc = extractJavaMethodToc(`
15
+ public class Foo {
16
+ public void foo() {}
17
+ public int bar(int x) { return x; }
18
+ }
19
+ `);
20
+ expect(toc).toEqual([
21
+ { kind: 'method', label: 'Method', name: 'foo()', line: 3 },
22
+ { kind: 'method', label: 'Method', name: 'bar(x)', line: 4 },
23
+ ]);
24
+ });
25
+
26
+ test('returns top-level methods from a compact source file', () => {
27
+ const toc = extractJavaMethodToc(`
28
+ void hello() {
29
+ System.out.println("Hello");
30
+ }
31
+
32
+ void main() {
33
+ hello();
34
+ }
35
+ `);
36
+ expect(toc).toEqual([
37
+ { kind: 'method', label: 'Method', name: 'hello()', line: 2 },
38
+ { kind: 'method', label: 'Method', name: 'main()', line: 6 },
39
+ ]);
40
+ });
41
+
42
+ test('returns empty array when class has no methods or fields', () => {
43
+ const toc = extractJavaMethodToc(`
44
+ public class Empty {
45
+ }
46
+ `);
47
+ expect(toc).toEqual([]);
48
+ });
49
+
50
+ test('returns constructor from a class', () => {
51
+ const toc = extractJavaMethodToc(`
52
+ public class Point {
53
+ public Point(int x) {}
54
+ }
55
+ `);
56
+ expect(toc).toEqual([
57
+ { kind: 'constructor', label: 'Constructor', name: 'Point(x)', line: 3 },
58
+ ]);
59
+ });
60
+
61
+ test('excludes methods on inner classes', () => {
62
+ const toc = extractJavaMethodToc(`
63
+ public class Outer {
64
+ public void outerMethod() {}
65
+ static class Inner {
66
+ public void innerMethod() {}
67
+ }
68
+ }
69
+ `);
70
+ expect(toc.map(e => e.name)).toEqual(['outerMethod()']);
71
+ });
72
+
73
+ test('returns default method from an interface', () => {
74
+ const toc = extractJavaMethodToc(`
75
+ public interface Greeter {
76
+ default String greet(String name) { return "Hello, " + name; }
77
+ }
78
+ `);
79
+ expect(toc).toEqual([
80
+ { kind: 'method', label: 'Method', name: 'greet(name)', line: 3 },
81
+ ]);
82
+ });
83
+
84
+ test('returns fields from a class with type but not access modifier', () => {
85
+ const toc = extractJavaMethodToc(`
86
+ public class Counter {
87
+ private int count;
88
+ public String label;
89
+ }
90
+ `);
91
+ expect(toc.map(e => e.name)).toEqual(['int count', 'String label']);
92
+ });
93
+
94
+ test('returns one entry per variable in a multi-variable declaration', () => {
95
+ const toc = extractJavaMethodToc(`
96
+ public class Coords {
97
+ int x, y;
98
+ }
99
+ `);
100
+ expect(toc.map(e => e.name)).toEqual(['int x', 'int y']);
101
+ });
102
+
103
+ test('returns array type field', () => {
104
+ const toc = extractJavaMethodToc(`
105
+ public class Arr {
106
+ int[] values;
107
+ }
108
+ `);
109
+ expect(toc).toEqual([
110
+ { kind: 'field', label: 'Field', name: 'int[] values', line: 3 },
111
+ ]);
112
+ });
113
+
114
+ test('returns generic type field', () => {
115
+ const toc = extractJavaMethodToc(`
116
+ public class Container {
117
+ List<String> items;
118
+ }
119
+ `);
120
+ expect(toc).toEqual([
121
+ { kind: 'field', label: 'Field', name: 'List<String> items', line: 3 },
122
+ ]);
123
+ });
124
+
125
+ test('returns interface constant', () => {
126
+ const toc = extractJavaMethodToc(`
127
+ public interface Config {
128
+ int TIMEOUT = 30;
129
+ }
130
+ `);
131
+ expect(toc).toEqual([
132
+ { kind: 'field', label: 'Field', name: 'int TIMEOUT', line: 3 },
133
+ ]);
134
+ });
135
+
136
+ test('returns abstract interface methods (no body)', () => {
137
+ const toc = extractJavaMethodToc(`
138
+ public interface Greeter {
139
+ String greet(String name);
140
+ }
141
+ `);
142
+ expect(toc).toEqual([
143
+ { kind: 'method', label: 'Method', name: 'greet(name)', line: 3 },
144
+ ]);
145
+ });
146
+ });
147
+
148
+ describe('renderCodeWithComments', () => {
149
+ test('renders build-time line rows for code segments', () => {
150
+ const html = renderCodeWithComments('alpha\n\nbeta\n', 'java', {
151
+ basePath: '/',
152
+ internalDomains: [],
153
+ });
154
+
155
+ expect(html).toContain('<span class="code-row">');
156
+ expect(html).toContain('id="L1" href="#L1"');
157
+ expect(html).toContain('id="L2" href="#L2"');
158
+ expect(html).toContain('id="L3" href="#L3"');
159
+ expect(html).not.toContain('id="L4" href="#L4"');
160
+ expect(html).toContain('<code class="shiki language-java">');
161
+ });
162
+ });
@@ -0,0 +1,15 @@
1
+ const util = require('util');
2
+
3
+ const R = (str, ...args) => util.styleText(['red'], String.raw(str, ...args));
4
+ const G = (str, ...args) => util.styleText(['green'], String.raw(str, ...args));
5
+ const B = (str, ...args) => util.styleText(['blue'], String.raw(str, ...args));
6
+ const Y = (str, ...args) =>
7
+ util.styleText(['yellow'], String.raw(str, ...args));
8
+ const L = (str, ...args) =>
9
+ util.styleText(['blackBright'], String.raw(str, ...args));
10
+ const P = (str, ...args) =>
11
+ util.styleText(['magenta'], String.raw(str, ...args));
12
+ const I = (str, ...args) =>
13
+ util.styleText(['italic', 'bold'], String.raw(str, ...args));
14
+
15
+ module.exports = { R, G, B, Y, L, P, I };
@@ -0,0 +1,147 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const _ = require('lodash');
5
+ const CopyPlugin = require('copy-webpack-plugin');
6
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
7
+ const GenerateContentAssetsPlugin = require('./generate-content-assets-plugin');
8
+ const PagefindPlugin = require('./pagefind-plugin');
9
+ const GenerateFaviconPlugin = require('./generate-favicon-plugin');
10
+ const GenerateManifestPlugin = require('./generate-manifest-plugin');
11
+ const GenerateFontsPlugin = require('./generate-fonts-plugin');
12
+ const { getDistDir, createDefinePlugin } = require('./util');
13
+ const { isFeatureEnabled } = require('./features');
14
+ const {
15
+ getContentDir,
16
+ getPackageDir,
17
+ getProjectDir,
18
+ getPublicDir,
19
+ } = require('./utils/paths');
20
+ const { parseHsl } = require('./utils/parse-hsl');
21
+
22
+ const distDir = getDistDir();
23
+
24
+ function renderThemeScss(siteVariables) {
25
+ const templatePath = path.join(getPackageDir(), 'templates/_theme.scss');
26
+ const template = fs.readFileSync(templatePath, 'utf-8');
27
+ const { hue, saturation, lightness } = parseHsl(siteVariables.themeColor);
28
+ const tintHue = siteVariables.tintHue ?? 20;
29
+ const tintAmount = siteVariables.tintAmount ?? 100;
30
+ const rendered = _.template(template)({
31
+ themeHue: hue,
32
+ themeSaturation: saturation,
33
+ themeLightness: lightness,
34
+ tintHue,
35
+ tintAmount,
36
+ });
37
+
38
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
39
+ const configDir = path.join(tmpDir, 'config');
40
+ fs.mkdirSync(configDir);
41
+ fs.writeFileSync(path.join(configDir, '_theme.scss'), rendered);
42
+
43
+ return tmpDir;
44
+ }
45
+
46
+ function createModuleRules(siteVariables) {
47
+ const packageDir = getPackageDir();
48
+ const themeDir = renderThemeScss(siteVariables);
49
+
50
+ return [
51
+ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader' } },
52
+ {
53
+ test: /\.tsx?$/,
54
+ exclude: /node_modules/,
55
+ loader: 'ts-loader',
56
+ options: { configFile: path.resolve(packageDir, 'tsconfig.json') },
57
+ },
58
+ {
59
+ test: /\.(sa|sc|c)ss$/,
60
+ use: [
61
+ MiniCssExtractPlugin.loader,
62
+ {
63
+ loader: 'css-loader',
64
+ options: {
65
+ url: {
66
+ // Don't bundle fonts (they are handled by GenerateFontsPlugin)
67
+ filter: url => !url.endsWith('.woff2'),
68
+ },
69
+ },
70
+ },
71
+ {
72
+ loader: 'sass-loader',
73
+ options: { sassOptions: { loadPaths: [themeDir, getProjectDir()] } },
74
+ },
75
+ ],
76
+ },
77
+ ];
78
+ }
79
+
80
+ async function createPlugins(
81
+ siteVariables,
82
+ { defineIsDev = false, plugins = [] } = {},
83
+ ) {
84
+ return [
85
+ new GenerateContentAssetsPlugin(siteVariables),
86
+ createDefinePlugin(siteVariables, defineIsDev),
87
+ new MiniCssExtractPlugin({
88
+ filename: '[name].css',
89
+ chunkFilename: '[id].css',
90
+ }),
91
+ new CopyPlugin({
92
+ patterns: [
93
+ { from: getPublicDir(), to: '.', noErrorOnMissing: true },
94
+ {
95
+ from: '**/*.{png,jpg,jpeg,gif,svg,txt,zip}',
96
+ context: getContentDir(),
97
+ to: '[path][name][ext]',
98
+ noErrorOnMissing: true,
99
+ },
100
+ ],
101
+ }),
102
+ isFeatureEnabled(siteVariables, 'search')
103
+ ? new PagefindPlugin(siteVariables)
104
+ : null,
105
+ isFeatureEnabled(siteVariables, 'favicon')
106
+ ? new GenerateFaviconPlugin(siteVariables)
107
+ : null,
108
+ isFeatureEnabled(siteVariables, 'favicon')
109
+ ? new GenerateManifestPlugin(siteVariables)
110
+ : null,
111
+ new GenerateFontsPlugin(),
112
+ ...plugins,
113
+ require('./print-flair-plugin'),
114
+ ].filter(Boolean);
115
+ }
116
+
117
+ async function createBaseConfig({
118
+ mode,
119
+ siteVariables,
120
+ entry,
121
+ devtool,
122
+ defineIsDev = false,
123
+ optimization,
124
+ plugins,
125
+ }) {
126
+ const packageDir = getPackageDir();
127
+ return {
128
+ mode,
129
+ entry,
130
+ output: {
131
+ path: distDir,
132
+ publicPath: siteVariables.basePath,
133
+ filename: '[name].bundle.js',
134
+ },
135
+ resolve: { extensions: ['.ts', '.js', '.json'] },
136
+ resolveLoader: {
137
+ modules: [path.resolve(packageDir, 'node_modules'), 'node_modules'],
138
+ },
139
+ devtool,
140
+ module: { rules: createModuleRules(siteVariables) },
141
+ ...(optimization && { optimization }),
142
+ plugins: await createPlugins(siteVariables, { defineIsDev, plugins }),
143
+ stats: 'errors-only',
144
+ };
145
+ }
146
+
147
+ module.exports = { createBaseConfig };
@@ -0,0 +1,23 @@
1
+ const path = require('path');
2
+ const ContentWatchPlugin = require('./content-watch-plugin');
3
+ const { createBaseConfig } = require('./config.base');
4
+ const { getDevSiteVariables } = require('./site-variables');
5
+ const { getPackageDir } = require('./utils/paths');
6
+
7
+ module.exports = async (env = {}) => {
8
+ const siteVariables = getDevSiteVariables();
9
+ const packageDir = getPackageDir();
10
+ const entry = { index: path.resolve(packageDir, 'src/index.ts') };
11
+ if (env.watchMode) {
12
+ entry.reload = path.resolve(packageDir, 'webpack/watch-reload-client.js');
13
+ }
14
+
15
+ return createBaseConfig({
16
+ mode: 'development',
17
+ devtool: 'inline-source-map',
18
+ siteVariables,
19
+ entry,
20
+ defineIsDev: true,
21
+ plugins: [new ContentWatchPlugin(siteVariables)],
22
+ });
23
+ };
@@ -0,0 +1,32 @@
1
+ const path = require('path');
2
+ const TerserPlugin = require('terser-webpack-plugin');
3
+ const { createBaseConfig } = require('./config.base');
4
+ const { compileTemplates } = require('./templates');
5
+ const { getProdSiteVariables } = require('./site-variables');
6
+ const { getPackageDir } = require('./utils/paths');
7
+
8
+ const siteVariables = getProdSiteVariables();
9
+
10
+ module.exports = async () => {
11
+ compileTemplates(siteVariables);
12
+
13
+ return createBaseConfig({
14
+ mode: 'production',
15
+ siteVariables,
16
+ entry: { index: path.resolve(getPackageDir(), 'src/index.ts') },
17
+ devtool: false,
18
+ optimization: {
19
+ minimizer: [
20
+ /*
21
+ * Bun's event loop doesn't track TerserPlugin's worker threads,
22
+ * causing a premature exit; set parallel to false to prevent worker
23
+ * threads from being used.
24
+ */
25
+ new TerserPlugin({
26
+ parallel: false,
27
+ terserOptions: { output: { comments: false } },
28
+ }),
29
+ ],
30
+ },
31
+ });
32
+ };
@@ -0,0 +1,153 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const {
4
+ getContentDir,
5
+ getContentFiles,
6
+ getBuildContentFiles,
7
+ } = require('./util');
8
+ const { setWatchState } = require('./build-state');
9
+ const {
10
+ compileTemplates,
11
+ getHtmlTemplatesDir,
12
+ getJsonDataDir,
13
+ JSON_DATA_FILES,
14
+ } = require('./templates');
15
+ const { getProjectDir } = require('./utils/paths');
16
+
17
+ let _needsRestart = false;
18
+
19
+ class ContentWatchPlugin {
20
+ constructor(siteVariables) {
21
+ this.siteVariables = siteVariables;
22
+ this.siteConfigPath = path.resolve(
23
+ getProjectDir(),
24
+ 'config',
25
+ 'site.dev.json',
26
+ );
27
+ }
28
+
29
+ apply(compiler) {
30
+ let lastSig = null;
31
+
32
+ compiler.hooks.make.tap('ContentWatchPlugin', compilation => {
33
+ const htmlTemplatesDir = getHtmlTemplatesDir();
34
+ const jsonDataDir = getJsonDataDir();
35
+
36
+ // Refresh templates cache from disk
37
+ try {
38
+ compileTemplates(this.siteVariables);
39
+ } catch (err) {
40
+ compilation.errors.push(err);
41
+ for (const fileName of fs.readdirSync(htmlTemplatesDir)) {
42
+ compilation.fileDependencies.add(
43
+ path.join(htmlTemplatesDir, fileName),
44
+ );
45
+ }
46
+ for (const dataFile of JSON_DATA_FILES) {
47
+ const dataPath = path.join(jsonDataDir, dataFile);
48
+ if (fs.existsSync(dataPath)) {
49
+ compilation.fileDependencies.add(dataPath);
50
+ }
51
+ }
52
+ return;
53
+ }
54
+
55
+ const contentDir = getContentDir();
56
+ const modifiedFiles = new Set(
57
+ [...(compiler.modifiedFiles || [])].map(filePath =>
58
+ path.resolve(filePath),
59
+ ),
60
+ );
61
+ const normalizedContentDir = path.resolve(contentDir) + path.sep;
62
+ const normalizedHtmlDir = path.resolve(htmlTemplatesDir) + path.sep;
63
+ const contentFiles = getContentFiles(
64
+ contentDir,
65
+ Object.keys(this.siteVariables.codeLanguages),
66
+ );
67
+ const buildContentFiles = getBuildContentFiles(
68
+ contentDir,
69
+ Object.keys(this.siteVariables.codeLanguages),
70
+ );
71
+
72
+ // Detect structural changes (files added or deleted) that require a
73
+ // fresh compiler with updated content asset caches
74
+ const sig = buildContentFiles.slice().sort().join('\0');
75
+ let structureChanged = false;
76
+ if (lastSig !== null && sig !== lastSig) {
77
+ _needsRestart = true;
78
+ structureChanged = true;
79
+ }
80
+ lastSig = sig;
81
+
82
+ const changedContentFiles = new Set(
83
+ [...modifiedFiles].filter(filePath =>
84
+ filePath.startsWith(normalizedContentDir),
85
+ ),
86
+ );
87
+
88
+ // Check if any HTML template or JSON data file changed
89
+ const jsonDataPaths = JSON_DATA_FILES.map(f =>
90
+ path.resolve(jsonDataDir, f),
91
+ );
92
+ const templatesChanged = [...modifiedFiles].some(
93
+ filePath =>
94
+ filePath === path.resolve(htmlTemplatesDir) ||
95
+ filePath.startsWith(normalizedHtmlDir) ||
96
+ jsonDataPaths.includes(filePath),
97
+ );
98
+
99
+ setWatchState(compiler, {
100
+ contentFiles,
101
+ buildContentFiles,
102
+ changedContentFiles,
103
+ templatesChanged,
104
+ structureChanged,
105
+ });
106
+
107
+ // Tell webpack to watch all content files
108
+ for (const filePath of contentFiles) {
109
+ compilation.fileDependencies.add(filePath);
110
+ }
111
+
112
+ // Watch content directories so webpack detects newly added files
113
+ function addDirs(dir) {
114
+ compilation.contextDependencies.add(dir);
115
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
116
+ if (entry.isDirectory()) {
117
+ addDirs(path.join(dir, entry.name));
118
+ }
119
+ }
120
+ }
121
+ addDirs(contentDir);
122
+
123
+ // Watch HTML template files from the package
124
+ for (const fileName of fs.readdirSync(htmlTemplatesDir)) {
125
+ compilation.fileDependencies.add(path.join(htmlTemplatesDir, fileName));
126
+ }
127
+
128
+ // Watch JSON data files from the project config
129
+ for (const dataFile of JSON_DATA_FILES) {
130
+ const dataPath = path.join(jsonDataDir, dataFile);
131
+ if (fs.existsSync(dataPath)) {
132
+ compilation.fileDependencies.add(dataPath);
133
+ }
134
+ }
135
+
136
+ // Watch the site config file and restart when it changes
137
+ compilation.fileDependencies.add(this.siteConfigPath);
138
+ const siteConfigChanged = modifiedFiles.has(
139
+ path.resolve(this.siteConfigPath),
140
+ );
141
+ if (siteConfigChanged) {
142
+ _needsRestart = true;
143
+ }
144
+ });
145
+ }
146
+ }
147
+
148
+ ContentWatchPlugin.needsRestart = () => _needsRestart;
149
+ ContentWatchPlugin.clearRestart = () => {
150
+ _needsRestart = false;
151
+ };
152
+
153
+ module.exports = ContentWatchPlugin;
@@ -0,0 +1,62 @@
1
+ const textToId = require('./text-to-id');
2
+
3
+ module.exports = function deflistIdPlugin(md) {
4
+ md.core.ruler.push('deflist_id_injector', function (state) {
5
+ const tokens = state.tokens;
6
+ const used = new Map();
7
+
8
+ function slugify(str) {
9
+ let slug = textToId(str);
10
+
11
+ if (!slug) {
12
+ slug = 'term';
13
+ }
14
+
15
+ if (used.has(slug)) {
16
+ const n = used.get(slug) + 1;
17
+ used.set(slug, n);
18
+ slug = `${slug}-${n}`;
19
+ } else {
20
+ used.set(slug, 1);
21
+ }
22
+
23
+ return slug;
24
+ }
25
+
26
+ for (let i = 0; i < tokens.length; i++) {
27
+ if (tokens[i].type !== 'dt_open') {
28
+ continue;
29
+ }
30
+
31
+ let termText = '';
32
+ for (let j = i + 1; j < tokens.length; j++) {
33
+ const t = tokens[j];
34
+
35
+ if (t.type === 'dt_close') {
36
+ break;
37
+ }
38
+
39
+ if (t.type === 'text') {
40
+ termText += t.content;
41
+ } else if (t.type === 'inline' && t.children) {
42
+ for (const child of t.children) {
43
+ if (child.type === 'text') {
44
+ termText += child.content;
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ if (!termText) {
51
+ continue;
52
+ }
53
+
54
+ const slug = slugify(termText);
55
+ const token = new state.Token('html_inline', '', 0);
56
+ token.content = `<a id="${slug}"></a>`;
57
+ tokens.splice(i + 1, 0, token);
58
+ // Skip token we just inserted
59
+ i++;
60
+ }
61
+ });
62
+ };