clarity-theme 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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/README.zh-CN.md +297 -0
  4. package/app/app.config.ts +110 -0
  5. package/app/app.vue +5 -0
  6. package/app/assets/css/_variable.scss +8 -0
  7. package/app/assets/css/animation.scss +50 -0
  8. package/app/assets/css/article.scss +152 -0
  9. package/app/assets/css/color.scss +58 -0
  10. package/app/assets/css/font.scss +35 -0
  11. package/app/assets/css/main.scss +230 -0
  12. package/app/assets/css/reusable.scss +186 -0
  13. package/app/assets/icons/solar-rewind-back-bold-duotone.svg +1 -0
  14. package/app/assets/icons/solar-rewind-forward-bold-duotone.svg +1 -0
  15. package/app/components/blog/BlogAside.vue +63 -0
  16. package/app/components/blog/BlogFooter.vue +61 -0
  17. package/app/components/blog/BlogHeader.global.vue +146 -0
  18. package/app/components/blog/BlogPanel.vue +79 -0
  19. package/app/components/blog/BlogSidebar.vue +160 -0
  20. package/app/components/blog/BlogWidget.vue +134 -0
  21. package/app/components/blog/Mask.vue +42 -0
  22. package/app/components/blog/SkipToContent.vue +22 -0
  23. package/app/components/blog/ThemeToggle.vue +50 -0
  24. package/app/components/content/Alert.vue +96 -0
  25. package/app/components/content/Badge.vue +90 -0
  26. package/app/components/content/Blur.vue +22 -0
  27. package/app/components/content/CardList.vue +32 -0
  28. package/app/components/content/Chat.vue +79 -0
  29. package/app/components/content/Copy.vue +134 -0
  30. package/app/components/content/EmojiClock.vue +53 -0
  31. package/app/components/content/FeedCard.vue +212 -0
  32. package/app/components/content/FeedGroup.vue +130 -0
  33. package/app/components/content/Folding.vue +69 -0
  34. package/app/components/content/Key.vue +164 -0
  35. package/app/components/content/LinkBanner.vue +59 -0
  36. package/app/components/content/LinkCard.vue +74 -0
  37. package/app/components/content/MdTitle.vue +26 -0
  38. package/app/components/content/Mermaid.vue +133 -0
  39. package/app/components/content/MusicScore.vue +178 -0
  40. package/app/components/content/Pic.vue +55 -0
  41. package/app/components/content/Poetry.vue +41 -0
  42. package/app/components/content/ProseA.vue +40 -0
  43. package/app/components/content/ProseCode.vue +95 -0
  44. package/app/components/content/ProsePre.vue +291 -0
  45. package/app/components/content/ProseTable.vue +70 -0
  46. package/app/components/content/Quote.vue +51 -0
  47. package/app/components/content/Tab.vue +100 -0
  48. package/app/components/content/Timeline.vue +73 -0
  49. package/app/components/content/Tip.vue +52 -0
  50. package/app/components/content/VideoEmbed.vue +127 -0
  51. package/app/components/partial/Button.vue +98 -0
  52. package/app/components/partial/DlGroup.vue +69 -0
  53. package/app/components/partial/Dropdown.vue +54 -0
  54. package/app/components/partial/Error.vue +70 -0
  55. package/app/components/partial/Expand.vue +51 -0
  56. package/app/components/partial/IconNavList.vue +42 -0
  57. package/app/components/partial/Pagination.vue +113 -0
  58. package/app/components/partial/RadioGroup.vue +77 -0
  59. package/app/components/partial/Secret.vue +23 -0
  60. package/app/components/partial/Slider.vue +96 -0
  61. package/app/components/partial/Toggle.vue +70 -0
  62. package/app/components/popover/Lightbox.vue +77 -0
  63. package/app/components/popover/Search.vue +214 -0
  64. package/app/components/popover/SearchItem.vue +64 -0
  65. package/app/components/post/Archive.vue +104 -0
  66. package/app/components/post/Article.vue +138 -0
  67. package/app/components/post/Comment.vue +275 -0
  68. package/app/components/post/Excerpt.vue +66 -0
  69. package/app/components/post/OrderToggle.vue +101 -0
  70. package/app/components/post/PostFooter.vue +80 -0
  71. package/app/components/post/PostHeader.vue +147 -0
  72. package/app/components/post/PostSurround.vue +111 -0
  73. package/app/components/post/Slide.vue +215 -0
  74. package/app/components/util/Date.vue +62 -0
  75. package/app/components/util/HydrateSafe.vue +10 -0
  76. package/app/components/util/Img.vue +38 -0
  77. package/app/components/util/Link.vue +11 -0
  78. package/app/components/widget/BlogLog.vue +27 -0
  79. package/app/components/widget/BlogStats.vue +39 -0
  80. package/app/components/widget/BlogTech.vue +65 -0
  81. package/app/components/widget/Empty.vue +3 -0
  82. package/app/components/widget/Toc.vue +129 -0
  83. package/app/composables/useArticle.ts +118 -0
  84. package/app/composables/useAvoid.ts +87 -0
  85. package/app/composables/useCopy.ts +21 -0
  86. package/app/composables/usePagination.ts +70 -0
  87. package/app/composables/useShiki.ts +129 -0
  88. package/app/composables/useToc.ts +57 -0
  89. package/app/composables/useWidgets.ts +56 -0
  90. package/app/error.vue +34 -0
  91. package/app/layouts/default.vue +66 -0
  92. package/app/pages/[...slug].vue +59 -0
  93. package/app/pages/archive.vue +207 -0
  94. package/app/pages/index.vue +83 -0
  95. package/app/pages/link.vue +65 -0
  96. package/app/pages/preview.vue +59 -0
  97. package/app/plugins/easter-egg.ts +23 -0
  98. package/app/plugins/init.ts +6 -0
  99. package/app/plugins/tippy.ts +22 -0
  100. package/app/shiki.config.ts +8 -0
  101. package/app/stores/layout.ts +34 -0
  102. package/app/stores/search.ts +37 -0
  103. package/app/types/article.ts +16 -0
  104. package/app/types/feed.ts +1 -0
  105. package/app/types/index.ts +14 -0
  106. package/app/types/modal.ts +16 -0
  107. package/app/types/nav.ts +11 -0
  108. package/app/utils/anim.ts +37 -0
  109. package/app/utils/img.ts +83 -0
  110. package/config/app.ts +88 -0
  111. package/config/content.d.mts +1 -0
  112. package/config/content.mjs +69 -0
  113. package/config/content.ts +91 -0
  114. package/config/define.mjs +21 -0
  115. package/config/define.ts +23 -0
  116. package/config/feed.ts +42 -0
  117. package/config/feeds.empty.ts +4 -0
  118. package/config/index.d.mts +1 -0
  119. package/config/index.mjs +6 -0
  120. package/config/index.ts +6 -0
  121. package/config/public.ts +71 -0
  122. package/config/schema.d.mts +1 -0
  123. package/config/schema.mjs +103 -0
  124. package/config/schema.ts +129 -0
  125. package/config/server.ts +35 -0
  126. package/img/index.d.mts +1 -0
  127. package/img/index.mjs +78 -0
  128. package/img/index.ts +8 -0
  129. package/modules/clarity-config/anti-mirror-client.ts +11 -0
  130. package/modules/clarity-config/index.ts +324 -0
  131. package/nuxt.config.ts +225 -0
  132. package/package.json +148 -0
  133. package/public/assets/atom.css +274 -0
  134. package/public/assets/atom.xsl +110 -0
  135. package/public/fonts/AlimamaFangYuanTi.woff2 +0 -0
  136. package/remark-plugins/rehype-meta-slots.d.mts +1 -0
  137. package/remark-plugins/rehype-meta-slots.mjs +49 -0
  138. package/remark-plugins/rehype-meta-slots.ts +58 -0
  139. package/remark-plugins/remark-code-component.d.mts +1 -0
  140. package/remark-plugins/remark-code-component.mjs +25 -0
  141. package/remark-plugins/remark-code-component.ts +41 -0
  142. package/server/api/stats.get.ts +109 -0
  143. package/server/routes/atom.xml.get.ts +101 -0
  144. package/server/routes/subscriptions.opml.get.ts +70 -0
  145. package/server/utils/clarity.ts +11 -0
  146. package/shared/types/content.ts +30 -0
  147. package/shared/utils/clarity.ts +34 -0
  148. package/shared/utils/icon.ts +218 -0
  149. package/shared/utils/link.ts +52 -0
  150. package/shared/utils/str.ts +88 -0
  151. package/shared/utils/time.ts +116 -0
