retypeset-odyssey 0.1.0

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +168 -0
  3. package/astro.config.ts +18 -0
  4. package/default-config.yaml +136 -0
  5. package/discover-collections.ts +160 -0
  6. package/integration.ts +394 -0
  7. package/package.json +105 -0
  8. package/patches/@qwik.dev__partytown@0.11.2.patch +98 -0
  9. package/public/_redirects +819 -0
  10. package/public/feeds/atom-style.xsl +105 -0
  11. package/public/feeds/rss-style.xsl +105 -0
  12. package/public/fonts/EarlySummer-VF-Split/00785494587e3487ac63a0e7e4fa30f0.woff2 +0 -0
  13. package/public/fonts/EarlySummer-VF-Split/08e5d941a4c76fad7b68e7a937ebb21f.woff2 +0 -0
  14. package/public/fonts/EarlySummer-VF-Split/1268e5072156188d601f1eeb4473655d.woff2 +0 -0
  15. package/public/fonts/EarlySummer-VF-Split/12a385475353c815d7a5add53ee51e37.woff2 +0 -0
  16. package/public/fonts/EarlySummer-VF-Split/12b11ca08223c65a21fc731d59dcfc11.woff2 +0 -0
  17. package/public/fonts/EarlySummer-VF-Split/16d6676d3cb645c520ee6df8a1f89afd.woff2 +0 -0
  18. package/public/fonts/EarlySummer-VF-Split/2912a75ffef95e7a5ae9e2b2311ad61d.woff2 +0 -0
  19. package/public/fonts/EarlySummer-VF-Split/298d96ea561e419a4104bc9fc18499ce.woff2 +0 -0
  20. package/public/fonts/EarlySummer-VF-Split/2a2c71acc17ec39f6780835899e53096.woff2 +0 -0
  21. package/public/fonts/EarlySummer-VF-Split/2a7e2d0e59d3f638074c50fab39fdef1.woff2 +0 -0
  22. package/public/fonts/EarlySummer-VF-Split/36931fc4370e1670ed76af5d3feccba2.woff2 +0 -0
  23. package/public/fonts/EarlySummer-VF-Split/3a68fdc792e4a9e0399a04e32d0cc2e3.woff2 +0 -0
  24. package/public/fonts/EarlySummer-VF-Split/4054d6a4d6b37719b51e0f71da6e7cd9.woff2 +0 -0
  25. package/public/fonts/EarlySummer-VF-Split/429cb25f825c3cbde6bfac5b36ae9675.woff2 +0 -0
  26. package/public/fonts/EarlySummer-VF-Split/42a9efc11298368ecdc1b85ab46f0b4f.woff2 +0 -0
  27. package/public/fonts/EarlySummer-VF-Split/432018d2bdc9df92a7662056eb2b1261.woff2 +0 -0
  28. package/public/fonts/EarlySummer-VF-Split/44a6fb782f2a01560faa0f95248b60ef.woff2 +0 -0
  29. package/public/fonts/EarlySummer-VF-Split/45367b060e8ba0aa2507e6b91b86620b.woff2 +0 -0
  30. package/public/fonts/EarlySummer-VF-Split/571db7564bda7c1a93542881b8976f4b.woff2 +0 -0
  31. package/public/fonts/EarlySummer-VF-Split/58d55eeef4cf455e86a1142b1f3110d3.woff2 +0 -0
  32. package/public/fonts/EarlySummer-VF-Split/59ea41e77309160a0f63cdc76a010202.woff2 +0 -0
  33. package/public/fonts/EarlySummer-VF-Split/5d19d9174e568db4755981aa2e4ab380.woff2 +0 -0
  34. package/public/fonts/EarlySummer-VF-Split/5e811eb3b4175ee93d7ec000bf4631c2.woff2 +0 -0
  35. package/public/fonts/EarlySummer-VF-Split/6268e0cd5d66d6fe05b331f259e7b9e4.woff2 +0 -0
  36. package/public/fonts/EarlySummer-VF-Split/6549844aa3d833ca06a68a8e839db465.woff2 +0 -0
  37. package/public/fonts/EarlySummer-VF-Split/714b459658a7321ceeb1e1386ce165c2.woff2 +0 -0
  38. package/public/fonts/EarlySummer-VF-Split/7511d97a469915013683eae06cb21cd9.woff2 +0 -0
  39. package/public/fonts/EarlySummer-VF-Split/7784b4ebe543d13f62f6f6e05beb0b2e.woff2 +0 -0
  40. package/public/fonts/EarlySummer-VF-Split/77c9bea70b3c6ab24e1497d5468c825b.woff2 +0 -0
  41. package/public/fonts/EarlySummer-VF-Split/789ebea9e81df623e930b86de98fbfab.woff2 +0 -0
  42. package/public/fonts/EarlySummer-VF-Split/885bb7ab0717e8a47fc17f953adcdbf1.woff2 +0 -0
  43. package/public/fonts/EarlySummer-VF-Split/896c58aff69a9a857764cee0663bc56d.woff2 +0 -0
  44. package/public/fonts/EarlySummer-VF-Split/8fb6fc01c59d1e3ad1910b58dec7f5e7.woff2 +0 -0
  45. package/public/fonts/EarlySummer-VF-Split/95be5462b91b9a0458797cdc89d94cb5.woff2 +0 -0
  46. package/public/fonts/EarlySummer-VF-Split/9a5b2724f983ca0fc0d5ff8d10c41396.woff2 +0 -0
  47. package/public/fonts/EarlySummer-VF-Split/9ffe17f9c0e4cc4356cb3f08ffdb9c6d.woff2 +0 -0
  48. package/public/fonts/EarlySummer-VF-Split/EarlySummer-VF-Subset.woff2 +0 -0
  49. package/public/fonts/EarlySummer-VF-Split/EarlySummerSerif License.txt +91 -0
  50. package/public/fonts/EarlySummer-VF-Split/a097ef49be62cd2565aca45600e1e3ac.woff2 +0 -0
  51. package/public/fonts/EarlySummer-VF-Split/a17a1ae6063088e5b3a48c06b816929a.woff2 +0 -0
  52. package/public/fonts/EarlySummer-VF-Split/a83fdcfc5ecf2f6996704b0c02758689.woff2 +0 -0
  53. package/public/fonts/EarlySummer-VF-Split/a8cf15ff9b71e59407d8406866ff6f99.woff2 +0 -0
  54. package/public/fonts/EarlySummer-VF-Split/af530ed51dd519e4456f8a5e259e908b.woff2 +0 -0
  55. package/public/fonts/EarlySummer-VF-Split/b195a8924915deec4aa9c3ec777cc93f.woff2 +0 -0
  56. package/public/fonts/EarlySummer-VF-Split/b4b6bb5df9239dd67b52ca858fd2a506.woff2 +0 -0
  57. package/public/fonts/EarlySummer-VF-Split/b7592e1e027923f19e0e55dfdac69668.woff2 +0 -0
  58. package/public/fonts/EarlySummer-VF-Split/b965859f69d8ccceaf0e2d6292afbcfb.woff2 +0 -0
  59. package/public/fonts/EarlySummer-VF-Split/bbe9333f1ff242bd96ecb23ff9e723b1.woff2 +0 -0
  60. package/public/fonts/EarlySummer-VF-Split/be758580e295339ea98f0240b9869f24.woff2 +0 -0
  61. package/public/fonts/EarlySummer-VF-Split/c07099e1d025617f6d40966986e1941b.woff2 +0 -0
  62. package/public/fonts/EarlySummer-VF-Split/c1b593dda62fdeb7dde3af02016da282.woff2 +0 -0
  63. package/public/fonts/EarlySummer-VF-Split/c89f0335910a68a0958f2846108370e8.woff2 +0 -0
  64. package/public/fonts/EarlySummer-VF-Split/ca49aa409fdedd3f2f894cd20a16640a.woff2 +0 -0
  65. package/public/fonts/EarlySummer-VF-Split/ccd4a28d2f63797e0183c87792e20b75.woff2 +0 -0
  66. package/public/fonts/EarlySummer-VF-Split/d2718da923fce8e7ea229d65e306e92c.woff2 +0 -0
  67. package/public/fonts/EarlySummer-VF-Split/d893e9b307d96041e9cfcbd03761b9f4.woff2 +0 -0
  68. package/public/fonts/EarlySummer-VF-Split/dafaedaee41b75e21479d4ff324b6a34.woff2 +0 -0
  69. package/public/fonts/EarlySummer-VF-Split/db392af65f1867e5fd580eed2195df99.woff2 +0 -0
  70. package/public/fonts/EarlySummer-VF-Split/dc7c73a9e5577143ccd11e05ab55cb39.woff2 +0 -0
  71. package/public/fonts/EarlySummer-VF-Split/de396881189f747eba67685298363242.woff2 +0 -0
  72. package/public/fonts/EarlySummer-VF-Split/df625b213228bba22a7733d4eff8f148.woff2 +0 -0
  73. package/public/fonts/EarlySummer-VF-Split/e6e60b384f220b893ef31a926ece829a.woff2 +0 -0
  74. package/public/fonts/EarlySummer-VF-Split/e6e8ce2c5972ab665630bb705383d0fb.woff2 +0 -0
  75. package/public/fonts/EarlySummer-VF-Split/e963c7ed7104c2d6d68fcb5f952fe2f5.woff2 +0 -0
  76. package/public/fonts/EarlySummer-VF-Split/e966b23b4cd7783f43e31032d41784f4.woff2 +0 -0
  77. package/public/fonts/EarlySummer-VF-Split/edaac57c3856ec13128f4c6c3e00975c.woff2 +0 -0
  78. package/public/fonts/EarlySummer-VF-Split/ee54e0d86edf068c6c9cbddb76a856fe.woff2 +0 -0
  79. package/public/fonts/EarlySummer-VF-Split/f612c78a5544ff2dd3e8296ac3e58344.woff2 +0 -0
  80. package/public/fonts/EarlySummer-VF-Split/f9e539bd9b7bf999c3da82f5403ec3b6.woff2 +0 -0
  81. package/public/fonts/EarlySummer-VF-Split/fa5863b923ac15993c52a619f699ee63.woff2 +0 -0
  82. package/public/fonts/EarlySummer-VF-Split/fc759e56ec6f6e6d3d4cb163d62fb557.woff2 +0 -0
  83. package/public/fonts/Font Subset List/CJK Common Characters.txt +7534 -0
  84. package/public/fonts/Font Subset List/EarlySummer Subset.txt +3 -0
  85. package/public/fonts/Font Subset List/Japanese Kana + Korean Letters.txt +6123 -0
  86. package/public/fonts/Font Subset List/Latin + Cyrillic + Greek + Arabic Glyphs.txt +121 -0
  87. package/public/fonts/Font Subset List/unicode_range.py +49 -0
  88. package/public/fonts/NotoSansSC-Bold.otf +0 -0
  89. package/public/fonts/NotoSansSC-Regular.otf +0 -0
  90. package/public/fonts/STIX-Italic-VF.woff2 +0 -0
  91. package/public/fonts/STIX-VF.woff2 +0 -0
  92. package/public/fonts/Snell-Black-SF.woff2 +0 -0
  93. package/public/fonts/Snell-Bold-SF.woff2 +0 -0
  94. package/public/giscus/theme-dark.css +208 -0
  95. package/public/giscus/theme-light.css +208 -0
  96. package/public/icons/favicon.svg +4 -0
  97. package/public/icons/og-logo.png +0 -0
  98. package/public/robots.txt +4 -0
  99. package/public/sounds/tap_01.wav +0 -0
  100. package/public/sounds/tap_02.wav +0 -0
  101. package/public/sounds/tap_03.wav +0 -0
  102. package/public/sounds/tap_04.wav +0 -0
  103. package/public/sounds/tap_05.wav +0 -0
  104. package/public/sounds/type_01.wav +0 -0
  105. package/public/sounds/type_02.wav +0 -0
  106. package/public/sounds/type_03.wav +0 -0
  107. package/public/sounds/type_04.wav +0 -0
  108. package/public/sounds/type_05.wav +0 -0
  109. package/scripts/apply-lqip.ts +276 -0
  110. package/scripts/format-posts.ts +105 -0
  111. package/scripts/migration/README.md +52 -0
  112. package/scripts/migration/migrate-hexo.ts +185 -0
  113. package/scripts/migration/validate-abbrlinks.ts +161 -0
  114. package/scripts/new-post.ts +52 -0
  115. package/scripts/seo/generate-legacy-redirects.ts +407 -0
  116. package/scripts/update-theme.ts +46 -0
  117. package/src/assets/icons/copy-check.svg +3 -0
  118. package/src/assets/icons/copy-icon.svg +4 -0
  119. package/src/assets/icons/go-back.svg +3 -0
  120. package/src/assets/icons/heading-anchor.svg +4 -0
  121. package/src/assets/icons/lang-en.svg +3 -0
  122. package/src/assets/icons/lang-ja.svg +5 -0
  123. package/src/assets/icons/lang-zh.svg +5 -0
  124. package/src/assets/icons/language-switcher.svg +3 -0
  125. package/src/assets/icons/pin-icon.svg +3 -0
  126. package/src/assets/icons/search-icon.svg +3 -0
  127. package/src/assets/icons/theme-toggle.svg +3 -0
  128. package/src/assets/icons/toc-icon.svg +3 -0
  129. package/src/assets/icons/top-icon.svg +3 -0
  130. package/src/assets/lqip-map.json +10 -0
  131. package/src/components/Button.astro +152 -0
  132. package/src/components/CategoryList.astro +66 -0
  133. package/src/components/Comment/Giscus.astro +119 -0
  134. package/src/components/Comment/Index.astro +30 -0
  135. package/src/components/Comment/Twikoo.astro +114 -0
  136. package/src/components/Comment/Waline.astro +149 -0
  137. package/src/components/FloatingButtons.astro +101 -0
  138. package/src/components/Footer.astro +74 -0
  139. package/src/components/Header.astro +62 -0
  140. package/src/components/JournalList.astro +56 -0
  141. package/src/components/Navbar.astro +69 -0
  142. package/src/components/NoteList.astro +56 -0
  143. package/src/components/Pagination.astro +267 -0
  144. package/src/components/PostDate.astro +80 -0
  145. package/src/components/PostList.astro +87 -0
  146. package/src/components/SearchModal.astro +340 -0
  147. package/src/components/TagList.astro +135 -0
  148. package/src/components/Widgets/BackButton.astro +43 -0
  149. package/src/components/Widgets/CodeCopyButton.astro +47 -0
  150. package/src/components/Widgets/GithubCard.astro +110 -0
  151. package/src/components/Widgets/ImageZoom.astro +135 -0
  152. package/src/components/Widgets/MediaEmbed.astro +127 -0
  153. package/src/components/Widgets/SoundEffect.astro +179 -0
  154. package/src/components/Widgets/TOC.astro +198 -0
  155. package/src/config-schema.ts +164 -0
  156. package/src/config.ts +127 -0
  157. package/src/config.ts.example +205 -0
  158. package/src/content/about/_example-about-en.md +6 -0
  159. package/src/content/about/about-en.md +21 -0
  160. package/src/content/about/about-ja.md +21 -0
  161. package/src/content/about/about-zh.md +24 -0
  162. package/src/content.config.ts +247 -0
  163. package/src/env.d.ts +25 -0
  164. package/src/i18n/config.ts +65 -0
  165. package/src/i18n/lang.ts +70 -0
  166. package/src/i18n/path.ts +160 -0
  167. package/src/i18n/ui.ts +214 -0
  168. package/src/layouts/Head.astro +203 -0
  169. package/src/layouts/Layout.astro +69 -0
  170. package/src/pages/404.astro +20 -0
  171. package/src/pages/[...lang]/[...page].astro +48 -0
  172. package/src/pages/[...lang]/about.astro +28 -0
  173. package/src/pages/[...lang]/atom.xml.ts +14 -0
  174. package/src/pages/[...lang]/categories/index.astro +35 -0
  175. package/src/pages/[...lang]/journals/[slug].astro +89 -0
  176. package/src/pages/[...lang]/journals/index.astro +55 -0
  177. package/src/pages/[...lang]/journals/page/[page].astro +66 -0
  178. package/src/pages/[...lang]/notes/[slug].astro +88 -0
  179. package/src/pages/[...lang]/notes/index.astro +55 -0
  180. package/src/pages/[...lang]/notes/page/[page].astro +66 -0
  181. package/src/pages/[...lang]/posts/[slug].astro +101 -0
  182. package/src/pages/[...lang]/rss.xml.ts +14 -0
  183. package/src/pages/[...lang]/search.astro +65 -0
  184. package/src/pages/[...lang]/tags/[tag].astro +53 -0
  185. package/src/pages/[...lang]/tags/index.astro +36 -0
  186. package/src/pages/_dynamic/list.astro +101 -0
  187. package/src/pages/_dynamic/slug.astro +100 -0
  188. package/src/pages/og/[...image].ts +114 -0
  189. package/src/pages/robots.txt.ts +20 -0
  190. package/src/plugins/rehype-code-copy-button.mjs +82 -0
  191. package/src/plugins/rehype-external-links.mjs +18 -0
  192. package/src/plugins/rehype-heading-anchor.mjs +55 -0
  193. package/src/plugins/rehype-image-processor.mjs +77 -0
  194. package/src/plugins/remark-container-directives.mjs +135 -0
  195. package/src/plugins/remark-leaf-directives.mjs +184 -0
  196. package/src/plugins/remark-reading-time.mjs +11 -0
  197. package/src/styles/comment.css +205 -0
  198. package/src/styles/extension.css +180 -0
  199. package/src/styles/font.css +111 -0
  200. package/src/styles/global.css +91 -0
  201. package/src/styles/lqip.css +71 -0
  202. package/src/styles/markdown.css +276 -0
  203. package/src/styles/transition.css +173 -0
  204. package/src/types/global.d.ts +22 -0
  205. package/src/types/index.d.ts +111 -0
  206. package/src/utils/cache.ts +32 -0
  207. package/src/utils/content.ts +819 -0
  208. package/src/utils/description.ts +147 -0
  209. package/src/utils/dynamic-collections.ts +155 -0
  210. package/src/utils/feed.ts +238 -0
  211. package/src/utils/page.ts +107 -0
  212. package/tsconfig.json +13 -0
  213. package/uno.config.ts +75 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Generate and apply LQIP (Low-Quality Image Placeholders) to images
