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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/README.zh-CN.md +297 -0
- package/app/app.config.ts +110 -0
- package/app/app.vue +5 -0
- package/app/assets/css/_variable.scss +8 -0
- package/app/assets/css/animation.scss +50 -0
- package/app/assets/css/article.scss +152 -0
- package/app/assets/css/color.scss +58 -0
- package/app/assets/css/font.scss +35 -0
- package/app/assets/css/main.scss +230 -0
- package/app/assets/css/reusable.scss +186 -0
- package/app/assets/icons/solar-rewind-back-bold-duotone.svg +1 -0
- package/app/assets/icons/solar-rewind-forward-bold-duotone.svg +1 -0
- package/app/components/blog/BlogAside.vue +63 -0
- package/app/components/blog/BlogFooter.vue +61 -0
- package/app/components/blog/BlogHeader.global.vue +146 -0
- package/app/components/blog/BlogPanel.vue +79 -0
- package/app/components/blog/BlogSidebar.vue +160 -0
- package/app/components/blog/BlogWidget.vue +134 -0
- package/app/components/blog/Mask.vue +42 -0
- package/app/components/blog/SkipToContent.vue +22 -0
- package/app/components/blog/ThemeToggle.vue +50 -0
- package/app/components/content/Alert.vue +96 -0
- package/app/components/content/Badge.vue +90 -0
- package/app/components/content/Blur.vue +22 -0
- package/app/components/content/CardList.vue +32 -0
- package/app/components/content/Chat.vue +79 -0
- package/app/components/content/Copy.vue +134 -0
- package/app/components/content/EmojiClock.vue +53 -0
- package/app/components/content/FeedCard.vue +212 -0
- package/app/components/content/FeedGroup.vue +130 -0
- package/app/components/content/Folding.vue +69 -0
- package/app/components/content/Key.vue +164 -0
- package/app/components/content/LinkBanner.vue +59 -0
- package/app/components/content/LinkCard.vue +74 -0
- package/app/components/content/MdTitle.vue +26 -0
- package/app/components/content/Mermaid.vue +133 -0
- package/app/components/content/MusicScore.vue +178 -0
- package/app/components/content/Pic.vue +55 -0
- package/app/components/content/Poetry.vue +41 -0
- package/app/components/content/ProseA.vue +40 -0
- package/app/components/content/ProseCode.vue +95 -0
- package/app/components/content/ProsePre.vue +291 -0
- package/app/components/content/ProseTable.vue +70 -0
- package/app/components/content/Quote.vue +51 -0
- package/app/components/content/Tab.vue +100 -0
- package/app/components/content/Timeline.vue +73 -0
- package/app/components/content/Tip.vue +52 -0
- package/app/components/content/VideoEmbed.vue +127 -0
- package/app/components/partial/Button.vue +98 -0
- package/app/components/partial/DlGroup.vue +69 -0
- package/app/components/partial/Dropdown.vue +54 -0
- package/app/components/partial/Error.vue +70 -0
- package/app/components/partial/Expand.vue +51 -0
- package/app/components/partial/IconNavList.vue +42 -0
- package/app/components/partial/Pagination.vue +113 -0
- package/app/components/partial/RadioGroup.vue +77 -0
- package/app/components/partial/Secret.vue +23 -0
- package/app/components/partial/Slider.vue +96 -0
- package/app/components/partial/Toggle.vue +70 -0
- package/app/components/popover/Lightbox.vue +77 -0
- package/app/components/popover/Search.vue +214 -0
- package/app/components/popover/SearchItem.vue +64 -0
- package/app/components/post/Archive.vue +104 -0
- package/app/components/post/Article.vue +138 -0
- package/app/components/post/Comment.vue +275 -0
- package/app/components/post/Excerpt.vue +66 -0
- package/app/components/post/OrderToggle.vue +101 -0
- package/app/components/post/PostFooter.vue +80 -0
- package/app/components/post/PostHeader.vue +147 -0
- package/app/components/post/PostSurround.vue +111 -0
- package/app/components/post/Slide.vue +215 -0
- package/app/components/util/Date.vue +62 -0
- package/app/components/util/HydrateSafe.vue +10 -0
- package/app/components/util/Img.vue +38 -0
- package/app/components/util/Link.vue +11 -0
- package/app/components/widget/BlogLog.vue +27 -0
- package/app/components/widget/BlogStats.vue +39 -0
- package/app/components/widget/BlogTech.vue +65 -0
- package/app/components/widget/Empty.vue +3 -0
- package/app/components/widget/Toc.vue +129 -0
- package/app/composables/useArticle.ts +118 -0
- package/app/composables/useAvoid.ts +87 -0
- package/app/composables/useCopy.ts +21 -0
- package/app/composables/usePagination.ts +70 -0
- package/app/composables/useShiki.ts +129 -0
- package/app/composables/useToc.ts +57 -0
- package/app/composables/useWidgets.ts +56 -0
- package/app/error.vue +34 -0
- package/app/layouts/default.vue +66 -0
- package/app/pages/[...slug].vue +59 -0
- package/app/pages/archive.vue +207 -0
- package/app/pages/index.vue +83 -0
- package/app/pages/link.vue +65 -0
- package/app/pages/preview.vue +59 -0
- package/app/plugins/easter-egg.ts +23 -0
- package/app/plugins/init.ts +6 -0
- package/app/plugins/tippy.ts +22 -0
- package/app/shiki.config.ts +8 -0
- package/app/stores/layout.ts +34 -0
- package/app/stores/search.ts +37 -0
- package/app/types/article.ts +16 -0
- package/app/types/feed.ts +1 -0
- package/app/types/index.ts +14 -0
- package/app/types/modal.ts +16 -0
- package/app/types/nav.ts +11 -0
- package/app/utils/anim.ts +37 -0
- package/app/utils/img.ts +83 -0
- package/config/app.ts +88 -0
- package/config/content.d.mts +1 -0
- package/config/content.mjs +69 -0
- package/config/content.ts +91 -0
- package/config/define.mjs +21 -0
- package/config/define.ts +23 -0
- package/config/feed.ts +42 -0
- package/config/feeds.empty.ts +4 -0
- package/config/index.d.mts +1 -0
- package/config/index.mjs +6 -0
- package/config/index.ts +6 -0
- package/config/public.ts +71 -0
- package/config/schema.d.mts +1 -0
- package/config/schema.mjs +103 -0
- package/config/schema.ts +129 -0
- package/config/server.ts +35 -0
- package/img/index.d.mts +1 -0
- package/img/index.mjs +78 -0
- package/img/index.ts +8 -0
- package/modules/clarity-config/anti-mirror-client.ts +11 -0
- package/modules/clarity-config/index.ts +324 -0
- package/nuxt.config.ts +225 -0
- package/package.json +148 -0
- package/public/assets/atom.css +274 -0
- package/public/assets/atom.xsl +110 -0
- package/public/fonts/AlimamaFangYuanTi.woff2 +0 -0
- package/remark-plugins/rehype-meta-slots.d.mts +1 -0
- package/remark-plugins/rehype-meta-slots.mjs +49 -0
- package/remark-plugins/rehype-meta-slots.ts +58 -0
- package/remark-plugins/remark-code-component.d.mts +1 -0
- package/remark-plugins/remark-code-component.mjs +25 -0
- package/remark-plugins/remark-code-component.ts +41 -0
- package/server/api/stats.get.ts +109 -0
- package/server/routes/atom.xml.get.ts +101 -0
- package/server/routes/subscriptions.opml.get.ts +70 -0
- package/server/utils/clarity.ts +11 -0
- package/shared/types/content.ts +30 -0
- package/shared/utils/clarity.ts +34 -0
- package/shared/utils/icon.ts +218 -0
- package/shared/utils/link.ts +52 -0
- package/shared/utils/str.ts +88 -0
- 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
|
+
}
|