clarity-theme 0.1.0 → 0.1.2
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 +20 -20
- package/README.md +70 -4
- package/README.zh-CN.md +67 -4
- package/nuxt.config.ts +217 -214
- package/package.json +35 -37
- package/{app → src}/app.config.ts +110 -110
- package/{app → src}/app.vue +5 -5
- package/{app → src}/components/blog/BlogAside.vue +25 -25
- package/{app → src}/components/blog/BlogFooter.vue +25 -25
- package/{app → src}/components/blog/BlogHeader.global.vue +48 -48
- package/{app → src}/components/blog/BlogPanel.vue +43 -43
- package/{app → src}/components/blog/BlogSidebar.vue +53 -53
- package/{app → src}/components/blog/BlogWidget.vue +42 -42
- package/{app → src}/components/blog/Mask.vue +25 -25
- package/{app → src}/components/blog/SkipToContent.vue +9 -9
- package/{app → src}/components/blog/ThemeToggle.vue +22 -22
- package/{app → src}/components/content/Badge.vue +45 -45
- package/{app → src}/components/content/Blur.vue +14 -14
- package/{app → src}/components/content/CardList.vue +8 -8
- package/{app → src}/components/content/Chat.vue +41 -41
- package/{app → src}/components/content/Copy.vue +71 -71
- package/{app → src}/components/content/EmojiClock.vue +49 -49
- package/{app → src}/components/content/FeedCard.vue +96 -96
- package/{app → src}/components/content/FeedGroup.vue +61 -61
- package/{app → src}/components/content/Folding.vue +19 -19
- package/{app → src}/components/content/Key.vue +143 -143
- package/{app → src}/components/content/LinkBanner.vue +27 -27
- package/{app → src}/components/content/LinkCard.vue +28 -28
- package/{app → src}/components/content/MdTitle.vue +8 -8
- package/{app → src}/components/content/Mermaid.vue +99 -99
- package/{app → src}/components/content/MusicScore.vue +63 -63
- package/{app → src}/components/content/Pic.vue +49 -49
- package/{app → src}/components/content/Poetry.vue +27 -27
- package/{app → src}/components/content/ProseA.vue +24 -24
- package/{app → src}/components/content/ProseCode.vue +16 -16
- package/{app → src}/components/content/ProsePre.vue +117 -117
- package/{app → src}/components/content/ProseTable.vue +23 -23
- package/{app → src}/components/content/Quote.vue +21 -21
- package/{app → src}/components/content/Tab.vue +32 -32
- package/{app → src}/components/content/Timeline.vue +33 -33
- package/{app → src}/components/content/Tip.vue +40 -40
- package/{app → src}/components/content/VideoEmbed.vue +112 -112
- package/{app → src}/components/partial/Button.vue +36 -36
- package/{app → src}/components/partial/DlGroup.vue +31 -31
- package/{app → src}/components/partial/Dropdown.vue +21 -21
- package/{app → src}/components/partial/Error.vue +39 -39
- package/{app → src}/components/partial/Expand.vue +26 -26
- package/{app → src}/components/partial/IconNavList.vue +24 -24
- package/{app → src}/components/partial/Pagination.vue +62 -62
- package/{app → src}/components/partial/RadioGroup.vue +30 -30
- package/{app → src}/components/partial/Secret.vue +11 -11
- package/{app → src}/components/partial/Slider.vue +48 -48
- package/{app → src}/components/partial/Toggle.vue +17 -17
- package/{app → src}/components/popover/Lightbox.vue +44 -44
- package/{app → src}/components/popover/Search.vue +143 -143
- package/{app → src}/components/popover/SearchItem.vue +28 -28
- package/{app → src}/components/post/Archive.vue +40 -40
- package/{app → src}/components/post/Article.vue +49 -49
- package/{app → src}/components/post/Comment.vue +119 -119
- package/{app → src}/components/post/Excerpt.vue +40 -40
- package/{app → src}/components/post/OrderToggle.vue +70 -70
- package/{app → src}/components/post/PostFooter.vue +52 -52
- package/{app → src}/components/post/PostHeader.vue +66 -66
- package/{app → src}/components/post/PostSurround.vue +51 -51
- package/{app → src}/components/post/Slide.vue +85 -85
- package/{app → src}/components/util/Date.vue +62 -62
- package/{app → src}/components/util/HydrateSafe.vue +10 -10
- package/{app → src}/components/util/Img.vue +38 -38
- package/{app → src}/components/util/Link.vue +11 -11
- package/{app → src}/components/widget/BlogLog.vue +23 -23
- package/{app → src}/components/widget/BlogStats.vue +38 -38
- package/{app → src}/components/widget/BlogTech.vue +57 -57
- package/{app → src}/components/widget/Empty.vue +3 -3
- package/{app → src}/components/widget/Toc.vue +67 -67
- package/{app → src}/composables/useArticle.ts +115 -115
- package/{app → src}/composables/useAvoid.ts +87 -87
- package/{app → src}/composables/useCopy.ts +21 -21
- package/{app → src}/composables/usePagination.ts +70 -70
- package/{app → src}/composables/useShiki.ts +129 -129
- package/{app → src}/composables/useToc.ts +57 -57
- package/{app → src}/composables/useWidgets.ts +56 -56
- package/{config → src/config}/app.ts +62 -62
- package/{config → src/config}/content.ts +75 -75
- package/{config → src/config}/define.ts +3 -3
- package/src/config/feeds.empty.ts +6 -0
- package/{config → src/config}/index.ts +1 -1
- package/{config → src/config}/schema.ts +89 -89
- package/{app → src}/error.vue +31 -31
- package/{img → src/img}/index.ts +2 -2
- package/{app → src}/layouts/default.vue +25 -25
- package/{modules → src/modules}/clarity-config/index.ts +168 -162
- package/src/modules/clarity-source-layout/index.ts +78 -0
- package/{app → src}/pages/[...slug].vue +59 -59
- package/{app → src}/pages/archive.vue +129 -129
- package/{app → src}/pages/index.vue +76 -76
- package/{app → src}/pages/link.vue +62 -62
- package/{app → src}/pages/preview.vue +46 -46
- package/{app → src}/plugins/easter-egg.ts +23 -23
- package/{app → src}/plugins/init.ts +6 -6
- package/{app → src}/plugins/tippy.ts +22 -22
- package/{public → src/public}/assets/atom.css +274 -274
- package/{public → src/public}/assets/atom.xsl +109 -109
- package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.ts +58 -58
- package/{remark-plugins → src/remark-plugins}/remark-code-component.ts +41 -41
- package/{server → src/server}/api/stats.get.ts +84 -83
- package/{server → src/server}/routes/atom.xml.get.ts +85 -85
- package/{server → src/server}/routes/subscriptions.opml.get.ts +64 -64
- package/{shared → src/shared}/utils/clarity.ts +24 -24
- package/{shared → src/shared}/utils/icon.ts +218 -218
- package/{shared → src/shared}/utils/link.ts +52 -52
- package/{shared → src/shared}/utils/str.ts +88 -88
- package/{shared → src/shared}/utils/time.ts +115 -115
- package/{app → src}/shiki.config.ts +8 -8
- package/{app → src}/stores/layout.ts +34 -34
- package/{app → src}/stores/search.ts +37 -37
- package/{app → src}/types/article.ts +16 -16
- package/src/types/feed.ts +1 -0
- package/{app → src}/types/index.ts +14 -14
- package/{app → src}/types/nav.ts +11 -11
- package/{app → src}/utils/anim.ts +37 -37
- package/{app → src}/utils/img.ts +83 -83
- package/app/types/feed.ts +0 -1
- package/config/feeds.empty.ts +0 -4
- package/{app → src}/assets/css/_variable.scss +0 -0
- package/{app → src}/assets/css/animation.scss +0 -0
- package/{app → src}/assets/css/article.scss +0 -0
- package/{app → src}/assets/css/color.scss +0 -0
- package/{app → src}/assets/css/font.scss +0 -0
- package/{app → src}/assets/css/main.scss +0 -0
- package/{app → src}/assets/css/reusable.scss +0 -0
- package/{app → src}/assets/icons/solar-rewind-back-bold-duotone.svg +0 -0
- package/{app → src}/assets/icons/solar-rewind-forward-bold-duotone.svg +0 -0
- package/{app → src}/components/content/Alert.vue +0 -0
- package/{config → src/config}/content.d.mts +0 -0
- package/{config → src/config}/content.mjs +0 -0
- package/{config → src/config}/define.mjs +0 -0
- package/{config → src/config}/feed.ts +0 -0
- package/{config → src/config}/index.d.mts +0 -0
- package/{config → src/config}/index.mjs +0 -0
- package/{config → src/config}/public.ts +1 -1
- package/{config → src/config}/schema.d.mts +0 -0
- package/{config → src/config}/schema.mjs +0 -0
- package/{config → src/config}/server.ts +1 -1
- /package/{img → src/img}/index.d.mts +0 -0
- /package/{img → src/img}/index.mjs +0 -0
- /package/{modules → src/modules}/clarity-config/anti-mirror-client.ts +0 -0
- /package/{public → src/public}/fonts/AlimamaFangYuanTi.woff2 +0 -0
- /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.d.mts +0 -0
- /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.mjs +0 -0
- /package/{remark-plugins → src/remark-plugins}/remark-code-component.d.mts +0 -0
- /package/{remark-plugins → src/remark-plugins}/remark-code-component.mjs +0 -0
- /package/{server → src/server}/utils/clarity.ts +0 -0
- /package/{shared → src/shared}/types/content.ts +0 -0
- /package/{app → src}/types/modal.ts +0 -0
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
3
|
-
<xsl:output method="html" indent="yes" />
|
|
4
|
-
|
|
5
|
-
<xsl:template match="/">
|
|
6
|
-
<html lang="{atom:feed/atom:language}">
|
|
7
|
-
|
|
8
|
-
<head>
|
|
9
|
-
<meta charset="UTF-8" />
|
|
10
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
11
|
-
<title><xsl:value-of select="atom:feed/atom:title" /></title>
|
|
12
|
-
<link rel="stylesheet" href="/assets/atom.css" />
|
|
13
|
-
<link rel="icon" href="{atom:feed/atom:icon}" />
|
|
14
|
-
<script>
|
|
15
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
16
|
-
document.querySelectorAll('time').forEach((time) => {
|
|
17
|
-
const dateTime = new Date(time.dateTime);
|
|
18
|
-
if (!Number.isNaN(dateTime.getTime())) {
|
|
19
|
-
time.textContent = dateTime.toLocaleDateString();
|
|
20
|
-
time.title = dateTime.toLocaleString(undefined, { timeZoneName: 'long' });
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
</script>
|
|
25
|
-
</head>
|
|
26
|
-
|
|
27
|
-
<body>
|
|
28
|
-
<header class="logo-header">
|
|
29
|
-
<img class="logo" src="{atom:feed/atom:logo}" alt="" />
|
|
30
|
-
<div>
|
|
31
|
-
<h1 class="title"><xsl:value-of select="atom:feed/atom:title" /></h1>
|
|
32
|
-
<div class="subtitle"><xsl:value-of select="atom:feed/atom:subtitle" /></div>
|
|
33
|
-
</div>
|
|
34
|
-
</header>
|
|
35
|
-
|
|
36
|
-
<blockquote>
|
|
37
|
-
<p>本页面是 Atom 订阅源,可直接被订阅。</p>
|
|
38
|
-
<p class="description"><xsl:value-of select="atom:feed/atom:description" /></p>
|
|
39
|
-
</blockquote>
|
|
40
|
-
|
|
41
|
-
<main>
|
|
42
|
-
<xsl:apply-templates select="atom:feed/atom:entry" />
|
|
43
|
-
</main>
|
|
44
|
-
|
|
45
|
-
<footer>
|
|
46
|
-
<xsl:value-of select="atom:feed/atom:rights" />
|
|
47
|
-
<br />
|
|
48
|
-
由 <xsl:value-of select="atom:feed/atom:generator" /> 生成
|
|
49
|
-
</footer>
|
|
50
|
-
</body>
|
|
51
|
-
|
|
52
|
-
</html>
|
|
53
|
-
</xsl:template>
|
|
54
|
-
|
|
55
|
-
<xsl:template match="atom:entry">
|
|
56
|
-
<a href="#{atom:id}" class="entry">
|
|
57
|
-
<xsl:variable name="img-src"
|
|
58
|
-
select="substring-before(substring-after(substring-after(atom:content, '<img'), 'src="'), '"')" />
|
|
59
|
-
<xsl:if test="$img-src">
|
|
60
|
-
<img class="entry-image" src="{$img-src}" alt="{atom:title}" loading="lazy" />
|
|
61
|
-
</xsl:if>
|
|
62
|
-
|
|
63
|
-
<article>
|
|
64
|
-
<h2 class="entry-title">
|
|
65
|
-
<xsl:value-of select="atom:title" />
|
|
66
|
-
</h2>
|
|
67
|
-
|
|
68
|
-
<xsl:if test="atom:summary">
|
|
69
|
-
<div class="entry-summary">
|
|
70
|
-
<xsl:value-of select="atom:summary" />
|
|
71
|
-
</div>
|
|
72
|
-
</xsl:if>
|
|
73
|
-
|
|
74
|
-
<div class="entry-meta">
|
|
75
|
-
发布于
|
|
76
|
-
<time datetime="{atom:published}">
|
|
77
|
-
<xsl:value-of select="atom:published" />
|
|
78
|
-
</time>
|
|
79
|
-
|
|
80
|
-
<xsl:if test="atom:updated and atom:updated != atom:published">
|
|
81
|
-
· 更新于
|
|
82
|
-
<time datetime="{atom:updated}">
|
|
83
|
-
<xsl:value-of select="atom:updated" />
|
|
84
|
-
</time>
|
|
85
|
-
</xsl:if>
|
|
86
|
-
|
|
87
|
-
<xsl:if test="atom:category">
|
|
88
|
-
·
|
|
89
|
-
<xsl:for-each select="atom:category">
|
|
90
|
-
<xsl:value-of select="@term" />
|
|
91
|
-
</xsl:for-each>
|
|
92
|
-
</xsl:if>
|
|
93
|
-
</div>
|
|
94
|
-
</article>
|
|
95
|
-
</a>
|
|
96
|
-
|
|
97
|
-
<xsl:if test="atom:content">
|
|
98
|
-
<section class="entry-content-container" id="{atom:id}">
|
|
99
|
-
<a class="entry-content-close" href="#">×</a>
|
|
100
|
-
<figure class="entry-content">
|
|
101
|
-
<figcaption>
|
|
102
|
-
<xsl:value-of select="atom:title" />
|
|
103
|
-
</figcaption>
|
|
104
|
-
<xsl:value-of select="atom:content" disable-output-escaping="yes" />
|
|
105
|
-
</figure>
|
|
106
|
-
</section>
|
|
107
|
-
</xsl:if>
|
|
108
|
-
</xsl:template>
|
|
109
|
-
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
3
|
+
<xsl:output method="html" indent="yes" />
|
|
4
|
+
|
|
5
|
+
<xsl:template match="/">
|
|
6
|
+
<html lang="{atom:feed/atom:language}">
|
|
7
|
+
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="UTF-8" />
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
11
|
+
<title><xsl:value-of select="atom:feed/atom:title" /></title>
|
|
12
|
+
<link rel="stylesheet" href="/assets/atom.css" />
|
|
13
|
+
<link rel="icon" href="{atom:feed/atom:icon}" />
|
|
14
|
+
<script>
|
|
15
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
16
|
+
document.querySelectorAll('time').forEach((time) => {
|
|
17
|
+
const dateTime = new Date(time.dateTime);
|
|
18
|
+
if (!Number.isNaN(dateTime.getTime())) {
|
|
19
|
+
time.textContent = dateTime.toLocaleDateString();
|
|
20
|
+
time.title = dateTime.toLocaleString(undefined, { timeZoneName: 'long' });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
</head>
|
|
26
|
+
|
|
27
|
+
<body>
|
|
28
|
+
<header class="logo-header">
|
|
29
|
+
<img class="logo" src="{atom:feed/atom:logo}" alt="" />
|
|
30
|
+
<div>
|
|
31
|
+
<h1 class="title"><xsl:value-of select="atom:feed/atom:title" /></h1>
|
|
32
|
+
<div class="subtitle"><xsl:value-of select="atom:feed/atom:subtitle" /></div>
|
|
33
|
+
</div>
|
|
34
|
+
</header>
|
|
35
|
+
|
|
36
|
+
<blockquote>
|
|
37
|
+
<p>本页面是 Atom 订阅源,可直接被订阅。</p>
|
|
38
|
+
<p class="description"><xsl:value-of select="atom:feed/atom:description" /></p>
|
|
39
|
+
</blockquote>
|
|
40
|
+
|
|
41
|
+
<main>
|
|
42
|
+
<xsl:apply-templates select="atom:feed/atom:entry" />
|
|
43
|
+
</main>
|
|
44
|
+
|
|
45
|
+
<footer>
|
|
46
|
+
<xsl:value-of select="atom:feed/atom:rights" />
|
|
47
|
+
<br />
|
|
48
|
+
由 <xsl:value-of select="atom:feed/atom:generator" /> 生成
|
|
49
|
+
</footer>
|
|
50
|
+
</body>
|
|
51
|
+
|
|
52
|
+
</html>
|
|
53
|
+
</xsl:template>
|
|
54
|
+
|
|
55
|
+
<xsl:template match="atom:entry">
|
|
56
|
+
<a href="#{atom:id}" class="entry">
|
|
57
|
+
<xsl:variable name="img-src"
|
|
58
|
+
select="substring-before(substring-after(substring-after(atom:content, '<img'), 'src="'), '"')" />
|
|
59
|
+
<xsl:if test="$img-src">
|
|
60
|
+
<img class="entry-image" src="{$img-src}" alt="{atom:title}" loading="lazy" />
|
|
61
|
+
</xsl:if>
|
|
62
|
+
|
|
63
|
+
<article>
|
|
64
|
+
<h2 class="entry-title">
|
|
65
|
+
<xsl:value-of select="atom:title" />
|
|
66
|
+
</h2>
|
|
67
|
+
|
|
68
|
+
<xsl:if test="atom:summary">
|
|
69
|
+
<div class="entry-summary">
|
|
70
|
+
<xsl:value-of select="atom:summary" />
|
|
71
|
+
</div>
|
|
72
|
+
</xsl:if>
|
|
73
|
+
|
|
74
|
+
<div class="entry-meta">
|
|
75
|
+
发布于
|
|
76
|
+
<time datetime="{atom:published}">
|
|
77
|
+
<xsl:value-of select="atom:published" />
|
|
78
|
+
</time>
|
|
79
|
+
|
|
80
|
+
<xsl:if test="atom:updated and atom:updated != atom:published">
|
|
81
|
+
· 更新于
|
|
82
|
+
<time datetime="{atom:updated}">
|
|
83
|
+
<xsl:value-of select="atom:updated" />
|
|
84
|
+
</time>
|
|
85
|
+
</xsl:if>
|
|
86
|
+
|
|
87
|
+
<xsl:if test="atom:category">
|
|
88
|
+
·
|
|
89
|
+
<xsl:for-each select="atom:category">
|
|
90
|
+
<xsl:value-of select="@term" />
|
|
91
|
+
</xsl:for-each>
|
|
92
|
+
</xsl:if>
|
|
93
|
+
</div>
|
|
94
|
+
</article>
|
|
95
|
+
</a>
|
|
96
|
+
|
|
97
|
+
<xsl:if test="atom:content">
|
|
98
|
+
<section class="entry-content-container" id="{atom:id}">
|
|
99
|
+
<a class="entry-content-close" href="#">×</a>
|
|
100
|
+
<figure class="entry-content">
|
|
101
|
+
<figcaption>
|
|
102
|
+
<xsl:value-of select="atom:title" />
|
|
103
|
+
</figcaption>
|
|
104
|
+
<xsl:value-of select="atom:content" disable-output-escaping="yes" />
|
|
105
|
+
</figure>
|
|
106
|
+
</section>
|
|
107
|
+
</xsl:if>
|
|
108
|
+
</xsl:template>
|
|
109
|
+
|
|
110
110
|
</xsl:stylesheet>
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
import type { Element, Properties, Root, RootContent } from 'hast'
|
|
2
|
-
import type { MinimarkNode, MinimarkTree } from 'minimark'
|
|
3
|
-
import type { VFile } from 'vfile'
|
|
4
|
-
|
|
5
|
-
export interface MetaSlotsTree extends MinimarkTree {
|
|
6
|
-
props?: Properties
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
declare module 'vfile' {
|
|
10
|
-
interface DataMap {
|
|
11
|
-
slots?: Record<string, MetaSlotsTree>
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function fromHast(node: Element): MetaSlotsTree {
|
|
16
|
-
return {
|
|
17
|
-
props: node.properties,
|
|
18
|
-
type: 'minimark',
|
|
19
|
-
value: node.children.map(hastToMinimarkNode).filter(v => v !== undefined),
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function hastToMinimarkNode(input: RootContent): MinimarkNode | undefined {
|
|
24
|
-
if (input.type === 'comment' || input.type === 'doctype') {
|
|
25
|
-
return undefined
|
|
26
|
-
}
|
|
27
|
-
if (input.type === 'text' || input.type === 'raw') {
|
|
28
|
-
return input.value
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (input.tagName === 'code' && input.properties?.className && (input.properties.className as string[]).length === 0) {
|
|
32
|
-
delete input.properties.className
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return [
|
|
36
|
-
input.tagName,
|
|
37
|
-
input.properties || {},
|
|
38
|
-
...(input.children || []).map(hastToMinimarkNode).filter(v => v !== undefined),
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export default function rehypeMetaSlots() {
|
|
43
|
-
return (tree: Root, file: VFile) => {
|
|
44
|
-
file.data.slots ??= {}
|
|
45
|
-
|
|
46
|
-
for (let i = 0; i < tree.children.length; i++) {
|
|
47
|
-
const node = tree.children[i]!
|
|
48
|
-
|
|
49
|
-
if (node.type === 'element' && node.tagName.startsWith('meta-')) {
|
|
50
|
-
const metaName = node.tagName.slice('meta-'.length)
|
|
51
|
-
file.data.slots[metaName] = fromHast(node)
|
|
52
|
-
|
|
53
|
-
tree.children.splice(i, 1)
|
|
54
|
-
i--
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
import type { Element, Properties, Root, RootContent } from 'hast'
|
|
2
|
+
import type { MinimarkNode, MinimarkTree } from 'minimark'
|
|
3
|
+
import type { VFile } from 'vfile'
|
|
4
|
+
|
|
5
|
+
export interface MetaSlotsTree extends MinimarkTree {
|
|
6
|
+
props?: Properties
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare module 'vfile' {
|
|
10
|
+
interface DataMap {
|
|
11
|
+
slots?: Record<string, MetaSlotsTree>
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function fromHast(node: Element): MetaSlotsTree {
|
|
16
|
+
return {
|
|
17
|
+
props: node.properties,
|
|
18
|
+
type: 'minimark',
|
|
19
|
+
value: node.children.map(hastToMinimarkNode).filter(v => v !== undefined),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hastToMinimarkNode(input: RootContent): MinimarkNode | undefined {
|
|
24
|
+
if (input.type === 'comment' || input.type === 'doctype') {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
if (input.type === 'text' || input.type === 'raw') {
|
|
28
|
+
return input.value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (input.tagName === 'code' && input.properties?.className && (input.properties.className as string[]).length === 0) {
|
|
32
|
+
delete input.properties.className
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
input.tagName,
|
|
37
|
+
input.properties || {},
|
|
38
|
+
...(input.children || []).map(hastToMinimarkNode).filter(v => v !== undefined),
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function rehypeMetaSlots() {
|
|
43
|
+
return (tree: Root, file: VFile) => {
|
|
44
|
+
file.data.slots ??= {}
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < tree.children.length; i++) {
|
|
47
|
+
const node = tree.children[i]!
|
|
48
|
+
|
|
49
|
+
if (node.type === 'element' && node.tagName.startsWith('meta-')) {
|
|
50
|
+
const metaName = node.tagName.slice('meta-'.length)
|
|
51
|
+
file.data.slots[metaName] = fromHast(node)
|
|
52
|
+
|
|
53
|
+
tree.children.splice(i, 1)
|
|
54
|
+
i--
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import type { Parent, Root } from 'mdast'
|
|
2
|
-
import { visit } from 'unist-util-visit'
|
|
3
|
-
|
|
4
|
-
interface CodeComponentNode extends Parent {
|
|
5
|
-
type: 'codeComponent'
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
declare module 'mdast' {
|
|
9
|
-
interface RootContentMap {
|
|
10
|
-
codeComponent: CodeComponentNode
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface CodeComponent {
|
|
15
|
-
/** 渲染该语言代码块的组件 */
|
|
16
|
-
component: string
|
|
17
|
-
/** 组件上接收代码内容的属性 */
|
|
18
|
-
prop: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** 代码块语言到组件的映射 */
|
|
22
|
-
export type CodeComponents = Record<string, CodeComponent>
|
|
23
|
-
|
|
24
|
-
export default function remarkCodeComponent(components: CodeComponents = {}) {
|
|
25
|
-
return (tree: Root) => {
|
|
26
|
-
visit(tree, 'code', (node, index, parent) => {
|
|
27
|
-
const options = components[node.lang ?? '']
|
|
28
|
-
if (!options || !parent || index === undefined)
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
parent.children.splice(index, 1, {
|
|
32
|
-
type: 'codeComponent',
|
|
33
|
-
children: [],
|
|
34
|
-
data: {
|
|
35
|
-
hName: options.component,
|
|
36
|
-
hProperties: { [options.prop]: node.value },
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
import type { Parent, Root } from 'mdast'
|
|
2
|
+
import { visit } from 'unist-util-visit'
|
|
3
|
+
|
|
4
|
+
interface CodeComponentNode extends Parent {
|
|
5
|
+
type: 'codeComponent'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare module 'mdast' {
|
|
9
|
+
interface RootContentMap {
|
|
10
|
+
codeComponent: CodeComponentNode
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CodeComponent {
|
|
15
|
+
/** 渲染该语言代码块的组件 */
|
|
16
|
+
component: string
|
|
17
|
+
/** 组件上接收代码内容的属性 */
|
|
18
|
+
prop: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 代码块语言到组件的映射 */
|
|
22
|
+
export type CodeComponents = Record<string, CodeComponent>
|
|
23
|
+
|
|
24
|
+
export default function remarkCodeComponent(components: CodeComponents = {}) {
|
|
25
|
+
return (tree: Root) => {
|
|
26
|
+
visit(tree, 'code', (node, index, parent) => {
|
|
27
|
+
const options = components[node.lang ?? '']
|
|
28
|
+
if (!options || !parent || index === undefined)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
parent.children.splice(index, 1, {
|
|
32
|
+
type: 'codeComponent',
|
|
33
|
+
children: [],
|
|
34
|
+
data: {
|
|
35
|
+
hName: options.component,
|
|
36
|
+
hProperties: { [options.prop]: node.value },
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -6,28 +6,28 @@ interface StatsEntry {
|
|
|
6
6
|
posts: number
|
|
7
7
|
words: number
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
interface CategoryEntry {
|
|
11
|
-
name: string
|
|
12
|
-
posts: number
|
|
13
|
-
children?: CategoryEntry[]
|
|
14
|
-
}
|
|
15
|
-
|
|
9
|
+
|
|
10
|
+
interface CategoryEntry {
|
|
11
|
+
name: string
|
|
12
|
+
posts: number
|
|
13
|
+
children?: CategoryEntry[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
16
|
export default defineEventHandler(async (event) => {
|
|
17
17
|
const { site, stats: statsConfig, features } = useClarityServerConfig()
|
|
18
18
|
if (!features.stats) {
|
|
19
19
|
throw createError({ statusCode: 404, statusMessage: 'Stats output is disabled' })
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
const stats = {
|
|
23
|
-
total: { posts: 0, words: 0 },
|
|
24
|
-
annual: <Record<number, StatsEntry>>{},
|
|
25
|
-
categories: <CategoryEntry[]>[],
|
|
26
|
-
tags: <string[]>[],
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const existedPaths = new Set<string>()
|
|
30
|
-
|
|
21
|
+
|
|
22
|
+
const stats = {
|
|
23
|
+
total: { posts: 0, words: 0 },
|
|
24
|
+
annual: <Record<number, StatsEntry>>{},
|
|
25
|
+
categories: <CategoryEntry[]>[],
|
|
26
|
+
tags: <string[]>[],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const existedPaths = new Set<string>()
|
|
30
|
+
|
|
31
31
|
const query = queryCollection(event, 'content' as never)
|
|
32
32
|
if (statsConfig.includePaths.length) {
|
|
33
33
|
// @nuxt/content 3.16+ 的 orWhere 组内条件以 OR 连接:
|
|
@@ -36,74 +36,75 @@ export default defineEventHandler(async (event) => {
|
|
|
36
36
|
for (const path of statsConfig.includePaths) {
|
|
37
37
|
group.where('stem', 'LIKE', path)
|
|
38
38
|
}
|
|
39
|
+
return group
|
|
39
40
|
})
|
|
40
41
|
}
|
|
41
42
|
const posts = await query.all() as ClarityContentRow[]
|
|
42
|
-
|
|
43
|
-
const findOrCreateCategory = (
|
|
44
|
-
name: string,
|
|
45
|
-
tree: CategoryEntry[],
|
|
46
|
-
): CategoryEntry => {
|
|
47
|
-
let category = tree.find(entry => entry.name === name)
|
|
48
|
-
if (!category) {
|
|
49
|
-
category = { name, posts: 0 }
|
|
50
|
-
tree.push(category)
|
|
51
|
-
}
|
|
52
|
-
return category
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const post of posts) {
|
|
56
|
-
// 重复路径检测
|
|
57
|
-
if (existedPaths.has(post.path))
|
|
58
|
-
console.warn('文章存在重复路径', post.path)
|
|
59
|
-
existedPaths.add(post.path)
|
|
60
|
-
|
|
61
|
-
// 文章/总字数计数
|
|
62
|
-
stats.total.posts++
|
|
63
|
-
stats.total.words += post.readingTime.words
|
|
64
|
-
|
|
65
|
-
// 年文章/年字数计数
|
|
66
|
-
try {
|
|
67
|
-
const year = toZonedTemporal(post.date || '', site.timezone).year
|
|
68
|
-
if (!stats.annual[year]) {
|
|
69
|
-
stats.annual[year] = { posts: 0, words: 0 }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
stats.annual[year].posts++
|
|
73
|
-
stats.annual[year].words += post.readingTime.words
|
|
74
|
-
}
|
|
75
|
-
catch (e) {
|
|
76
|
-
console.warn(post.date
|
|
77
|
-
? `${post.path} 文章日期 ${post.date} 格式错误: "${e}"`
|
|
78
|
-
: `${post.path} 文章日期为空`)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 分类文章计数
|
|
82
|
-
const categories = post.categories || []
|
|
83
|
-
let currentLevel = stats.categories
|
|
84
|
-
|
|
85
|
-
for (const [index, categoryName] of categories.entries()) {
|
|
86
|
-
if (typeof categoryName !== 'string')
|
|
87
|
-
continue
|
|
88
|
-
|
|
89
|
-
const category = findOrCreateCategory(categoryName, currentLevel)
|
|
90
|
-
category.posts++
|
|
91
|
-
|
|
92
|
-
if (index < categories.length - 1) {
|
|
93
|
-
if (!category.children)
|
|
94
|
-
category.children = []
|
|
95
|
-
currentLevel = category.children
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 标签统计
|
|
100
|
-
const tags = post.tags || []
|
|
101
|
-
tags.filter((tag: any): tag is string => typeof tag === 'string')
|
|
102
|
-
.forEach((tag: string) => {
|
|
103
|
-
if (!stats.tags.includes(tag))
|
|
104
|
-
stats.tags.push(tag)
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
|
|
43
|
+
|
|
44
|
+
const findOrCreateCategory = (
|
|
45
|
+
name: string,
|
|
46
|
+
tree: CategoryEntry[],
|
|
47
|
+
): CategoryEntry => {
|
|
48
|
+
let category = tree.find(entry => entry.name === name)
|
|
49
|
+
if (!category) {
|
|
50
|
+
category = { name, posts: 0 }
|
|
51
|
+
tree.push(category)
|
|
52
|
+
}
|
|
53
|
+
return category
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const post of posts) {
|
|
57
|
+
// 重复路径检测
|
|
58
|
+
if (existedPaths.has(post.path))
|
|
59
|
+
console.warn('文章存在重复路径', post.path)
|
|
60
|
+
existedPaths.add(post.path)
|
|
61
|
+
|
|
62
|
+
// 文章/总字数计数
|
|
63
|
+
stats.total.posts++
|
|
64
|
+
stats.total.words += post.readingTime.words
|
|
65
|
+
|
|
66
|
+
// 年文章/年字数计数
|
|
67
|
+
try {
|
|
68
|
+
const year = toZonedTemporal(post.date || '', site.timezone).year
|
|
69
|
+
if (!stats.annual[year]) {
|
|
70
|
+
stats.annual[year] = { posts: 0, words: 0 }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
stats.annual[year].posts++
|
|
74
|
+
stats.annual[year].words += post.readingTime.words
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
console.warn(post.date
|
|
78
|
+
? `${post.path} 文章日期 ${post.date} 格式错误: "${e}"`
|
|
79
|
+
: `${post.path} 文章日期为空`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 分类文章计数
|
|
83
|
+
const categories = post.categories || []
|
|
84
|
+
let currentLevel = stats.categories
|
|
85
|
+
|
|
86
|
+
for (const [index, categoryName] of categories.entries()) {
|
|
87
|
+
if (typeof categoryName !== 'string')
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
const category = findOrCreateCategory(categoryName, currentLevel)
|
|
91
|
+
category.posts++
|
|
92
|
+
|
|
93
|
+
if (index < categories.length - 1) {
|
|
94
|
+
if (!category.children)
|
|
95
|
+
category.children = []
|
|
96
|
+
currentLevel = category.children
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 标签统计
|
|
101
|
+
const tags = post.tags || []
|
|
102
|
+
tags.filter((tag: any): tag is string => typeof tag === 'string')
|
|
103
|
+
.forEach((tag: string) => {
|
|
104
|
+
if (!stats.tags.includes(tag))
|
|
105
|
+
stats.tags.push(tag)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
108
109
|
return stats
|
|
109
110
|
})
|