最後活躍 1759781151

google-font-downloader.js 原始檔案
1#!/usr/bin/env node
2
3/*
4based on https://github.com/Bloggify/google-font-downloader
5with deepseek fixes
6*/
7
8"use strict";
9
10const Tilda = require("tilda")
11 , WritableStream = require("streamp").writable
12 , tinyreq = require("tinyreq")
13 , matchAll = require("match-all")
14 , path = require("path")
15 , crypto = require("crypto")
16 ;
17
18const USER_AGENT = "User-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
19
20new Tilda(`${__dirname}/package.json`, {
21 args: [
22 {
23 name: "url"
24 , desc: "The Google APIs url."
25 , required: true
26 }
27 ],
28 examples: [
29 "google-font-downloader https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700,700i",
30 "google-font-downloader https://fonts.googleapis.com/css?family=Roboto:300,400,500 --debug"
31 ]
32})
33.option([
34 {
35 opts: ["debug"]
36 , desc: "Save original downloaded CSS for debugging."
37 , name: "debug"
38 , type: Boolean
39 , default: false
40 },
41 {
42 opts: ["directory", "d"],
43 desc: "Directory where files are stored",
44 name: "directory",
45 default: "./fonts",
46 }, {
47 opts: ["timestamp", "t"],
48 desc: "Add a timestamp to the stylesheet file (default: 1)",
49 name: "timestamp",
50 default: 1,
51 }, {
52 opts: ["scss", "s"],
53 desc: "Use a scss-extension for the stylesheet for inclusion in a scss-project (default: 0)",
54 name: "scss",
55 default: 0,
56 }
57
58])
59.main(action => {
60 const url = action.args.url;
61 const debug = action.options.debug.value;
62 const data = {};
63
64 const font_directory = action.options.directory.value;
65 const timestamp = action.options.timestamp.value;
66 const scss = action.options.scss.value;
67
68 console.log(`Getting the external CSS: ${url}`);
69
70 if (debug) {
71 console.log("Debug mode: ON - original CSS will be saved")
72 }
73
74 // Функция для определения набора символов на основе комментария и unicode-range
75 function determineCharSet(comment, unicodeRange) {
76 // Сначала проверяем комментарий
77 const commentLower = comment.toLowerCase();
78
79 if (commentLower.includes('vietnamese')) return 'vietnamese';
80 if (commentLower.includes('cyrillic-ext')) return 'cyrillic-ext';
81 if (commentLower.includes('cyrillic')) return 'cyrillic';
82 if (commentLower.includes('greek-ext')) return 'greek-ext';
83 if (commentLower.includes('greek')) return 'greek';
84 if (commentLower.includes('latin-ext')) return 'latin-ext';
85 if (commentLower.includes('latin')) return 'latin';
86 if (commentLower.includes('arabic')) return 'arabic';
87 if (commentLower.includes('hebrew')) return 'hebrew';
88 if (commentLower.includes('thai')) return 'thai';
89 if (commentLower.includes('devanagari')) return 'devanagari';
90 if (commentLower.includes('bengali')) return 'bengali';
91 if (commentLower.includes('tamil')) return 'tamil';
92 if (commentLower.includes('telugu')) return 'telugu';
93 if (commentLower.includes('kannada')) return 'kannada';
94 if (commentLower.includes('malayalam')) return 'malayalam';
95 if (commentLower.includes('gujarati')) return 'gujarati';
96 if (commentLower.includes('oriya')) return 'oriya';
97 if (commentLower.includes('gurmukhi')) return 'gurmukhi';
98
99 // Если в комментарии нет информации, анализируем unicode-range
100 if (unicodeRange) {
101 if (unicodeRange.includes('U+0102-0103') || unicodeRange.includes('U+1EA0-1EF9')) return 'vietnamese';
102 if (unicodeRange.includes('U+0460-052F') || unicodeRange.includes('U+20B4') || unicodeRange.includes('U+2DE0-2DFF') || unicodeRange.includes('U+A640-A69F')) return 'cyrillic-ext';
103 if (unicodeRange.includes('U+0400-04FF') || unicodeRange.includes('U+0500-052F')) return 'cyrillic';
104 if (unicodeRange.includes('U+1F00-1FFF')) return 'greek-ext';
105 if (unicodeRange.includes('U+0370-03FF')) return 'greek';
106 if (unicodeRange.includes('U+0100-024F') || unicodeRange.includes('U+0259') || unicodeRange.includes('U+1E00-1EFF') || unicodeRange.includes('U+2020') || unicodeRange.includes('U+20A0-20AB') || unicodeRange.includes('U+20AD-20CF') || unicodeRange.includes('U+2113') || unicodeRange.includes('U+2C60-2C7F') || unicodeRange.includes('U+A720-A7FF')) return 'latin-ext';
107 if (unicodeRange.includes('U+0000-00FF') || unicodeRange.includes('U+0131') || unicodeRange.includes('U+0152-0153') || unicodeRange.includes('U+02BB-02BC') || unicodeRange.includes('U+02C6') || unicodeRange.includes('U+02DA') || unicodeRange.includes('U+02DC') || unicodeRange.includes('U+2000-206F') || unicodeRange.includes('U+2074') || unicodeRange.includes('U+20AC') || unicodeRange.includes('U+2122') || unicodeRange.includes('U+2191') || unicodeRange.includes('U+2193') || unicodeRange.includes('U+2212') || unicodeRange.includes('U+2215') || unicodeRange.includes('U+FEFF') || unicodeRange.includes('U+FFFD')) return 'latin';
108 if (unicodeRange.includes('U+0600-06FF') || unicodeRange.includes('U+0750-077F') || unicodeRange.includes('U+08A0-08FF') || unicodeRange.includes('U+FB50-FDFF') || unicodeRange.includes('U+FE70-FEFF') || unicodeRange.includes('U+1EE00-1EEFF')) return 'arabic';
109 if (unicodeRange.includes('U+0590-05FF') || unicodeRange.includes('U+FB1D-FB4F')) return 'hebrew';
110 if (unicodeRange.includes('U+0E00-0E7F')) return 'thai';
111 }
112
113 return 'latin'; // fallback
114 }
115
116 // Функция для добавления src-original в CSS
117 function addSrcOriginal(css, fontUrl, localPath) {
118 // Ищем блок @font-face, который содержит этот URL
119 const fontFaceRegex = new RegExp(`(@font-face\\s*\\{[^}]*?url\\([^)]*?${fontUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^)]*?\\)[^}]*?\\})`, 'g');
120
121 return css.replace(fontFaceRegex, (fontFaceBlock) => {
122 // Добавляем src-original после src
123 return fontFaceBlock.replace(/(src:[^;]+;)/, `$1\n src-original: url(${fontUrl});`);
124 });
125 }
126
127 tinyreq({
128 url,
129 headers: {
130 "user-agent": USER_AGENT
131 }
132 }).then(body => {
133 const matchFontFilesRegex = /url\((https\:\/\/fonts\.gstatic\.com\/.*)\) format/gm
134 // Регулярное выражение для извлечения информации о шрифте из CSS
135 const fontBlockRegex = /\/\*\s*(.*?)\s*\*\/\s*@font-face\s*\{([^}]+)\}/g
136 const fontFamilyRegex = /font-family:\s*['"]?(.*?)['"]?;/i
137 const fontStyleRegex = /font-style:\s*(\w+);/i
138 const fontWeightRegex = /font-weight:\s*(\d+);/i
139 const unicodeRangeRegex = /unicode-range:\s*([^;]+);/i
140
141 data.original_stylesheet = body
142 data.local_stylesheet = body
143 data.font_urls = matchAll(body, matchFontFilesRegex).toArray()
144
145 // Сохраняем оригинальный CSS если включен debug режим
146 if (debug) {
147 const originalCssFileName = `google-fonts-original-${Date.now()}.css`
148 const originalCssStream = new WritableStream(originalCssFileName)
149 console.log(`Debug: Saving original CSS to ${originalCssFileName}`)
150 originalCssStream.end(data.original_stylesheet)
151 }
152
153 // Создаем массив для хранения информации о шрифтах
154 data.fonts = []
155
156 // Извлекаем информацию о каждом шрифте из CSS блоков @font-face
157 let fontBlockMatch
158 let fontUrlIndex = 0
159
160 while ((fontBlockMatch = fontBlockRegex.exec(body)) !== null) {
161 if (fontUrlIndex >= data.font_urls.length) break;
162
163 const comment = fontBlockMatch[1] || ""
164 const fontFaceContent = fontBlockMatch[2]
165
166 const familyMatch = fontFaceContent.match(fontFamilyRegex)
167 const styleMatch = fontFaceContent.match(fontStyleRegex)
168 const weightMatch = fontFaceContent.match(fontWeightRegex)
169 const unicodeMatch = fontFaceContent.match(unicodeRangeRegex)
170
171 if (familyMatch && data.font_urls[fontUrlIndex]) {
172 const fontFamily = familyMatch[1].replace(/\s+/g, '_').toLowerCase()
173 const fontStyle = (styleMatch && styleMatch[1]) ? styleMatch[1] : 'normal'
174 const fontWeight = (weightMatch && weightMatch[1]) ? weightMatch[1] : '400'
175 const unicodeRange = unicodeMatch ? unicodeMatch[1] : null
176
177 // Определяем набор символов используя улучшенную функцию
178 const charSet = determineCharSet(comment, unicodeRange)
179
180 // Формируем понятное имя файла в новом формате
181 const fileExtension = path.extname(data.font_urls[fontUrlIndex].split('?')[0]) || '.woff2'
182 const fileName = `${fontFamily}.${fontWeight}.${charSet}.${fontStyle}${fileExtension}`
183 const localPath = `fonts/${fileName}`
184
185 data.fonts.push({
186 remote: data.font_urls[fontUrlIndex],
187 local: localPath,
188 family: fontFamily,
189 weight: fontWeight,
190 style: fontStyle,
191 charSet: charSet,
192 comment: comment,
193 unicodeRange: unicodeRange
194 })
195
196 if (debug) {
197 console.log(`Debug: Font ${fontUrlIndex + 1} - Comment: "${comment}", CharSet: ${charSet}`)
198 }
199
200 fontUrlIndex++
201 }
202 }
203
204 // Если не удалось извлечь информацию через CSS блоки, используем альтернативный метод
205 if (data.fonts.length === 0) {
206 console.log("Using alternative font naming method...")
207 data.fonts = data.font_urls.map((url, index) => {
208 // Пытаемся извлесть информацию из URL
209 const urlParts = url.split('/')
210 const fontFileName = urlParts[urlParts.length - 1].split('?')[0]
211
212 // Разбираем имя файла на компоненты
213 const nameParts = fontFileName.replace('.woff2', '').split('-')
214 let fontFamily = 'font'
215 let fontWeight = '400'
216 let fontStyle = 'normal'
217 let charSet = 'latin'
218
219 if (nameParts.length >= 2) {
220 fontFamily = nameParts[0]
221 const styleWeight = nameParts[1]
222
223 // Пытаемся определить вес и стиль
224 if (styleWeight.includes('italic')) {
225 fontStyle = 'italic'
226 fontWeight = styleWeight.replace('italic', '') || '400'
227 } else {
228 fontWeight = styleWeight
229 }
230
231 // Пытаемся определить набор символов из имени файла
232 if (nameParts.some(part => part.includes('vietnamese'))) charSet = 'vietnamese'
233 else if (nameParts.some(part => part.includes('cyrillic'))) charSet = 'cyrillic'
234 else if (nameParts.some(part => part.includes('greek'))) charSet = 'greek'
235 else if (nameParts.some(part => part.includes('latin'))) charSet = 'latin'
236 }
237
238 const fileExtension = path.extname(url.split('?')[0]) || '.woff2'
239 const fileName = `${fontFamily}.${fontWeight}.${charSet}.${fontStyle}${fileExtension}`
240 const localPath = `${font_directory}/${fileName}`
241
242 return {
243 remote: url,
244 local: localPath,
245 family: fontFamily,
246 weight: fontWeight,
247 style: fontStyle,
248 charSet: charSet
249 }
250 })
251 }
252
253 console.log(`Detected ${data.fonts.length} font files to download.`)
254
255 // Проверяем уникальность имен файлов
256 const fileNames = new Set()
257 const duplicates = []
258
259 data.fonts.forEach(font => {
260 if (fileNames.has(font.local)) {
261 duplicates.push(font.local)
262 }
263 fileNames.add(font.local)
264 })
265
266 if (duplicates.length > 0) {
267 console.log(`Warning: Found ${duplicates.length} duplicate file names. Adding unique identifiers.`)
268 // Добавляем суффиксы к дублирующимся файлам
269 const nameCount = {}
270 data.fonts.forEach(font => {
271 if (nameCount[font.local]) {
272 nameCount[font.local]++
273 const newLocal = font.local.replace(/\.(woff2|woff|ttf)$/, `.${nameCount[font.local]}.$1`)
274 data.local_stylesheet = data.local_stylesheet.replace(font.local, newLocal)
275 font.local = newLocal
276 } else {
277 nameCount[font.local] = 1
278 }
279 })
280 }
281
282 return Promise.all(data.fonts.map(c => {
283 // Сначала добавляем src-original
284 data.local_stylesheet = addSrcOriginal(data.local_stylesheet, c.remote, c.local)
285 // Затем заменяем URL на локальный путь
286 data.local_stylesheet = data.local_stylesheet.replace(c.remote, c.local)
287
288 return new Promise(res => {
289 const req = tinyreq({ url: c.remote, encoding: null, headers: { "user-agent": USER_AGENT } })
290 , stream = new WritableStream(c.local)
291
292 req.on("data", data => {
293 stream.write(data)
294 }).on("error", e => {
295 console.error("Failed to download " + c.remote)
296 console.error(e)
297 res()
298 }).on("end", () => {
299 console.log(`Downloaded ${c.remote} as ${c.local}`)
300 stream.end()
301 res()
302 })
303 })
304 }))
305 }).then(() => {
306// const ts = timestamp ? `-${Date.now()}` : '';
307 const fileName = `google-fonts${ timestamp ? `-${Date.now()}` : '' }.css`
308 , cssStream = new WritableStream(fileName)
309
310 console.log(`Writting the CSS into ${fileName}`)
311 cssStream.end(data.local_stylesheet)
312
313 if (debug) {
314 console.log("Debug: Process completed. Original CSS and modified CSS have been saved.")
315 }
316 }).catch(error => {
317 console.error("Error during font download process:", error)
318 if (debug) {
319 console.log("Debug: Error occurred. Check the original CSS file for debugging.")
320 }
321 })
322});
323
package.json 原始檔案
1{
2 "bin": {
3 "google-font-downloader": "bin/google-font-downloader.js"
4 },
5 "name": "google-font-downloader",
6 "description": "Download Google fonts by providing the url",
7 "keywords": [
8 "google",
9 "font",
10 "downloader",
11 "download",
12 "fonts",
13 "by",
14 "providing",
15 "the",
16 "url"
17 ],
18 "license": "MIT",
19 "version": "1.0.6",
20 "main": "lib/index.js",
21 "scripts": {
22 "test": "echo \"Error: no test specified\" && exit 1"
23 },
24 "author": "Bloggify <support@bloggify.org> (https://bloggify.org)",
25 "homepage": "https://github.com/Bloggify/google-font-downloader#readme",
26 "files": [
27 "bin/",
28 "app/",
29 "lib/",
30 "dist/",
31 "src/",
32 "scripts/",
33 "resources/",
34 "menu/",
35 "cli.js",
36 "index.js",
37 "bloggify.js",
38 "bloggify.json"
39 ],
40 "repository": {
41 "type": "git",
42 "url": "git+ssh://git@github.com/Bloggify/google-font-downloader.git"
43 },
44 "bugs": {
45 "url": "https://github.com/Bloggify/google-font-downloader/issues"
46 },
47 "dependencies": {
48 "match-all": "^1.2.4",
49 "streamp": "^2.2.8",
50 "tilda": "^4.4.13",
51 "tinyreq": "^3.4.0"
52 },
53 "blah": {
54 "h_img": "https://i.imgur.com/arpGZH6.png",
55 "description": [
56 {
57 "h4": "Usage"
58 },
59 {
60 "p": "You can use this tool to download Google Fonts for offline use, just by providing the Google APIs url."
61 },
62 {
63 "p": ":bulb: **Note**: It's not clear yet if Google Fonts are EU GDPR compliant (see [this issue](https://github.com/google/fonts/issues/1495)). This may be a good reason to download the Google Fonts you use on your server."
64 },
65 {
66 "h4": "How it works"
67 },
68 {
69 "p": "You need to provide the url to the Google APIs endpoint (e.g. `https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700,700i`) and you will get the following files/directories in the current working directory:"
70 },
71 {
72 "ul": [
73 "A file named `google-fonts-<timestamp>.css`—this will contain the CSS snippets that you need to copy in your app. You may need to update the paths to the font files.",
74 "A directory structure looking like this: `fonts/<font-name>/<version>/<font-file>`"
75 ]
76 },
77 {
78 "img": {
79 "title": "Example",
80 "source": "https://i.imgur.com/yGcOPKg.gif"
81 }
82 }
83 ]
84 }
85}
86