font-range 1.0.1 → 1.0.3

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 (49) hide show
  1. package/README.md +5 -5
  2. package/build/main.js +74 -22
  3. package/package.json +19 -16
  4. package/.editorconfig +0 -15
  5. package/.eslintignore +0 -2
  6. package/.eslintrc.json +0 -35
  7. package/.gitattributes +0 -3
  8. package/.prettierrc +0 -12
  9. package/.travis.yml +0 -22
  10. package/.vim/coc-settings.json +0 -6
  11. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +0 -35
  12. package/.yarn/plugins/@yarnpkg/plugin-typescript.cjs +0 -9
  13. package/.yarn/sdks/eslint/bin/eslint.js +0 -20
  14. package/.yarn/sdks/eslint/lib/api.js +0 -20
  15. package/.yarn/sdks/eslint/package.json +0 -6
  16. package/.yarn/sdks/integrations.yml +0 -6
  17. package/.yarn/sdks/prettier/index.js +0 -20
  18. package/.yarn/sdks/prettier/package.json +0 -6
  19. package/.yarn/sdks/typescript/bin/tsc +0 -20
  20. package/.yarn/sdks/typescript/bin/tsserver +0 -20
  21. package/.yarn/sdks/typescript/lib/tsc.js +0 -20
  22. package/.yarn/sdks/typescript/lib/tsserver.js +0 -223
  23. package/.yarn/sdks/typescript/lib/tsserverlibrary.js +0 -223
  24. package/.yarn/sdks/typescript/lib/typescript.js +0 -20
  25. package/.yarn/sdks/typescript/package.json +0 -6
  26. package/__tests__/css_load.test.ts +0 -52
  27. package/__tests__/font/NotoSansKR-Local.css +0 -1081
  28. package/__tests__/font/NotoSansKR-Regular.otf +0 -0
  29. package/__tests__/font/OFL.txt +0 -91
  30. package/__tests__/font/subset_glyphs.txt +0 -1
  31. package/__tests__/main.test.ts +0 -267
  32. package/__tests__/preset.test.ts +0 -64
  33. package/__tests__/shared.ts +0 -25
  34. package/jest.config.js +0 -22
  35. package/requirements.txt +0 -10
  36. package/resource/Korean_0.png +0 -0
  37. package/resource/Korean_1.png +0 -0
  38. package/resource/Korean_Japanese.png +0 -0
  39. package/resource/News_Chinese.gif +0 -0
  40. package/resource/News_Japanese.gif +0 -0
  41. package/resource/News_Japanese.png +0 -0
  42. package/resource/News_Korean.gif +0 -0
  43. package/resource/Noto_0.png +0 -0
  44. package/resource/Noto_1.png +0 -0
  45. package/src/main.ts +0 -482
  46. package/src/types.d.ts +0 -3
  47. package/src/worker.ts +0 -12
  48. package/tsconfig.json +0 -32
  49. package/tsconfig.release.json +0 -13