@@ -0,0 +1,101 @@
1
+ import type { ClarityContentRow } from '../../shared/types/content'
2
+ import { queryCollection } from '@nuxt/content/server'
3
+ import XmlBuilder from 'fast-xml-builder'
4
+ import { toZonedTemporal } from '../../shared/utils/time'
5
+
6
+ const runtimeConfig = useRuntimeConfig()
7
+
8
+ const builder = new XmlBuilder({
9
+ attributeNamePrefix: '$',
10
+ cdataPropName: '$',
11
+ format: true,
12
+ ignoreAttributes: false,
13
+ textNodeName: '_',
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { site, feed, features } = useClarityServerConfig()
18
+ if (!features.atom) {
19
+ throw createError({ statusCode: 404, statusMessage: 'Atom feed is disabled' })
20
+ }
21
+ const themeInfo = runtimeConfig.public.clarity as { theme: string, themeVersion: string, themeHomepage: string }
22
+
23
+ function formatIsoDate(date?: string) {
24
+ if (!date)
25
+ return
26
+ try {
27
+ return toZonedTemporal(date, site.timezone).toInstant().toString()
28
+ }
29
+ catch {
30
+ console.error('Invalid date format', date)
31
+ return date
32
+ }
33
+ }
34
+
35
+ function getUrl(path: string | undefined) {
36
+ return new URL(path ?? '', site.url).toString()
37
+ }
38
+
39
+ function renderContent(post: ClarityContentRow) {
40
+ return [
41
+ post.image && `<img src="${post.image}" alt="${post.title}" />`,
42
+ post.description && `<p>${post.description}</p>`,
43
+ `<a class="view-full" href="${getUrl(post.path)}" target="_blank">点击查看全文</a>`,
44
+ ].filter(Boolean).join(' ')
45
+ }
46
+
47
+ const posts = await queryCollection(event, 'content' as never)
48
+ .where('stem', 'LIKE', 'posts/%')
49
+ .order('updated', 'DESC')
50
+ .limit(feed.limit)
51
+ .all() as ClarityContentRow[]
52
+
53
+ const entries = posts.map(post => ({
54
+ id: getUrl(post.path),
55
+ title: post.title ?? '',
56
+ updated: formatIsoDate(post.updated),
57
+ author: { name: (post as any).author || site.author.name },
58
+ content: {
59
+ $type: 'html',
60
+ $: renderContent(post),
61
+ },
62
+ link: { $href: getUrl(post.path) },
63
+ summary: post.description,
64
+ category: { $term: post.categories?.[0] },
65
+ published: formatIsoDate(post.published ?? post.date),
66
+ }))
67
+
68
+ const feedData = {
69
+ $xmlns: 'http://www.w3.org/2005/Atom',
70
+ id: site.url,
71
+ title: site.title,
72
+ updated: runtimeConfig.public.buildTime,
73
+ description: site.description,
74
+ author: {
75
+ name: site.author.name,
76
+ email: site.author.email,
77
+ uri: site.author.homepage,
78
+ },
79
+ link: [
80
+ { $href: getUrl('atom.xml'), $rel: 'self' },
81
+ { $href: site.url, $rel: 'alternate' },
82
+ ],
83
+ language: site.language,
84
+ generator: {
85
+ $uri: themeInfo.themeHomepage,
86
+ $version: themeInfo.themeVersion,
87
+ _: themeInfo.theme,
88
+ },
89
+ icon: site.favicon,
90
+ logo: site.author.avatar,
91
+ rights: `© ${new Date().getFullYear()} ${site.author.name}`,
92
+ subtitle: site.subtitle || site.description,
93
+ entry: entries,
94
+ }
95
+
96
+ return builder.build({
97
+ '?xml': { $version: '1.0', $encoding: 'UTF-8' },
98
+ '?xml-stylesheet': feed.enableStyle ? { $type: 'text/xsl', $href: '/assets/atom.xsl' } : undefined,
99
+ 'feed': feedData,
100
+ })
101
+ })
@@ -0,0 +1,70 @@
1
+ import type { FeedEntry, FeedGroup } from '../../app/types/feed'
2
+ import XmlBuilder from 'fast-xml-builder'
3
+ import { toZonedTemporal } from '../../shared/utils/time'
4
+
5
+ const runtimeConfig = useRuntimeConfig()
6
+
7
+ const builder = new XmlBuilder({
8
+ attributeNamePrefix: '$',
9
+ format: true,
10
+ ignoreAttributes: false,
11
+ })
12
+
13
+ function mapEntry(item: FeedEntry, timeZone: string) {
14
+ return {
15
+ $text: item.title || item.sitenick || item.author,
16
+ $type: 'rss',
17
+ $xmlUrl: item.feed,
18
+ $created: toZonedTemporal(item.date, timeZone).toInstant().toString(),
19
+ $description: item.desc,
20
+ $htmlUrl: item.link || item.feed,
21
+ }
22
+ }
23
+
24
+ function flattenGroups(groups: FeedGroup[], timeZone: string) {
25
+ return groups.flatMap(({ entries }) => entries.filter(({ feed }) => feed).map(item => mapEntry(item, timeZone)))
26
+ }
27
+
28
+ export default defineEventHandler(async () => {
29
+ const { site, features } = useClarityServerConfig()
30
+ if (!features.opml) {
31
+ throw createError({ statusCode: 404, statusMessage: 'OPML subscription output is disabled' })
32
+ }
33
+ const feeds = (await import('#clarity/feeds')).default as FeedGroup[]
34
+ const myFeed: FeedEntry = {
35
+ author: site.author.name,
36
+ title: site.title,
37
+ desc: site.subtitle || site.description,
38
+ link: site.url,
39
+ feed: new URL('/atom.xml', site.url).toString(),
40
+ icon: site.favicon,
41
+ avatar: site.author.avatar || site.favicon,
42
+ date: site.established || '',
43
+ }
44
+
45
+ const outlines = [
46
+ mapEntry(myFeed, site.timezone),
47
+ ...flattenGroups(feeds, site.timezone),
48
+ ]
49
+
50
+ const opml = {
51
+ $version: '2.0',
52
+ head: {
53
+ title: `${site.title}的友链订阅`,
54
+ dateCreated: site.established
55
+ ? toZonedTemporal(site.established, site.timezone).toInstant().toString()
56
+ : undefined,
57
+ dateModified: runtimeConfig.public.buildTime,
58
+ ownerName: site.author.name,
59
+ ownerEmail: site.author.email,
60
+ ownerId: site.author.homepage,
61
+ docs: 'https://opml.org/spec2.opml',
62
+ },
63
+ body: { outline: outlines },
64
+ }
65
+
66
+ return builder.build({
67
+ '?xml': { $version: '1.0', $encoding: 'UTF-8' },
68
+ opml,
69
+ })
70
+ })
@@ -0,0 +1,11 @@
1
+ import type { ClarityServerConfig } from '../../config/server'
2
+
3
+ /**
4
+ * 获取服务端专用 Clarity 配置(完整 site/feed/stats 与 feature 路由开关)。
5
+ *
6
+ * 数据来自 Nitro 私有 runtimeConfig,不会进入客户端 bundle;
7
+ * 请只在 server/ 侧调用,客户端渲染请使用 useClarityConfig()。
8
+ */
9
+ export function useClarityServerConfig(): ClarityServerConfig {
10
+ return useRuntimeConfig().clarity as ClarityServerConfig
11
+ }
@@ -0,0 +1,30 @@
1
+ import type { PageCollectionItemBase } from '@nuxt/content'
2
+
3
+ /**
4
+ * Theme 内部使用的 content 集合行类型。
5
+ *
6
+ * 消费项目的精确集合类型由 @nuxt/content 按站点 schema 生成,并通过
7
+ * module augmentation 注入消费项目上下文。Theme 作为 node_modules Layer
8
+ * 解析到的 @nuxt/content 类型实例无法稳定共享该 augmentation(pnpm 严格
9
+ * 布局下会形成双实例),因此 Theme 内部只依赖自身 schema 的稳定字段。
10
+ */
11
+ export type ClarityContentRow = PageCollectionItemBase & {
12
+ title?: string
13
+ description?: string
14
+ date?: string
15
+ updated?: string
16
+ published?: string
17
+ categories?: string[]
18
+ tags?: string[]
19
+ type?: string
20
+ image?: string
21
+ recommend?: number
22
+ draft?: boolean
23
+ permalink?: string
24
+ readingTime: {
25
+ text: string
26
+ minutes: number
27
+ time: number
28
+ words: number
29
+ }
30
+ }
@@ -0,0 +1,34 @@
1
+ import type { ClarityAppConfig } from '../../config/app'
2
+ import type { FeedEntry } from '../../config/feed'
3
+ import type { ClaritySiteConfig } from '../../config/schema'
4
+
5
+ /** 获取完整 Clarity 配置(站点配置 + UI 配置) */
6
+ export function useClarityConfig(): ClarityAppConfig {
7
+ return useAppConfig().clarity as unknown as ClarityAppConfig
8
+ }
9
+
10
+ /** 获取站点配置(标题、作者、URL 等) */
11
+ export function useClaritySite(): ClaritySiteConfig {
12
+ return useClarityConfig().site
13
+ }
14
+
15
+ /** 获取文章配置的客户端子集(分类图标/颜色、排序方式) */
16
+ export function useClarityArticle(): ClarityAppConfig['article'] {
17
+ return useClarityConfig().article
18
+ }
19
+
20
+ /** 由站点配置生成本站订阅源条目(用于友链页与 OPML) */
21
+ export function useClaritySiteFeedEntry(): FeedEntry {
22
+ const site = useClaritySite()
23
+ return {
24
+ author: site.author.name,
25
+ title: site.title,
26
+ desc: site.subtitle || site.description,
27
+ link: site.url,
28
+ feed: new URL('/atom.xml', site.url).toString(),
29
+ icon: site.favicon,
30
+ avatar: site.author.avatar || site.favicon,
31
+ date: site.established || '',
32
+ comment: '这是我自己',
33
+ }
34
+ }
@@ -0,0 +1,218 @@
1
+ /** 友链架构图标映射 */
2
+ // @keep-sorted
3
+ const archIcons = {
4
+ 'Astro': 'simple-icons:astro',
5
+ 'Cloudflare': 'simple-icons:cloudflare',
6
+ 'Deno Deploy': 'simple-icons:deno',
7
+ 'EdgeOne': 'simple-icons:cloudnativebuild', // 不准确
8
+ 'Express': 'simple-icons:express',
9
+ 'Fly': 'tabler:air-balloon',
10
+ 'Framer': 'simple-icons:framer',
11
+ 'Ghost': 'simple-icons:ghost',
12
+ 'GitHub Pages': 'simple-icons:github',
13
+ 'Golang': 'simple-icons:go',
14
+ 'Gridea': 'tabler:square-rounded-letter-g-filled', // 不准确
15
+ 'Halo': 'material-symbols:h-mobiledata-badge', // 不准确
16
+ 'Hexo': 'simple-icons:hexo',
17
+ 'HTML': 'simple-icons:html5',
18
+ 'Hugo': 'simple-icons:hugo',
19
+ 'Jekyll': 'simple-icons:jekyll',
20
+ 'Material for MkDocs': 'simple-icons:materialformkdocs',
21
+ 'Netlify': 'simple-icons:netlify',
22
+ 'Next.js': 'simple-icons:nextdotjs',
23
+ 'Notion': 'simple-icons:notion',
24
+ 'NotionNext': 'simple-icons:notion',
25
+ 'Nuxt': 'simple-icons:nuxt',
26
+ 'PHP': 'simple-icons:php',
27
+ 'Python': 'simple-icons:python',
28
+ 'React': 'simple-icons:react',
29
+ 'Typecho': 'icon-park-solid:align-text-left-one', // 不准确
30
+ 'Valaxy': 'tabler:letter-v', // 不准确
31
+ 'Vercel': 'simple-icons:vercel',
32
+ 'VitePress': 'simple-icons:vitepress',
33
+ 'Vue': 'uim:vuejs',
34
+ 'VuePress': 'uim:vuejs',
35
+ 'WordPress': 'simple-icons:wordpress',
36
+ 'Zeabur': 'tabler:square-letter-z-filled', // 不准确
37
+ '国内 CDN': 'tabler:cloud-data-connection',
38
+ '服务器': 'tabler:server',
39
+ '虚拟主机': 'tabler:cloud-upload',
40
+ }
41
+
42
+ export type Arch = keyof typeof archIcons
43
+
44
+ export function getArchIcon(arch: Arch) {
45
+ return archIcons[arch] ?? ''
46
+ }
47
+
48
+ /** BlogTech Widget 构建平台图标映射 */
49
+ // @keep-sorted
50
+ export const ciIcons: Record<string, string> = {
51
+ 'Cloudflare Pages': 'devicon:cloudflare',
52
+ 'Cloudflare Workers': 'devicon:cloudflareworkers',
53
+ 'EdgeOne': 'https://edgeone.ai/favicon.ico',
54
+ 'GitHub Actions': 'ri:github-fill',
55
+ 'Netlify CI': 'catppuccin:netlify',
56
+ 'Vercel': 'ri:vercel-fill',
57
+ }
58
+
59
+ /** 主域名图标映射 */
60
+ // @keep-sorted
61
+ const mainDomainIcons: Record<string, string> = {
62
+ 'bilibili.com': 'ri:bilibili-fill',
63
+ 'creativecommons.org': 'ri:creative-commons-line',
64
+ 'feishu.cn': 'icon-park-outline:new-lark',
65
+ 'github.com': 'ri:github-fill',
66
+ 'github.io': 'ri:github-fill',
67
+ 'google.cn': 'ri:google-fill',
68
+ 'google.com': 'ri:google-fill',
69
+ 'jd.com': 'arcticons:jd-sports',
70
+ 'larkoffice.com': 'icon-park-outline:new-lark',
71
+ 'microsoft.com': 'ri:microsoft-fill',
72
+ 'netlify.app': 'simple-icons:netlify',
73
+ 'pages.dev': 'simple-icons:cloudflare',
74
+ 'qq.com': 'ri:qq-fill',
75
+ 'taobao.com': 'ri:taobao-fill',
76
+ 'thisis.host': 'tabler:star-filled',
77
+ 'tmall.com': 'ri:taobao-fill',
78
+ 'v2ex.com': 'simple-icons:v2ex',
79
+ 'vercel.app': 'simple-icons:vercel',
80
+ 'zabaur.app': 'tabler:square-letter-z-filled',
81
+ 'zhihu.com': 'ri:zhihu-line',
82
+ }
83
+
84
+ /** 专门域名图标映射,优先级高于主域名图标 */
85
+ // @keep-sorted
86
+ export const domainIcons: Record<string, string> = {
87
+ 'developer.mozilla.org': 'simple-icons:mdnwebdocs',
88
+ 'h5.qzone.qq.com': 'simple-icons:qzone',
89
+ 'mp.weixin.qq.com': 'ri:wechat-fill',
90
+ }
91
+
92
+ export function getDomainIcon(url: string) {
93
+ const domain = getDomain(url)
94
+ const mainDomain = getMainDomain(url, true)
95
+ if (domain in domainIcons)
96
+ return domainIcons[domain]
97
+ return mainDomainIcons[mainDomain]
98
+ }
99
+
100
+ /** 文件名后缀图标映射,优先级高于代码块语言图标映射 */
101
+ // @keep-sorted
102
+ const file2icon: Record<string, string> = {
103
+ '.babelrc.js': 'catppuccin:babel',
104
+ '.babelrc': 'catppuccin:babel',
105
+ '.crt': 'catppuccin:certificate',
106
+ '.editorconfig': 'catppuccin:editorconfig',
107
+ '.env': 'catppuccin:env',
108
+ '.gitattributes': 'catppuccin:git',
109
+ '.gitconfig': 'catppuccin:git',
110
+ '.gitignore': 'catppuccin:git',
111
+ '.gitkeep': 'catppuccin:git',
112
+ '.gitlab-ci.yml': 'catppuccin:gitlab',
113
+ '.gitmodules': 'catppuccin:git',
114
+ '.key': 'catppuccin:key',
115
+ '.npmrc': 'catppuccin:npm',
116
+ '.patch': 'catppuccin:git',
117
+ '.prettierrc': 'catppuccin:prettier',
118
+ 'astro.config.mjs': 'catppuccin:astro-config',
119
+ 'CHANGELOG.md': 'catppuccin:changelog',
120
+ 'CODE_OF_CONDUCT.md': 'catppuccin:code-of-conduct',
121
+ 'CODEOWNERS': 'catppuccin:codeowners',
122
+ 'CONTRIBUTING.md': 'catppuccin:contributing',
123
+ 'docker-compose.yml': 'catppuccin:docker-compose',
124
+ 'eslint.config.js': 'catppuccin:eslint',
125
+ 'eslint.config.mjs': 'catppuccin:eslint',
126
+ 'LICENSE': 'catppuccin:license',
127
+ 'netlify.toml': 'catppuccin:netlify',
128
+ 'next.config.ts': 'catppuccin:next',
129
+ 'nuxt.config.ts': 'catppuccin:nuxt',
130
+ 'package.json': 'catppuccin:package-json',
131
+ 'pnpm-workspace.yaml': 'catppuccin:pnpm',
132
+ 'postcss.config.js': 'catppuccin:postcss',
133
+ 'prettier.config.js': 'catppuccin:prettier',
134
+ 'pyproject.toml': 'catppuccin:python-config',
135
+ 'README.md': 'catppuccin:readme',
136
+ 'renovate.json': 'catppuccin:renovate',
137
+ 'requirements.txt': 'catppuccin:python-config',
138
+ 'robots.txt': 'catppuccin:robots',
139
+ 'rollup.config.js': 'catppuccin:rollup',
140
+ 'SECURITY.md': 'catppuccin:security',
141
+ 'stylelint.config.js': 'catppuccin:stylelint',
142
+ 'stylelint.config.mjs': 'catppuccin:stylelint',
143
+ 'tailwind.config.js': 'catppuccin:tailwind',
144
+ 'tsconfig.json': 'catppuccin:typescript-config',
145
+ 'verccel.json': 'catppuccin:vercel',
146
+ 'vite.config.js': 'catppuccin:vite',
147
+ 'vite.config.ts': 'catppuccin:vite',
148
+ 'webpack.config.js': 'catppuccin:webpack',
149
+ 'yarn.lock': 'catppuccin:yarn',
150
+ }
151
+
152
+ export function getFileIcon(filename?: string) {
153
+ if (!filename)
154
+ return undefined
155
+ const extension = Object.keys(file2icon).find(ext => filename.endsWith(ext))
156
+ return extension ? file2icon[extension] : undefined
157
+ }
158
+
159
+ /**
160
+ * 代码块语言简写或别名到 Catppuccin 图标库中的语言名映射
161
+ *
162
+ * 将 `blogConfig.shiki.langs` 的部分后缀名简写
163
+ * 转换为代码块语言对应的 Iconify Catppuccin 图标
164
+ */
165
+ // @keep-sorted
166
+ const ext2lang: Record<string, string> = {
167
+ 'bat': 'catppuccin:batch',
168
+ 'c': 'catppuccin:c',
169
+ 'c++': 'catppuccin:cpp',
170
+ 'cpp': 'catppuccin:cpp',
171
+ 'css': 'catppuccin:css',
172
+ 'diff': 'catppuccin:diff',
173
+ 'dockerfile': 'catppuccin:docker',
174
+ 'gql': 'catppuccin:graphql',
175
+ 'hs': 'catppuccin:haskell',
176
+ 'html': 'catppuccin:html',
177
+ 'ini': 'catppuccin:properties',
178
+ 'java': 'catppuccin:java',
179
+ 'js': 'catppuccin:javascript',
180
+ 'json': 'catppuccin:json',
181
+ 'jsonc': 'catppuccin:json',
182
+ 'jsx': 'catppuccin:javascript-react',
183
+ 'log': 'catppuccin:log',
184
+ 'make': 'catppuccin:makefile',
185
+ 'makefile': 'catppuccin:makefile',
186
+ 'matlab': 'catppuccin:matlab',
187
+ 'md': 'catppuccin:markdown',
188
+ 'mdc': 'catppuccin:markdown',
189
+ 'mdx': 'catppuccin:markdown',
190
+ 'mermaid': 'catppuccin:mermaid',
191
+ 'mmd': 'catppuccin:mermaid',
192
+ 'powershell': 'catppuccin:powershell',
193
+ 'ps': 'catppuccin:powershell',
194
+ 'ps1': 'catppuccin:powershell',
195
+ 'py': 'catppuccin:python',
196
+ 'python': 'catppuccin:python',
197
+ 'rs': 'catppuccin:rust',
198
+ 'scss': 'catppuccin:sass',
199
+ 'sh': 'catppuccin:bash',
200
+ 'shell': 'catppuccin:bash',
201
+ 'shellscript': 'catppuccin:bash',
202
+ 'sql': 'catppuccin:database',
203
+ 'ssh-config': 'catppuccin:properties',
204
+ 'ssh': 'catppuccin:properties',
205
+ 'toml': 'catppuccin:toml',
206
+ 'ts': 'catppuccin:typescript',
207
+ 'tsx': 'catppuccin:typescript-react',
208
+ 'vb': 'catppuccin:visual-studio',
209
+ 'vue': 'catppuccin:vue',
210
+ 'xml': 'catppuccin:xml',
211
+ 'yaml': 'catppuccin:yaml',
212
+ 'yml': 'catppuccin:yaml',
213
+ 'zsh': 'catppuccin:bash',
214
+ }
215
+
216
+ export function getLangIcon(extension = 'file') {
217
+ return ext2lang[extension] ?? 'catppuccin:file'
218
+ }
@@ -0,0 +1,52 @@
1
+ import { fromUrl, parseDomain, ParseResultType } from 'parse-domain'
2
+ import { isPathFile } from 'site-config-stack/urls'
3
+
4
+ const domainTip: Record<string, string> = {
5
+ 'github.io': 'GitHub Pages 域名',
6
+ 'netlify.app': 'Netlify 域名',
7
+ 'pages.dev': 'Cloudflare 域名',
8
+ 'thisis.host': '纸鹿提供的域名',
9
+ 'vercel.app': 'Vercel 域名',
10
+ 'zeabur.app': 'Zeabur 域名',
11
+ }
12
+
13
+ export function getDomain(url: string) {
14
+ const domain = fromUrl(url)
15
+ return typeof domain === 'symbol' ? url : domain
16
+ }
17
+
18
+ export function getMainDomain(url: string, useIcann?: boolean) {
19
+ const hostname = getDomain(url)
20
+ const parseResult = parseDomain(hostname)
21
+ if (parseResult.type !== ParseResultType.Listed)
22
+ return hostname
23
+ const { domain, topLevelDomains } = useIcann ? parseResult.icann : parseResult
24
+ return `${domain}.${topLevelDomains.join('.')}`
25
+ }
26
+
27
+ export function getDomainType(mainDomain: string) {
28
+ return domainTip[mainDomain]
29
+ }
30
+
31
+ const githubUsernameRegex = /github\.com\/([a-zA-Z0-9-]+)(?:\/[^/]+)?(\/?)$/
32
+
33
+ export function getGithubUsername(url?: string) {
34
+ if (!url)
35
+ return ''
36
+ return url.match(githubUsernameRegex)?.[1] ?? ''
37
+ }
38
+
39
+ export function isExtLink(url?: string) {
40
+ return url
41
+ ? url.includes(':') || url.startsWith('//') || !!isPathFile(url)
42
+ : false
43
+ }
44
+
45
+ export function safelyDecodeUriComponent(str: string) {
46
+ try {
47
+ return decodeURIComponent(str)
48
+ }
49
+ catch {
50
+ return str
51
+ }
52
+ }
@@ -0,0 +1,88 @@
1
+ import { toArray } from '@vueuse/core'
2
+ import { escape, escapeRegExp } from 'es-toolkit/string'
3
+
4
+ // @keep-sorted
5
+ const promptLanguageMap: Record<string, string> = {
6
+ '#': 'sh',
7
+ '$': 'sh',
8
+ 'CMD': 'bat',
9
+ 'PS': 'powershell',
10
+ }
11
+
12
+ export function formatNumber(num?: number) {
13
+ if (typeof num !== 'number')
14
+ return ''
15
+ const intervals = [
16
+ { label: '万亿', threshold: 1e12 },
17
+ { label: '亿', threshold: 1e8 },
18
+ { label: '万', threshold: 1e4 },
19
+ ]
20
+ for (const interval of intervals) {
21
+ if (num >= interval.threshold)
22
+ return `${(num / interval.threshold).toFixed(2)}${interval.label}`
23
+ }
24
+ return num.toString()
25
+ }
26
+
27
+ interface FormatBytesOptions {
28
+ decimals?: number
29
+ binary?: boolean
30
+ unitSeparator?: string
31
+ }
32
+
33
+ export function formatBytes(bytes: number, options: FormatBytesOptions = {}) {
34
+ const {
35
+ decimals = 2,
36
+ binary = true,
37
+ unitSeparator = ' ',
38
+ } = options
39
+
40
+ if (bytes === 0)
41
+ return `0${unitSeparator}Bytes`
42
+
43
+ const base = binary ? 1024 : 1000
44
+ const units = binary
45
+ ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
46
+ : ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
47
+
48
+ const i = Math.floor(Math.log(bytes) / Math.log(base))
49
+ const value = Number.parseFloat((bytes / base ** i).toFixed(decimals))
50
+
51
+ return `${value}${unitSeparator}${units[i]}`
52
+ }
53
+
54
+ export function getPromptLanguage(prompt: string | boolean) {
55
+ if (typeof prompt === 'boolean')
56
+ return 'text'
57
+ for (const promptPrefix in promptLanguageMap) {
58
+ if (prompt.startsWith(promptPrefix))
59
+ return promptLanguageMap[promptPrefix] ?? 'text'
60
+ }
61
+ return 'text'
62
+ }
63
+
64
+ export function joinWith(strings: (string | undefined)[], separator = '\n') {
65
+ return strings.filter(Boolean).join(separator)
66
+ }
67
+
68
+ export function highlightHtml(text: string, words: string | string[] | undefined, className?: string) {
69
+ const validTerms = toArray(words)
70
+ .filter((t): t is string => !!t?.trim())
71
+ .map(t => t.toLowerCase())
72
+
73
+ const highlightRegex = new RegExp(`(${Array.from(validTerms, escapeRegExp).join('|')})`, 'gi')
74
+
75
+ return text
76
+ .split(highlightRegex)
77
+ .map(part => part && validTerms.includes(part.toLowerCase())
78
+ ? `<mark${className ? ` class="${className}"` : ''}>${escape(part)}</mark>`
79
+ : escape(part))
80
+ .join('')
81
+ .replace(/\n+/g, '<br>')
82
+ }
83
+
84
+ export function removeHtmlTags(str?: string) {
85
+ if (typeof str !== 'string')
86
+ return ''
87
+ return str.replace(/<[^>]+(>|$)/g, '')
88
+ }