@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.
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/bin/tada.js +361 -0
- package/config/authors.json +1 -0
- package/config/nav.json +28 -0
- package/content/index.md +19 -0
- package/content/lectures/01/Pair.java.md +296 -0
- package/content/lectures/01/Rectangle.java +80 -0
- package/content/lectures/01/demo.py +9 -0
- package/content/lectures/01/index.md +39 -0
- package/content/lectures/01/lecture1.pdf +0 -0
- package/content/lectures/index.md +25 -0
- package/content/markdown.md +379 -0
- package/content/problem_sets/index.md +6 -0
- package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
- package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
- package/fonts/google-sans-code/LICENSE.txt +93 -0
- package/fonts/inter/InterVariable-Italic.ttf +0 -0
- package/fonts/inter/InterVariable.ttf +0 -0
- package/fonts/inter/LICENSE.txt +92 -0
- package/package.json +70 -0
- package/public/avatars/alex.jpg +0 -0
- package/public/test.txt +1 -0
- package/src/_mixins.scss +4 -0
- package/src/anchor/README.md +6 -0
- package/src/anchor/index.ts +34 -0
- package/src/anchor/style.scss +48 -0
- package/src/code/README.md +5 -0
- package/src/code/index.ts +113 -0
- package/src/code/style.scss +101 -0
- package/src/code.scss +54 -0
- package/src/header/README.md +8 -0
- package/src/header/index.ts +43 -0
- package/src/header/style.scss +228 -0
- package/src/index.ts +73 -0
- package/src/layout.scss +144 -0
- package/src/literate/style.scss +60 -0
- package/src/print/README.md +4 -0
- package/src/print/index.ts +32 -0
- package/src/print/style.scss +82 -0
- package/src/question/README.md +3 -0
- package/src/question/index.ts +25 -0
- package/src/question/style.scss +116 -0
- package/src/search/README.md +6 -0
- package/src/search/index.ts +574 -0
- package/src/search/style.scss +217 -0
- package/src/style.scss +815 -0
- package/src/timezone/index.test.ts +100 -0
- package/src/timezone/index.ts +298 -0
- package/src/timezone/style.scss +16 -0
- package/src/timezone/timezones.json +58 -0
- package/src/toc/README.md +3 -0
- package/src/toc/index.ts +322 -0
- package/src/toc/style.scss +203 -0
- package/src/top/README.md +4 -0
- package/src/top/index.ts +75 -0
- package/src/util.ts +122 -0
- package/templates/_author.html +27 -0
- package/templates/_bottom.html +3 -0
- package/templates/_download.html +1 -0
- package/templates/_heading.html +19 -0
- package/templates/_nav.html +18 -0
- package/templates/_theme.scss +97 -0
- package/templates/_top.html +87 -0
- package/templates/authors.schema.json +13 -0
- package/templates/code.html +31 -0
- package/templates/default.html +13 -0
- package/templates/literate.html +16 -0
- package/templates/nav.schema.json +27 -0
- package/tsconfig.json +15 -0
- package/types/dev.ts +3 -0
- package/types/sass.d.ts +1 -0
- package/types/site-variables.d.ts +16 -0
- package/webpack/apply-base-path-plugin.js +78 -0
- package/webpack/build-state.js +97 -0
- package/webpack/code.test.js +162 -0
- package/webpack/colors.js +15 -0
- package/webpack/config.base.js +147 -0
- package/webpack/config.dev.js +23 -0
- package/webpack/config.prod.js +32 -0
- package/webpack/content-watch-plugin.js +153 -0
- package/webpack/deflist-id-plugin.js +62 -0
- package/webpack/external-links-plugin.js +37 -0
- package/webpack/features.js +5 -0
- package/webpack/flair.json +1 -0
- package/webpack/generate-content-assets-plugin.js +308 -0
- package/webpack/generate-favicon-plugin.js +198 -0
- package/webpack/generate-fonts-plugin.js +69 -0
- package/webpack/generate-manifest-plugin.js +116 -0
- package/webpack/globals.js +74 -0
- package/webpack/heading-subtitle-plugin.js +80 -0
- package/webpack/json-schema.js +19 -0
- package/webpack/log.js +143 -0
- package/webpack/markdown-plugins.test.js +203 -0
- package/webpack/pagefind-plugin.js +379 -0
- package/webpack/pagefind-plugin.test.js +131 -0
- package/webpack/pdf-text.js +163 -0
- package/webpack/print-flair-plugin.js +22 -0
- package/webpack/reachability.js +273 -0
- package/webpack/reachability.test.js +80 -0
- package/webpack/serve.js +104 -0
- package/webpack/site-variables.js +53 -0
- package/webpack/site.schema.json +67 -0
- package/webpack/templates.js +128 -0
- package/webpack/text-to-id.js +8 -0
- package/webpack/toc-plugin.js +167 -0
- package/webpack/util.js +49 -0
- package/webpack/utils/code.js +439 -0
- package/webpack/utils/content-files.js +147 -0
- package/webpack/utils/define-plugin.js +20 -0
- package/webpack/utils/file-types.js +26 -0
- package/webpack/utils/front-matter.js +57 -0
- package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
- package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
- package/webpack/utils/literate-java.js +153 -0
- package/webpack/utils/markdown.js +244 -0
- package/webpack/utils/parse-hsl.js +8 -0
- package/webpack/utils/paths.js +58 -0
- package/webpack/utils/render.js +466 -0
- package/webpack/utils/shiki-highlighter.js +26 -0
- package/webpack/validate-internal-links-plugin.js +155 -0
- package/webpack/watch-reachability-state.js +273 -0
- package/webpack/watch-reachability-state.test.js +198 -0
- package/webpack/watch-reload-client.js +54 -0
- package/webpack/watch.js +166 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { makeLogger } = require('./log');
|
|
2
|
+
|
|
3
|
+
const log = makeLogger(__filename);
|
|
4
|
+
|
|
5
|
+
module.exports = function externalLinks(md, siteVariables) {
|
|
6
|
+
function addClass(token) {
|
|
7
|
+
if (token.type === 'link_open') {
|
|
8
|
+
const href = token.attrGet('href');
|
|
9
|
+
|
|
10
|
+
if (href.match(/^https?:\/\/.*$/)) {
|
|
11
|
+
const url = new URL(href);
|
|
12
|
+
if (!siteVariables.internalDomains.includes(url.host)) {
|
|
13
|
+
const classAttr = token.attrGet('class');
|
|
14
|
+
let newClassAttr;
|
|
15
|
+
|
|
16
|
+
if (classAttr) {
|
|
17
|
+
newClassAttr = classAttr + ' external';
|
|
18
|
+
} else {
|
|
19
|
+
newClassAttr = 'external';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
log.debug`${href} -> "${newClassAttr}"`;
|
|
23
|
+
token.attrSet('class', newClassAttr);
|
|
24
|
+
|
|
25
|
+
log.debug`${href} -> target="_blank"`;
|
|
26
|
+
token.attrSet('target', '_blank');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
token.children?.map(addClass);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
md.core.ruler.push('external_links', state => {
|
|
35
|
+
state.tokens.map(addClass);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
["Tada", "Voilà", "Presto", "Boom", "Bam", "Ka-pow", "Shazam", "Eureka"]
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { RawSource } = require('webpack').sources;
|
|
3
|
+
const { makeLogger } = require('./log');
|
|
4
|
+
const { isFeatureEnabled } = require('./features');
|
|
5
|
+
const { initHighlighter } = require('./utils/shiki-highlighter');
|
|
6
|
+
const {
|
|
7
|
+
getBuildContentFiles,
|
|
8
|
+
getContentDir,
|
|
9
|
+
getValidInternalTargets,
|
|
10
|
+
renderCodePageAsset,
|
|
11
|
+
renderCopiedContentAsset,
|
|
12
|
+
renderLiterateJavaPageAsset,
|
|
13
|
+
renderPlainTextPageAsset,
|
|
14
|
+
} = require('./util');
|
|
15
|
+
const { isLiterateJava } = require('./utils/file-types');
|
|
16
|
+
const { getWatchState, setBuildDelta } = require('./build-state');
|
|
17
|
+
|
|
18
|
+
const log = makeLogger(__filename);
|
|
19
|
+
|
|
20
|
+
function normalizePaths(paths) {
|
|
21
|
+
return [...paths];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class GenerateContentAssetsPlugin {
|
|
25
|
+
constructor(siteVariables) {
|
|
26
|
+
this.siteVariables = siteVariables || {};
|
|
27
|
+
this.loggedCodeDisabled = false;
|
|
28
|
+
this.sourceFileCache = new Map();
|
|
29
|
+
this.lastBuildFiles = new Set();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getBuildContentFiles(compiler) {
|
|
33
|
+
const watchState = getWatchState(compiler);
|
|
34
|
+
if (watchState?.buildContentFiles?.size) {
|
|
35
|
+
return normalizePaths(watchState.buildContentFiles);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return getBuildContentFiles(
|
|
39
|
+
getContentDir(),
|
|
40
|
+
Object.keys(this.siteVariables.codeLanguages || {}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getDirtySourceFiles(compiler, buildContentFiles) {
|
|
45
|
+
const watchState = getWatchState(compiler);
|
|
46
|
+
const isWatch = !!compiler.watching;
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
!isWatch ||
|
|
50
|
+
this.lastBuildFiles.size === 0 ||
|
|
51
|
+
!watchState ||
|
|
52
|
+
watchState.templatesChanged
|
|
53
|
+
) {
|
|
54
|
+
return new Set(buildContentFiles);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const buildFileSet = new Set(buildContentFiles);
|
|
58
|
+
const dirtySourceFiles = new Set();
|
|
59
|
+
for (const filePath of watchState.changedContentFiles) {
|
|
60
|
+
if (buildFileSet.has(filePath)) {
|
|
61
|
+
dirtySourceFiles.add(filePath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return dirtySourceFiles;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isCopiedAssetSource(filePath) {
|
|
68
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
69
|
+
const codeExtensions = this.siteVariables.codeLanguages || {};
|
|
70
|
+
return (
|
|
71
|
+
ext === '.pdf' ||
|
|
72
|
+
Object.prototype.hasOwnProperty.call(codeExtensions, ext.slice(1))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getDirtyCopiedSourceFiles(compiler, buildContentFiles) {
|
|
77
|
+
const watchState = getWatchState(compiler);
|
|
78
|
+
const isWatch = !!compiler.watching;
|
|
79
|
+
|
|
80
|
+
if (!isWatch || this.lastBuildFiles.size === 0 || !watchState) {
|
|
81
|
+
return buildContentFiles.filter(filePath =>
|
|
82
|
+
this.isCopiedAssetSource(filePath),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const buildFileSet = new Set(buildContentFiles);
|
|
87
|
+
const dirtyCopiedSourceFiles = [];
|
|
88
|
+
for (const filePath of watchState.changedContentFiles) {
|
|
89
|
+
if (!buildFileSet.has(filePath) || !this.isCopiedAssetSource(filePath)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
dirtyCopiedSourceFiles.push(filePath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return dirtyCopiedSourceFiles;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
renderSourceAssets(filePath, contentDir, validInternalTargets, compilation) {
|
|
99
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
100
|
+
const assets = [];
|
|
101
|
+
|
|
102
|
+
if (isLiterateJava(filePath)) {
|
|
103
|
+
if (isFeatureEnabled(this.siteVariables, 'literateJava')) {
|
|
104
|
+
assets.push(
|
|
105
|
+
...renderLiterateJavaPageAsset({
|
|
106
|
+
filePath,
|
|
107
|
+
contentDir,
|
|
108
|
+
siteVariables: this.siteVariables,
|
|
109
|
+
compilation,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return assets;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (ext === '.html' || ext === '.md' || ext === '.markdown') {
|
|
117
|
+
assets.push(
|
|
118
|
+
...renderPlainTextPageAsset({
|
|
119
|
+
filePath,
|
|
120
|
+
contentDir,
|
|
121
|
+
siteVariables: this.siteVariables,
|
|
122
|
+
validInternalTargets,
|
|
123
|
+
compilation,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
return assets;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (ext === '.pdf') {
|
|
130
|
+
return assets;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const codeExtensions = this.siteVariables.codeLanguages || {};
|
|
134
|
+
if (ext.slice(1) in codeExtensions) {
|
|
135
|
+
if (isFeatureEnabled(this.siteVariables, 'code')) {
|
|
136
|
+
assets.push(
|
|
137
|
+
...renderCodePageAsset({
|
|
138
|
+
filePath,
|
|
139
|
+
contentDir,
|
|
140
|
+
siteVariables: this.siteVariables,
|
|
141
|
+
compilation,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
} else if (!this.loggedCodeDisabled) {
|
|
145
|
+
log.info`Not generating source code pages due to site.features.code = false`;
|
|
146
|
+
this.loggedCodeDisabled = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return assets;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
emitUncachedAssets(compilation, copiedSourceFiles, contentDir) {
|
|
154
|
+
for (const filePath of copiedSourceFiles) {
|
|
155
|
+
for (const asset of renderCopiedContentAsset({ filePath, contentDir })) {
|
|
156
|
+
const source = new RawSource(asset.content);
|
|
157
|
+
if (compilation.getAsset(asset.assetPath)) {
|
|
158
|
+
compilation.updateAsset(asset.assetPath, source);
|
|
159
|
+
} else {
|
|
160
|
+
compilation.emitAsset(asset.assetPath, source);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
updateSourceCache(
|
|
167
|
+
filePath,
|
|
168
|
+
assets,
|
|
169
|
+
changedHtmlAssetPaths,
|
|
170
|
+
removedHtmlAssetPaths,
|
|
171
|
+
) {
|
|
172
|
+
const previousAssets = this.sourceFileCache.get(filePath) || [];
|
|
173
|
+
const nextAssetPaths = new Set(assets.map(asset => asset.assetPath));
|
|
174
|
+
|
|
175
|
+
for (const asset of previousAssets) {
|
|
176
|
+
if (nextAssetPaths.has(asset.assetPath)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (asset.assetPath.endsWith('.html')) {
|
|
180
|
+
removedHtmlAssetPaths.add(asset.assetPath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const asset of assets) {
|
|
185
|
+
if (asset.assetPath.endsWith('.html')) {
|
|
186
|
+
changedHtmlAssetPaths.add(asset.assetPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.sourceFileCache.set(filePath, assets);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
emitCachedAssets(compilation, buildContentFiles) {
|
|
194
|
+
for (const filePath of buildContentFiles) {
|
|
195
|
+
const assets = this.sourceFileCache.get(filePath) || [];
|
|
196
|
+
for (const asset of assets) {
|
|
197
|
+
const source = new RawSource(asset.content);
|
|
198
|
+
if (compilation.getAsset(asset.assetPath)) {
|
|
199
|
+
compilation.updateAsset(asset.assetPath, source);
|
|
200
|
+
} else {
|
|
201
|
+
compilation.emitAsset(asset.assetPath, source);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
pruneRemovedSources(buildFileSet, removedHtmlAssetPaths) {
|
|
208
|
+
for (const filePath of [...this.lastBuildFiles]) {
|
|
209
|
+
if (buildFileSet.has(filePath)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const previousAssets = this.sourceFileCache.get(filePath) || [];
|
|
214
|
+
for (const asset of previousAssets) {
|
|
215
|
+
if (asset.assetPath.endsWith('.html')) {
|
|
216
|
+
removedHtmlAssetPaths.add(asset.assetPath);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
this.sourceFileCache.delete(filePath);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
apply(compiler) {
|
|
224
|
+
const pluginName = 'GenerateContentAssetsPlugin';
|
|
225
|
+
|
|
226
|
+
compiler.hooks.beforeCompile.tapPromise(pluginName, async () => {
|
|
227
|
+
const langs = [
|
|
228
|
+
...new Set([
|
|
229
|
+
'plaintext',
|
|
230
|
+
'text',
|
|
231
|
+
...Object.values(this.siteVariables.codeLanguages || {}),
|
|
232
|
+
]),
|
|
233
|
+
];
|
|
234
|
+
await initHighlighter(langs);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
compiler.hooks.thisCompilation.tap(pluginName, compilation => {
|
|
238
|
+
compilation.hooks.processAssets.tap(
|
|
239
|
+
{
|
|
240
|
+
name: pluginName,
|
|
241
|
+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
|
|
242
|
+
},
|
|
243
|
+
() => {
|
|
244
|
+
const contentDir = getContentDir();
|
|
245
|
+
const buildContentFiles = this.getBuildContentFiles(compiler);
|
|
246
|
+
log.info`Processing ${buildContentFiles.length} content file(s)`;
|
|
247
|
+
const buildFileSet = new Set(buildContentFiles);
|
|
248
|
+
const dirtySourceFiles = this.getDirtySourceFiles(
|
|
249
|
+
compiler,
|
|
250
|
+
buildContentFiles,
|
|
251
|
+
);
|
|
252
|
+
const dirtyCopiedSourceFiles = this.getDirtyCopiedSourceFiles(
|
|
253
|
+
compiler,
|
|
254
|
+
buildContentFiles,
|
|
255
|
+
);
|
|
256
|
+
const changedHtmlAssetPaths = new Set();
|
|
257
|
+
const removedHtmlAssetPaths = new Set();
|
|
258
|
+
const validInternalTargets = getValidInternalTargets(
|
|
259
|
+
contentDir,
|
|
260
|
+
buildContentFiles,
|
|
261
|
+
Object.keys(this.siteVariables.codeLanguages || {}),
|
|
262
|
+
);
|
|
263
|
+
const watchState = getWatchState(compiler);
|
|
264
|
+
|
|
265
|
+
this.pruneRemovedSources(buildFileSet, removedHtmlAssetPaths);
|
|
266
|
+
|
|
267
|
+
for (const filePath of dirtySourceFiles) {
|
|
268
|
+
let assets;
|
|
269
|
+
try {
|
|
270
|
+
assets = this.renderSourceAssets(
|
|
271
|
+
filePath,
|
|
272
|
+
contentDir,
|
|
273
|
+
validInternalTargets,
|
|
274
|
+
compilation,
|
|
275
|
+
);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
compilation.errors.push(err);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
this.updateSourceCache(
|
|
281
|
+
filePath,
|
|
282
|
+
assets,
|
|
283
|
+
changedHtmlAssetPaths,
|
|
284
|
+
removedHtmlAssetPaths,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.emitCachedAssets(compilation, buildContentFiles);
|
|
289
|
+
this.emitUncachedAssets(
|
|
290
|
+
compilation,
|
|
291
|
+
dirtyCopiedSourceFiles,
|
|
292
|
+
contentDir,
|
|
293
|
+
);
|
|
294
|
+
this.lastBuildFiles = buildFileSet;
|
|
295
|
+
|
|
296
|
+
setBuildDelta(compiler, {
|
|
297
|
+
changedSourceFiles: dirtySourceFiles,
|
|
298
|
+
changedHtmlAssetPaths,
|
|
299
|
+
removedHtmlAssetPaths,
|
|
300
|
+
templatesChanged: Boolean(watchState?.templatesChanged),
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = GenerateContentAssetsPlugin;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fontkit = require('fontkit');
|
|
3
|
+
const sharp = require('sharp');
|
|
4
|
+
const { default: pngToIco } = require('png-to-ico');
|
|
5
|
+
const { makeLogger } = require('./log');
|
|
6
|
+
const { getPackageDir } = require('./utils/paths');
|
|
7
|
+
|
|
8
|
+
const log = makeLogger(__filename);
|
|
9
|
+
|
|
10
|
+
const FONT_PATH = path.join(
|
|
11
|
+
getPackageDir(),
|
|
12
|
+
'fonts',
|
|
13
|
+
'inter',
|
|
14
|
+
'InterVariable.ttf',
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
function createFaviconSvg(size, color, symbol, font, cssWeight) {
|
|
18
|
+
if (size < 10) {
|
|
19
|
+
throw new Error(`size is too small: ${size}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!symbol) {
|
|
23
|
+
throw new Error(`invalid symbol: ${symbol}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (symbol.length > 5) {
|
|
27
|
+
throw new Error(`symbol is too long (must be <= 5 chars): ${symbol}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Apply variation axes for variable fonts
|
|
31
|
+
const instance =
|
|
32
|
+
typeof font.getVariation === 'function' &&
|
|
33
|
+
font.variationAxes &&
|
|
34
|
+
Object.keys(font.variationAxes).length > 0
|
|
35
|
+
? font.getVariation({ wght: cssWeight })
|
|
36
|
+
: font;
|
|
37
|
+
|
|
38
|
+
const run = instance.layout(symbol);
|
|
39
|
+
|
|
40
|
+
// Compute combined bounding box across all glyphs (font units, y-up)
|
|
41
|
+
let x = 0;
|
|
42
|
+
let totalMinX = Infinity,
|
|
43
|
+
totalMaxX = -Infinity;
|
|
44
|
+
let totalMinY = Infinity,
|
|
45
|
+
totalMaxY = -Infinity;
|
|
46
|
+
const advances = [];
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < run.glyphs.length; i++) {
|
|
49
|
+
const bbox = run.glyphs[i].bbox;
|
|
50
|
+
totalMinX = Math.min(totalMinX, x + bbox.minX);
|
|
51
|
+
totalMaxX = Math.max(totalMaxX, x + bbox.maxX);
|
|
52
|
+
totalMinY = Math.min(totalMinY, bbox.minY);
|
|
53
|
+
totalMaxY = Math.max(totalMaxY, bbox.maxY);
|
|
54
|
+
advances.push(x);
|
|
55
|
+
x += run.positions[i].xAdvance;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const textW = totalMaxX - totalMinX;
|
|
59
|
+
const textH = totalMaxY - totalMinY;
|
|
60
|
+
|
|
61
|
+
if (textW <= 0 || textH <= 0) {
|
|
62
|
+
throw new Error(`Could not get glyph bounds for symbol: ${symbol}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pad = size * 0.1;
|
|
66
|
+
const contentW = size - pad * 2;
|
|
67
|
+
const contentH = size - pad * 2;
|
|
68
|
+
|
|
69
|
+
// Scale font units to SVG pixels to fill padded content area
|
|
70
|
+
const s = Math.min(contentW / textW, contentH / textH);
|
|
71
|
+
|
|
72
|
+
// Font coords are y-up; SVG is y-down. The outer transform flips y via scale(s, -s).
|
|
73
|
+
// We want the bbox center to land at (size/2, size/2).
|
|
74
|
+
const bboxCenterX = (totalMinX + totalMaxX) / 2;
|
|
75
|
+
const bboxCenterY = (totalMinY + totalMaxY) / 2;
|
|
76
|
+
const tx = size / 2 - bboxCenterX * s;
|
|
77
|
+
const ty = size / 2 + bboxCenterY * s;
|
|
78
|
+
|
|
79
|
+
// Each glyph is translated by its cumulative x-advance (in font units).
|
|
80
|
+
// The outer group handles y-flip and centering.
|
|
81
|
+
const paths = run.glyphs
|
|
82
|
+
.map((glyph, i) => {
|
|
83
|
+
const d = glyph.path.toSVG();
|
|
84
|
+
return advances[i] === 0
|
|
85
|
+
? `<path d="${d}" fill="#fff" />`
|
|
86
|
+
: `<path d="${d}" transform="translate(${advances[i]} 0)" fill="#fff" />`;
|
|
87
|
+
})
|
|
88
|
+
.join('\n ');
|
|
89
|
+
|
|
90
|
+
return `<svg viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
91
|
+
<rect width="${size}" height="${size}" rx="${size / 10}" fill="${color}" />
|
|
92
|
+
<g transform="translate(${tx.toFixed(2)} ${ty.toFixed(2)}) scale(${s.toFixed(4)} ${(-s).toFixed(4)})">
|
|
93
|
+
${paths}
|
|
94
|
+
</g>
|
|
95
|
+
</svg>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class GenerateFaviconPlugin {
|
|
99
|
+
_cachedAssets = null;
|
|
100
|
+
|
|
101
|
+
constructor(siteVariables, options = {}) {
|
|
102
|
+
this.options = {
|
|
103
|
+
sizes: options.sizes || [16, 32, 48, 64, 128, 192, 256, 512, 1024],
|
|
104
|
+
svgSize: options.svgSize || 512,
|
|
105
|
+
filenameBase: options.filenameBase || 'favicon',
|
|
106
|
+
color: siteVariables.faviconColor,
|
|
107
|
+
symbol: siteVariables.faviconSymbol,
|
|
108
|
+
fontWeight: siteVariables.faviconFontWeight || 700,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
apply(compiler) {
|
|
113
|
+
compiler.hooks.thisCompilation.tap('GenerateFaviconPlugin', compilation => {
|
|
114
|
+
const wp = compilation.compiler.webpack || {};
|
|
115
|
+
const { RawSource } =
|
|
116
|
+
(wp.sources && wp.sources) || require('webpack-sources');
|
|
117
|
+
|
|
118
|
+
compilation.hooks.processAssets.tapPromise(
|
|
119
|
+
{
|
|
120
|
+
name: 'GenerateFaviconPlugin',
|
|
121
|
+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
|
|
122
|
+
},
|
|
123
|
+
async assets => {
|
|
124
|
+
if (this._cachedAssets) {
|
|
125
|
+
for (const [name, source] of this._cachedAssets) {
|
|
126
|
+
compilation.emitAsset(name, source);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { color, symbol, sizes, filenameBase, svgSize, fontWeight } =
|
|
132
|
+
this.options;
|
|
133
|
+
|
|
134
|
+
if (!color || !symbol) {
|
|
135
|
+
throw new Error('Missing required "color" and/or "symbol" options');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this._cachedAssets = new Map();
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
log.info`Generating favicons for "${symbol}"`;
|
|
142
|
+
log.debug`Using font file: ${FONT_PATH}`;
|
|
143
|
+
const font = fontkit.openSync(FONT_PATH);
|
|
144
|
+
|
|
145
|
+
const svgMarkup = createFaviconSvg(
|
|
146
|
+
svgSize,
|
|
147
|
+
color,
|
|
148
|
+
symbol,
|
|
149
|
+
font,
|
|
150
|
+
fontWeight,
|
|
151
|
+
);
|
|
152
|
+
const svgSource = new RawSource(svgMarkup);
|
|
153
|
+
compilation.emitAsset(`${filenameBase}.svg`, svgSource);
|
|
154
|
+
this._cachedAssets.set(`${filenameBase}.svg`, svgSource);
|
|
155
|
+
|
|
156
|
+
const pngBuffers = await Promise.all(
|
|
157
|
+
sizes.map(async size => {
|
|
158
|
+
const svgForSize = createFaviconSvg(
|
|
159
|
+
size,
|
|
160
|
+
color,
|
|
161
|
+
symbol,
|
|
162
|
+
font,
|
|
163
|
+
fontWeight,
|
|
164
|
+
);
|
|
165
|
+
const buf = await sharp(Buffer.from(svgForSize))
|
|
166
|
+
.png()
|
|
167
|
+
.toBuffer();
|
|
168
|
+
|
|
169
|
+
const pngName = `${filenameBase}-${size}.png`;
|
|
170
|
+
const pngSource = new RawSource(buf);
|
|
171
|
+
compilation.emitAsset(pngName, pngSource);
|
|
172
|
+
this._cachedAssets.set(pngName, pngSource);
|
|
173
|
+
return { size, buf };
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const icoBuffer = await pngToIco(
|
|
178
|
+
pngBuffers
|
|
179
|
+
.filter(b => b.size <= 256)
|
|
180
|
+
.sort((a, b) => a.size - b.size)
|
|
181
|
+
.map(x => x.buf),
|
|
182
|
+
);
|
|
183
|
+
const icoSource = new RawSource(icoBuffer);
|
|
184
|
+
compilation.emitAsset(`${filenameBase}.ico`, icoSource);
|
|
185
|
+
this._cachedAssets.set(`${filenameBase}.ico`, icoSource);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
this._cachedAssets = null;
|
|
188
|
+
compilation.errors.push(
|
|
189
|
+
new Error(`Error: ${err && err.message ? err.message : err}`),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = GenerateFaviconPlugin;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const wawoff2 = require('wawoff2');
|
|
4
|
+
const { getPackageDir } = require('./utils/paths');
|
|
5
|
+
const { makeLogger } = require('./log');
|
|
6
|
+
|
|
7
|
+
const log = makeLogger(__filename);
|
|
8
|
+
const FONTS_DIR = path.join(getPackageDir(), 'fonts');
|
|
9
|
+
|
|
10
|
+
class GenerateFontsPlugin {
|
|
11
|
+
_cachedAssets = null;
|
|
12
|
+
|
|
13
|
+
apply(compiler) {
|
|
14
|
+
compiler.hooks.thisCompilation.tap('GenerateFontsPlugin', compilation => {
|
|
15
|
+
const { RawSource } =
|
|
16
|
+
compilation.compiler.webpack?.sources || require('webpack-sources');
|
|
17
|
+
|
|
18
|
+
compilation.hooks.processAssets.tapPromise(
|
|
19
|
+
{
|
|
20
|
+
name: 'GenerateFontsPlugin',
|
|
21
|
+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
|
|
22
|
+
},
|
|
23
|
+
async () => {
|
|
24
|
+
if (this._cachedAssets) {
|
|
25
|
+
for (const [name, source] of this._cachedAssets) {
|
|
26
|
+
compilation.emitAsset(name, source);
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this._cachedAssets = new Map();
|
|
32
|
+
|
|
33
|
+
log.info`Bundling fonts from ${FONTS_DIR}`;
|
|
34
|
+
|
|
35
|
+
for (const family of fs.readdirSync(FONTS_DIR)) {
|
|
36
|
+
const familyDir = path.join(FONTS_DIR, family);
|
|
37
|
+
if (!fs.statSync(familyDir).isDirectory()) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const file of fs.readdirSync(familyDir)) {
|
|
42
|
+
const filePath = path.join(familyDir, file);
|
|
43
|
+
let source;
|
|
44
|
+
|
|
45
|
+
if (file.endsWith('.ttf')) {
|
|
46
|
+
const ttfBuf = fs.readFileSync(filePath);
|
|
47
|
+
const woff2Buf = Buffer.from(await wawoff2.compress(ttfBuf));
|
|
48
|
+
const outName = file.replace(/\.ttf$/, '.woff2');
|
|
49
|
+
const assetName = `${family}/${outName}`;
|
|
50
|
+
|
|
51
|
+
source = new RawSource(woff2Buf);
|
|
52
|
+
compilation.emitAsset(assetName, source);
|
|
53
|
+
this._cachedAssets.set(assetName, source);
|
|
54
|
+
log.debug`Converted ${family}/${file} to ${outName}`;
|
|
55
|
+
} else {
|
|
56
|
+
const assetName = `${family}/${file}`;
|
|
57
|
+
source = new RawSource(fs.readFileSync(filePath));
|
|
58
|
+
compilation.emitAsset(assetName, source);
|
|
59
|
+
this._cachedAssets.set(assetName, source);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = GenerateFontsPlugin;
|