3
+ * Source: https://frzi.medium.com/lqip-css-73dc6dda2529
4
+ * Usage: pnpm apply-lqip
5
+ */
6
+
7
+ import type { HTMLElement } from 'node-html-parser'
8
+ import fs from 'node:fs/promises'
9
+ import path from 'node:path'
10
+ import process from 'node:process'
11
+ import glob from 'fast-glob'
12
+ import { parse } from 'node-html-parser'
13
+ import sharp from 'sharp'
14
+
15
+ const distDir = 'dist'
16
+ const assetsDir = 'src/assets'
17
+ const lqipMapPath = 'src/assets/lqip-map.json'
18
+
19
+ interface LqipMap {
20
+ [path: string]: string
21
+ }
22
+
23
+ interface ImageStats {
24
+ total: number
25
+ cached: number
26
+ new: number
27
+ }
28
+
29
+ interface FileMapping {
30
+ filePath: string
31
+ webUrl: string
32
+ }
33
+
34
+ // Pack RGB color into 11 bits (4 bits R, 4 bits G, 3 bits B)
35
+ function packColor11Bit(r: number, g: number, b: number): number {
36
+ const pr = Math.round((r / 255) * 15)
37
+ const pg = Math.round((g / 255) * 15)
38
+ const pb = Math.round((b / 255) * 7)
39
+ return (pr << 7) | (pg << 3) | pb
40
+ }
41
+
42
+ // Pack RGB color into 10 bits (3 bits R, 4 bits G, 3 bits B)
43
+ function packColor10Bit(r: number, g: number, b: number): number {
44
+ const pr = Math.round((r / 255) * 7)
45
+ const pg = Math.round((g / 255) * 15)
46
+ const pb = Math.round((b / 255) * 7)
47
+ return (pr << 7) | (pg << 3) | pb
48
+ }
49
+
50
+ async function generateLqipValue(imagePath: string): Promise<string | null> {
51
+ try {
52
+ const instance = sharp(imagePath)
53
+
54
+ // Resize to 3x3 to get key colors (Top-Left, Center, Bottom-Right)
55
+ const buffer = await instance
56
+ .resize(3, 3, { fit: 'fill' })
57
+ .removeAlpha() // Force RGB output
58
+ .raw()
59
+ .toBuffer()
60
+
61
+ // Extract colors at specific positions
62
+ // 0: Top-Left, 4: Center, 8: Bottom-Right
63
+ // Each pixel is 3 bytes (RGB)
64
+ const getPixel = (index: number) => ({
65
+ r: buffer[index * 3],
66
+ g: buffer[index * 3 + 1],
67
+ b: buffer[index * 3 + 2],
68
+ })
69
+
70
+ const c0 = getPixel(0)
71
+ const c1 = getPixel(4)
72
+ const c2 = getPixel(8)
73
+
74
+ // Pack colors: [Color0 11b] [Color1 11b] [Color2 10b]
75
+ const pc0 = packColor11Bit(c0.r, c0.g, c0.b)
76
+ const pc1 = packColor11Bit(c1.r, c1.g, c1.b)
77
+ const pc2 = packColor10Bit(c2.r, c2.g, c2.b)
78
+
79
+ // Combine into a 32-bit integer
80
+ const combined = (BigInt(pc0) << 21n) | (BigInt(pc1) << 10n) | BigInt(pc2)
81
+
82
+ // Convert to 8-digit hex string
83
+ return combined.toString(16).padStart(8, '0')
84
+ }
85
+ catch (error) {
86
+ console.error(`⚠️ Failed to process image: ${imagePath}`, error)
87
+ return null
88
+ }
89
+ }
90
+
91
+ /**
92
+ * LQIP processing functions
93
+ * Image analysis, mapping generation, and HTML application
94
+ */
95
+ async function loadExistingLqipMap(): Promise<LqipMap> {
96
+ try {
97
+ const data = await fs.readFile(lqipMapPath, 'utf-8')
98
+ return JSON.parse(data) as LqipMap
99
+ }
100
+ catch {
101
+ return {} as LqipMap
102
+ }
103
+ }
104
+
105
+ async function scanAndAnalyzeImages(): Promise<{ fileMappings: FileMapping[], imageStats: ImageStats, existingMap: LqipMap }> {
106
+ await fs.mkdir(assetsDir, { recursive: true })
107
+
108
+ const webpFiles = await glob('_astro/**/*.webp', {
109
+ cwd: distDir,
110
+ absolute: true,
111
+ })
112
+
113
+ const existingMap = await loadExistingLqipMap()
114
+
115
+ const fileMappings = webpFiles.map(filePath => ({
116
+ filePath,
117
+ webUrl: `/${path.relative(distDir, filePath).replace(/\\/g, '/')}`,
118
+ }))
119
+
120
+ const { cached, new: newCount } = fileMappings.reduce((acc, { webUrl }) => {
121
+ existingMap[webUrl] !== undefined ? acc.cached++ : acc.new++
122
+ return acc
123
+ }, { cached: 0, new: 0 })
124
+
125
+ return {
126
+ fileMappings,
127
+ imageStats: { total: fileMappings.length, cached, new: newCount },
128
+ existingMap,
129
+ }
130
+ }
131
+
132
+ function cleanLqipMap(existingMap: LqipMap, fileMappings: FileMapping[]): LqipMap {
133
+ return fileMappings.reduce((acc, { webUrl }) => {
134
+ if (existingMap[webUrl] !== undefined) {
135
+ acc[webUrl] = existingMap[webUrl]
136
+ }
137
+ return acc
138
+ }, {} as LqipMap)
139
+ }
140
+
141
+ async function processNewImages(fileMappings: FileMapping[], stats: ImageStats, cleanedMap: LqipMap): Promise<LqipMap> {
142
+ const newMap = { ...cleanedMap }
143
+ let processed = 0
144
+ const concurrencyLimit = 10
145
+
146
+ const processFile = async ({ filePath, webUrl }: FileMapping) => {
147
+ const lqipValue = await generateLqipValue(filePath)
148
+ if (lqipValue !== null) {
149
+ newMap[webUrl] = lqipValue
150
+ }
151
+ processed++
152
+ if (processed % 10 === 0 || processed === stats.new) {
153
+ console.log(`🔄 Processing: ${processed}/${stats.new}`)
154
+ }
155
+ }
156
+
157
+ const toProcess = fileMappings.filter(m => cleanedMap[m.webUrl] === undefined)
158
+
159
+ for (let i = 0; i < toProcess.length; i += concurrencyLimit) {
160
+ const batch = toProcess.slice(i, i + concurrencyLimit)
161
+ await Promise.all(batch.map(processFile))
162
+ }
163
+
164
+ console.log(`✅ Generated LQIP styles for ${stats.new} new images`)
165
+
166
+ const isNewFile = Object.keys(cleanedMap).length === 0
167
+ await fs.writeFile(lqipMapPath, `${JSON.stringify(newMap, null, 2)}\n`)
168
+ console.log(`📁 LQIP mapping ${isNewFile ? 'saved to' : 'updated in'} ${lqipMapPath}`)
169
+
170
+ return newMap
171
+ }
172
+
173
+ function processImage(img: HTMLElement, lqipMap: LqipMap): boolean {
174
+ const src = img.getAttribute('src')
175
+ if (!src) {
176
+ return false
177
+ }
178
+
179
+ const lqipValue = lqipMap[src]
180
+ if (lqipValue === undefined) {
181
+ return false
182
+ }
183
+
184
+ const currentStyle = img.getAttribute('style') ?? ''
185
+ if (currentStyle.includes('--lqip:')) {
186
+ return false
187
+ }
188
+
189
+ const newStyle = currentStyle
190
+ ? `${currentStyle}; --lqip:#${lqipValue}`
191
+ : `--lqip:#${lqipValue}`
192
+
193
+ img.setAttribute('style', newStyle)
194
+ return true
195
+ }
196
+
197
+ async function applyLqipToHtml(lqipMap: LqipMap): Promise<number> {
198
+ const htmlFiles = await glob('**/*.html', { cwd: distDir })
199
+ let totalApplied = 0
200
+
201
+ for (const htmlFile of htmlFiles) {
202
+ try {
203
+ const filePath = `${distDir}/${htmlFile}`
204
+ const root = parse(await fs.readFile(filePath, 'utf-8'))
205
+ const images = root.querySelectorAll('img')
206
+
207
+ if (images.length === 0) {
208
+ continue
209
+ }
210
+
211
+ let hasChanges = false
212
+ for (const img of images) {
213
+ const wasUpdated = processImage(img, lqipMap)
214
+ if (wasUpdated) {
215
+ totalApplied++
216
+ hasChanges = true
217
+ }
218
+ }
219
+
220
+ if (hasChanges) {
221
+ await fs.writeFile(filePath, root.toString())
222
+ }
223
+ }
224
+ catch (error) {
225
+ console.warn(`⚠️ Failed to process ${htmlFile}:`, error)
226
+ continue
227
+ }
228
+ }
229
+
230
+ return totalApplied
231
+ }
232
+
233
+ /**
234
+ * Main workflow
235
+ * Coordinates LQIP generation and application process
236
+ */
237
+ async function main() {
238
+ console.log('🔍 Starting LQIP processing...')
239
+
240
+ const { fileMappings, imageStats, existingMap } = await scanAndAnalyzeImages()
241
+
242
+ if (imageStats.total === 0) {
243
+ console.log('✨ No images found to process')
244
+ return
245
+ }
246
+
247
+ console.log(`📦 Found ${imageStats.total} images (${imageStats.cached} cached, ${imageStats.new} new)`)
248
+
249
+ const cleanedMap = cleanLqipMap(existingMap, fileMappings)
250
+
251
+ let lqipMap: LqipMap
252
+ if (imageStats.new > 0) {
253
+ lqipMap = await processNewImages(fileMappings, imageStats, cleanedMap)
254
+ }
255
+ else {
256
+ lqipMap = cleanedMap
257
+
258
+ if (Object.keys(existingMap).length > Object.keys(cleanedMap).length) {
259
+ await fs.writeFile(lqipMapPath, `${JSON.stringify(cleanedMap, null, 2)}\n`)
260
+ }
261
+ }
262
+
263
+ const appliedCount = await applyLqipToHtml(lqipMap)
264
+
265
+ if (appliedCount === 0) {
266
+ console.log('✨ All images already have LQIP styles')
267
+ return
268
+ }
269
+
270
+ console.log(`✨ Successfully applied LQIP styles to ${appliedCount} images`)
271
+ }
272
+
273
+ main().catch((error) => {
274
+ console.error('❌ LQIP processing failed:', error)
275
+ process.exit(1)
276
+ })
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Format posts by fixing spaces and punctuations between CJK
3
+ * Project: https://github.com/huacnlee/autocorrect
4
+ * Usage: pnpm format-posts
5
+ */
6
+
7
+ import { readFile, writeFile } from 'node:fs/promises'
8
+ import process from 'node:process'
9
+ import { format } from 'autocorrect-node'
10
+ import fg from 'fast-glob'
11
+
12
+ interface MarkdownContent {
13
+ frontmatter: string
14
+ body: string
15
+ hasFrontmatter: boolean
16
+ }
17
+
18
+ // Split Markdown file into frontmatter and content
19
+ function splitContent(content: string): MarkdownContent {
20
+ const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)$/m)
21
+ if (!match) {
22
+ return {
23
+ frontmatter: '',
24
+ body: content,
25
+ hasFrontmatter: false,
26
+ }
27
+ }
28
+
29
+ return {
30
+ frontmatter: match[1],
31
+ body: match[2],
32
+ hasFrontmatter: true,
33
+ }
34
+ }
35
+
36
+ // Get all Markdown files to process
37
+ async function getMarkdownFiles(): Promise<string[]> {
38
+ console.log('🔍 Scanning Markdown files...')
39
+ const files = await fg(['content/**/*.{md,mdx}'])
40
+ console.log(`📦 Found ${files.length} Markdown files`)
41
+ return files
42
+ }
43
+
44
+ // Format a single Markdown file
45
+ async function formatSingleFile(filePath: string): Promise<boolean> {
46
+ const content = await readFile(filePath, 'utf8')
47
+ const { frontmatter, body, hasFrontmatter } = splitContent(content)
48
+
49
+ const formattedBody = format(body)
50
+ const newContent = hasFrontmatter
51
+ ? `---\n${frontmatter}\n---\n${formattedBody}`
52
+ : formattedBody
53
+
54
+ // Skip if content hasn't changed
55
+ if (content === newContent) {
56
+ return false
57
+ }
58
+
59
+ // Write updated content to file
60
+ await writeFile(filePath, newContent, 'utf8')
61
+ console.log(`✅ ${filePath}`)
62
+ return true
63
+ }
64
+
65
+ // Report formatting results
66
+ function reportResults(changedCount: number, errorCount: number) {
67
+ if (changedCount === 0) {
68
+ console.log('✅ Check complete, no files needed formatting changes')
69
+ }
70
+ else {
71
+ console.log(`✨ Formatted ${changedCount} files successfully`)
72
+ }
73
+
74
+ if (errorCount > 0) {
75
+ console.log(`⚠️ ${errorCount} files failed to format`)
76
+ }
77
+ }
78
+
79
+ // Main function to format all Markdown files
80
+ async function formatMarkdownFiles(): Promise<void> {
81
+ const files = await getMarkdownFiles()
82
+
83
+ let changedCount = 0
84
+ let errorCount = 0
85
+
86
+ for (const file of files) {
87
+ try {
88
+ const wasChanged = await formatSingleFile(file)
89
+ if (wasChanged) {
90
+ changedCount++
91
+ }
92
+ }
93
+ catch (error) {
94
+ console.error(`❌ ${file}:`, error)
95
+ errorCount++
96
+ }
97
+ }
98
+
99
+ reportResults(changedCount, errorCount)
100
+ }
101
+
102
+ formatMarkdownFiles().catch((error) => {
103
+ console.error('❌ Execution failed:', error)
104
+ process.exit(1)
105
+ })
@@ -0,0 +1,52 @@
1
+ # Migration Scripts
2
+
3
+ **ONE-TIME USE ONLY** - These scripts are for the initial Hexo → Astro migration and should NOT be part of CI/CD pipelines.
4
+
5
+ ## Scripts
6
+
7
+ ### migrate-hexo.ts
8
+
9
+ Copies and converts blog posts from the Hexo Blog-src repository to Astro format.
10
+
11
+ **What it does:**
12
+ 1. Reads markdown files from `Blog-src/source/_posts/`
13
+ 2. Converts Hexo frontmatter to Astro-compatible format
14
+ 3. Preserves abbrlinks for URL compatibility
15
+ 4. Copies to `content/posts/`
16
+
17
+ **Usage:**
18
+ ```bash
19
+ pnpm tsx scripts/migration/migrate-hexo.ts
20
+ ```
21
+
22
+ ### validate-abbrlinks.ts
23
+
24
+ Validates that all abbrlinks are preserved correctly after migration.
25
+
26
+ **What it does:**
27
+ 1. Reads all posts in `content/posts/`
28
+ 2. Extracts abbrlink from frontmatter
29
+ 3. Compares against original Blog-src posts
30
+ 4. Reports any missing or changed abbrlinks
31
+
32
+ **Usage:**
33
+ ```bash
34
+ pnpm tsx scripts/migration/validate-abbrlinks.ts
35
+ ```
36
+
37
+ ## When to Run
38
+
39
+ - **migrate-hexo.ts**: Once, at the start of migration project
40
+ - **validate-abbrlinks.ts**: After migration to verify URL preservation
41
+
42
+ ## NOT for CI/CD
43
+
44
+ These scripts read from the local `Blog-src` directory and are designed for one-time migration only. For deployment automation, see `../deployment/` and `../../.github/workflows/`.
45
+
46
+ ## Post-Migration
47
+
48
+ After successful migration:
49
+ 1. Verify all posts render correctly
50
+ 2. Run Playwright tests: `pnpm test`
51
+ 3. Check URL format matches: `/posts/:abbrlink.html`
52
+ 4. Archive these scripts (they won't be needed again)
@@ -0,0 +1,185 @@
1
+ /**
2
+ * ONE-TIME MIGRATION SCRIPT
3
+ *
4
+ * Migrates blog posts from Hexo (Blog-src) to Astro format.
5
+ * Run once at the start of the migration project.
6
+ *
7
+ * Usage: pnpm tsx scripts/migration/migrate-hexo.ts
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+
13
+ // Configuration - adjust paths as needed
14
+ const HEXO_POSTS_DIR = '../Blog-src/source/_posts'
15
+ const ASTRO_POSTS_DIR = './content/posts'
16
+
17
+ interface HexoFrontmatter {
18
+ title: string
19
+ date: string
20
+ tags?: string | string[]
21
+ categories?: string | string[]
22
+ abbrlink?: string
23
+ mathjax?: boolean
24
+ copyright?: string
25
+ }
26
+
27
+ function parseHexoFrontmatter(content: string): { frontmatter: HexoFrontmatter; body: string } {
28
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
29
+ if (!match) {
30
+ throw new Error('Invalid frontmatter format')
31
+ }
32
+
33
+ const yamlContent = match[1]
34
+ const body = match[2]
35
+
36
+ // Simple YAML parsing for common fields
37
+ const frontmatter: HexoFrontmatter = {
38
+ title: '',
39
+ date: '',
40
+ }
41
+
42
+ const lines = yamlContent.split('\n')
43
+ let currentKey = ''
44
+ let inArray = false
45
+ let arrayValues: string[] = []
46
+
47
+ for (const line of lines) {
48
+ if (line.match(/^(\w+):/)) {
49
+ // Save previous array if exists
50
+ if (inArray && currentKey) {
51
+ (frontmatter as any)[currentKey] = arrayValues
52
+ arrayValues = []
53
+ inArray = false
54
+ }
55
+
56
+ const [key, ...valueParts] = line.split(':')
57
+ currentKey = key.trim()
58
+ const value = valueParts.join(':').trim()
59
+
60
+ if (value) {
61
+ (frontmatter as any)[currentKey] = value
62
+ }
63
+ } else if (line.match(/^\s+-\s+/)) {
64
+ // Array item
65
+ inArray = true
66
+ const value = line.replace(/^\s+-\s+/, '').trim()
67
+ arrayValues.push(value)
68
+ }
69
+ }
70
+
71
+ // Save final array if exists
72
+ if (inArray && currentKey) {
73
+ (frontmatter as any)[currentKey] = arrayValues
74
+ }
75
+
76
+ return { frontmatter, body }
77
+ }
78
+
79
+ function convertToAstroFrontmatter(hexo: HexoFrontmatter): string {
80
+ const lines: string[] = ['---']
81
+
82
+ // Required fields
83
+ lines.push(`title: "${hexo.title.replace(/"/g, '\\"')}"`)
84
+ lines.push(`date: ${hexo.date}`)
85
+
86
+ // Optional abbrlink (critical for URL preservation)
87
+ if (hexo.abbrlink) {
88
+ lines.push(`abbrlink: ${hexo.abbrlink}`)
89
+ }
90
+
91
+ // Tags
92
+ if (hexo.tags) {
93
+ const tags = Array.isArray(hexo.tags) ? hexo.tags : [hexo.tags]
94
+ lines.push('tags:')
95
+ tags.forEach(tag => lines.push(` - ${tag}`))
96
+ }
97
+
98
+ // Categories (converted to tags in Astro/Retypeset)
99
+ if (hexo.categories) {
100
+ const categories = Array.isArray(hexo.categories) ? hexo.categories : [hexo.categories]
101
+ lines.push('categories:')
102
+ categories.forEach(cat => lines.push(` - ${cat}`))
103
+ }
104
+
105
+ // Math support
106
+ if (hexo.mathjax) {
107
+ lines.push('math: true')
108
+ }
109
+
110
+ lines.push('---')
111
+ return lines.join('\n')
112
+ }
113
+
114
+ function migratePost(hexoPath: string, astroPath: string): void {
115
+ const content = fs.readFileSync(hexoPath, 'utf-8')
116
+
117
+ try {
118
+ const { frontmatter, body } = parseHexoFrontmatter(content)
119
+ const astroFrontmatter = convertToAstroFrontmatter(frontmatter)
120
+
121
+ // Convert Hexo excerpt marker to Astro
122
+ const convertedBody = body.replace(/<!-- more -->/g, '<!-- excerpt -->')
123
+
124
+ const astroContent = `${astroFrontmatter}\n${convertedBody}`
125
+
126
+ fs.writeFileSync(astroPath, astroContent)
127
+ console.log(`✓ Migrated: ${path.basename(hexoPath)}`)
128
+ } catch (error) {
129
+ console.error(`✗ Failed: ${path.basename(hexoPath)} - ${error}`)
130
+ }
131
+ }
132
+
133
+ function main(): void {
134
+ console.log('='.repeat(60))
135
+ console.log('Hexo → Astro Migration Script (ONE-TIME)')
136
+ console.log('='.repeat(60))
137
+
138
+ // Resolve paths relative to project root
139
+ const projectRoot = process.cwd()
140
+ const hexoDir = path.resolve(projectRoot, HEXO_POSTS_DIR)
141
+ const astroDir = path.resolve(projectRoot, ASTRO_POSTS_DIR)
142
+
143
+ if (!fs.existsSync(hexoDir)) {
144
+ console.error(`Error: Hexo posts directory not found: ${hexoDir}`)
145
+ console.log('Please adjust HEXO_POSTS_DIR in the script.')
146
+ process.exit(1)
147
+ }
148
+
149
+ // Create output directory if needed
150
+ if (!fs.existsSync(astroDir)) {
151
+ fs.mkdirSync(astroDir, { recursive: true })
152
+ }
153
+
154
+ // Get all markdown files
155
+ const files = fs.readdirSync(hexoDir).filter(f => f.endsWith('.md'))
156
+
157
+ console.log(`Found ${files.length} posts to migrate`)
158
+ console.log('')
159
+
160
+ let success = 0
161
+ let failed = 0
162
+
163
+ for (const file of files) {
164
+ const hexoPath = path.join(hexoDir, file)
165
+ const astroPath = path.join(astroDir, file)
166
+
167
+ try {
168
+ migratePost(hexoPath, astroPath)
169
+ success++
170
+ } catch {
171
+ failed++
172
+ }
173
+ }
174
+
175
+ console.log('')
176
+ console.log('='.repeat(60))
177
+ console.log(`Migration complete: ${success} succeeded, ${failed} failed`)
178
+ console.log('='.repeat(60))
179
+
180
+ if (failed > 0) {
181
+ process.exit(1)
182
+ }
183
+ }
184
+
185
+ main()