@hibi_10000/grunt-webfont 2.0.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.
- package/License.md +24 -0
- package/Readme.md +695 -0
- package/bin/eotlitetool.py +474 -0
- package/bin/fontforge/generate.py +133 -0
- package/dist/engines/fontforge.js +84 -0
- package/dist/engines/fontforge.js.map +1 -0
- package/dist/engines/node.js +164 -0
- package/dist/engines/node.js.map +1 -0
- package/dist/package.js +105 -0
- package/dist/package.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/util/util.js +105 -0
- package/dist/util/util.js.map +1 -0
- package/dist/webfont.d.ts +8 -0
- package/dist/webfont.js +609 -0
- package/dist/webfont.js.map +1 -0
- package/package.json +86 -0
- package/tasks/engines/fontforge.ts +124 -0
- package/tasks/engines/node.ts +213 -0
- package/tasks/engines/nodedeps.d.ts +22 -0
- package/tasks/types.ts +169 -0
- package/tasks/util/util.ts +119 -0
- package/tasks/webfont.ts +833 -0
- package/templates/bem.css +56 -0
- package/templates/bem.json +4 -0
- package/templates/bootstrap.css +114 -0
- package/templates/bootstrap.json +4 -0
- package/templates/demo.html +97 -0
- package/tsconfig.json +31 -0
package/tasks/webfont.ts
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG to webfont converter for Grunt
|
|
3
|
+
*
|
|
4
|
+
* @requires ttfautohint
|
|
5
|
+
* @author Artem Sapegin (http://sapegin.me), Hibi_10000
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import { globSync } from 'glob';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import _ from 'lodash';
|
|
14
|
+
import ttf2woff2 from 'ttf2woff2';
|
|
15
|
+
|
|
16
|
+
import fontforge from './engines/fontforge.ts';
|
|
17
|
+
import node from './engines/node.ts';
|
|
18
|
+
import * as wf from './util/util.ts';
|
|
19
|
+
import type { Logger, CustomOutput, TemplateOptions, Config, Options, OptionsInternal, Context } from './types.ts';
|
|
20
|
+
|
|
21
|
+
import packageJson from '../package.json' with { type: "json" };
|
|
22
|
+
|
|
23
|
+
export default (grunt: IGrunt): void => {
|
|
24
|
+
grunt.registerMultiTask('webfont', 'Compile separate SVG files to webfont', function() {
|
|
25
|
+
/**
|
|
26
|
+
* Consola to Grunt logger adapter.
|
|
27
|
+
*/
|
|
28
|
+
const logger: Logger = {
|
|
29
|
+
log: {
|
|
30
|
+
warn: (...args) => {
|
|
31
|
+
grunt.log.warn.apply(null, args);
|
|
32
|
+
},
|
|
33
|
+
error: (...args) => {
|
|
34
|
+
grunt.warn.apply(null, args);
|
|
35
|
+
},
|
|
36
|
+
info: (...args) => {
|
|
37
|
+
grunt.log.writeln.apply(null, args);
|
|
38
|
+
},
|
|
39
|
+
verbose: (...args) => {
|
|
40
|
+
grunt.log.verbose.writeln.apply(null, args);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
fail: {
|
|
44
|
+
fatal: (...args) => {
|
|
45
|
+
grunt.fail.fatal.apply(null, args);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const allDone = this.async();
|
|
51
|
+
const params = this.data;
|
|
52
|
+
const options = this.options<Options>(undefined);
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* Check for `src` param on target config
|
|
56
|
+
*/
|
|
57
|
+
this.requiresConfig([this.name, this.target, 'src'].join('.'));
|
|
58
|
+
|
|
59
|
+
webfont(this.name, this.target, this.filesSrc, params, options, logger).finally(allDone);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const webfont = async (name: string, target: string, filesSrc: string[], params: Config, options: Options, logger?: Logger): Promise<void> => {
|
|
64
|
+
if (!logger) logger = wf.consolaLogger;
|
|
65
|
+
const md5 = crypto.createHash('md5');
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* Check for `dest` param on either target config or global options object
|
|
69
|
+
*/
|
|
70
|
+
if ((params.dest === undefined) && (options.dest === undefined)) {
|
|
71
|
+
logger.log.warn(`Required property ${name}.${target}.dest or ${name}.${target}.options.dest missing.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.skip) {
|
|
75
|
+
completeTask();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Source files
|
|
80
|
+
const files = filesSrc.filter(isSvgFile)
|
|
81
|
+
if (!files.length) {
|
|
82
|
+
logger.log.warn('Specified empty list of source SVG files.');
|
|
83
|
+
completeTask();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// path must be a string, see https://nodejs.org/api/path.html#path_path_extname_path
|
|
88
|
+
if (typeof options.template !== 'string') {
|
|
89
|
+
options.template = '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Options
|
|
93
|
+
let o: OptionsInternal = {
|
|
94
|
+
logger: logger,
|
|
95
|
+
fontBaseName: options.font || 'icons',
|
|
96
|
+
destCss: options.destCss || params.destCss || params.dest,
|
|
97
|
+
destScss: options.destScss || params.destScss || params.destCss || params.dest,
|
|
98
|
+
destSass: options.destSass || params.destSass || params.destCss || params.dest,
|
|
99
|
+
destLess: options.destLess || params.destLess || params.destCss || params.dest,
|
|
100
|
+
destStyl: options.destStyl || params.destStyl || params.destCss || params.dest,
|
|
101
|
+
dest: options.dest || params.dest,
|
|
102
|
+
relativeFontPath: options.relativeFontPath,
|
|
103
|
+
fontPathVariables: options.fontPathVariables || false,
|
|
104
|
+
addHashes: options.hashes !== false,
|
|
105
|
+
addLigatures: options.ligatures === true,
|
|
106
|
+
template: options.template,
|
|
107
|
+
syntax: options.syntax || 'bem',
|
|
108
|
+
templateOptions: options.templateOptions || {},
|
|
109
|
+
stylesheets: options.stylesheets || [options.stylesheet || path.extname(options.template).replace(/^\./, '') || 'css'],
|
|
110
|
+
htmlDemo: options.htmlDemo !== false,
|
|
111
|
+
htmlDemoTemplate: options.htmlDemoTemplate,
|
|
112
|
+
htmlDemoFilename: options.htmlDemoFilename,
|
|
113
|
+
styles: optionToArray(options.styles, 'font,icon'),
|
|
114
|
+
types: optionToArray(options.types, 'eot,woff,ttf'),
|
|
115
|
+
order: optionToArray(options.order, wf.fontFormats),
|
|
116
|
+
embed: options.embed === true ? ['woff'] : optionToArray(options.embed, false),
|
|
117
|
+
rename: options.rename || path.basename,
|
|
118
|
+
engine: options.engine || 'fontforge',
|
|
119
|
+
autoHint: options.autoHint !== false,
|
|
120
|
+
codepoints: options.codepoints,
|
|
121
|
+
codepointsFile: options.codepointsFile,
|
|
122
|
+
startCodepoint: options.startCodepoint || wf.UNICODE_PUA_START,
|
|
123
|
+
ie7: options.ie7 === true,
|
|
124
|
+
normalize: options.normalize === true,
|
|
125
|
+
optimize: options.optimize === false ? false : true,
|
|
126
|
+
round: options.round !== undefined ? options.round : 10e12,
|
|
127
|
+
fontHeight: options.fontHeight !== undefined ? options.fontHeight : 512,
|
|
128
|
+
descent: options.descent !== undefined ? options.descent : 64,
|
|
129
|
+
version: options.version !== undefined ? options.version : false,
|
|
130
|
+
cache: options.cache || path.join(import.meta.dirname, '..', '.cache'),
|
|
131
|
+
callback: options.callback,
|
|
132
|
+
customOutputs: options.customOutputs,
|
|
133
|
+
execMaxBuffer: options.execMaxBuffer || 1024 * 200,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
o = Object.assign<OptionsInternal, Partial<OptionsInternal>>(o, {
|
|
137
|
+
fontName: o.fontBaseName,
|
|
138
|
+
destCssPaths: {
|
|
139
|
+
css: o.destCss,
|
|
140
|
+
scss: o.destScss,
|
|
141
|
+
sass: o.destSass,
|
|
142
|
+
less: o.destLess,
|
|
143
|
+
styl: o.destStyl
|
|
144
|
+
},
|
|
145
|
+
relativeFontPath: o.relativeFontPath || path.relative(o.destCss, o.dest),
|
|
146
|
+
destHtml: options.destHtml || o.destCss,
|
|
147
|
+
fontfaceStyles: has(o.styles, 'font'),
|
|
148
|
+
baseStyles: has(o.styles, 'icon'),
|
|
149
|
+
extraStyles: has(o.styles, 'extra'),
|
|
150
|
+
files: files,
|
|
151
|
+
glyphs: [],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
o.hash = getHash();
|
|
155
|
+
o.fontFilename = template(options.fontFilename || o.fontBaseName, o);
|
|
156
|
+
o.fontFamilyName = template(options.fontFamilyName || o.fontBaseName, o);
|
|
157
|
+
|
|
158
|
+
// “Rename” files
|
|
159
|
+
o.glyphs = o.files.map((file) => {
|
|
160
|
+
return o.rename(file).replace(path.extname(file), '');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Check or generate codepoints
|
|
164
|
+
// @todo Codepoint can be a Unicode code or character.
|
|
165
|
+
let currentCodepoint = o.startCodepoint;
|
|
166
|
+
if (!o.codepoints) o.codepoints = {};
|
|
167
|
+
if (o.codepointsFile) o.codepoints = readCodepointsFromFile();
|
|
168
|
+
o.glyphs.forEach((name) => {
|
|
169
|
+
if (!o.codepoints[name]) {
|
|
170
|
+
o.codepoints[name] = getNextCodepoint();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
if (o.codepointsFile) saveCodepointsToFile();
|
|
174
|
+
|
|
175
|
+
// Check if we need to generate font
|
|
176
|
+
const previousHash = readHash(name, target);
|
|
177
|
+
logger.log.verbose('New hash:', o.hash, '- previous hash:', previousHash);
|
|
178
|
+
if (o.hash === previousHash) {
|
|
179
|
+
logger.log.verbose('Config and source files weren’t changed since last run, checking resulting files...');
|
|
180
|
+
let regenerationNeeded = false;
|
|
181
|
+
|
|
182
|
+
const generatedFiles = wf.generatedFontFiles(o);
|
|
183
|
+
if (!generatedFiles.length){
|
|
184
|
+
regenerationNeeded = true;
|
|
185
|
+
} else {
|
|
186
|
+
generatedFiles.push(getDemoFilePath());
|
|
187
|
+
o.stylesheets.forEach((stylesheet) => {
|
|
188
|
+
generatedFiles.push(getCssFilePath(stylesheet));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
regenerationNeeded = generatedFiles.some((filename) => {
|
|
192
|
+
if (!filename) return false;
|
|
193
|
+
if (!fs.existsSync(filename)) {
|
|
194
|
+
logger.log.verbose('File', filename, ' is missed.');
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (!regenerationNeeded) {
|
|
201
|
+
logger.log.info(`Font ${chalk.cyan(o.fontName)} wasn’t changed since last run.`);
|
|
202
|
+
completeTask();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Save new hash and run
|
|
208
|
+
saveHash(name, target, o.hash);
|
|
209
|
+
try {
|
|
210
|
+
createOutputDirs();
|
|
211
|
+
cleanOutputDir();
|
|
212
|
+
await generateFont();
|
|
213
|
+
generateWoff2Font();
|
|
214
|
+
generateStylesheets();
|
|
215
|
+
await generateDemoHtml();
|
|
216
|
+
generateCustomOutputs();
|
|
217
|
+
printDone();
|
|
218
|
+
} finally {
|
|
219
|
+
completeTask();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Call callback function if it was specified in the options.
|
|
224
|
+
*/
|
|
225
|
+
function completeTask(): void {
|
|
226
|
+
if (o && ((typeof o.callback) === 'function')) {
|
|
227
|
+
o.callback(o.fontName, o.types, o.glyphs, o.hash);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Calculate hash to flush browser cache.
|
|
233
|
+
* Hash is based on source SVG files contents, task options and grunt-webfont version.
|
|
234
|
+
*/
|
|
235
|
+
function getHash(): string {
|
|
236
|
+
// Source SVG files contents
|
|
237
|
+
o.files.forEach((file) => {
|
|
238
|
+
md5.update(fs.readFileSync(file, 'utf8'));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Options
|
|
242
|
+
md5.update(JSON.stringify(o));
|
|
243
|
+
|
|
244
|
+
// grunt-webfont version
|
|
245
|
+
md5.update(packageJson.version);
|
|
246
|
+
|
|
247
|
+
// Templates
|
|
248
|
+
if (o.template) {
|
|
249
|
+
md5.update(fs.readFileSync(o.template, 'utf8'));
|
|
250
|
+
}
|
|
251
|
+
if (o.htmlDemoTemplate) {
|
|
252
|
+
md5.update(fs.readFileSync(o.htmlDemoTemplate, 'utf8'));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return md5.digest('hex');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create output directory
|
|
260
|
+
*/
|
|
261
|
+
function createOutputDirs(): void {
|
|
262
|
+
o.stylesheets.forEach((stylesheet) => {
|
|
263
|
+
fs.mkdirSync(option(o.destCssPaths, stylesheet), { recursive: true });
|
|
264
|
+
});
|
|
265
|
+
fs.mkdirSync(o.dest, { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Clean output directory
|
|
270
|
+
*/
|
|
271
|
+
function cleanOutputDir(): void {
|
|
272
|
+
const htmlDemoFileMask = path.posix.join(o.destCss, `${o.fontBaseName}*.{css,html}`);
|
|
273
|
+
const files = globSync(htmlDemoFileMask).concat(wf.generatedFontFiles(o));
|
|
274
|
+
files.forEach(file => {
|
|
275
|
+
fs.unlinkSync(file);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Generate font using selected engine
|
|
281
|
+
*/
|
|
282
|
+
async function generateFont(): Promise<void> {
|
|
283
|
+
const f = o.engine === 'node' ? node : fontforge;
|
|
284
|
+
const result = await f(o);
|
|
285
|
+
if (result === false) {
|
|
286
|
+
// Font was not created, exit
|
|
287
|
+
completeTask();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (result) {
|
|
292
|
+
o = Object.assign(o, result);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Converts TTF font to WOFF2.
|
|
298
|
+
*/
|
|
299
|
+
function generateWoff2Font(): void {
|
|
300
|
+
if (!has(o.types, 'woff2')) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Read TTF font
|
|
305
|
+
const ttfFontPath = wf.getFontPath(o, 'ttf');
|
|
306
|
+
const ttfFont = fs.readFileSync(ttfFontPath);
|
|
307
|
+
|
|
308
|
+
// Remove TTF font if not needed
|
|
309
|
+
if (!has(o.types, 'ttf')) {
|
|
310
|
+
fs.unlinkSync(ttfFontPath);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Convert to WOFF2
|
|
314
|
+
const woffFont = ttf2woff2(ttfFont);
|
|
315
|
+
|
|
316
|
+
// Save
|
|
317
|
+
const woff2FontPath = wf.getFontPath(o, 'woff2');
|
|
318
|
+
fs.writeFileSync(woff2FontPath, woffFont);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Generate CSS
|
|
323
|
+
*/
|
|
324
|
+
function generateStylesheets(): void {
|
|
325
|
+
// Convert codepoints to array of strings
|
|
326
|
+
const codepoints: string[] = [];
|
|
327
|
+
o.glyphs.forEach((name) => {
|
|
328
|
+
codepoints.push(o.codepoints[name].toString(16));
|
|
329
|
+
});
|
|
330
|
+
//@ts-ignore
|
|
331
|
+
o.codepoints = codepoints;
|
|
332
|
+
|
|
333
|
+
// Prepage glyph names to use as CSS classes
|
|
334
|
+
o.glyphs = o.glyphs.map(classnameize);
|
|
335
|
+
|
|
336
|
+
o.stylesheets.sort((a, b) => {
|
|
337
|
+
return a === 'css' ? 1 : -1;
|
|
338
|
+
}).forEach(generateStylesheet);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Generate CSS
|
|
343
|
+
*
|
|
344
|
+
* @param stylesheet type: css, scss, ...
|
|
345
|
+
*/
|
|
346
|
+
function generateStylesheet(stylesheet: string): void {
|
|
347
|
+
o.relativeFontPath = normalizePath(o.relativeFontPath);
|
|
348
|
+
|
|
349
|
+
// Generate font URLs to use in @font-face
|
|
350
|
+
const fontSrcs: { 0: string[], 1: string[] } = { 0: [], 1: [] };
|
|
351
|
+
o.order.forEach((type: 'eot'|'woff2'|'woff'|'ttf'|'svg') => {
|
|
352
|
+
if (!has(o.types, type)) return;
|
|
353
|
+
const fontSrc1 = wf.fontsSrcsMap[type][0];
|
|
354
|
+
if (fontSrc1) fontSrcs[0].push(generateFontSrc(type, fontSrc1, stylesheet));
|
|
355
|
+
fontSrcs[1].push(generateFontSrc(type, wf.fontsSrcsMap[type][1], stylesheet));
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Convert urls to strings that could be used in CSS
|
|
359
|
+
const fontSrcSeparator = option(wf.fontSrcSeparators, stylesheet);
|
|
360
|
+
o.fontSrc1 = fontSrcs[0].join(fontSrcSeparator);
|
|
361
|
+
o.fontSrc2 = fontSrcs[1].join(fontSrcSeparator);
|
|
362
|
+
o.fontRawSrcs = [fontSrcs[0], fontSrcs[1]];
|
|
363
|
+
|
|
364
|
+
// Read JSON file corresponding to CSS template
|
|
365
|
+
const templateJson = readTemplate(o.template, o.syntax, '.json', true);
|
|
366
|
+
if (templateJson) o = Object.assign<OptionsInternal, Required<TemplateOptions>>(o, JSON.parse(templateJson.template));
|
|
367
|
+
|
|
368
|
+
// Now override values with templateOptions
|
|
369
|
+
if (o.templateOptions) o = Object.assign(o, o.templateOptions);
|
|
370
|
+
|
|
371
|
+
// Generate CSS
|
|
372
|
+
const ext = path.extname(o.template) || '.css'; // Use extension of o.template file if given, or default to .css
|
|
373
|
+
o.cssTemplate = readTemplate(o.template, o.syntax, ext);
|
|
374
|
+
const cssContext = Object.assign<OptionsInternal, Partial<Context>>(o, {
|
|
375
|
+
iconsStyles: true,
|
|
376
|
+
stylesheet: stylesheet,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
let css = renderTemplate(o.cssTemplate, cssContext);
|
|
380
|
+
|
|
381
|
+
// Fix CSS preprocessors comments: single line comments will be removed after compilation
|
|
382
|
+
if (has(['sass', 'scss', 'less', 'styl'], stylesheet)) {
|
|
383
|
+
css = css.replace(/\/\* *(.*?) *\*\//g, '// $1');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Save file
|
|
387
|
+
fs.writeFileSync(getCssFilePath(stylesheet), css);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Gets the codepoints from the set filepath in o.codepointsFile
|
|
392
|
+
*/
|
|
393
|
+
function readCodepointsFromFile(): { [key: string]: number } {
|
|
394
|
+
if (!o.codepointsFile) return {};
|
|
395
|
+
if (!fs.existsSync(o.codepointsFile)){
|
|
396
|
+
logger.log.verbose('Codepoints file not found');
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const buffer = fs.readFileSync(o.codepointsFile);
|
|
401
|
+
return JSON.parse(buffer.toString());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Saves the codespoints to the set file
|
|
406
|
+
*/
|
|
407
|
+
function saveCodepointsToFile(): void {
|
|
408
|
+
if (!o.codepointsFile) return;
|
|
409
|
+
const codepointsToString = JSON.stringify(o.codepoints, null, 4);
|
|
410
|
+
try {
|
|
411
|
+
fs.writeFileSync(o.codepointsFile, codepointsToString);
|
|
412
|
+
logger.log.verbose(`Codepoints saved to file "${o.codepointsFile}".`);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
logger.log.error(err.message);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/*
|
|
419
|
+
* Prepares base context for templates
|
|
420
|
+
*/
|
|
421
|
+
function prepareBaseTemplateContext(): Context {
|
|
422
|
+
const context = Object.assign<{}, Context>({}, o);
|
|
423
|
+
return context;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/*
|
|
427
|
+
* Makes custom extends necessary for use with preparing the template context
|
|
428
|
+
* object for the HTML demo.
|
|
429
|
+
*/
|
|
430
|
+
function prepareHtmlTemplateContext(): Context {
|
|
431
|
+
|
|
432
|
+
let context = prepareBaseTemplateContext();
|
|
433
|
+
|
|
434
|
+
let htmlStyles;
|
|
435
|
+
|
|
436
|
+
// Prepare relative font paths for injection into @font-face refs in HTML
|
|
437
|
+
const relativeRe = new RegExp(_.escapeRegExp(o.relativeFontPath).replace(/[=!:\/]/g, '\\$&'), 'g');
|
|
438
|
+
const htmlRelativeFontPath = normalizePath(path.relative(o.destHtml, o.dest));
|
|
439
|
+
const _fontSrc1 = o.fontSrc1.replace(relativeRe, htmlRelativeFontPath);
|
|
440
|
+
const _fontSrc2 = o.fontSrc2.replace(relativeRe, htmlRelativeFontPath);
|
|
441
|
+
|
|
442
|
+
context = Object.assign<Context, Partial<Context>>(context, {
|
|
443
|
+
fontSrc1: _fontSrc1,
|
|
444
|
+
fontSrc2: _fontSrc2,
|
|
445
|
+
fontfaceStyles: true,
|
|
446
|
+
baseStyles: true,
|
|
447
|
+
extraStyles: false,
|
|
448
|
+
iconsStyles: true,
|
|
449
|
+
stylesheet: 'css',
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Prepares CSS for injection into <style> tag at to of HTML
|
|
453
|
+
htmlStyles = renderTemplate(o.cssTemplate, context);
|
|
454
|
+
context = Object.assign<Context, Partial<Context>>(context, {
|
|
455
|
+
styles: htmlStyles,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return context;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Iterator function used as callback by looping construct below to
|
|
463
|
+
* render "custom output" via mini configuration objects specified in
|
|
464
|
+
* the array `options.customOutputs`.
|
|
465
|
+
*
|
|
466
|
+
* @param outputConfig
|
|
467
|
+
*/
|
|
468
|
+
function generateCustomOutput(outputConfig: CustomOutput): void {
|
|
469
|
+
|
|
470
|
+
// Accesses context
|
|
471
|
+
let context = prepareBaseTemplateContext();
|
|
472
|
+
context = Object.assign(context, outputConfig.context);
|
|
473
|
+
|
|
474
|
+
// Prepares config attributes related to template filepath
|
|
475
|
+
const templatePath = outputConfig.template;
|
|
476
|
+
const extension = path.extname(templatePath);
|
|
477
|
+
const syntax = outputConfig.syntax || '';
|
|
478
|
+
|
|
479
|
+
// Renders template with given context
|
|
480
|
+
const template = readTemplate(templatePath, syntax, extension);
|
|
481
|
+
const output = renderTemplate(template, context);
|
|
482
|
+
|
|
483
|
+
// Prepares config attributes related to destination filepath
|
|
484
|
+
const dest = outputConfig.dest || o.dest;
|
|
485
|
+
|
|
486
|
+
let filepath;
|
|
487
|
+
let destParent;
|
|
488
|
+
let destName;
|
|
489
|
+
|
|
490
|
+
if (path.extname(dest) === '') {
|
|
491
|
+
// If user specifies a directory, filename should be same as template
|
|
492
|
+
destParent = dest;
|
|
493
|
+
destName = path.basename(outputConfig.template);
|
|
494
|
+
filepath = path.join(dest, destName);
|
|
495
|
+
} else {
|
|
496
|
+
// If user specifies a file, that is our filepath
|
|
497
|
+
destParent = path.dirname(dest);
|
|
498
|
+
filepath = dest;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Ensure existence of parent directory and output to file as desired
|
|
502
|
+
fs.mkdirSync(destParent, { recursive: true });
|
|
503
|
+
fs.writeFileSync(filepath, output);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/*
|
|
507
|
+
* Iterates over entries in the `options.customOutputs` object and,
|
|
508
|
+
* on a config-by-config basis, generates the desired results.
|
|
509
|
+
*/
|
|
510
|
+
function generateCustomOutputs(): void {
|
|
511
|
+
if (!o.customOutputs || o.customOutputs.length < 1) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
o.customOutputs.forEach(generateCustomOutput);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Generate HTML demo page
|
|
520
|
+
*/
|
|
521
|
+
async function generateDemoHtml(): Promise<void> {
|
|
522
|
+
if (!o.htmlDemo) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const context = prepareHtmlTemplateContext();
|
|
527
|
+
|
|
528
|
+
// Generate HTML
|
|
529
|
+
const demoTemplate = readTemplate(o.htmlDemoTemplate, 'demo', '.html');
|
|
530
|
+
const demo = renderTemplate(demoTemplate, context);
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
await fs.promises.mkdir(getDemoPath(), { recursive: true });
|
|
534
|
+
} catch (err) {
|
|
535
|
+
if (err) {
|
|
536
|
+
logger.log.info(err);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// Save file
|
|
541
|
+
fs.writeFileSync(getDemoFilePath(), demo);
|
|
542
|
+
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Print log
|
|
547
|
+
*/
|
|
548
|
+
function printDone(): void {
|
|
549
|
+
logger.log.info(`Font ${chalk.cyan(o.fontName)} with ${o.glyphs.length} glyphs created.`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Helpers
|
|
555
|
+
*/
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Convert a string of comma separated words into an array
|
|
559
|
+
*
|
|
560
|
+
* @param val Input string
|
|
561
|
+
* @param defVal Default value
|
|
562
|
+
*/
|
|
563
|
+
function optionToArray(val: string | false, defVal: string | false): string[] {
|
|
564
|
+
if (val === undefined) {
|
|
565
|
+
val = defVal;
|
|
566
|
+
}
|
|
567
|
+
if (!val) {
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
if (typeof val !== 'string') {
|
|
571
|
+
return val;
|
|
572
|
+
}
|
|
573
|
+
return val.split(',').map((value) => value.trim());
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Check if a value exists in an array
|
|
578
|
+
*
|
|
579
|
+
* @param haystack Array to find the needle in
|
|
580
|
+
* @param needle Value to find
|
|
581
|
+
* @return Needle was found
|
|
582
|
+
*/
|
|
583
|
+
function has(haystack: string[] | string, needle: string): boolean {
|
|
584
|
+
return haystack.indexOf(needle) !== -1;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Return a specified option if it exists in an object or `_default` otherwise
|
|
589
|
+
*
|
|
590
|
+
* @param map Options object
|
|
591
|
+
* @param key Option to find in the object
|
|
592
|
+
*/
|
|
593
|
+
function option(map: { _default?: string, [key: string]: string }, key: string): string {
|
|
594
|
+
if (key in map) {
|
|
595
|
+
return map[key];
|
|
596
|
+
} else {
|
|
597
|
+
return map._default;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Find next unused codepoint.
|
|
603
|
+
*/
|
|
604
|
+
function getNextCodepoint(): number {
|
|
605
|
+
while (Object.values(o.codepoints).includes(currentCodepoint)) {
|
|
606
|
+
currentCodepoint++;
|
|
607
|
+
}
|
|
608
|
+
return currentCodepoint;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Check whether file is SVG or not
|
|
613
|
+
*
|
|
614
|
+
* @param filepath File path
|
|
615
|
+
*/
|
|
616
|
+
function isSvgFile(filepath: string): boolean {
|
|
617
|
+
return path.extname(filepath).toLowerCase() === '.svg';
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Convert font file to data:uri and remove source file
|
|
622
|
+
*
|
|
623
|
+
* @param fontFile Font file path
|
|
624
|
+
* @return Base64 encoded string
|
|
625
|
+
*/
|
|
626
|
+
function embedFont(fontFile: string): string {
|
|
627
|
+
// Convert to data:uri
|
|
628
|
+
const dataUri = fs.readFileSync(fontFile, 'base64');
|
|
629
|
+
const type = path.extname(fontFile).substring(1);
|
|
630
|
+
const fontUrl = `data:application/x-font-${type};charset=utf-8;base64,${dataUri}`;
|
|
631
|
+
|
|
632
|
+
// Remove font file
|
|
633
|
+
fs.unlinkSync(fontFile);
|
|
634
|
+
|
|
635
|
+
return fontUrl;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Append a slash to end of a filepath if it not exists and make all slashes forward
|
|
640
|
+
*
|
|
641
|
+
* @param filepath File path
|
|
642
|
+
*/
|
|
643
|
+
function normalizePath(filepath: string): string {
|
|
644
|
+
if (!filepath.length) return filepath;
|
|
645
|
+
|
|
646
|
+
// Make all slashes forward
|
|
647
|
+
filepath = filepath.replace(/\\/g, '/');
|
|
648
|
+
|
|
649
|
+
// Make sure path ends with a slash
|
|
650
|
+
if (!filepath.endsWith('/')) {
|
|
651
|
+
filepath += '/';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return filepath;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Generate URL for @font-face
|
|
659
|
+
*
|
|
660
|
+
* @param type Type of font
|
|
661
|
+
* @param font URL or Base64 string
|
|
662
|
+
* @param stylesheet type: css, scss, ...
|
|
663
|
+
*/
|
|
664
|
+
function generateFontSrc(type: string, font: { ext: string, format?: string, embeddable?: boolean }, stylesheet: string): string {
|
|
665
|
+
const filename = template(`${o.fontFilename}${font.ext}`, o);
|
|
666
|
+
let fontPathVariableName = `${o.fontFamilyName}-font-path`;
|
|
667
|
+
|
|
668
|
+
let url;
|
|
669
|
+
if (font.embeddable && has(o.embed, type)) {
|
|
670
|
+
url = embedFont(path.join(o.dest, filename));
|
|
671
|
+
} else {
|
|
672
|
+
if (o.fontPathVariables && stylesheet !== 'css') {
|
|
673
|
+
if (stylesheet === 'less') {
|
|
674
|
+
fontPathVariableName = `@${fontPathVariableName}`;
|
|
675
|
+
o.fontPathVariable = `${fontPathVariableName} : "${o.relativeFontPath}";`;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
fontPathVariableName = `$${fontPathVariableName}`;
|
|
679
|
+
o.fontPathVariable = `${fontPathVariableName} : "${o.relativeFontPath}" !default;`;
|
|
680
|
+
}
|
|
681
|
+
url = filename;
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
url = `${o.relativeFontPath}${filename}`;
|
|
685
|
+
}
|
|
686
|
+
if (o.addHashes) {
|
|
687
|
+
if (url.indexOf('#iefix') === -1) { // Do not add hashes for OldIE
|
|
688
|
+
// Put hash at the end of an URL or before #hash
|
|
689
|
+
url = url.replace(/(#|$)/, `?${o.hash}$1`);
|
|
690
|
+
} else {
|
|
691
|
+
url = url.replace(/(#|$)/, `${o.hash}$1`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let src = `url("${url}")`;
|
|
697
|
+
if (o.fontPathVariables && stylesheet !== 'css') {
|
|
698
|
+
if (stylesheet === 'less') {
|
|
699
|
+
src = `url("@{${fontPathVariableName.replace('@','')}}${url}")`;
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
src = `url(${fontPathVariableName} + "${url}")`;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (font.format) src += ` format("${font.format}")`;
|
|
707
|
+
|
|
708
|
+
return src;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Read the template file
|
|
713
|
+
*
|
|
714
|
+
* @param template Template file path
|
|
715
|
+
* @param syntax Syntax (bem, bootstrap, etc.)
|
|
716
|
+
* @param ext Extension of the template
|
|
717
|
+
* @return \{filename: 'Template filename', template: 'Template code'}
|
|
718
|
+
*/
|
|
719
|
+
function readTemplate(template: string, syntax: string, ext: string, optional?: boolean): { filename: string, template: string } {
|
|
720
|
+
const filename = template
|
|
721
|
+
? path.resolve(template.replace(path.extname(template), ext))
|
|
722
|
+
: path.join(import.meta.dirname, `../templates/${syntax}${ext}`)
|
|
723
|
+
;
|
|
724
|
+
if (fs.existsSync(filename)) {
|
|
725
|
+
return {
|
|
726
|
+
filename: filename,
|
|
727
|
+
template: fs.readFileSync(filename, 'utf8'),
|
|
728
|
+
};
|
|
729
|
+
} else if (!optional) {
|
|
730
|
+
logger.fail.fatal(`Cannot find template at path: ${filename}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Render template with error reporting
|
|
736
|
+
*
|
|
737
|
+
* @param template {filename: 'Template filename', template: 'Template code'}
|
|
738
|
+
* @param context Template context
|
|
739
|
+
*/
|
|
740
|
+
function renderTemplate(template: { filename: string, template: string }, context: Context): string {
|
|
741
|
+
try {
|
|
742
|
+
const func = _.template(template.template);
|
|
743
|
+
return func(context);
|
|
744
|
+
} catch (e) {
|
|
745
|
+
logger.fail.fatal(`Error while rendering template ${template.filename}: ${e.message}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Basic template function: replaces {variables}
|
|
751
|
+
*
|
|
752
|
+
* @param tmpl Template code
|
|
753
|
+
* @param context Values object
|
|
754
|
+
*/
|
|
755
|
+
function template(tmpl: string, context: OptionsInternal): string {
|
|
756
|
+
return tmpl.replace(/\{([^\}]+)\}/g, (m, key: keyof OptionsInternal) => {
|
|
757
|
+
return context[key] as string;
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Prepare string to use as CSS class name
|
|
763
|
+
*
|
|
764
|
+
* @param str
|
|
765
|
+
*/
|
|
766
|
+
function classnameize(str: string): string {
|
|
767
|
+
return str.trim().replace(/\s+/g, '-');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Return path of CSS file.
|
|
772
|
+
*
|
|
773
|
+
* @param stylesheet (css, scss, ...)
|
|
774
|
+
*/
|
|
775
|
+
function getCssFilePath(stylesheet: string): string {
|
|
776
|
+
const cssFilePrefix = option(wf.cssFilePrefixes, stylesheet);
|
|
777
|
+
return path.join(option(o.destCssPaths, stylesheet), `${cssFilePrefix}${o.fontBaseName}.${stylesheet}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Return path of HTML demo file or `null` if its generation was disabled.
|
|
782
|
+
*/
|
|
783
|
+
function getDemoFilePath(): string {
|
|
784
|
+
if (!o.htmlDemo) return null;
|
|
785
|
+
const name = o.htmlDemoFilename || o.fontBaseName;
|
|
786
|
+
return path.join(o.destHtml, `${name}.html`);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Return path of HTML demo file or `null` if feature was disabled
|
|
791
|
+
*/
|
|
792
|
+
function getDemoPath(): string {
|
|
793
|
+
if (!o.htmlDemo) return null;
|
|
794
|
+
return o.destHtml;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Save hash to cache file.
|
|
799
|
+
*
|
|
800
|
+
* @param name Task name (webfont).
|
|
801
|
+
* @param target Task target name.
|
|
802
|
+
* @param hash Hash.
|
|
803
|
+
*/
|
|
804
|
+
function saveHash(name: string, target: string, hash: string): void {
|
|
805
|
+
const filepath = getHashPath(name, target);
|
|
806
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
807
|
+
fs.writeFileSync(filepath, hash);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Read hash from cache file or `null` if file don’t exist.
|
|
812
|
+
*
|
|
813
|
+
* @param name Task name (webfont).
|
|
814
|
+
* @param target Task target name.
|
|
815
|
+
*/
|
|
816
|
+
function readHash(name: string, target: string): string {
|
|
817
|
+
const filepath = getHashPath(name, target);
|
|
818
|
+
if (fs.existsSync(filepath)) {
|
|
819
|
+
return fs.readFileSync(filepath, 'utf8');
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Return path to cache file.
|
|
826
|
+
*
|
|
827
|
+
* @param name Task name (webfont).
|
|
828
|
+
* @param target Task target name.
|
|
829
|
+
*/
|
|
830
|
+
function getHashPath(name: string, target: string): string {
|
|
831
|
+
return path.join(o.cache, name, target, 'hash');
|
|
832
|
+
}
|
|
833
|
+
};
|