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,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,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,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>
|