@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.
@@ -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
+ };