google-font-downloader.js
· 15 KiB · JavaScript
Неформатований
#!/usr/bin/env node
/*
based on https://github.com/Bloggify/google-font-downloader
with deepseek fixes
*/
"use strict";
const Tilda = require("tilda")
, WritableStream = require("streamp").writable
, tinyreq = require("tinyreq")
, matchAll = require("match-all")
, path = require("path")
, crypto = require("crypto")
;
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"
new Tilda(`${__dirname}/package.json`, {
args: [
{
name: "url"
, desc: "The Google APIs url."
, required: true
}
],
examples: [
"google-font-downloader https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700,700i",
"google-font-downloader https://fonts.googleapis.com/css?family=Roboto:300,400,500 --debug"
]
})
.option([
{
opts: ["debug"]
, desc: "Save original downloaded CSS for debugging."
, name: "debug"
, type: Boolean
, default: false
},
{
opts: ["directory", "d"],
desc: "Directory where files are stored",
name: "directory",
default: "./fonts",
}, {
opts: ["timestamp", "t"],
desc: "Add a timestamp to the stylesheet file (default: 1)",
name: "timestamp",
default: 1,
}, {
opts: ["scss", "s"],
desc: "Use a scss-extension for the stylesheet for inclusion in a scss-project (default: 0)",
name: "scss",
default: 0,
}
])
.main(action => {
const url = action.args.url;
const debug = action.options.debug.value;
const data = {};
const font_directory = action.options.directory.value;
const timestamp = action.options.timestamp.value;
const scss = action.options.scss.value;
console.log(`Getting the external CSS: ${url}`);
if (debug) {
console.log("Debug mode: ON - original CSS will be saved")
}
// Функция для определения набора символов на основе комментария и unicode-range
function determineCharSet(comment, unicodeRange) {
// Сначала проверяем комментарий
const commentLower = comment.toLowerCase();
if (commentLower.includes('vietnamese')) return 'vietnamese';
if (commentLower.includes('cyrillic-ext')) return 'cyrillic-ext';
if (commentLower.includes('cyrillic')) return 'cyrillic';
if (commentLower.includes('greek-ext')) return 'greek-ext';
if (commentLower.includes('greek')) return 'greek';
if (commentLower.includes('latin-ext')) return 'latin-ext';
if (commentLower.includes('latin')) return 'latin';
if (commentLower.includes('arabic')) return 'arabic';
if (commentLower.includes('hebrew')) return 'hebrew';
if (commentLower.includes('thai')) return 'thai';
if (commentLower.includes('devanagari')) return 'devanagari';
if (commentLower.includes('bengali')) return 'bengali';
if (commentLower.includes('tamil')) return 'tamil';
if (commentLower.includes('telugu')) return 'telugu';
if (commentLower.includes('kannada')) return 'kannada';
if (commentLower.includes('malayalam')) return 'malayalam';
if (commentLower.includes('gujarati')) return 'gujarati';
if (commentLower.includes('oriya')) return 'oriya';
if (commentLower.includes('gurmukhi')) return 'gurmukhi';
// Если в комментарии нет информации, анализируем unicode-range
if (unicodeRange) {
if (unicodeRange.includes('U+0102-0103') || unicodeRange.includes('U+1EA0-1EF9')) return 'vietnamese';
if (unicodeRange.includes('U+0460-052F') || unicodeRange.includes('U+20B4') || unicodeRange.includes('U+2DE0-2DFF') || unicodeRange.includes('U+A640-A69F')) return 'cyrillic-ext';
if (unicodeRange.includes('U+0400-04FF') || unicodeRange.includes('U+0500-052F')) return 'cyrillic';
if (unicodeRange.includes('U+1F00-1FFF')) return 'greek-ext';
if (unicodeRange.includes('U+0370-03FF')) return 'greek';
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';
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';
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';
if (unicodeRange.includes('U+0590-05FF') || unicodeRange.includes('U+FB1D-FB4F')) return 'hebrew';
if (unicodeRange.includes('U+0E00-0E7F')) return 'thai';
}
return 'latin'; // fallback
}
// Функция для добавления src-original в CSS
function addSrcOriginal(css, fontUrl, localPath) {
// Ищем блок @font-face, который содержит этот URL
const fontFaceRegex = new RegExp(`(@font-face\\s*\\{[^}]*?url\\([^)]*?${fontUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^)]*?\\)[^}]*?\\})`, 'g');
return css.replace(fontFaceRegex, (fontFaceBlock) => {
// Добавляем src-original после src
return fontFaceBlock.replace(/(src:[^;]+;)/, `$1\n src-original: url(${fontUrl});`);
});
}
tinyreq({
url,
headers: {
"user-agent": USER_AGENT
}
}).then(body => {
const matchFontFilesRegex = /url\((https\:\/\/fonts\.gstatic\.com\/.*)\) format/gm
// Регулярное выражение для извлечения информации о шрифте из CSS
const fontBlockRegex = /\/\*\s*(.*?)\s*\*\/\s*@font-face\s*\{([^}]+)\}/g
const fontFamilyRegex = /font-family:\s*['"]?(.*?)['"]?;/i
const fontStyleRegex = /font-style:\s*(\w+);/i
const fontWeightRegex = /font-weight:\s*(\d+);/i
const unicodeRangeRegex = /unicode-range:\s*([^;]+);/i
data.original_stylesheet = body
data.local_stylesheet = body
data.font_urls = matchAll(body, matchFontFilesRegex).toArray()
// Сохраняем оригинальный CSS если включен debug режим
if (debug) {
const originalCssFileName = `google-fonts-original-${Date.now()}.css`
const originalCssStream = new WritableStream(originalCssFileName)
console.log(`Debug: Saving original CSS to ${originalCssFileName}`)
originalCssStream.end(data.original_stylesheet)
}
// Создаем массив для хранения информации о шрифтах
data.fonts = []
// Извлекаем информацию о каждом шрифте из CSS блоков @font-face
let fontBlockMatch
let fontUrlIndex = 0
while ((fontBlockMatch = fontBlockRegex.exec(body)) !== null) {
if (fontUrlIndex >= data.font_urls.length) break;
const comment = fontBlockMatch[1] || ""
const fontFaceContent = fontBlockMatch[2]
const familyMatch = fontFaceContent.match(fontFamilyRegex)
const styleMatch = fontFaceContent.match(fontStyleRegex)
const weightMatch = fontFaceContent.match(fontWeightRegex)
const unicodeMatch = fontFaceContent.match(unicodeRangeRegex)
if (familyMatch && data.font_urls[fontUrlIndex]) {
const fontFamily = familyMatch[1].replace(/\s+/g, '_').toLowerCase()
const fontStyle = (styleMatch && styleMatch[1]) ? styleMatch[1] : 'normal'
const fontWeight = (weightMatch && weightMatch[1]) ? weightMatch[1] : '400'
const unicodeRange = unicodeMatch ? unicodeMatch[1] : null
// Определяем набор символов используя улучшенную функцию
const charSet = determineCharSet(comment, unicodeRange)
// Формируем понятное имя файла в новом формате
const fileExtension = path.extname(data.font_urls[fontUrlIndex].split('?')[0]) || '.woff2'
const fileName = `${fontFamily}.${fontWeight}.${charSet}.${fontStyle}${fileExtension}`
const localPath = `fonts/${fileName}`
data.fonts.push({
remote: data.font_urls[fontUrlIndex],
local: localPath,
family: fontFamily,
weight: fontWeight,
style: fontStyle,
charSet: charSet,
comment: comment,
unicodeRange: unicodeRange
})
if (debug) {
console.log(`Debug: Font ${fontUrlIndex + 1} - Comment: "${comment}", CharSet: ${charSet}`)
}
fontUrlIndex++
}
}
// Если не удалось извлечь информацию через CSS блоки, используем альтернативный метод
if (data.fonts.length === 0) {
console.log("Using alternative font naming method...")
data.fonts = data.font_urls.map((url, index) => {
// Пытаемся извлесть информацию из URL
const urlParts = url.split('/')
const fontFileName = urlParts[urlParts.length - 1].split('?')[0]
// Разбираем имя файла на компоненты
const nameParts = fontFileName.replace('.woff2', '').split('-')
let fontFamily = 'font'
let fontWeight = '400'
let fontStyle = 'normal'
let charSet = 'latin'
if (nameParts.length >= 2) {
fontFamily = nameParts[0]
const styleWeight = nameParts[1]
// Пытаемся определить вес и стиль
if (styleWeight.includes('italic')) {
fontStyle = 'italic'
fontWeight = styleWeight.replace('italic', '') || '400'
} else {
fontWeight = styleWeight
}
// Пытаемся определить набор символов из имени файла
if (nameParts.some(part => part.includes('vietnamese'))) charSet = 'vietnamese'
else if (nameParts.some(part => part.includes('cyrillic'))) charSet = 'cyrillic'
else if (nameParts.some(part => part.includes('greek'))) charSet = 'greek'
else if (nameParts.some(part => part.includes('latin'))) charSet = 'latin'
}
const fileExtension = path.extname(url.split('?')[0]) || '.woff2'
const fileName = `${fontFamily}.${fontWeight}.${charSet}.${fontStyle}${fileExtension}`
const localPath = `${font_directory}/${fileName}`
return {
remote: url,
local: localPath,
family: fontFamily,
weight: fontWeight,
style: fontStyle,
charSet: charSet
}
})
}
console.log(`Detected ${data.fonts.length} font files to download.`)
// Проверяем уникальность имен файлов
const fileNames = new Set()
const duplicates = []
data.fonts.forEach(font => {
if (fileNames.has(font.local)) {
duplicates.push(font.local)
}
fileNames.add(font.local)
})
if (duplicates.length > 0) {
console.log(`Warning: Found ${duplicates.length} duplicate file names. Adding unique identifiers.`)
// Добавляем суффиксы к дублирующимся файлам
const nameCount = {}
data.fonts.forEach(font => {
if (nameCount[font.local]) {
nameCount[font.local]++
const newLocal = font.local.replace(/\.(woff2|woff|ttf)$/, `.${nameCount[font.local]}.$1`)
data.local_stylesheet = data.local_stylesheet.replace(font.local, newLocal)
font.local = newLocal
} else {
nameCount[font.local] = 1
}
})
}
return Promise.all(data.fonts.map(c => {
// Сначала добавляем src-original
data.local_stylesheet = addSrcOriginal(data.local_stylesheet, c.remote, c.local)
// Затем заменяем URL на локальный путь
data.local_stylesheet = data.local_stylesheet.replace(c.remote, c.local)
return new Promise(res => {
const req = tinyreq({ url: c.remote, encoding: null, headers: { "user-agent": USER_AGENT } })
, stream = new WritableStream(c.local)
req.on("data", data => {
stream.write(data)
}).on("error", e => {
console.error("Failed to download " + c.remote)
console.error(e)
res()
}).on("end", () => {
console.log(`Downloaded ${c.remote} as ${c.local}`)
stream.end()
res()
})
})
}))
}).then(() => {
// const ts = timestamp ? `-${Date.now()}` : '';
const fileName = `google-fonts${ timestamp ? `-${Date.now()}` : '' }.css`
, cssStream = new WritableStream(fileName)
console.log(`Writting the CSS into ${fileName}`)
cssStream.end(data.local_stylesheet)
if (debug) {
console.log("Debug: Process completed. Original CSS and modified CSS have been saved.")
}
}).catch(error => {
console.error("Error during font download process:", error)
if (debug) {
console.log("Debug: Error occurred. Check the original CSS file for debugging.")
}
})
});
| 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 | }); |
| 323 |
package.json
· 2.3 KiB · JSON
Неформатований
{
"bin": {
"google-font-downloader": "bin/google-font-downloader.js"
},
"name": "google-font-downloader",
"description": "Download Google fonts by providing the url",
"keywords": [
"google",
"font",
"downloader",
"download",
"fonts",
"by",
"providing",
"the",
"url"
],
"license": "MIT",
"version": "1.0.6",
"main": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Bloggify <support@bloggify.org> (https://bloggify.org)",
"homepage": "https://github.com/Bloggify/google-font-downloader#readme",
"files": [
"bin/",
"app/",
"lib/",
"dist/",
"src/",
"scripts/",
"resources/",
"menu/",
"cli.js",
"index.js",
"bloggify.js",
"bloggify.json"
],
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/Bloggify/google-font-downloader.git"
},
"bugs": {
"url": "https://github.com/Bloggify/google-font-downloader/issues"
},
"dependencies": {
"match-all": "^1.2.4",
"streamp": "^2.2.8",
"tilda": "^4.4.13",
"tinyreq": "^3.4.0"
},
"blah": {
"h_img": "https://i.imgur.com/arpGZH6.png",
"description": [
{
"h4": "Usage"
},
{
"p": "You can use this tool to download Google Fonts for offline use, just by providing the Google APIs url."
},
{
"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."
},
{
"h4": "How it works"
},
{
"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:"
},
{
"ul": [
"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.",
"A directory structure looking like this: `fonts/<font-name>/<version>/<font-file>`"
]
},
{
"img": {
"title": "Example",
"source": "https://i.imgur.com/yGcOPKg.gif"
}
}
]
}
}
| 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 |