Last active 1759781151

KarelWintersky revised this gist 1759781150. Go to revision

2 files changed, 407 insertions

google-font-downloader.js(file created)

@@ -0,0 +1,322 @@
1 + #!/usr/bin/env node
2 +
3 + /*
4 + based on https://github.com/Bloggify/google-font-downloader
5 + with deepseek fixes
6 + */
7 +
8 + "use strict";
9 +
10 + const 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 +
18 + const 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 +
20 + new 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 + });

package.json(file created)

@@ -0,0 +1,85 @@
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 + }
Newer Older