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,161 @@
1
+ /**
2
+ * ONE-TIME VALIDATION SCRIPT
3
+ *
4
+ * Validates that all abbrlinks from the original Hexo blog are preserved
5
+ * in the Astro migration. Critical for URL compatibility.
6
+ *
7
+ * Usage: pnpm tsx scripts/migration/validate-abbrlinks.ts
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+
13
+ // Configuration
14
+ const HEXO_POSTS_DIR = '../Blog-src/source/_posts'
15
+ const ASTRO_POSTS_DIR = './content/posts'
16
+
17
+ interface ValidationResult {
18
+ file: string
19
+ hexoAbbrlink?: string
20
+ astroAbbrlink?: string
21
+ status: 'match' | 'mismatch' | 'missing_hexo' | 'missing_astro'
22
+ }
23
+
24
+ function extractAbbrlink(content: string): string | undefined {
25
+ const match = content.match(/abbrlink:\s*([a-f0-9]+)/i)
26
+ return match ? match[1] : undefined
27
+ }
28
+
29
+ function validateAbbrlinks(): void {
30
+ console.log('='.repeat(60))
31
+ console.log('Abbrlink Validation Script (ONE-TIME)')
32
+ console.log('='.repeat(60))
33
+
34
+ const projectRoot = process.cwd()
35
+ const hexoDir = path.resolve(projectRoot, HEXO_POSTS_DIR)
36
+ const astroDir = path.resolve(projectRoot, ASTRO_POSTS_DIR)
37
+
38
+ // Build map of Hexo abbrlinks
39
+ const hexoAbbrlinks = new Map<string, string>()
40
+
41
+ if (fs.existsSync(hexoDir)) {
42
+ const hexoFiles = fs.readdirSync(hexoDir).filter(f => f.endsWith('.md'))
43
+ for (const file of hexoFiles) {
44
+ const content = fs.readFileSync(path.join(hexoDir, file), 'utf-8')
45
+ const abbrlink = extractAbbrlink(content)
46
+ if (abbrlink) {
47
+ hexoAbbrlinks.set(file, abbrlink)
48
+ }
49
+ }
50
+ console.log(`Found ${hexoAbbrlinks.size} abbrlinks in Hexo posts`)
51
+ } else {
52
+ console.log('⚠ Hexo directory not found - validating Astro posts only')
53
+ }
54
+
55
+ // Build map of Astro abbrlinks
56
+ const astroAbbrlinks = new Map<string, string>()
57
+
58
+ if (fs.existsSync(astroDir)) {
59
+ const astroFiles = fs.readdirSync(astroDir).filter(f => f.endsWith('.md'))
60
+ for (const file of astroFiles) {
61
+ const content = fs.readFileSync(path.join(astroDir, file), 'utf-8')
62
+ const abbrlink = extractAbbrlink(content)
63
+ if (abbrlink) {
64
+ astroAbbrlinks.set(file, abbrlink)
65
+ }
66
+ }
67
+ console.log(`Found ${astroAbbrlinks.size} abbrlinks in Astro posts`)
68
+ } else {
69
+ console.error('Error: Astro posts directory not found')
70
+ process.exit(1)
71
+ }
72
+
73
+ // Validate
74
+ console.log('')
75
+ console.log('Validation Results:')
76
+ console.log('-'.repeat(60))
77
+
78
+ const results: ValidationResult[] = []
79
+ const allFiles = new Set([...hexoAbbrlinks.keys(), ...astroAbbrlinks.keys()])
80
+
81
+ for (const file of allFiles) {
82
+ const hexoAbbrlink = hexoAbbrlinks.get(file)
83
+ const astroAbbrlink = astroAbbrlinks.get(file)
84
+
85
+ let status: ValidationResult['status']
86
+
87
+ if (!hexoAbbrlink && !astroAbbrlink) {
88
+ continue // Neither has abbrlink, skip
89
+ } else if (!hexoAbbrlink) {
90
+ status = 'missing_hexo'
91
+ } else if (!astroAbbrlink) {
92
+ status = 'missing_astro'
93
+ } else if (hexoAbbrlink === astroAbbrlink) {
94
+ status = 'match'
95
+ } else {
96
+ status = 'mismatch'
97
+ }
98
+
99
+ results.push({ file, hexoAbbrlink, astroAbbrlink, status })
100
+ }
101
+
102
+ // Summary
103
+ const matches = results.filter(r => r.status === 'match')
104
+ const mismatches = results.filter(r => r.status === 'mismatch')
105
+ const missingHexo = results.filter(r => r.status === 'missing_hexo')
106
+ const missingAstro = results.filter(r => r.status === 'missing_astro')
107
+
108
+ console.log(`✓ Matching: ${matches.length}`)
109
+ console.log(`⚠ Missing in Hexo (new posts): ${missingHexo.length}`)
110
+
111
+ if (mismatches.length > 0) {
112
+ console.log(`✗ Mismatched: ${mismatches.length}`)
113
+ console.log('')
114
+ console.log('MISMATCH DETAILS:')
115
+ for (const r of mismatches) {
116
+ console.log(` ${r.file}: Hexo=${r.hexoAbbrlink}, Astro=${r.astroAbbrlink}`)
117
+ }
118
+ }
119
+
120
+ if (missingAstro.length > 0) {
121
+ console.log(`✗ Missing in Astro: ${missingAstro.length}`)
122
+ console.log('')
123
+ console.log('MISSING IN ASTRO:')
124
+ for (const r of missingAstro) {
125
+ console.log(` ${r.file}: ${r.hexoAbbrlink}`)
126
+ }
127
+ }
128
+
129
+ // Check for duplicate abbrlinks
130
+ console.log('')
131
+ console.log('Checking for duplicates...')
132
+ const abbrlinkCounts = new Map<string, string[]>()
133
+ for (const [file, abbrlink] of astroAbbrlinks) {
134
+ const files = abbrlinkCounts.get(abbrlink) || []
135
+ files.push(file)
136
+ abbrlinkCounts.set(abbrlink, files)
137
+ }
138
+
139
+ const duplicates = [...abbrlinkCounts.entries()].filter(([, files]) => files.length > 1)
140
+ if (duplicates.length > 0) {
141
+ console.log(`✗ Found ${duplicates.length} duplicate abbrlinks:`)
142
+ for (const [abbrlink, files] of duplicates) {
143
+ console.log(` ${abbrlink}: ${files.join(', ')}`)
144
+ }
145
+ } else {
146
+ console.log('✓ No duplicate abbrlinks found')
147
+ }
148
+
149
+ console.log('')
150
+ console.log('='.repeat(60))
151
+
152
+ // Exit with error if validation failed
153
+ if (mismatches.length > 0 || missingAstro.length > 0 || duplicates.length > 0) {
154
+ console.log('Validation FAILED - please fix the issues above')
155
+ process.exit(1)
156
+ } else {
157
+ console.log('Validation PASSED')
158
+ }
159
+ }
160
+
161
+ validateAbbrlinks()
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Create a new post with frontmatter
3
+ * Usage: pnpm new-post <title>
4
+ */
5
+
6
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
7
+ import { basename, dirname, extname, join } from 'node:path'
8
+ import process from 'node:process'
9
+ import { themeConfig } from '../src/config'
10
+
11
+ // Process file path
12
+ const rawPath = process.argv[2] ?? 'new-post'
13
+ const baseName = basename(rawPath).replace(/\.(md|mdx)$/, '')
14
+ const targetFile = ['.md', '.mdx'].includes(extname(rawPath))
15
+ ? rawPath
16
+ : `${rawPath}.md`
17
+ const fullPath = join('content/posts', targetFile)
18
+
19
+ // Check if file already exists
20
+ if (existsSync(fullPath)) {
21
+ console.error(`❌ File already exists: ${fullPath}`)
22
+ process.exit(1)
23
+ }
24
+
25
+ // Create directory structure
26
+ mkdirSync(dirname(fullPath), { recursive: true })
27
+
28
+ // Prepare file content
29
+ const content = `---
30
+ title: ${baseName}
31
+ published: ${new Date().toISOString()}
32
+ description: ''
33
+ updated: ''
34
+ tags:
35
+ - Tag
36
+ draft: false
37
+ pin: 0
38
+ toc: ${themeConfig.global.toc}
39
+ lang: ''
40
+ abbrlink: ''
41
+ ---
42
+ `
43
+
44
+ // Write to file
45
+ try {
46
+ writeFileSync(fullPath, content)
47
+ console.log(`✅ Post created: ${fullPath}`)
48
+ }
49
+ catch (error) {
50
+ console.error('❌ Failed to create post:', error)
51
+ process.exit(1)
52
+ }
@@ -0,0 +1,407 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, relative, resolve } from 'node:path'
3
+ import process from 'node:process'
4
+ import fg from 'fast-glob'
5
+ import { allLocales, defaultLocale } from '../../src/config'
6
+
7
+ type RouteKind = 'posts' | 'notes' | 'journals'
8
+
9
+ interface CollectionSpec {
10
+ kind: RouteKind
11
+ baseDir: string
12
+ }
13
+
14
+ interface ContentEntry {
15
+ kind: RouteKind
16
+ id: string
17
+ baseId: string
18
+ lang: string
19
+ abbrlink: string
20
+ title: string
21
+ draft: boolean
22
+ }
23
+
24
+ interface GroupMeta {
25
+ key: string
26
+ kind: RouteKind
27
+ baseId: string
28
+ }
29
+
30
+ const COLLECTIONS: CollectionSpec[] = [
31
+ { kind: 'posts', baseDir: 'content/posts' },
32
+ { kind: 'journals', baseDir: 'content/journals' },
33
+ { kind: 'notes', baseDir: 'content/notes' },
34
+ ]
35
+
36
+ const KIND_PRIORITY: Record<RouteKind, number> = {
37
+ posts: 0,
38
+ notes: 1,
39
+ journals: 2,
40
+ }
41
+
42
+ // Explicit owner mapping for known legacy collisions verified against old-site URLs.
43
+ const LEGACY_ABBRLINK_OWNER_OVERRIDES: Record<string, string> = {
44
+ fbd0b1b0: 'posts:Mixture-Density-Network',
45
+ fc1cc4fb: 'notes:Leetcode面试高频题分类刷题总结',
46
+ }
47
+
48
+ const OUTPUT_FILE = 'public/_redirects'
49
+ const EOL = '\n'
50
+ const conflicts: string[] = []
51
+ const skippedDrafts: string[] = []
52
+ const ownershipWarnings: string[] = []
53
+
54
+ function stripQuotes(value: string): string {
55
+ const trimmed = value.trim()
56
+ const singleQuoted = trimmed.match(/^'(.*)'$/)
57
+ if (singleQuoted) {
58
+ return singleQuoted[1].trim()
59
+ }
60
+
61
+ const doubleQuoted = trimmed.match(/^"(.*)"$/)
62
+ if (doubleQuoted) {
63
+ return doubleQuoted[1].trim()
64
+ }
65
+
66
+ return trimmed
67
+ }
68
+
69
+ function parseBoolean(value: string): boolean {
70
+ return /^(true|1|yes)$/i.test(value.trim())
71
+ }
72
+
73
+ function getFrontmatter(content: string): string {
74
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
75
+ return match?.[1] ?? ''
76
+ }
77
+
78
+ function getFrontmatterField(frontmatter: string, field: string): string {
79
+ const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
80
+ const regex = new RegExp(`^${escapedField}\\s*:\\s*(.*)$`, 'm')
81
+ const match = frontmatter.match(regex)
82
+ if (!match) {
83
+ return ''
84
+ }
85
+
86
+ return stripQuotes(match[1] ?? '')
87
+ }
88
+
89
+ function isLegacyAbbrlink(value: string): boolean {
90
+ return /^[0-9a-f]{6,8}$/i.test(value.trim())
91
+ }
92
+
93
+ function slugifyPathSegment(input: string): string {
94
+ const slug = input
95
+ .normalize('NFKC')
96
+ .trim()
97
+ .toLowerCase()
98
+ .replace(/[\s_]+/g, '-')
99
+ .replace(/[^\p{Letter}\p{Number}]+/gu, '-')
100
+ .replace(/-+/g, '-')
101
+ .replace(/^-|-$/g, '')
102
+
103
+ return slug
104
+ }
105
+
106
+ function getBaseId(id: string): string {
107
+ const normalizedId = id.trim()
108
+ for (const lang of allLocales) {
109
+ const suffix = `.${lang}`
110
+ if (normalizedId.endsWith(suffix)) {
111
+ return normalizedId.slice(0, -suffix.length)
112
+ }
113
+ }
114
+
115
+ return normalizedId
116
+ }
117
+
118
+ function getLangFromId(id: string): string {
119
+ for (const lang of allLocales) {
120
+ const suffix = `.${lang}`
121
+ if (id.endsWith(suffix)) {
122
+ return lang
123
+ }
124
+ }
125
+
126
+ return ''
127
+ }
128
+
129
+ function buildPath(kind: RouteKind, slug: string, lang: string): string {
130
+ return lang === defaultLocale ? `/${kind}/${slug}` : `/${lang}/${kind}/${slug}`
131
+ }
132
+
133
+ function addRedirect(
134
+ redirectMap: Map<string, string>,
135
+ sourcePath: string,
136
+ targetPath: string,
137
+ context: string,
138
+ ) {
139
+ const existing = redirectMap.get(sourcePath)
140
+ if (existing && existing !== targetPath) {
141
+ conflicts.push(`conflict: ${sourcePath} => ${existing} (ignored ${targetPath}, ${context})`)
142
+ return
143
+ }
144
+
145
+ redirectMap.set(sourcePath, targetPath)
146
+ }
147
+
148
+ function getLegacyAbbrlinks(entries: ContentEntry[]): string[] {
149
+ const unique = new Set<string>()
150
+ for (const entry of entries) {
151
+ const abbrlink = entry.abbrlink.trim().toLowerCase()
152
+ if (isLegacyAbbrlink(abbrlink)) {
153
+ unique.add(abbrlink)
154
+ }
155
+ }
156
+
157
+ return [...unique].sort((left, right) => left.localeCompare(right))
158
+ }
159
+
160
+ function parseGroupKey(groupKey: string): GroupMeta {
161
+ const [kindString, ...rest] = groupKey.split(':')
162
+ return {
163
+ key: groupKey,
164
+ kind: kindString as RouteKind,
165
+ baseId: rest.join(':'),
166
+ }
167
+ }
168
+
169
+ function compareGroupKeys(left: string, right: string): number {
170
+ const leftMeta = parseGroupKey(left)
171
+ const rightMeta = parseGroupKey(right)
172
+ const kindDiff = (KIND_PRIORITY[leftMeta.kind] ?? Number.MAX_SAFE_INTEGER)
173
+ - (KIND_PRIORITY[rightMeta.kind] ?? Number.MAX_SAFE_INTEGER)
174
+ if (kindDiff !== 0) {
175
+ return kindDiff
176
+ }
177
+
178
+ return left.localeCompare(right)
179
+ }
180
+
181
+ function resolveAbbrlinkOwners(groupedEntries: Map<string, ContentEntry[]>): Map<string, string> {
182
+ const abbrlinkCandidates = new Map<string, Set<string>>()
183
+
184
+ for (const [groupKey, groupEntries] of groupedEntries) {
185
+ for (const abbrlink of getLegacyAbbrlinks(groupEntries)) {
186
+ const candidates = abbrlinkCandidates.get(abbrlink) ?? new Set<string>()
187
+ candidates.add(groupKey)
188
+ abbrlinkCandidates.set(abbrlink, candidates)
189
+ }
190
+ }
191
+
192
+ const owners = new Map<string, string>()
193
+ for (const [abbrlink, candidateSet] of abbrlinkCandidates) {
194
+ const candidates = [...candidateSet]
195
+ let owner = ''
196
+ const override = LEGACY_ABBRLINK_OWNER_OVERRIDES[abbrlink]
197
+
198
+ if (override) {
199
+ if (candidateSet.has(override)) {
200
+ owner = override
201
+ } else {
202
+ ownershipWarnings.push(
203
+ `override_miss: ${abbrlink} => ${override} (candidates: ${candidates.join(', ')})`,
204
+ )
205
+ }
206
+ }
207
+
208
+ if (!owner) {
209
+ const sortedCandidates = [...candidates].sort(compareGroupKeys)
210
+ owner = sortedCandidates[0] ?? ''
211
+ if (sortedCandidates.length > 1) {
212
+ ownershipWarnings.push(
213
+ `owner_inferred: ${abbrlink} => ${owner} (candidates: ${sortedCandidates.join(', ')})`,
214
+ )
215
+ }
216
+ }
217
+
218
+ if (owner) {
219
+ owners.set(abbrlink, owner)
220
+ }
221
+ }
222
+
223
+ return owners
224
+ }
225
+
226
+ function buildByLang(entries: ContentEntry[]): Partial<Record<string, ContentEntry>> {
227
+ const byLangExplicit: Partial<Record<string, ContentEntry>> = {}
228
+ const baseEntry = entries.find(entry => !entry.lang)
229
+
230
+ for (const locale of allLocales) {
231
+ byLangExplicit[locale] = entries.find(entry => entry.lang === locale)
232
+ }
233
+
234
+ // Keep language inference aligned with src/utils/content.ts
235
+ if (baseEntry) {
236
+ const zhEntry = byLangExplicit.zh
237
+ const enEntry = byLangExplicit.en
238
+
239
+ if (allLocales.includes('zh') && !zhEntry) {
240
+ byLangExplicit.zh = baseEntry
241
+ }
242
+
243
+ if (allLocales.includes('en') && !enEntry && zhEntry) {
244
+ byLangExplicit.en = baseEntry
245
+ }
246
+
247
+ // Generic fallback for projects where default locale is not zh.
248
+ if (!byLangExplicit[defaultLocale]) {
249
+ byLangExplicit[defaultLocale] = baseEntry
250
+ }
251
+ }
252
+
253
+ return byLangExplicit
254
+ }
255
+
256
+ function parseEntries(): ContentEntry[] {
257
+ const entries: ContentEntry[] = []
258
+
259
+ for (const collection of COLLECTIONS) {
260
+ const files = fg.sync(`${collection.baseDir}/**/*.{md,mdx}`, { dot: false })
261
+
262
+ for (const file of files) {
263
+ const absoluteFilePath = resolve(file)
264
+ const raw = readFileSync(absoluteFilePath, 'utf8')
265
+ const frontmatter = getFrontmatter(raw)
266
+ const relPath = relative(resolve(collection.baseDir), absoluteFilePath).replaceAll('\\', '/')
267
+ const id = relPath.replace(/\.(md|mdx)$/i, '')
268
+ const lang = getFrontmatterField(frontmatter, 'lang') || getLangFromId(id)
269
+ const abbrlink = getFrontmatterField(frontmatter, 'abbrlink')
270
+ const title = getFrontmatterField(frontmatter, 'title')
271
+ const draft = parseBoolean(getFrontmatterField(frontmatter, 'draft'))
272
+
273
+ entries.push({
274
+ kind: collection.kind,
275
+ id,
276
+ baseId: getBaseId(id),
277
+ lang,
278
+ abbrlink,
279
+ title,
280
+ draft,
281
+ })
282
+ }
283
+ }
284
+
285
+ return entries
286
+ }
287
+
288
+ function generateRedirectLines(entries: ContentEntry[]): string[] {
289
+ const groupedEntries = new Map<string, ContentEntry[]>()
290
+
291
+ for (const entry of entries) {
292
+ if (entry.draft || !entry.title.trim()) {
293
+ if (entry.abbrlink && isLegacyAbbrlink(entry.abbrlink)) {
294
+ skippedDrafts.push(`${entry.kind}:${entry.id}:${entry.abbrlink}`)
295
+ }
296
+ continue
297
+ }
298
+
299
+ const groupKey = `${entry.kind}:${entry.baseId}`
300
+ const bucket = groupedEntries.get(groupKey) ?? []
301
+ bucket.push(entry)
302
+ groupedEntries.set(groupKey, bucket)
303
+ }
304
+
305
+ const abbrlinkOwners = resolveAbbrlinkOwners(groupedEntries)
306
+ const redirectMap = new Map<string, string>()
307
+
308
+ for (const [groupKey, groupEntries] of groupedEntries) {
309
+ const [kindString, baseId] = groupKey.split(':')
310
+ const kind = kindString as RouteKind
311
+ const slug = slugifyPathSegment(baseId) || baseId
312
+ const ownedAbbrlinks = getLegacyAbbrlinks(groupEntries)
313
+ .filter(abbrlink => abbrlinkOwners.get(abbrlink) === groupKey)
314
+
315
+ if (ownedAbbrlinks.length === 0) {
316
+ continue
317
+ }
318
+
319
+ const byLang = buildByLang(groupEntries)
320
+ const supportedLangs = allLocales.filter(locale => Boolean(byLang[locale]))
321
+ if (supportedLangs.length === 0) {
322
+ continue
323
+ }
324
+
325
+ const fallbackLang = supportedLangs[0] ?? defaultLocale
326
+ const targetLang = supportedLangs.includes(defaultLocale) ? defaultLocale : fallbackLang
327
+ const defaultTargetPath = buildPath(kind, slug, targetLang)
328
+
329
+ const context = `${kind}:${baseId}`
330
+
331
+ // Normalize accidental .html variants of current slug URLs.
332
+ addRedirect(redirectMap, buildPath(kind, `${slug}.html`, targetLang), defaultTargetPath, context)
333
+
334
+ for (const abbrlink of ownedAbbrlinks) {
335
+ // Legacy Hexo links were /posts/:abbrlink.html for every article.
336
+ addRedirect(redirectMap, `/posts/${abbrlink}.html`, defaultTargetPath, context)
337
+ addRedirect(redirectMap, `/posts/${abbrlink}`, defaultTargetPath, context)
338
+ }
339
+
340
+ for (const lang of supportedLangs) {
341
+ if (lang === defaultLocale) {
342
+ continue
343
+ }
344
+
345
+ const localizedTargetPath = buildPath(kind, slug, lang)
346
+ addRedirect(redirectMap, buildPath(kind, `${slug}.html`, lang), localizedTargetPath, context)
347
+ for (const abbrlink of ownedAbbrlinks) {
348
+ addRedirect(redirectMap, `/${lang}/posts/${abbrlink}.html`, localizedTargetPath, context)
349
+ addRedirect(redirectMap, `/${lang}/posts/${abbrlink}`, localizedTargetPath, context)
350
+ }
351
+ }
352
+ }
353
+
354
+ return [...redirectMap.entries()]
355
+ .sort(([left], [right]) => left.localeCompare(right))
356
+ .map(([sourcePath, targetPath]) => `${sourcePath} ${targetPath} 301`)
357
+ }
358
+
359
+ function main() {
360
+ if (process.env.ENABLE_LEGACY_REDIRECTS === 'false') {
361
+ console.log('[generate-legacy-redirects] skipped because ENABLE_LEGACY_REDIRECTS=false')
362
+ return
363
+ }
364
+
365
+ const entries = parseEntries()
366
+ const redirectLines = generateRedirectLines(entries)
367
+
368
+ const output = [
369
+ '# AUTO-GENERATED FILE. DO NOT EDIT MANUALLY.',
370
+ '# Generated by: pnpm generate-legacy-redirects',
371
+ ...redirectLines,
372
+ '',
373
+ ].join(EOL)
374
+
375
+ mkdirSync(dirname(OUTPUT_FILE), { recursive: true })
376
+ writeFileSync(OUTPUT_FILE, output, 'utf8')
377
+
378
+ if (conflicts.length > 0) {
379
+ const sample = conflicts.slice(0, 10)
380
+ console.warn(`[generate-legacy-redirects] ${conflicts.length} redirect conflicts detected`)
381
+ for (const item of sample) {
382
+ console.warn(` - ${item}`)
383
+ }
384
+ if (conflicts.length > sample.length) {
385
+ console.warn(` - ... and ${conflicts.length - sample.length} more`)
386
+ }
387
+ }
388
+
389
+ if (skippedDrafts.length > 0) {
390
+ console.warn(`[generate-legacy-redirects] skipped ${skippedDrafts.length} draft/unpublished entries with legacy abbrlink`)
391
+ }
392
+
393
+ if (ownershipWarnings.length > 0) {
394
+ const sample = ownershipWarnings.slice(0, 10)
395
+ console.warn(`[generate-legacy-redirects] ${ownershipWarnings.length} ownership warnings detected`)
396
+ for (const item of sample) {
397
+ console.warn(` - ${item}`)
398
+ }
399
+ if (ownershipWarnings.length > sample.length) {
400
+ console.warn(` - ... and ${ownershipWarnings.length - sample.length} more`)
401
+ }
402
+ }
403
+
404
+ console.log(`[generate-legacy-redirects] entries=${entries.length}, redirects=${redirectLines.length}, output=${OUTPUT_FILE}`)
405
+ }
406
+
407
+ main()
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Update theme from upstream repository
3
+ * Usage: pnpm update-theme
4
+ */
5
+
6
+ import { execSync } from 'node:child_process'
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import process from 'node:process'
10
+
11
+ // Check and set up the remote repository
12
+ try {
13
+ execSync('git remote get-url upstream', { stdio: 'ignore' })
14
+ }
15
+ catch {
16
+ execSync('git remote add upstream https://github.com/radishzzz/astro-theme-retypeset.git', { stdio: 'inherit' })
17
+ }
18
+
19
+ // Update theme from upstream repository
20
+ try {
21
+ execSync('git fetch upstream', { stdio: 'inherit' })
22
+
23
+ const beforeHash = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim()
24
+ execSync('git merge upstream/master --allow-unrelated-histories', { stdio: 'inherit' })
25
+ const afterHash = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim()
26
+
27
+ if (beforeHash === afterHash) {
28
+ console.log('✅ Already up to date')
29
+ }
30
+ else {
31
+ console.log('✨ Updated successfully')
32
+ }
33
+ }
34
+ catch (error) {
35
+ // Check if there's a merge conflict
36
+ const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim()
37
+ const mergeHeadPath = path.join(gitDir, 'MERGE_HEAD')
38
+
39
+ if (fs.existsSync(mergeHeadPath)) {
40
+ console.log('⚠️ Update fetched with merge conflicts. Please resolve manually')
41
+ }
42
+ else {
43
+ console.error('❌ Update failed:', error)
44
+ process.exit(1)
45
+ }
46
+ }
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"/>
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"/>
3
+ <path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"/>
4
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M17.8.7L4,12l13.8,11.4.7-.9L7.2,12,18.5,1.5l-.7-.8Z"/>
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"/>
3
+ <path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"/>
4
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M19.18 21.1 12.4 1.89h-1.21L4.52 21.1l-2.53.2v.81h6.37v-.81l-2.83-.2 2.02-5.97h7.58l2.02 5.97-3.34.2v.81H22v-.81l-2.83-.2zM7.85 14.33l3.44-10.21 3.54 10.21z"/>
3
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <g transform="translate(-0.83,21.94) scale(0.0258,-0.0258)">
3
+ <path d="M138 612C145 613 150 613 154 613C156 613 163 613 172 612C252 609 297 607 307 607H347C342 534 342 529 341 456C196 393 109 281 109 158C109 69 163 6 238 6C308 6 384 47 461 127C539 206 593 292 651 427C759 399 816 339 816 253C816 164 753 91 644 53C596 36 553 28 494 26C512 -1 517 -12 526 -41C616 -30 673 -14 730 16C833 70 887 149 887 247C887 364 810 449 672 481C689 528 700 555 703 561L634 582C630 552 622 522 610 492C585 494 573 495 552 495C499 495 455 489 404 475C405 524 405 524 411 609C504 612 656 629 765 649C784 652 787 652 800 654L796 724C720 697 578 676 415 668C422 746 427 780 436 808L358 813C359 806 359 800 359 798C359 791 357 752 356 741C352 690 352 690 351 666C333 665 329 665 326 665C234 665 181 669 139 681ZM341 393C341 272 347 199 361 122C316 86 283 71 246 71C203 71 174 107 174 162C174 212 196 265 235 311C263 343 292 366 341 393ZM415 164C415 169 415 174 415 175C415 181 415 183 414 189C405 272 404 289 403 418C449 433 496 441 542 441C553 441 558 441 590 439C544 334 502 264 437 189C429 181 428 179 418 163Z"/>
4
+ </g>
5
+ </svg>