package/src/main.ts DELETED
@@ -1,482 +0,0 @@
1
- import { join, parse } from "path";
2
- import { createReadStream, createWriteStream, existsSync } from "fs";
3
- import { mkdir } from "fs/promises";
4
-
5
- import Piscina from "piscina";
6
- import fetch, { Headers } from "@esm2cjs/node-fetch";
7
- import { parse as cssAST, ParseOptions, walk } from "css-tree";
8
- import type { Declaration } from "css-tree";
9
-
10
- import type WorkerFn from "./worker";
11
- import type { RequiredByValueExcept } from "./types";
12
-
13
- // == Worker ===================================================================
14
- type WorkerRT = ReturnType<typeof WorkerFn>;
15
-
16
- class Worker {
17
- private static instance: Piscina;
18
- private constructor() { }
19
-
20
- public static getInstance(): Piscina {
21
- if(!Worker.instance) {
22
- Worker.instance = new Piscina({
23
- filename: join(__dirname, "../", "build", "worker.js")
24
- });
25
- }
26
- return Worker.instance;
27
- }
28
- }
29
-
30
- // == Resouce Basics ===========================================================
31
- export const targets = {
32
- weston: "https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap",
33
- korean: "https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap",
34
- japanese: "https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap",
35
- chinese: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap",
36
- chinese_traditional: "https://fonts.googleapis.com/css2?family=Noto+Sans+TC&display=swap",
37
- };
38
-
39
- function getFontName(url: string) {
40
- const encodedURL = decodeURI(url);
41
- const urlObj = new URL(encodedURL);
42
- const urlParams = urlObj.searchParams;
43
- const fontName = urlParams.get("family");
44
- return fontName;
45
- }
46
-
47
- // https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
48
- function validURL(str: string) {
49
- const pattern = new RegExp("^(https?:\\/\\/)?"+ // protocol
50
- "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|"+ // domain name
51
- "((\\d{1,3}\\.){3}\\d{1,3}))"+ // OR ip (v4) address
52
- "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*"+ // port and path
53
- "(\\?[;&a-z\\d%_.~+=-]*)?"+ // query string
54
- "(\\#[-a-z\\d_]*)?$","i"); // fragment locator
55
- return !!pattern.test(str);
56
- }
57
-
58
- function getCSSPath(dirPath: string, url: string) {
59
- if (validURL(url)) {
60
- const fontName = getFontName(url);
61
- const cssPath = join(dirPath, fontName + ".css");
62
- return cssPath;
63
- }
64
-
65
- if (!existsSync(url)) {
66
- throw new Error("Not vaild URL or PATH: " + url);
67
- }
68
- return url;
69
- }
70
-
71
- // == CSS I/O ==================================================================
72
- async function saveCSS(path: string, url: string) {
73
- // Fake header
74
- const version = "109.0";
75
- const headers = new Headers({
76
- "Accept": "text/html,application/xhtml+xml,application/xml;",
77
- "User-Agent": `Mozilla/5.0 (Windows NT 10.0; rv:${ version }) Gecko/20100101 Firefox/${ version }`
78
- });
79
-
80
- const res = await fetch(url, {
81
- method: "GET",
82
- headers: headers
83
- });
84
- if(res.status !== 200) {
85
- throw new Error("Not vaild URL: " + url);
86
- }
87
-
88
- return new Promise<void>((resolve, reject) => {
89
- const fileStream = createWriteStream(path);
90
- res.body.pipe(fileStream);
91
- res.body.on("error", (err) => {
92
- console.log("File write Error.");
93
- reject(err);
94
- });
95
- res.body.on("close", () => {
96
- fileStream.close();
97
- });
98
- fileStream.on("close", () => {
99
- resolve();
100
- });
101
- });
102
- }
103
-
104
- async function readCSS(path: string) {
105
- return new Promise<string>((resolve, reject) => {
106
- const readData: (string | Buffer)[] = [];
107
- createReadStream(path)
108
- .on("data", (data) => {
109
- readData.push(data);
110
- })
111
- .on("end", () => {
112
- const css = readData.join("");
113
-
114
- resolve(css);
115
- })
116
- .on("error", reject);
117
- });
118
- }
119
-
120
- // == CSS Parse ================================================================
121
- const parseOptions: ParseOptions = {
122
- parseAtrulePrelude: false,
123
- parseRulePrelude: false,
124
- parseValue: false
125
- };
126
-
127
- async function loadAST(dirPath: string, url = targets.korean, parseOption = parseOptions) {
128
- const cssPath = getCSSPath(dirPath, url);
129
- if (!existsSync(dirPath)) {
130
- await mkdir(dirPath);
131
- }
132
- if (!existsSync(cssPath)) {
133
- await saveCSS(cssPath, url);
134
- }
135
-
136
- const css = await readCSS(cssPath);
137
- return cssAST(css, parseOption);
138
- }
139
-
140
- interface BlockI {
141
- src: string;
142
- unicodes: string;
143
- }
144
- function setBlock(block: BlockI, elem: Declaration, blockProp: keyof BlockI, elemProp: string) {
145
- if(elem.property === elemProp) {
146
- if(elem.value.type === "Raw") {
147
- block[blockProp] = elem.value.value;
148
- }
149
- }
150
- }
151
-
152
- export async function parseCSS(dirPath = "src", url = targets.korean) {
153
- const ast = await loadAST(dirPath, url);
154
- const parsed = [] as BlockI[];
155
-
156
- walk(ast, { visit: "Atrule", enter(node) {
157
- if(node.name === "font-face") {
158
- const block = {
159
- src: "",
160
- unicodes: ""
161
- };
162
- walk(node, { visit: "Declaration", enter(elem) {
163
- setBlock(block, elem, "src", "src");
164
- setBlock(block, elem, "unicodes", "unicode-range");
165
- }});
166
-
167
- if(block.src !== "" || block.unicodes !== "") {
168
- parsed.push(block);
169
- }
170
- }
171
- }});
172
- return parsed;
173
- }
174
-
175
- // == Options - Basics =========================================================
176
- export interface FontDefaultOptionI {
177
- saveDir: string;
178
- format: Format;
179
- nameFormat: string;
180
- logFormat: string;
181
- defaultArgs: string[];
182
- etcArgs: string[];
183
- }
184
- export interface FontRangeOptionI extends FontDefaultOptionI {
185
- fromCSS: "default" | "srcIndex" | "srcName";
186
- }
187
- export interface FontSubsetOptionI extends FontDefaultOptionI {
188
- textFile: string;
189
- text: string;
190
- }
191
- export interface FontPipeOptionI extends FontRangeOptionI, FontSubsetOptionI {
192
- cssFile: string;
193
- }
194
- type ArgOptionT<I> = FontDefaultOptionI["saveDir"] | Partial<I>;
195
- type FontRangeOptionT = ArgOptionT<FontRangeOptionI>;
196
- type FontSubsetOptionT = ArgOptionT<FontSubsetOptionI>;
197
- type FontPipeOptionT = Partial<FontPipeOptionI>;
198
- type ArgOptionsT = FontRangeOptionT | FontSubsetOptionT | FontPipeOptionT;
199
- type Format = "otf" | "ttf" | "woff2" | "woff" | "woff-zopfli";
200
-
201
- export const defaultArgs = [
202
- "--layout-features=*",
203
- "--glyph-names",
204
- "--symbol-cmap",
205
- "--legacy-cmap",
206
- "--notdef-glyph",
207
- "--notdef-outline",
208
- "--recommended-glyphs",
209
- "--name-legacy",
210
- "--drop-tables=",
211
- "--name-IDs=*",
212
- "--name-languages=*"
213
- ];
214
-
215
- function getDefaultOptions(): RequiredByValueExcept<FontDefaultOptionI, "saveDir"> {
216
- return {
217
- format: "woff2",
218
- nameFormat: "{NAME}_{INDEX}{EXT}",
219
- logFormat: "Convert {ORIGIN} -> {OUTPUT}",
220
- defaultArgs: defaultArgs,
221
- etcArgs: []
222
- };
223
- }
224
-
225
- // == Options - Get Info =======================================================
226
- function getFormat(format: Format) {
227
- switch(format) {
228
- case "otf": return "otf";
229
- case "ttf": return "ttf";
230
- case "woff2": return "woff2";
231
- case "woff": return "woff";
232
- case "woff-zopfli": return "woff";
233
- }
234
- }
235
-
236
- function formatOption(format: Format) {
237
- const formatName = getFormat(format);
238
-
239
- if(format === "otf" || format === "ttf") return [""];
240
- return [
241
- "--flavor=" + formatName,
242
- (format === "woff-zopfli") ? "--with-zopfli" : ""
243
- ];
244
- }
245
-
246
- function fileNameInit(nameFormat: string, fontName: string, fontExt: string) {
247
- return nameFormat
248
- .replace( "{NAME}", fontName)
249
- .replace( "{EXT}", fontExt );
250
- }
251
- function getFileName(initName: string, index?: number | string) {
252
- return initName
253
- .replace("{INDEX}",
254
- (typeof index === "number")
255
- ? index.toString()
256
- : (typeof index === "string")
257
- ? index
258
- : ""
259
- );
260
- }
261
- function getConsoleLog(logFormat: string, origin: string, output: string) {
262
- return logFormat
263
- .replace("{ORIGIN}", origin)
264
- .replace("{OUTPUT}", output);
265
- }
266
-
267
- function getOptionInfos(fontPath = "", fontOption?: ArgOptionsT, indexIndicate = "") {
268
- const options: FontPipeOptionT = Object.assign(
269
- { fromCSS: "default" } satisfies Partial<FontRangeOptionI>,
270
- getDefaultOptions(),
271
- typeof(fontOption) === "string"
272
- ? { saveDir: fontOption }
273
- : fontOption
274
- );
275
-
276
- const format = options.format;
277
- const pathInfo = parse(fontPath);
278
- const fontDir = pathInfo.dir;
279
- const fontBase = pathInfo.base;
280
- const fontName = pathInfo.name;
281
- const fontExt = "." + getFormat(format);
282
-
283
- const dirPath = Object.prototype.hasOwnProperty.call(options, "saveDir") ? options["saveDir"] : fontDir;
284
- const nameFormat = options.nameFormat;
285
- const logFormat = options.logFormat;
286
-
287
- const initName = fileNameInit(nameFormat, fontName, fontExt);
288
- const output = getFileName(initName, indexIndicate);
289
- const logMsg = getConsoleLog(logFormat, fontBase, output);
290
-
291
- const convertOption = formatOption(format);
292
- const defaultOption = options.defaultArgs;
293
- const etcOption = options.etcArgs;
294
- const baseOption = [
295
- ...convertOption,
296
- ...defaultOption,
297
- ...etcOption
298
- ].filter(option => option !== "");
299
-
300
- const worker = Worker.getInstance();
301
-
302
- return {
303
- dirPath,
304
- initName,
305
- logMsg,
306
-
307
- baseOption,
308
- worker,
309
-
310
- fromCSS: options.fromCSS
311
- };
312
- }
313
-
314
- // == Options - Others =========================================================
315
- function getSrcInfo(src: string) {
316
- const first = src.split(",").find((str) => {
317
- return str.indexOf("url(") === 0;
318
- });
319
- if(typeof first === "undefined") return {
320
- base: "",
321
- index: 0
322
- };
323
-
324
- const reStr = "url\\(";
325
- const reMdl = "(.+?)";
326
- const reEnd = "\\)";
327
- const quote = "(\\\\?['\"])?";
328
- const regex = new RegExp(
329
- reStr + quote + reMdl + quote + reEnd
330
- );
331
-
332
- const urlContent = first.match(regex)[2];
333
- const parsedURL = parse(urlContent);
334
- return {
335
- base: parsedURL.base,
336
- index: parseInt(
337
- parsedURL.name.split(".").pop() // google font index at latest
338
- )
339
- };
340
- }
341
-
342
- function getSaveOption(dirPath: string, initName: string, index?: number) {
343
- const fileName = getFileName(initName, index);
344
- return ("--output-file=" + join(dirPath, fileName));
345
- }
346
-
347
- function getSubsetOption(fontSubsetOption?: FontSubsetOptionT) {
348
- if(
349
- typeof fontSubsetOption !== "undefined" &&
350
- typeof fontSubsetOption !== "string"
351
- ) {
352
- if("textFile" in fontSubsetOption) {
353
- return ("--text-file=" + fontSubsetOption.textFile);
354
- }
355
- if("text" in fontSubsetOption) {
356
- return ("--text=" + fontSubsetOption.text );
357
- }
358
- }
359
- return "--glyphs=*";
360
- }
361
-
362
- // == Main =====================================================================
363
- export async function fontRange(fontPath = "", url = targets.korean, fontRangeOption?: FontRangeOptionT) {
364
- const {
365
- dirPath,
366
- initName,
367
- logMsg,
368
-
369
- baseOption,
370
- worker,
371
-
372
- fromCSS
373
- } = getOptionInfos(fontPath, fontRangeOption, "n");
374
-
375
- const ranges = await parseCSS(dirPath, url);
376
- const result = ranges.map(async ({src, unicodes}, i) => {
377
- const srcInfo = getSrcInfo(src);
378
- const saveOption = getSaveOption(
379
- dirPath,
380
- (fromCSS === "srcName" && srcInfo.base !== "")
381
- ? srcInfo.base
382
- : initName,
383
- (fromCSS === "srcIndex" && srcInfo.base !== "")
384
- ? srcInfo.index
385
- : i
386
- );
387
- const unicodeRanges = unicodes.split(", ").join(",");
388
- const unicodeOption = "--unicodes=" + unicodeRanges;
389
-
390
- const options = [fontPath, saveOption, unicodeOption, ...baseOption];
391
- const logInfo = (i === 0) ? logMsg : "";
392
- const result: WorkerRT = await worker.run({ options, log: logInfo});
393
- return result;
394
- });
395
-
396
- return await Promise.all(result);
397
- }
398
-
399
- export async function fontSubset(fontPath = "", fontSubsetOption?: FontSubsetOptionT) {
400
- const {
401
- dirPath,
402
- initName,
403
- logMsg,
404
-
405
- baseOption,
406
- worker
407
- } = getOptionInfos(fontPath, fontSubsetOption);
408
- if(!existsSync(dirPath)) await mkdir(dirPath);
409
-
410
- const subsetOption = getSubsetOption(fontSubsetOption);
411
- const saveOption = getSaveOption(dirPath, initName);
412
-
413
- const options = [fontPath, saveOption, subsetOption, ...baseOption];
414
- const result: WorkerRT = await worker.run({ options, log: logMsg});
415
- return result;
416
- }
417
-
418
- // == Pipeline =================================================================
419
- export interface FontPipeI {
420
- fontPath: string;
421
- option?: FontPipeOptionT;
422
- }
423
- function fontPipeExec(subsetTarget: FontPipeI) {
424
- const { fontPath, option } = subsetTarget;
425
-
426
- return ((typeof option !== "undefined") &&
427
- (typeof option.cssFile !== "undefined"))
428
- ? fontRange(fontPath, option.cssFile, option)
429
- : fontSubset(fontPath, option);
430
- }
431
-
432
- function shardNum(shardStr: string, content: string) {
433
- const num = Math.abs(parseInt(shardStr, 10));
434
-
435
- if(isNaN(num) || num <= 0) {
436
- throw new Error("<" + content + "> must be a positive number");
437
- }
438
- return num;
439
- }
440
- function getShardInfo(shardEnv: string) {
441
- const [indexStr, totalStr] = shardEnv.split("/");
442
- const index = shardNum(indexStr, "index");
443
- const total = shardNum(totalStr, "total");
444
-
445
- if(index > total) {
446
- throw new Error("<index> must be less then <total>")
447
- }
448
- return [index, total] as const;
449
- }
450
-
451
- interface ShardI {
452
- shard: string;
453
- shardFormat: string;
454
- }
455
- type ShardT = ShardI["shard"] | Partial<ShardI>
456
- export async function fontPipe(subsetList: FontPipeI[], shard?: ShardT) {
457
- const shardEnv = (typeof shard === "object" && typeof shard.shard === "string")
458
- ? shard.shard
459
- : (typeof shard === "object" || typeof shard === "undefined")
460
- ? process.env.SHARD || "1/1"
461
- : shard;
462
- const shardFormat = (typeof shard === "object" && typeof shard.shardFormat === "string")
463
- ? shard.shardFormat
464
- : "== {START}/{END} ==========";
465
-
466
- const [index, total] = getShardInfo(shardEnv);
467
- const shardSize = Math.ceil(subsetList.length / total);
468
- const shardStart = shardSize * (index - 1);
469
- const shardEnd = shardSize * index;
470
-
471
- if(shardEnv !== "1/1") {
472
- const shardMsg = shardFormat
473
- .replace("{START}", index.toString())
474
- .replace("{END}", total.toString());
475
- console.log(shardMsg);
476
- }
477
-
478
- const result = subsetList
479
- .slice(shardStart, shardEnd)
480
- .map(fontPipeExec);
481
- return await Promise.all(result);
482
- }
package/src/types.d.ts DELETED
@@ -1,3 +0,0 @@
1
- // https://stackoverflow.com/questions/52703321/make-some-properties-optional-in-a-typescript-type
2
- type Diff<T, U> = T extends U ? never : T;
3
- export type RequiredByValueExcept<T, TOptional extends keyof T> = Pick<T, Diff<keyof T, TOptional>> & Partial<T>;
package/src/worker.ts DELETED
@@ -1,12 +0,0 @@
1
- import { execaSync } from "@esm2cjs/execa";
2
-
3
- export interface SubsetI {
4
- options: string[],
5
- log?: string
6
- }
7
- export default function subset({options, log = ""}: SubsetI) {
8
- if(log !== "") {
9
- console.log(log);
10
- }
11
- return execaSync("pyftsubset", options);
12
- }
package/tsconfig.json DELETED
@@ -1,32 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es6",
4
- "lib": [
5
- "esnext", "dom", "dom.iterable", "scripthost"
6
- ],
7
- "module": "commonjs",
8
- "moduleResolution": "node",
9
- "esModuleInterop": true,
10
- "allowSyntheticDefaultImports": true,
11
- "allowJs": true,
12
- "importHelpers": true,
13
- "jsx": "react",
14
- "alwaysStrict": true,
15
- "sourceMap": true,
16
- "forceConsistentCasingInFileNames": true,
17
- "noFallthroughCasesInSwitch": true,
18
- "noImplicitReturns": true,
19
- "noUnusedLocals": true,
20
- "noUnusedParameters": true,
21
- "noImplicitAny": false,
22
- "noImplicitThis": false,
23
- "strictNullChecks": false
24
- },
25
- "include": [
26
- "src/**/*",
27
- "__tests__/**/*"
28
- ],
29
- "exclude": [
30
- "build/**/*"
31
- ]
32
- }
@@ -1,13 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "src",
5
- "outDir": "build",
6
- "removeComments": true,
7
- "declaration": true,
8
- "sourceMap": false
9
- },
10
- "include": [
11
- "src/**/*"
12
- ]
13
- }