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.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/astro.config.ts +18 -0
- package/default-config.yaml +136 -0
- package/discover-collections.ts +160 -0
- package/integration.ts +394 -0
- package/package.json +105 -0
- package/patches/@qwik.dev__partytown@0.11.2.patch +98 -0
- package/public/_redirects +819 -0
- package/public/feeds/atom-style.xsl +105 -0
- package/public/feeds/rss-style.xsl +105 -0
- package/public/fonts/EarlySummer-VF-Split/00785494587e3487ac63a0e7e4fa30f0.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/08e5d941a4c76fad7b68e7a937ebb21f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/1268e5072156188d601f1eeb4473655d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/12a385475353c815d7a5add53ee51e37.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/12b11ca08223c65a21fc731d59dcfc11.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/16d6676d3cb645c520ee6df8a1f89afd.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2912a75ffef95e7a5ae9e2b2311ad61d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/298d96ea561e419a4104bc9fc18499ce.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2a2c71acc17ec39f6780835899e53096.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2a7e2d0e59d3f638074c50fab39fdef1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/36931fc4370e1670ed76af5d3feccba2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/3a68fdc792e4a9e0399a04e32d0cc2e3.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/4054d6a4d6b37719b51e0f71da6e7cd9.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/429cb25f825c3cbde6bfac5b36ae9675.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/42a9efc11298368ecdc1b85ab46f0b4f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/432018d2bdc9df92a7662056eb2b1261.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/44a6fb782f2a01560faa0f95248b60ef.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/45367b060e8ba0aa2507e6b91b86620b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/571db7564bda7c1a93542881b8976f4b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/58d55eeef4cf455e86a1142b1f3110d3.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/59ea41e77309160a0f63cdc76a010202.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/5d19d9174e568db4755981aa2e4ab380.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/5e811eb3b4175ee93d7ec000bf4631c2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/6268e0cd5d66d6fe05b331f259e7b9e4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/6549844aa3d833ca06a68a8e839db465.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/714b459658a7321ceeb1e1386ce165c2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/7511d97a469915013683eae06cb21cd9.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/7784b4ebe543d13f62f6f6e05beb0b2e.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/77c9bea70b3c6ab24e1497d5468c825b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/789ebea9e81df623e930b86de98fbfab.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/885bb7ab0717e8a47fc17f953adcdbf1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/896c58aff69a9a857764cee0663bc56d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/8fb6fc01c59d1e3ad1910b58dec7f5e7.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/95be5462b91b9a0458797cdc89d94cb5.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/9a5b2724f983ca0fc0d5ff8d10c41396.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/9ffe17f9c0e4cc4356cb3f08ffdb9c6d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/EarlySummer-VF-Subset.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/EarlySummerSerif License.txt +91 -0
- package/public/fonts/EarlySummer-VF-Split/a097ef49be62cd2565aca45600e1e3ac.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a17a1ae6063088e5b3a48c06b816929a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a83fdcfc5ecf2f6996704b0c02758689.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a8cf15ff9b71e59407d8406866ff6f99.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/af530ed51dd519e4456f8a5e259e908b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b195a8924915deec4aa9c3ec777cc93f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b4b6bb5df9239dd67b52ca858fd2a506.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b7592e1e027923f19e0e55dfdac69668.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b965859f69d8ccceaf0e2d6292afbcfb.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/bbe9333f1ff242bd96ecb23ff9e723b1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/be758580e295339ea98f0240b9869f24.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c07099e1d025617f6d40966986e1941b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c1b593dda62fdeb7dde3af02016da282.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c89f0335910a68a0958f2846108370e8.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ca49aa409fdedd3f2f894cd20a16640a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ccd4a28d2f63797e0183c87792e20b75.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/d2718da923fce8e7ea229d65e306e92c.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/d893e9b307d96041e9cfcbd03761b9f4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/dafaedaee41b75e21479d4ff324b6a34.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/db392af65f1867e5fd580eed2195df99.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/dc7c73a9e5577143ccd11e05ab55cb39.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/de396881189f747eba67685298363242.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/df625b213228bba22a7733d4eff8f148.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e6e60b384f220b893ef31a926ece829a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e6e8ce2c5972ab665630bb705383d0fb.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e963c7ed7104c2d6d68fcb5f952fe2f5.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e966b23b4cd7783f43e31032d41784f4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/edaac57c3856ec13128f4c6c3e00975c.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ee54e0d86edf068c6c9cbddb76a856fe.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/f612c78a5544ff2dd3e8296ac3e58344.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/f9e539bd9b7bf999c3da82f5403ec3b6.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/fa5863b923ac15993c52a619f699ee63.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/fc759e56ec6f6e6d3d4cb163d62fb557.woff2 +0 -0
- package/public/fonts/Font Subset List/CJK Common Characters.txt +7534 -0
- package/public/fonts/Font Subset List/EarlySummer Subset.txt +3 -0
- package/public/fonts/Font Subset List/Japanese Kana + Korean Letters.txt +6123 -0
- package/public/fonts/Font Subset List/Latin + Cyrillic + Greek + Arabic Glyphs.txt +121 -0
- package/public/fonts/Font Subset List/unicode_range.py +49 -0
- package/public/fonts/NotoSansSC-Bold.otf +0 -0
- package/public/fonts/NotoSansSC-Regular.otf +0 -0
- package/public/fonts/STIX-Italic-VF.woff2 +0 -0
- package/public/fonts/STIX-VF.woff2 +0 -0
- package/public/fonts/Snell-Black-SF.woff2 +0 -0
- package/public/fonts/Snell-Bold-SF.woff2 +0 -0
- package/public/giscus/theme-dark.css +208 -0
- package/public/giscus/theme-light.css +208 -0
- package/public/icons/favicon.svg +4 -0
- package/public/icons/og-logo.png +0 -0
- package/public/robots.txt +4 -0
- package/public/sounds/tap_01.wav +0 -0
- package/public/sounds/tap_02.wav +0 -0
- package/public/sounds/tap_03.wav +0 -0
- package/public/sounds/tap_04.wav +0 -0
- package/public/sounds/tap_05.wav +0 -0
- package/public/sounds/type_01.wav +0 -0
- package/public/sounds/type_02.wav +0 -0
- package/public/sounds/type_03.wav +0 -0
- package/public/sounds/type_04.wav +0 -0
- package/public/sounds/type_05.wav +0 -0
- package/scripts/apply-lqip.ts +276 -0
- package/scripts/format-posts.ts +105 -0
- package/scripts/migration/README.md +52 -0
- package/scripts/migration/migrate-hexo.ts +185 -0
- package/scripts/migration/validate-abbrlinks.ts +161 -0
- package/scripts/new-post.ts +52 -0
- package/scripts/seo/generate-legacy-redirects.ts +407 -0
- package/scripts/update-theme.ts +46 -0
- package/src/assets/icons/copy-check.svg +3 -0
- package/src/assets/icons/copy-icon.svg +4 -0
- package/src/assets/icons/go-back.svg +3 -0
- package/src/assets/icons/heading-anchor.svg +4 -0
- package/src/assets/icons/lang-en.svg +3 -0
- package/src/assets/icons/lang-ja.svg +5 -0
- package/src/assets/icons/lang-zh.svg +5 -0
- package/src/assets/icons/language-switcher.svg +3 -0
- package/src/assets/icons/pin-icon.svg +3 -0
- package/src/assets/icons/search-icon.svg +3 -0
- package/src/assets/icons/theme-toggle.svg +3 -0
- package/src/assets/icons/toc-icon.svg +3 -0
- package/src/assets/icons/top-icon.svg +3 -0
- package/src/assets/lqip-map.json +10 -0
- package/src/components/Button.astro +152 -0
- package/src/components/CategoryList.astro +66 -0
- package/src/components/Comment/Giscus.astro +119 -0
- package/src/components/Comment/Index.astro +30 -0
- package/src/components/Comment/Twikoo.astro +114 -0
- package/src/components/Comment/Waline.astro +149 -0
- package/src/components/FloatingButtons.astro +101 -0
- package/src/components/Footer.astro +74 -0
- package/src/components/Header.astro +62 -0
- package/src/components/JournalList.astro +56 -0
- package/src/components/Navbar.astro +69 -0
- package/src/components/NoteList.astro +56 -0
- package/src/components/Pagination.astro +267 -0
- package/src/components/PostDate.astro +80 -0
- package/src/components/PostList.astro +87 -0
- package/src/components/SearchModal.astro +340 -0
- package/src/components/TagList.astro +135 -0
- package/src/components/Widgets/BackButton.astro +43 -0
- package/src/components/Widgets/CodeCopyButton.astro +47 -0
- package/src/components/Widgets/GithubCard.astro +110 -0
- package/src/components/Widgets/ImageZoom.astro +135 -0
- package/src/components/Widgets/MediaEmbed.astro +127 -0
- package/src/components/Widgets/SoundEffect.astro +179 -0
- package/src/components/Widgets/TOC.astro +198 -0
- package/src/config-schema.ts +164 -0
- package/src/config.ts +127 -0
- package/src/config.ts.example +205 -0
- package/src/content/about/_example-about-en.md +6 -0
- package/src/content/about/about-en.md +21 -0
- package/src/content/about/about-ja.md +21 -0
- package/src/content/about/about-zh.md +24 -0
- package/src/content.config.ts +247 -0
- package/src/env.d.ts +25 -0
- package/src/i18n/config.ts +65 -0
- package/src/i18n/lang.ts +70 -0
- package/src/i18n/path.ts +160 -0
- package/src/i18n/ui.ts +214 -0
- package/src/layouts/Head.astro +203 -0
- package/src/layouts/Layout.astro +69 -0
- package/src/pages/404.astro +20 -0
- package/src/pages/[...lang]/[...page].astro +48 -0
- package/src/pages/[...lang]/about.astro +28 -0
- package/src/pages/[...lang]/atom.xml.ts +14 -0
- package/src/pages/[...lang]/categories/index.astro +35 -0
- package/src/pages/[...lang]/journals/[slug].astro +89 -0
- package/src/pages/[...lang]/journals/index.astro +55 -0
- package/src/pages/[...lang]/journals/page/[page].astro +66 -0
- package/src/pages/[...lang]/notes/[slug].astro +88 -0
- package/src/pages/[...lang]/notes/index.astro +55 -0
- package/src/pages/[...lang]/notes/page/[page].astro +66 -0
- package/src/pages/[...lang]/posts/[slug].astro +101 -0
- package/src/pages/[...lang]/rss.xml.ts +14 -0
- package/src/pages/[...lang]/search.astro +65 -0
- package/src/pages/[...lang]/tags/[tag].astro +53 -0
- package/src/pages/[...lang]/tags/index.astro +36 -0
- package/src/pages/_dynamic/list.astro +101 -0
- package/src/pages/_dynamic/slug.astro +100 -0
- package/src/pages/og/[...image].ts +114 -0
- package/src/pages/robots.txt.ts +20 -0
- package/src/plugins/rehype-code-copy-button.mjs +82 -0
- package/src/plugins/rehype-external-links.mjs +18 -0
- package/src/plugins/rehype-heading-anchor.mjs +55 -0
- package/src/plugins/rehype-image-processor.mjs +77 -0
- package/src/plugins/remark-container-directives.mjs +135 -0
- package/src/plugins/remark-leaf-directives.mjs +184 -0
- package/src/plugins/remark-reading-time.mjs +11 -0
- package/src/styles/comment.css +205 -0
- package/src/styles/extension.css +180 -0
- package/src/styles/font.css +111 -0
- package/src/styles/global.css +91 -0
- package/src/styles/lqip.css +71 -0
- package/src/styles/markdown.css +276 -0
- package/src/styles/transition.css +173 -0
- package/src/types/global.d.ts +22 -0
- package/src/types/index.d.ts +111 -0
- package/src/utils/cache.ts +32 -0
- package/src/utils/content.ts +819 -0
- package/src/utils/description.ts +147 -0
- package/src/utils/dynamic-collections.ts +155 -0
- package/src/utils/feed.ts +238 -0
- package/src/utils/page.ts +107 -0
- package/tsconfig.json +13 -0
- 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()
|