build-fonts.mjs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import fs from 'node:fs/promises'
  2. import { createWriteStream } from 'node:fs'
  3. import { basename, join } from 'node:path'
  4. import { tmpdir } from 'node:os'
  5. import { execSync } from 'node:child_process'
  6. import yauzl from 'yauzl'
  7. // brew install woff2
  8. const families = [
  9. {
  10. folder: 'dm-mono',
  11. url: 'https://github.com/googlefonts/dm-mono',
  12. fonts: [
  13. 'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Italic.ttf',
  14. 'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Medium.ttf',
  15. 'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-MediumItalic.ttf',
  16. 'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Regular.ttf',
  17. ],
  18. },
  19. {
  20. folder: 'font-awesome',
  21. url: 'https://fontawesome.com/v4/',
  22. archive: 'https://fontawesome.com/v4/assets/font-awesome-4.7.0.zip',
  23. fonts: ['font-awesome-4.7.0/fonts/fontawesome-webfont.woff2'],
  24. },
  25. {
  26. folder: 'lato',
  27. url: 'https://www.latofonts.com/',
  28. archive: 'https://www.latofonts.com/download/lato2oflweb-zip/',
  29. fonts: [
  30. 'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Bold.woff2',
  31. 'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-BoldItalic.woff2',
  32. 'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Italic.woff2',
  33. 'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Regular.woff2',
  34. ],
  35. },
  36. {
  37. folder: 'merriweather',
  38. url: 'https://github.com/SorkinType/Merriweather',
  39. fonts: [
  40. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Bold.woff2',
  41. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-BoldItalic.woff2',
  42. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Italic.woff2',
  43. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Regular.woff2',
  44. ],
  45. license:
  46. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/OFL.txt',
  47. },
  48. {
  49. folder: 'noto-sans',
  50. url: 'https://notofonts.github.io/#latin-greek-cyrillic',
  51. fonts: [
  52. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Italic.ttf',
  53. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Medium.ttf',
  54. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-MediumItalic.ttf',
  55. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Regular.ttf',
  56. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-SemiBold.ttf',
  57. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-SemiBoldItalic.ttf',
  58. ],
  59. license:
  60. 'https://raw.githubusercontent.com/notofonts/latin-greek-cyrillic/refs/heads/main/OFL.txt',
  61. },
  62. {
  63. folder: 'noto-serif',
  64. url: 'https://notofonts.github.io/#latin-greek-cyrillic',
  65. fonts: [
  66. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSerif/unhinted/slim-variable-ttf/NotoSerif%5Bwght%5D.ttf',
  67. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSerif/unhinted/slim-variable-ttf/NotoSerif-Italic%5Bwght%5D.ttf',
  68. ],
  69. license:
  70. 'https://raw.githubusercontent.com/notofonts/latin-greek-cyrillic/refs/heads/main/OFL.txt',
  71. },
  72. {
  73. folder: 'open-dyslexic-mono',
  74. url: 'https://github.com/antijingoist/opendyslexic',
  75. fonts: [
  76. 'https://github.com/antijingoist/open-dyslexic/blob/master/otf/OpenDyslexicMono-Regular.otf',
  77. ],
  78. license: 'https://github.com/antijingoist/opendyslexic/blob/master/OFL.txt',
  79. },
  80. {
  81. folder: 'open-sans',
  82. url: 'https://github.com/googlefonts/opensans',
  83. fonts: [
  84. 'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Bold.ttf',
  85. 'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Light.ttf',
  86. 'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Regular.ttf',
  87. 'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-SemiBold.ttf',
  88. ],
  89. license:
  90. 'https://raw.githubusercontent.com/googlefonts/opensans/refs/heads/main/OFL.txt',
  91. },
  92. {
  93. folder: 'source-code-pro',
  94. url: 'https://github.com/adobe-fonts/source-code-pro',
  95. fonts: [
  96. 'https://github.com/adobe-fonts/source-code-pro/raw/refs/heads/release/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2',
  97. ],
  98. license:
  99. 'https://raw.githubusercontent.com/adobe-fonts/source-code-pro/refs/heads/release/LICENSE.md',
  100. },
  101. {
  102. folder: 'STIXTwoMath',
  103. url: 'https://github.com/stipub/stixfonts',
  104. fonts: [
  105. 'https://github.com/stipub/stixfonts/raw/refs/heads/master/fonts/static_otf_woff2/STIXTwoMath-Regular.woff2',
  106. ],
  107. license:
  108. 'https://raw.githubusercontent.com/stipub/stixfonts/refs/heads/master/OFL.txt',
  109. },
  110. ]
  111. const fetchFile = async (url, path) => {
  112. console.log(`${url}\n${path}`)
  113. const response = await fetch(url)
  114. if (!response.ok) {
  115. throw new Error(response.statusText)
  116. }
  117. await fs.writeFile(path, response.body)
  118. }
  119. for (const family of families) {
  120. if (!family.folder) {
  121. throw new Error('Missing family information')
  122. }
  123. await fs.mkdir(family.folder, { recursive: true })
  124. const fonts = new Set(family.fonts)
  125. if (family.archive) {
  126. const dir = await fs.mkdtemp(join(tmpdir(), 'fonts-'))
  127. const filename = decodeURIComponent(basename(family.archive))
  128. const path = `${dir}/${filename}`
  129. await fetchFile(family.archive, path)
  130. await new Promise((resolve, reject) => {
  131. yauzl.open(path, { lazyEntries: true }, (err, zipfile) => {
  132. if (err) {
  133. reject(err)
  134. } else {
  135. zipfile.on('entry', entry => {
  136. if (fonts.has(entry.fileName)) {
  137. console.log(entry.fileName)
  138. zipfile.openReadStream(entry, (err, readStream) => {
  139. if (err) {
  140. reject(err)
  141. } else {
  142. const path = `${family.folder}/${basename(entry.fileName)}`
  143. const output = createWriteStream(path)
  144. readStream.on('end', async () => {
  145. output.close()
  146. if (path.endsWith('.ttf')) {
  147. execSync(`woff2_compress "${path}"`)
  148. await fs.unlink(path)
  149. }
  150. zipfile.readEntry()
  151. })
  152. readStream.pipe(output)
  153. }
  154. })
  155. } else {
  156. zipfile.readEntry()
  157. }
  158. })
  159. zipfile.on('error', reject)
  160. zipfile.on('end', resolve)
  161. zipfile.readEntry()
  162. }
  163. })
  164. })
  165. await fs.unlink(path)
  166. } else {
  167. for (const url of fonts) {
  168. const filename = decodeURIComponent(basename(url))
  169. const path = `${family.folder}/${filename}`
  170. await fetchFile(url, path)
  171. if (path.endsWith('.ttf')) {
  172. execSync(`woff2_compress "${path}"`)
  173. await fs.unlink(path)
  174. }
  175. }
  176. }
  177. if (family.license) {
  178. const url = family.license
  179. const filename = decodeURIComponent(basename(url))
  180. const path = `${family.folder}/${filename}`
  181. await fetchFile(url, path)
  182. }
  183. }