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.
Files changed (154) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +70 -4
  3. package/README.zh-CN.md +67 -4
  4. package/nuxt.config.ts +217 -214
  5. package/package.json +35 -37
  6. package/{app → src}/app.config.ts +110 -110
  7. package/{app → src}/app.vue +5 -5
  8. package/{app → src}/components/blog/BlogAside.vue +25 -25
  9. package/{app → src}/components/blog/BlogFooter.vue +25 -25
  10. package/{app → src}/components/blog/BlogHeader.global.vue +48 -48
  11. package/{app → src}/components/blog/BlogPanel.vue +43 -43
  12. package/{app → src}/components/blog/BlogSidebar.vue +53 -53
  13. package/{app → src}/components/blog/BlogWidget.vue +42 -42
  14. package/{app → src}/components/blog/Mask.vue +25 -25
  15. package/{app → src}/components/blog/SkipToContent.vue +9 -9
  16. package/{app → src}/components/blog/ThemeToggle.vue +22 -22
  17. package/{app → src}/components/content/Badge.vue +45 -45
  18. package/{app → src}/components/content/Blur.vue +14 -14
  19. package/{app → src}/components/content/CardList.vue +8 -8
  20. package/{app → src}/components/content/Chat.vue +41 -41
  21. package/{app → src}/components/content/Copy.vue +71 -71
  22. package/{app → src}/components/content/EmojiClock.vue +49 -49
  23. package/{app → src}/components/content/FeedCard.vue +96 -96
  24. package/{app → src}/components/content/FeedGroup.vue +61 -61
  25. package/{app → src}/components/content/Folding.vue +19 -19
  26. package/{app → src}/components/content/Key.vue +143 -143
  27. package/{app → src}/components/content/LinkBanner.vue +27 -27
  28. package/{app → src}/components/content/LinkCard.vue +28 -28
  29. package/{app → src}/components/content/MdTitle.vue +8 -8
  30. package/{app → src}/components/content/Mermaid.vue +99 -99
  31. package/{app → src}/components/content/MusicScore.vue +63 -63
  32. package/{app → src}/components/content/Pic.vue +49 -49
  33. package/{app → src}/components/content/Poetry.vue +27 -27
  34. package/{app → src}/components/content/ProseA.vue +24 -24
  35. package/{app → src}/components/content/ProseCode.vue +16 -16
  36. package/{app → src}/components/content/ProsePre.vue +117 -117
  37. package/{app → src}/components/content/ProseTable.vue +23 -23
  38. package/{app → src}/components/content/Quote.vue +21 -21
  39. package/{app → src}/components/content/Tab.vue +32 -32
  40. package/{app → src}/components/content/Timeline.vue +33 -33
  41. package/{app → src}/components/content/Tip.vue +40 -40
  42. package/{app → src}/components/content/VideoEmbed.vue +112 -112
  43. package/{app → src}/components/partial/Button.vue +36 -36
  44. package/{app → src}/components/partial/DlGroup.vue +31 -31
  45. package/{app → src}/components/partial/Dropdown.vue +21 -21
  46. package/{app → src}/components/partial/Error.vue +39 -39
  47. package/{app → src}/components/partial/Expand.vue +26 -26
  48. package/{app → src}/components/partial/IconNavList.vue +24 -24
  49. package/{app → src}/components/partial/Pagination.vue +62 -62
  50. package/{app → src}/components/partial/RadioGroup.vue +30 -30
  51. package/{app → src}/components/partial/Secret.vue +11 -11
  52. package/{app → src}/components/partial/Slider.vue +48 -48
  53. package/{app → src}/components/partial/Toggle.vue +17 -17
  54. package/{app → src}/components/popover/Lightbox.vue +44 -44
  55. package/{app → src}/components/popover/Search.vue +143 -143
  56. package/{app → src}/components/popover/SearchItem.vue +28 -28
  57. package/{app → src}/components/post/Archive.vue +40 -40
  58. package/{app → src}/components/post/Article.vue +49 -49
  59. package/{app → src}/components/post/Comment.vue +119 -119
  60. package/{app → src}/components/post/Excerpt.vue +40 -40
  61. package/{app → src}/components/post/OrderToggle.vue +70 -70
  62. package/{app → src}/components/post/PostFooter.vue +52 -52
  63. package/{app → src}/components/post/PostHeader.vue +66 -66
  64. package/{app → src}/components/post/PostSurround.vue +51 -51
  65. package/{app → src}/components/post/Slide.vue +85 -85
  66. package/{app → src}/components/util/Date.vue +62 -62
  67. package/{app → src}/components/util/HydrateSafe.vue +10 -10
  68. package/{app → src}/components/util/Img.vue +38 -38
  69. package/{app → src}/components/util/Link.vue +11 -11
  70. package/{app → src}/components/widget/BlogLog.vue +23 -23
  71. package/{app → src}/components/widget/BlogStats.vue +38 -38
  72. package/{app → src}/components/widget/BlogTech.vue +57 -57
  73. package/{app → src}/components/widget/Empty.vue +3 -3
  74. package/{app → src}/components/widget/Toc.vue +67 -67
  75. package/{app → src}/composables/useArticle.ts +115 -115
  76. package/{app → src}/composables/useAvoid.ts +87 -87
  77. package/{app → src}/composables/useCopy.ts +21 -21
  78. package/{app → src}/composables/usePagination.ts +70 -70
  79. package/{app → src}/composables/useShiki.ts +129 -129
  80. package/{app → src}/composables/useToc.ts +57 -57
  81. package/{app → src}/composables/useWidgets.ts +56 -56
  82. package/{config → src/config}/app.ts +62 -62
  83. package/{config → src/config}/content.ts +75 -75
  84. package/{config → src/config}/define.ts +3 -3
  85. package/src/config/feeds.empty.ts +6 -0
  86. package/{config → src/config}/index.ts +1 -1
  87. package/{config → src/config}/schema.ts +89 -89
  88. package/{app → src}/error.vue +31 -31
  89. package/{img → src/img}/index.ts +2 -2
  90. package/{app → src}/layouts/default.vue +25 -25
  91. package/{modules → src/modules}/clarity-config/index.ts +168 -162
  92. package/src/modules/clarity-source-layout/index.ts +78 -0
  93. package/{app → src}/pages/[...slug].vue +59 -59
  94. package/{app → src}/pages/archive.vue +129 -129
  95. package/{app → src}/pages/index.vue +76 -76
  96. package/{app → src}/pages/link.vue +62 -62
  97. package/{app → src}/pages/preview.vue +46 -46
  98. package/{app → src}/plugins/easter-egg.ts +23 -23
  99. package/{app → src}/plugins/init.ts +6 -6
  100. package/{app → src}/plugins/tippy.ts +22 -22
  101. package/{public → src/public}/assets/atom.css +274 -274
  102. package/{public → src/public}/assets/atom.xsl +109 -109
  103. package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.ts +58 -58
  104. package/{remark-plugins → src/remark-plugins}/remark-code-component.ts +41 -41
  105. package/{server → src/server}/api/stats.get.ts +84 -83
  106. package/{server → src/server}/routes/atom.xml.get.ts +85 -85
  107. package/{server → src/server}/routes/subscriptions.opml.get.ts +64 -64
  108. package/{shared → src/shared}/utils/clarity.ts +24 -24
  109. package/{shared → src/shared}/utils/icon.ts +218 -218
  110. package/{shared → src/shared}/utils/link.ts +52 -52
  111. package/{shared → src/shared}/utils/str.ts +88 -88
  112. package/{shared → src/shared}/utils/time.ts +115 -115
  113. package/{app → src}/shiki.config.ts +8 -8
  114. package/{app → src}/stores/layout.ts +34 -34
  115. package/{app → src}/stores/search.ts +37 -37
  116. package/{app → src}/types/article.ts +16 -16
  117. package/src/types/feed.ts +1 -0
  118. package/{app → src}/types/index.ts +14 -14
  119. package/{app → src}/types/nav.ts +11 -11
  120. package/{app → src}/utils/anim.ts +37 -37
  121. package/{app → src}/utils/img.ts +83 -83
  122. package/app/types/feed.ts +0 -1
  123. package/config/feeds.empty.ts +0 -4
  124. package/{app → src}/assets/css/_variable.scss +0 -0
  125. package/{app → src}/assets/css/animation.scss +0 -0
  126. package/{app → src}/assets/css/article.scss +0 -0
  127. package/{app → src}/assets/css/color.scss +0 -0
  128. package/{app → src}/assets/css/font.scss +0 -0
  129. package/{app → src}/assets/css/main.scss +0 -0
  130. package/{app → src}/assets/css/reusable.scss +0 -0
  131. package/{app → src}/assets/icons/solar-rewind-back-bold-duotone.svg +0 -0
  132. package/{app → src}/assets/icons/solar-rewind-forward-bold-duotone.svg +0 -0
  133. package/{app → src}/components/content/Alert.vue +0 -0
  134. package/{config → src/config}/content.d.mts +0 -0
  135. package/{config → src/config}/content.mjs +0 -0
  136. package/{config → src/config}/define.mjs +0 -0
  137. package/{config → src/config}/feed.ts +0 -0
  138. package/{config → src/config}/index.d.mts +0 -0
  139. package/{config → src/config}/index.mjs +0 -0
  140. package/{config → src/config}/public.ts +1 -1
  141. package/{config → src/config}/schema.d.mts +0 -0
  142. package/{config → src/config}/schema.mjs +0 -0
  143. package/{config → src/config}/server.ts +1 -1
  144. /package/{img → src/img}/index.d.mts +0 -0
  145. /package/{img → src/img}/index.mjs +0 -0
  146. /package/{modules → src/modules}/clarity-config/anti-mirror-client.ts +0 -0
  147. /package/{public → src/public}/fonts/AlimamaFangYuanTi.woff2 +0 -0
  148. /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.d.mts +0 -0
  149. /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.mjs +0 -0
  150. /package/{remark-plugins → src/remark-plugins}/remark-code-component.d.mts +0 -0
  151. /package/{remark-plugins → src/remark-plugins}/remark-code-component.mjs +0 -0
  152. /package/{server → src/server}/utils/clarity.ts +0 -0
  153. /package/{shared → src/shared}/types/content.ts +0 -0
  154. /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, '&lt;img'), 'src=&quot;'), '&quot;')" />
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, '&lt;img'), 'src=&quot;'), '&quot;')" />
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
  })