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
package/config/public.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClarityAuthor,
|
|
3
|
+
ClarityArticleConfig,
|
|
4
|
+
ClarityChangelogEntry,
|
|
5
|
+
ClarityConfig,
|
|
6
|
+
ClarityIntegrationsConfig,
|
|
7
|
+
ClaritySiteConfig,
|
|
8
|
+
} from './schema'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 由 clarity.config.ts 注入 appConfig 的客户端可见配置。
|
|
12
|
+
*
|
|
13
|
+
* 可见性约定(详见 docs/config-api-audit.md):
|
|
14
|
+
* - 只有客户端渲染确实需要的字段进入 appConfig;
|
|
15
|
+
* - feed、完整 stats、features 与 site.author.email 等服务端/构建期字段
|
|
16
|
+
* 改由 Nitro 私有 runtimeConfig 提供(config/server.ts),不进入客户端 bundle;
|
|
17
|
+
* - 仅构建期使用的字段(integrations.scripts、article.hidePostPrefix、
|
|
18
|
+
* article.robotsNotIndex)由模块直接消费,同样不进入 appConfig。
|
|
19
|
+
* 这些配置允许进入客户端 bundle,不包含任何密钥(secret 只允许 runtimeConfig)。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** appConfig 中暴露的 integrations 子集:scripts 仅构建期注入 <head> */
|
|
23
|
+
export type ClarityPublicIntegrationsConfig = Omit<ClarityIntegrationsConfig, 'scripts'>
|
|
24
|
+
|
|
25
|
+
/** appConfig 中暴露的 site 子集:author.email 仅用于服务端 feed/meta 输出 */
|
|
26
|
+
export type ClarityPublicSiteConfig = Omit<ClaritySiteConfig, 'author'> & {
|
|
27
|
+
author: Omit<ClarityAuthor, 'email'>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** appConfig 中暴露的 article 子集:其余字段仅构建期使用 */
|
|
31
|
+
export type ClarityPublicArticleConfig = Pick<ClarityArticleConfig, 'categories' | 'order'>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* appConfig 中暴露的 stats 子集:由 includePaths 派生的展示事实,
|
|
35
|
+
* 决定 BlogStats 标签使用「文章字数」还是「总字数」,不透出完整匹配规则。
|
|
36
|
+
*/
|
|
37
|
+
export interface ClarityPublicStatsConfig {
|
|
38
|
+
postsOnly: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ClarityPublicConfig {
|
|
42
|
+
site: ClarityPublicSiteConfig
|
|
43
|
+
article: ClarityPublicArticleConfig
|
|
44
|
+
stats: ClarityPublicStatsConfig
|
|
45
|
+
integrations: ClarityPublicIntegrationsConfig
|
|
46
|
+
changelog: ClarityChangelogEntry[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function toPublicClarityConfig(config: ClarityConfig): ClarityPublicConfig {
|
|
50
|
+
return {
|
|
51
|
+
site: {
|
|
52
|
+
...config.site,
|
|
53
|
+
author: {
|
|
54
|
+
name: config.site.author.name,
|
|
55
|
+
avatar: config.site.author.avatar,
|
|
56
|
+
homepage: config.site.author.homepage,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
article: {
|
|
60
|
+
categories: config.article.categories,
|
|
61
|
+
order: config.article.order,
|
|
62
|
+
},
|
|
63
|
+
stats: {
|
|
64
|
+
postsOnly: config.stats.includePaths.length > 0,
|
|
65
|
+
},
|
|
66
|
+
integrations: config.integrations.twikoo
|
|
67
|
+
? { twikoo: config.integrations.twikoo }
|
|
68
|
+
: {},
|
|
69
|
+
changelog: config.changelog,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './schema.ts'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* clarity.config 运行时 Schema(JS 实现)。
|
|
5
|
+
*
|
|
6
|
+
* 类型真源在同目录 schema.ts(含完整 JSDoc),
|
|
7
|
+
* 本文件供 Node 原生 TS 剥离受限场景(node_modules 内)
|
|
8
|
+
* 与运行时链使用;修改 schema.ts 后需同步本文件。
|
|
9
|
+
*
|
|
10
|
+
* Schema 一律使用 strictObject:未知字段直接报错而非静默丢弃。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const clarityHeadScriptSchema = z.record(z.string(), z.union([
|
|
14
|
+
z.string(),
|
|
15
|
+
z.number(),
|
|
16
|
+
z.boolean(),
|
|
17
|
+
]))
|
|
18
|
+
|
|
19
|
+
export const clarityAuthorSchema = z.strictObject({
|
|
20
|
+
name: z.string().min(1),
|
|
21
|
+
avatar: z.string().optional(),
|
|
22
|
+
email: z.string().optional(),
|
|
23
|
+
homepage: z.string().optional(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const claritySiteSchema = z.strictObject({
|
|
27
|
+
title: z.string().min(1),
|
|
28
|
+
subtitle: z.string().optional(),
|
|
29
|
+
description: z.string().min(1),
|
|
30
|
+
/** 站点规范 URL(以 / 结尾,用于 new URL() 相对路径解析) */
|
|
31
|
+
url: z.string().url().refine(
|
|
32
|
+
value => value.endsWith('/'),
|
|
33
|
+
'站点 URL 必须以 / 结尾(例如 https://example.com/)',
|
|
34
|
+
),
|
|
35
|
+
language: z.string().default('zh-CN'),
|
|
36
|
+
timezone: z.string().default('Asia/Shanghai'),
|
|
37
|
+
established: z.string().optional(),
|
|
38
|
+
favicon: z.string().default('/favicon.svg'),
|
|
39
|
+
author: clarityAuthorSchema,
|
|
40
|
+
copyright: z.strictObject({
|
|
41
|
+
abbr: z.string().optional(),
|
|
42
|
+
name: z.string().optional(),
|
|
43
|
+
url: z.string().optional(),
|
|
44
|
+
}).optional(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export const clarityArticleSchema = z.strictObject({
|
|
48
|
+
defaultCategory: z.string().default('未分类'),
|
|
49
|
+
categories: z.record(z.string(), z.strictObject({
|
|
50
|
+
icon: z.string().optional(),
|
|
51
|
+
color: z.string().optional(),
|
|
52
|
+
})).default({}),
|
|
53
|
+
types: z.record(z.string(), z.looseObject({})).default({ tech: {} }),
|
|
54
|
+
order: z.record(z.string(), z.string()).default({ date: '创建日期', updated: '更新日期' }),
|
|
55
|
+
hidePostPrefix: z.boolean().default(true),
|
|
56
|
+
robotsNotIndex: z.array(z.string()).default([]),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export const clarityFeedSchema = z.strictObject({
|
|
60
|
+
limit: z.number().int().positive().default(50),
|
|
61
|
+
enableStyle: z.boolean().default(true),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export const clarityStatsSchema = z.strictObject({
|
|
65
|
+
includePaths: z.array(z.string()).default([]),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export const clarityIntegrationsSchema = z.strictObject({
|
|
69
|
+
twikoo: z.strictObject({
|
|
70
|
+
envId: z.string(),
|
|
71
|
+
preload: z.string().optional(),
|
|
72
|
+
}).optional(),
|
|
73
|
+
scripts: z.array(clarityHeadScriptSchema).default([]),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const clarityAntiMirrorSchema = z.union([
|
|
77
|
+
z.boolean(),
|
|
78
|
+
z.strictObject({
|
|
79
|
+
blacklist: z.array(z.string()).default([]),
|
|
80
|
+
}),
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
export const clarityFeaturesSchema = z.strictObject({
|
|
84
|
+
atom: z.boolean().default(true),
|
|
85
|
+
opml: z.boolean().default(true),
|
|
86
|
+
stats: z.boolean().default(true),
|
|
87
|
+
antiMirror: clarityAntiMirrorSchema.default(false),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export const clarityChangelogEntrySchema = z.strictObject({
|
|
91
|
+
date: z.string(),
|
|
92
|
+
text: z.string(),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export const clarityConfigSchema = z.strictObject({
|
|
96
|
+
site: claritySiteSchema,
|
|
97
|
+
article: clarityArticleSchema.prefault({}),
|
|
98
|
+
feed: clarityFeedSchema.prefault({}),
|
|
99
|
+
stats: clarityStatsSchema.prefault({}),
|
|
100
|
+
integrations: clarityIntegrationsSchema.prefault({}),
|
|
101
|
+
features: clarityFeaturesSchema.prefault({}),
|
|
102
|
+
changelog: z.array(clarityChangelogEntrySchema).default([]),
|
|
103
|
+
})
|
package/config/schema.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema 一律使用 strictObject:
|
|
5
|
+
* 未知字段直接报错,而不是被静默丢弃(防止拼写错误悄悄失效)。
|
|
6
|
+
* 唯一例外:article.types 的值使用 looseObject,作为版式扩展位。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 注入到 <head> 的脚本(统计、评论等第三方脚本) */
|
|
10
|
+
export const clarityHeadScriptSchema = z.record(z.string(), z.union([
|
|
11
|
+
z.string(),
|
|
12
|
+
z.number(),
|
|
13
|
+
z.boolean(),
|
|
14
|
+
]))
|
|
15
|
+
|
|
16
|
+
export const clarityAuthorSchema = z.strictObject({
|
|
17
|
+
name: z.string().min(1),
|
|
18
|
+
avatar: z.string().optional(),
|
|
19
|
+
email: z.string().optional(),
|
|
20
|
+
homepage: z.string().optional(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const claritySiteSchema = z.strictObject({
|
|
24
|
+
title: z.string().min(1),
|
|
25
|
+
subtitle: z.string().optional(),
|
|
26
|
+
description: z.string().min(1),
|
|
27
|
+
/** 站点规范 URL(以 / 结尾,用于 new URL() 相对路径解析) */
|
|
28
|
+
url: z.string().url().refine(
|
|
29
|
+
value => value.endsWith('/'),
|
|
30
|
+
'站点 URL 必须以 / 结尾(例如 https://example.com/)',
|
|
31
|
+
),
|
|
32
|
+
language: z.string().default('zh-CN'),
|
|
33
|
+
timezone: z.string().default('Asia/Shanghai'),
|
|
34
|
+
/** 建站日期 */
|
|
35
|
+
established: z.string().optional(),
|
|
36
|
+
favicon: z.string().default('/favicon.svg'),
|
|
37
|
+
author: clarityAuthorSchema,
|
|
38
|
+
copyright: z.strictObject({
|
|
39
|
+
abbr: z.string().optional(),
|
|
40
|
+
name: z.string().optional(),
|
|
41
|
+
url: z.string().optional(),
|
|
42
|
+
}).optional(),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const clarityArticleSchema = z.strictObject({
|
|
46
|
+
defaultCategory: z.string().default('未分类'),
|
|
47
|
+
categories: z.record(z.string(), z.strictObject({
|
|
48
|
+
icon: z.string().optional(),
|
|
49
|
+
color: z.string().optional(),
|
|
50
|
+
})).default({}),
|
|
51
|
+
/** 文章版式,首个为默认版式 */
|
|
52
|
+
types: z.record(z.string(), z.looseObject({})).default({ tech: {} }),
|
|
53
|
+
/** 分类排序方式,键为排序字段,值为显示名称 */
|
|
54
|
+
order: z.record(z.string(), z.string()).default({ date: '创建日期', updated: '更新日期' }),
|
|
55
|
+
/** 隐藏基于文件路由(非自定义链接)URL 中的 /post 路径前缀,仅模块构建期使用 */
|
|
56
|
+
hidePostPrefix: z.boolean().default(true),
|
|
57
|
+
/** 禁止搜索引擎收录的路径,仅模块构建期使用 */
|
|
58
|
+
robotsNotIndex: z.array(z.string()).default([]),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
export const clarityFeedSchema = z.strictObject({
|
|
62
|
+
/** 订阅源最大文章数量 */
|
|
63
|
+
limit: z.number().int().positive().default(50),
|
|
64
|
+
/** 订阅源是否启用 XSLT 样式 */
|
|
65
|
+
enableStyle: z.boolean().default(true),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export const clarityStatsSchema = z.strictObject({
|
|
69
|
+
/**
|
|
70
|
+
* 统计范围,匹配 content 下不含扩展名的路径(stem);空数组统计全部内容
|
|
71
|
+
* 使用 SQL LIKE 语法: % 匹配任意长度字符, _ 匹配单个字符
|
|
72
|
+
*/
|
|
73
|
+
includePaths: z.array(z.string()).default([]),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const clarityIntegrationsSchema = z.strictObject({
|
|
77
|
+
/** Twikoo 评论系统,未配置时不渲染评论区 */
|
|
78
|
+
twikoo: z.strictObject({
|
|
79
|
+
envId: z.string(),
|
|
80
|
+
/** 预连接地址,默认使用 envId */
|
|
81
|
+
preload: z.string().optional(),
|
|
82
|
+
}).optional(),
|
|
83
|
+
/** 向 <head> 中添加的脚本,仅模块构建期注入,不进入 appConfig */
|
|
84
|
+
scripts: z.array(clarityHeadScriptSchema).default([]),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
export const clarityAntiMirrorSchema = z.union([
|
|
88
|
+
z.boolean(),
|
|
89
|
+
z.strictObject({
|
|
90
|
+
/** 需要跳转回源站的镜像站域名后缀 */
|
|
91
|
+
blacklist: z.array(z.string()).default([]),
|
|
92
|
+
}),
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
export const clarityFeaturesSchema = z.strictObject({
|
|
96
|
+
atom: z.boolean().default(true),
|
|
97
|
+
opml: z.boolean().default(true),
|
|
98
|
+
stats: z.boolean().default(true),
|
|
99
|
+
antiMirror: clarityAntiMirrorSchema.default(false),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
export const clarityChangelogEntrySchema = z.strictObject({
|
|
103
|
+
date: z.string(),
|
|
104
|
+
text: z.string(),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
export const clarityConfigSchema = z.strictObject({
|
|
108
|
+
site: claritySiteSchema,
|
|
109
|
+
article: clarityArticleSchema.prefault({}),
|
|
110
|
+
feed: clarityFeedSchema.prefault({}),
|
|
111
|
+
stats: clarityStatsSchema.prefault({}),
|
|
112
|
+
integrations: clarityIntegrationsSchema.prefault({}),
|
|
113
|
+
features: clarityFeaturesSchema.prefault({}),
|
|
114
|
+
/** 更新日志组件数据,按时间倒序展示 */
|
|
115
|
+
changelog: z.array(clarityChangelogEntrySchema).default([]),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export type ClarityHeadScript = z.output<typeof clarityHeadScriptSchema>
|
|
119
|
+
export type ClarityAuthor = z.output<typeof clarityAuthorSchema>
|
|
120
|
+
export type ClaritySiteConfig = z.output<typeof claritySiteSchema>
|
|
121
|
+
export type ClarityArticleConfig = z.output<typeof clarityArticleSchema>
|
|
122
|
+
export type ClarityFeedConfig = z.output<typeof clarityFeedSchema>
|
|
123
|
+
export type ClarityStatsConfig = z.output<typeof clarityStatsSchema>
|
|
124
|
+
export type ClarityIntegrationsConfig = z.output<typeof clarityIntegrationsSchema>
|
|
125
|
+
export type ClarityAntiMirrorConfig = z.output<typeof clarityAntiMirrorSchema>
|
|
126
|
+
export type ClarityFeaturesConfig = z.output<typeof clarityFeaturesSchema>
|
|
127
|
+
export type ClarityChangelogEntry = z.output<typeof clarityChangelogEntrySchema>
|
|
128
|
+
export type ClarityConfig = z.output<typeof clarityConfigSchema>
|
|
129
|
+
export type ClarityConfigInput = z.input<typeof clarityConfigSchema>
|
package/config/server.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClarityConfig,
|
|
3
|
+
ClarityFeedConfig,
|
|
4
|
+
ClarityFeaturesConfig,
|
|
5
|
+
ClaritySiteConfig,
|
|
6
|
+
ClarityStatsConfig,
|
|
7
|
+
} from './schema'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 注入 Nitro 私有 runtimeConfig(非 public)的服务端配置形状。
|
|
11
|
+
*
|
|
12
|
+
* 这些字段只允许在 server/ 侧通过 useClarityServerConfig() 读取:
|
|
13
|
+
* - site 完整形状(含 author.email,用于 Atom/OPML 等公开元数据输出);
|
|
14
|
+
* - feed / stats 完整配置;
|
|
15
|
+
* - features 中需要运行时路由守卫的开关(antiMirror 仅构建期注入脚本,不需要运行时值)。
|
|
16
|
+
*/
|
|
17
|
+
export interface ClarityServerConfig {
|
|
18
|
+
site: ClaritySiteConfig
|
|
19
|
+
feed: ClarityFeedConfig
|
|
20
|
+
stats: ClarityStatsConfig
|
|
21
|
+
features: Pick<ClarityFeaturesConfig, 'atom' | 'opml' | 'stats'>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function toServerClarityConfig(config: ClarityConfig): ClarityServerConfig {
|
|
25
|
+
return {
|
|
26
|
+
site: config.site,
|
|
27
|
+
feed: config.feed,
|
|
28
|
+
stats: config.stats,
|
|
29
|
+
features: {
|
|
30
|
+
atom: config.features.atom,
|
|
31
|
+
opml: config.features.opml,
|
|
32
|
+
stats: config.features.stats,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
package/img/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.ts'
|
package/img/index.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* img 工具运行时实现(JS)。
|
|
3
|
+
* 类型真源在同目录 img.ts(enum 已转为 const 数值对象)。
|
|
4
|
+
* 消费项目通过 `clarity-theme/img` 使用;Node 原生 TS 剥离
|
|
5
|
+
* 不允许 node_modules 内的 TS 文件,因此运行时必须是 .mjs。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// @keep-sorted
|
|
9
|
+
const services = {
|
|
10
|
+
baidu: 'https://image.baidu.com/search/down?url=',
|
|
11
|
+
/** https://webp.se/fly/ */
|
|
12
|
+
fly: 'https://fly.webp.se/?url=',
|
|
13
|
+
/** https://wsrv.nl/docs/ */
|
|
14
|
+
weserv: 'https://wsrv.nl/?url=',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// https://wsrv.nl/docs/quick-reference.html
|
|
18
|
+
export function getWsrvGhAvatar(name = '', options = { size: 92 }) {
|
|
19
|
+
const srcUrl = `github.com/${name}.png?size=${options.size}`
|
|
20
|
+
delete options.size
|
|
21
|
+
|
|
22
|
+
const params = new URLSearchParams(srcUrl)
|
|
23
|
+
Object.entries(options).forEach(([key, value]) => params.set(key, value))
|
|
24
|
+
return services.weserv + params.toString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// https://docs.webp.se/public-services/github-avatar/
|
|
28
|
+
export function getGithubAvatar(name = '', options = { size: 120 }) {
|
|
29
|
+
return `https://avatars-githubusercontent-webp.webp.se/${name}?s=${options.size}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const getGithubIcon = (name = '') => getWsrvGhAvatar(name, { size: 32, mask: 'circle' })
|
|
33
|
+
|
|
34
|
+
export const OicqAvatarSize = {
|
|
35
|
+
Size1080: 0,
|
|
36
|
+
Size40: 1,
|
|
37
|
+
Size40_: 2,
|
|
38
|
+
Size100: 3,
|
|
39
|
+
Size140: 4,
|
|
40
|
+
Size640: 5,
|
|
41
|
+
Size40__: 40,
|
|
42
|
+
Size100_: 100,
|
|
43
|
+
Size640_: 640,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// https://users.qzone.qq.com/fcg-bin/cgi_get_portrait.fcg?uins=
|
|
47
|
+
export function getOicqAvatar(qq = '', size = OicqAvatarSize.Size140) {
|
|
48
|
+
return `https://q1.qlogo.cn/g?b=qq&nk=${qq}&s=${size}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const QgroupAvatarSize = {
|
|
52
|
+
Size640: 0,
|
|
53
|
+
Size100: 100,
|
|
54
|
+
Size640_: 640,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getOciqGroupAvatar(group = '', size = QgroupAvatarSize.Size100) {
|
|
58
|
+
return `https://p.qlogo.cn/gh/${group}/${group}/${size}/`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// https://github.com/microlinkhq/unavatar
|
|
62
|
+
// https://docs.webp.se/public-services/unavatar/
|
|
63
|
+
export function getFavicon(domain, options) {
|
|
64
|
+
const { provider = 'gstatic', size = 32 } = options || {}
|
|
65
|
+
if (provider === 'gstatic')
|
|
66
|
+
return `https://t0.gstatic.cn/faviconV2?client=SOCIAL&fallback_opts=SIZE&url=http://${domain}&size=${size}`
|
|
67
|
+
return `https://unavatar.webp.se/${provider}/${domain}?w=${size}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getImgUrl(src, service) {
|
|
71
|
+
if (!service)
|
|
72
|
+
return src
|
|
73
|
+
if (service === true)
|
|
74
|
+
service = 'fly'
|
|
75
|
+
if (service in services)
|
|
76
|
+
return services[service] + src
|
|
77
|
+
return src
|
|
78
|
+
}
|
package/img/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default (sourcesEncoded: string[], targetEncoded: string) => {
|
|
2
|
+
const sources = sourcesEncoded.map(atob)
|
|
3
|
+
const canonicalHost = new URL(atob(targetEncoded)).host
|
|
4
|
+
const canonical = document.querySelector<HTMLLinkElement>('link[rel="canonical"]')
|
|
5
|
+
const isBadMirror = sources.some(domain => location.hostname.endsWith(domain))
|
|
6
|
+
if (isBadMirror) {
|
|
7
|
+
if (canonical)
|
|
8
|
+
canonical.href = canonical.href.replace(location.host, canonicalHost)
|
|
9
|
+
location.host = canonicalHost
|
|
10
|
+
}
|
|
11
|
+
}
|