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,129 +1,129 @@
1
- import type { BundledLanguage, CodeToHastOptions, HighlighterCore, ShikiTransformer } from 'shiki'
2
- import { transformerColorizedBrackets } from '@shikijs/colorized-brackets'
3
- import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, transformerRenderIndentGuides, transformerRenderWhitespace } from '@shikijs/transformers'
4
-
5
- type TransformerOptions = Array<
6
- | 'ignoreColorizedBrackets'
7
- | 'ignoreRenderWhitespace'
8
- | 'ignoreRenderIndentGuides'
9
- >
10
- type ShikiOptions = CodeToHastOptions<BundledLanguage, string>
11
-
12
- const plainTextLanguages = new Set(['ansi', 'log', 'text'])
13
-
14
- interface ShikiCodeOptions {
15
- language: string
16
- transformerOptions?: TransformerOptions
17
- shikiOptions?: Omit<ShikiOptions, 'lang'>
18
- }
19
-
20
- type ShikiHtmlOptions = ShikiCodeOptions & {
21
- embeddedLanguages?: boolean
22
- }
23
-
24
- function getEmbeddedMarkdownLanguages(code: string, language: string) {
25
- if (language !== 'markdown' && !language.startsWith('md'))
26
- return []
27
-
28
- // 处理 Markdown 高亮内代码块中的语言
29
- // 加载 TeX 语言有概率导致 LaTeX 语言高亮炸掉
30
- const mdLangRegex = /^\s*`{3,}(\S+)/gm
31
- return [...new Set(
32
- Array
33
- .from(code.matchAll(mdLangRegex), match => match[1])
34
- .filter(lang => lang !== undefined),
35
- )]
36
- }
37
-
38
- function transformerUnwrap(): ShikiTransformer {
39
- return {
40
- root: (hast) => {
41
- const lines = (hast.children[0] as any).children[0].children
42
-
43
- return {
44
- type: 'root',
45
- children: lines,
46
- }
47
- },
48
- line(node, line) {
49
- node.properties['data-line'] = line
50
- },
51
- }
52
- }
53
-
54
- function getTransformers(options: ShikiCodeOptions): ShikiTransformer[] {
55
- const ignored = new Set(options.transformerOptions)
56
- const inline = options.shikiOptions?.structure === 'inline'
57
- const ignoreInvisibleChars = inline || plainTextLanguages.has(options.language)
58
-
59
- return [
60
- transformerNotationDiff(),
61
- transformerNotationHighlight(),
62
- transformerNotationWordHighlight(),
63
- transformerNotationFocus(),
64
- transformerNotationErrorLevel(),
65
- ignored.has('ignoreRenderIndentGuides') || ignoreInvisibleChars ? {} : transformerRenderIndentGuides(),
66
- ignored.has('ignoreRenderWhitespace') || ignoreInvisibleChars ? {} : transformerRenderWhitespace(),
67
- ignored.has('ignoreColorizedBrackets') ? {} : transformerColorizedBrackets(),
68
- inline ? {} : transformerUnwrap(),
69
- ]
70
- }
71
-
72
- export default function useShiki() {
73
- const shikiStore = useShikiStore()
74
- let shiki: HighlighterCore
75
-
76
- function getOptions(options: ShikiCodeOptions): ShikiOptions {
77
- // Shiki 未收录的语言会在高亮时抛错,回退为纯文本
78
- // ansi 是内置特殊语言,不在已加载语法列表中
79
- const language = options.language === 'ansi' || shiki.getLoadedLanguages().includes(options.language) ? options.language : 'text'
80
-
81
- return {
82
- ...shikiStore.options,
83
- lang: language,
84
- transformers: getTransformers({ ...options, language }),
85
- ...options.shikiOptions,
86
- }
87
- }
88
-
89
- async function codeToHtml(code: string, language: string): Promise<string>
90
- async function codeToHtml(code: string, options: ShikiHtmlOptions): Promise<string>
91
- async function codeToHtml(code: string, languageOrOptions: string | ShikiHtmlOptions): Promise<string> {
92
- const options = typeof languageOrOptions === 'string' ? { language: languageOrOptions } : languageOrOptions
93
- shiki = await shikiStore.load()
94
- await shikiStore.loadLang(options.language)
95
-
96
- if (typeof languageOrOptions !== 'string' && languageOrOptions.embeddedLanguages)
97
- await shikiStore.loadLang(...getEmbeddedMarkdownLanguages(code, options.language))
98
-
99
- return shiki.codeToHtml(code, getOptions(options))
100
- }
101
-
102
- async function mountPlain(target: HTMLElement, language: string): Promise<void>
103
- async function mountPlain(target: HTMLElement, options: ShikiCodeOptions): Promise<void>
104
- async function mountPlain(target: HTMLElement, languageOrOptions: string | ShikiCodeOptions): Promise<void> {
105
- const options = typeof languageOrOptions === 'string' ? { language: languageOrOptions } : languageOrOptions
106
- shiki = await shikiStore.load()
107
- const { createPlainShiki } = await import('plain-shiki')
108
-
109
- await shikiStore.loadLang(options.language)
110
- createPlainShiki(shiki).mount(target, getOptions(options))
111
- }
112
-
113
- async function mountInline(target: HTMLElement, code: string, options: ShikiCodeOptions): Promise<void> {
114
- target.classList.add('shiki')
115
- target.insertAdjacentHTML('afterbegin', await codeToHtml(code, {
116
- ...options,
117
- shikiOptions: {
118
- ...options.shikiOptions,
119
- structure: 'inline',
120
- },
121
- }))
122
- }
123
-
124
- return {
125
- codeToHtml,
126
- mountInline,
127
- mountPlain,
128
- }
129
- }
1
+ import type { BundledLanguage, CodeToHastOptions, HighlighterCore, ShikiTransformer } from 'shiki'
2
+ import { transformerColorizedBrackets } from '@shikijs/colorized-brackets'
3
+ import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, transformerRenderIndentGuides, transformerRenderWhitespace } from '@shikijs/transformers'
4
+
5
+ type TransformerOptions = Array<
6
+ | 'ignoreColorizedBrackets'
7
+ | 'ignoreRenderWhitespace'
8
+ | 'ignoreRenderIndentGuides'
9
+ >
10
+ type ShikiOptions = CodeToHastOptions<BundledLanguage, string>
11
+
12
+ const plainTextLanguages = new Set(['ansi', 'log', 'text'])
13
+
14
+ interface ShikiCodeOptions {
15
+ language: string
16
+ transformerOptions?: TransformerOptions
17
+ shikiOptions?: Omit<ShikiOptions, 'lang'>
18
+ }
19
+
20
+ type ShikiHtmlOptions = ShikiCodeOptions & {
21
+ embeddedLanguages?: boolean
22
+ }
23
+
24
+ function getEmbeddedMarkdownLanguages(code: string, language: string) {
25
+ if (language !== 'markdown' && !language.startsWith('md'))
26
+ return []
27
+
28
+ // 处理 Markdown 高亮内代码块中的语言
29
+ // 加载 TeX 语言有概率导致 LaTeX 语言高亮炸掉
30
+ const mdLangRegex = /^\s*`{3,}(\S+)/gm
31
+ return [...new Set(
32
+ Array
33
+ .from(code.matchAll(mdLangRegex), match => match[1])
34
+ .filter(lang => lang !== undefined),
35
+ )]
36
+ }
37
+
38
+ function transformerUnwrap(): ShikiTransformer {
39
+ return {
40
+ root: (hast) => {
41
+ const lines = (hast.children[0] as any).children[0].children
42
+
43
+ return {
44
+ type: 'root',
45
+ children: lines,
46
+ }
47
+ },
48
+ line(node, line) {
49
+ node.properties['data-line'] = line
50
+ },
51
+ }
52
+ }
53
+
54
+ function getTransformers(options: ShikiCodeOptions): ShikiTransformer[] {
55
+ const ignored = new Set(options.transformerOptions)
56
+ const inline = options.shikiOptions?.structure === 'inline'
57
+ const ignoreInvisibleChars = inline || plainTextLanguages.has(options.language)
58
+
59
+ return [
60
+ transformerNotationDiff(),
61
+ transformerNotationHighlight(),
62
+ transformerNotationWordHighlight(),
63
+ transformerNotationFocus(),
64
+ transformerNotationErrorLevel(),
65
+ ignored.has('ignoreRenderIndentGuides') || ignoreInvisibleChars ? {} : transformerRenderIndentGuides(),
66
+ ignored.has('ignoreRenderWhitespace') || ignoreInvisibleChars ? {} : transformerRenderWhitespace(),
67
+ ignored.has('ignoreColorizedBrackets') ? {} : transformerColorizedBrackets(),
68
+ inline ? {} : transformerUnwrap(),
69
+ ]
70
+ }
71
+
72
+ export default function useShiki() {
73
+ const shikiStore = useShikiStore()
74
+ let shiki: HighlighterCore
75
+
76
+ function getOptions(options: ShikiCodeOptions): ShikiOptions {
77
+ // Shiki 未收录的语言会在高亮时抛错,回退为纯文本
78
+ // ansi 是内置特殊语言,不在已加载语法列表中
79
+ const language = options.language === 'ansi' || shiki.getLoadedLanguages().includes(options.language) ? options.language : 'text'
80
+
81
+ return {
82
+ ...shikiStore.options,
83
+ lang: language,
84
+ transformers: getTransformers({ ...options, language }),
85
+ ...options.shikiOptions,
86
+ }
87
+ }
88
+
89
+ async function codeToHtml(code: string, language: string): Promise<string>
90
+ async function codeToHtml(code: string, options: ShikiHtmlOptions): Promise<string>
91
+ async function codeToHtml(code: string, languageOrOptions: string | ShikiHtmlOptions): Promise<string> {
92
+ const options = typeof languageOrOptions === 'string' ? { language: languageOrOptions } : languageOrOptions
93
+ shiki = await shikiStore.load()
94
+ await shikiStore.loadLang(options.language)
95
+
96
+ if (typeof languageOrOptions !== 'string' && languageOrOptions.embeddedLanguages)
97
+ await shikiStore.loadLang(...getEmbeddedMarkdownLanguages(code, options.language))
98
+
99
+ return shiki.codeToHtml(code, getOptions(options))
100
+ }
101
+
102
+ async function mountPlain(target: HTMLElement, language: string): Promise<void>
103
+ async function mountPlain(target: HTMLElement, options: ShikiCodeOptions): Promise<void>
104
+ async function mountPlain(target: HTMLElement, languageOrOptions: string | ShikiCodeOptions): Promise<void> {
105
+ const options = typeof languageOrOptions === 'string' ? { language: languageOrOptions } : languageOrOptions
106
+ shiki = await shikiStore.load()
107
+ const { createPlainShiki } = await import('plain-shiki')
108
+
109
+ await shikiStore.loadLang(options.language)
110
+ createPlainShiki(shiki).mount(target, getOptions(options))
111
+ }
112
+
113
+ async function mountInline(target: HTMLElement, code: string, options: ShikiCodeOptions): Promise<void> {
114
+ target.classList.add('shiki')
115
+ target.insertAdjacentHTML('afterbegin', await codeToHtml(code, {
116
+ ...options,
117
+ shikiOptions: {
118
+ ...options.shikiOptions,
119
+ structure: 'inline',
120
+ },
121
+ }))
122
+ }
123
+
124
+ return {
125
+ codeToHtml,
126
+ mountInline,
127
+ mountPlain,
128
+ }
129
+ }
@@ -1,57 +1,57 @@
1
- import type { Toc, TocLink } from '@nuxt/content'
2
- import type { MaybeComputedElementRef } from '@vueuse/core'
3
-
4
- interface TocList {
5
- id: string
6
- offsetTop: number | undefined
7
- }
8
-
9
- export function useToc(toc: MaybeRefOrGetter<Toc | undefined>, scrollableEl?: MaybeComputedElementRef) {
10
- const { height: bodyHeight } = useElementSize(document?.body)
11
- const { y: scrollY } = useWindowScroll()
12
-
13
- function flattenToc(tocTree: TocLink[], tocList: TocList[] = []) {
14
- tocTree.forEach(({ id, children }) => {
15
- const headingOffset = document?.getElementById(id)?.offsetTop
16
- tocList.push({ id, offsetTop: headingOffset })
17
- children && flattenToc(children, tocList)
18
- })
19
- return tocList
20
- }
21
-
22
- const tocOffsets = computedWithControl(
23
- refDebounced(bodyHeight),
24
- () => flattenToc(toValue(toc)?.links || []).reverse(),
25
- )
26
-
27
- function getActiveHeading() {
28
- const scrollMargin = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('scroll-margin-top'))
29
- const scrollOffset = scrollY.value + (scrollMargin || 64)
30
- // 为兼容性不使用 findLast,而是使用倒序的 tocOffsets
31
- return tocOffsets.value.find(item => item.offsetTop && item.offsetTop <= scrollOffset)?.id
32
- }
33
-
34
- const activeHeadingId = computedWithControl(
35
- refThrottled(scrollY, undefined, true),
36
- () => document && getActiveHeading(),
37
- )
38
-
39
- function scrollToActiveTocItem() {
40
- const el = unrefElement(scrollableEl)
41
- const active = el?.querySelector<HTMLLinkElement>(`a[href="#${activeHeadingId.value}"]`)
42
-
43
- if (el && active) {
44
- el.scrollTo({
45
- top: active.offsetTop - el.clientHeight / 4,
46
- behavior: 'smooth',
47
- })
48
- }
49
- }
50
-
51
- watch(activeHeadingId, scrollToActiveTocItem)
52
-
53
- return {
54
- tocOffsets,
55
- activeHeadingId,
56
- }
57
- }
1
+ import type { Toc, TocLink } from '@nuxt/content'
2
+ import type { MaybeComputedElementRef } from '@vueuse/core'
3
+
4
+ interface TocList {
5
+ id: string
6
+ offsetTop: number | undefined
7
+ }
8
+
9
+ export function useToc(toc: MaybeRefOrGetter<Toc | undefined>, scrollableEl?: MaybeComputedElementRef) {
10
+ const { height: bodyHeight } = useElementSize(document?.body)
11
+ const { y: scrollY } = useWindowScroll()
12
+
13
+ function flattenToc(tocTree: TocLink[], tocList: TocList[] = []) {
14
+ tocTree.forEach(({ id, children }) => {
15
+ const headingOffset = document?.getElementById(id)?.offsetTop
16
+ tocList.push({ id, offsetTop: headingOffset })
17
+ children && flattenToc(children, tocList)
18
+ })
19
+ return tocList
20
+ }
21
+
22
+ const tocOffsets = computedWithControl(
23
+ refDebounced(bodyHeight),
24
+ () => flattenToc(toValue(toc)?.links || []).reverse(),
25
+ )
26
+
27
+ function getActiveHeading() {
28
+ const scrollMargin = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('scroll-margin-top'))
29
+ const scrollOffset = scrollY.value + (scrollMargin || 64)
30
+ // 为兼容性不使用 findLast,而是使用倒序的 tocOffsets
31
+ return tocOffsets.value.find(item => item.offsetTop && item.offsetTop <= scrollOffset)?.id
32
+ }
33
+
34
+ const activeHeadingId = computedWithControl(
35
+ refThrottled(scrollY, undefined, true),
36
+ () => document && getActiveHeading(),
37
+ )
38
+
39
+ function scrollToActiveTocItem() {
40
+ const el = unrefElement(scrollableEl)
41
+ const active = el?.querySelector<HTMLLinkElement>(`a[href="#${activeHeadingId.value}"]`)
42
+
43
+ if (el && active) {
44
+ el.scrollTo({
45
+ top: active.offsetTop - el.clientHeight / 4,
46
+ behavior: 'smooth',
47
+ })
48
+ }
49
+ }
50
+
51
+ watch(activeHeadingId, scrollToActiveTocItem)
52
+
53
+ return {
54
+ tocOffsets,
55
+ activeHeadingId,
56
+ }
57
+ }
@@ -1,56 +1,56 @@
1
- import { pascalCase } from 'es-toolkit/string'
2
- import {
3
- ContentRenderer,
4
- LazyBlogWidget,
5
- LazyWidgetBlogLog,
6
- LazyWidgetBlogStats,
7
- LazyWidgetBlogTech,
8
- LazyWidgetEmpty,
9
- LazyWidgetToc,
10
- } from '#components'
11
-
12
- // @keep-sorted
13
- const rawWidgets = {
14
- LazyWidgetBlogLog,
15
- LazyWidgetBlogStats,
16
- LazyWidgetBlogTech,
17
- LazyWidgetEmpty,
18
- LazyWidgetToc,
19
- }
20
-
21
- type RawWidgetName = keyof typeof rawWidgets
22
-
23
- /** 若首字母大写还需移除`-`前缀 */
24
- type KebabCase<S extends string> = S extends `${infer First}${infer Rest}`
25
- ? `${First extends Capitalize<First> ? '-' : ''}${Lowercase<First>}${KebabCase<Rest>}`
26
- : ''
27
-
28
- type RemovePrefix<S extends string, Prefix extends string> = S extends `${Prefix}${infer Rest}` ? Rest : S
29
-
30
- export type WidgetName = RemovePrefix<KebabCase<RawWidgetName>, '-lazy-widget-'> | `meta-aside-${string}`
31
-
32
- export default function useWidgets(widgetList: MaybeRefOrGetter<WidgetName[]>) {
33
- const { metaSlots } = useArticle()
34
-
35
- function renderMetaSlots(widgetName: WidgetName) {
36
- const slotsTree = metaSlots.value?.[widgetName.slice('meta-'.length)]
37
- return h(
38
- LazyBlogWidget,
39
- { card: !slotsTree, ...slotsTree?.props },
40
- () => slotsTree
41
- ? h(ContentRenderer, { value: slotsTree })
42
- : `${widgetName} 不存在`,
43
- )
44
- }
45
-
46
- const widgets = computed(() => toValue(widgetList).map(widgetName => ({
47
- name: widgetName,
48
- comp: widgetName.startsWith('meta-aside-')
49
- ? renderMetaSlots(widgetName)
50
- : rawWidgets[`LazyWidget${pascalCase(widgetName)}` as RawWidgetName],
51
- })))
52
-
53
- return {
54
- widgets,
55
- }
56
- }
1
+ import { pascalCase } from 'es-toolkit/string'
2
+ import {
3
+ ContentRenderer,
4
+ LazyBlogWidget,
5
+ LazyWidgetBlogLog,
6
+ LazyWidgetBlogStats,
7
+ LazyWidgetBlogTech,
8
+ LazyWidgetEmpty,
9
+ LazyWidgetToc,
10
+ } from '#components'
11
+
12
+ // @keep-sorted
13
+ const rawWidgets = {
14
+ LazyWidgetBlogLog,
15
+ LazyWidgetBlogStats,
16
+ LazyWidgetBlogTech,
17
+ LazyWidgetEmpty,
18
+ LazyWidgetToc,
19
+ }
20
+
21
+ type RawWidgetName = keyof typeof rawWidgets
22
+
23
+ /** 若首字母大写还需移除`-`前缀 */
24
+ type KebabCase<S extends string> = S extends `${infer First}${infer Rest}`
25
+ ? `${First extends Capitalize<First> ? '-' : ''}${Lowercase<First>}${KebabCase<Rest>}`
26
+ : ''
27
+
28
+ type RemovePrefix<S extends string, Prefix extends string> = S extends `${Prefix}${infer Rest}` ? Rest : S
29
+
30
+ export type WidgetName = RemovePrefix<KebabCase<RawWidgetName>, '-lazy-widget-'> | `meta-aside-${string}`
31
+
32
+ export default function useWidgets(widgetList: MaybeRefOrGetter<WidgetName[]>) {
33
+ const { metaSlots } = useArticle()
34
+
35
+ function renderMetaSlots(widgetName: WidgetName) {
36
+ const slotsTree = metaSlots.value?.[widgetName.slice('meta-'.length)]
37
+ return h(
38
+ LazyBlogWidget,
39
+ { card: !slotsTree, ...slotsTree?.props },
40
+ () => slotsTree
41
+ ? h(ContentRenderer, { value: slotsTree })
42
+ : `${widgetName} 不存在`,
43
+ )
44
+ }
45
+
46
+ const widgets = computed(() => toValue(widgetList).map(widgetName => ({
47
+ name: widgetName,
48
+ comp: widgetName.startsWith('meta-aside-')
49
+ ? renderMetaSlots(widgetName)
50
+ : rawWidgets[`LazyWidget${pascalCase(widgetName)}` as RawWidgetName],
51
+ })))
52
+
53
+ return {
54
+ widgets,
55
+ }
56
+ }
@@ -1,75 +1,75 @@
1
- import type { NavItem } from '../app/types/nav'
1
+ import type { NavItem } from '../types/nav'
2
2
  import type { ClarityPublicConfig } from './public'
3
3
 
4
4
  export interface NavGroup {
5
5
  title: string
6
6
  items: NavItem[]
7
7
  }
8
-
9
- /** Theme 提供的 UI 默认值,全部可由消费项目在 app/app.config.ts 中覆盖 */
10
- export interface ClarityUiConfig {
11
- component: {
12
- alert: {
13
- defaultStyle: 'card' | 'flat'
14
- }
15
- codeblock: {
16
- triggerRows: number
17
- collapsedRows: number
18
- enableIndentGuide: boolean
19
- indent: number
20
- tabSize: number
21
- }
22
- excerpt: {
23
- animation: boolean
24
- caret: string
25
- }
26
- slide: {
27
- showTitle: boolean
28
- }
29
- stats: {
30
- /** 归档页面每年标题对应的年龄 */
31
- birthYear?: number
32
- }
33
- }
34
- footer: {
35
- /** 页脚版权信息,支持 <br> 等内联 HTML */
36
- copyright: string
37
- /** 侧边栏底部图标导航 */
38
- iconNav: NavItem[]
8
+
9
+ /** Theme 提供的 UI 默认值,全部可由消费项目在 app/app.config.ts 中覆盖 */
10
+ export interface ClarityUiConfig {
11
+ component: {
12
+ alert: {
13
+ defaultStyle: 'card' | 'flat'
14
+ }
15
+ codeblock: {
16
+ triggerRows: number
17
+ collapsedRows: number
18
+ enableIndentGuide: boolean
19
+ indent: number
20
+ tabSize: number
21
+ }
22
+ excerpt: {
23
+ animation: boolean
24
+ caret: string
25
+ }
26
+ slide: {
27
+ showTitle: boolean
28
+ }
29
+ stats: {
30
+ /** 归档页面每年标题对应的年龄 */
31
+ birthYear?: number
32
+ }
33
+ }
34
+ footer: {
35
+ /** 页脚版权信息,支持 <br> 等内联 HTML */
36
+ copyright: string
37
+ /** 侧边栏底部图标导航 */
38
+ iconNav: NavItem[]
39
39
  /** 页脚站点地图 */
40
40
  nav: NavGroup[]
41
- }
42
- header: {
43
- /** 左侧栏顶部 Logo,默认使用 site.author.avatar */
44
- logo: string
45
- /** 展示标题文本,否则仅展示 Logo */
46
- showTitle: boolean
47
- /** 标题下的副标题,默认使用 site.subtitle */
48
- subtitle: string
49
- /** 标题后的随机表情 */
50
- emojiTail: string[]
51
- }
52
- link: {
53
- /** 无订阅源展示静音图标 */
54
- remindNoFeed: boolean
55
- /** 友链分组内随机排序 */
56
- randomInGroup: boolean
57
- }
41
+ }
42
+ header: {
43
+ /** 左侧栏顶部 Logo,默认使用 site.author.avatar */
44
+ logo: string
45
+ /** 展示标题文本,否则仅展示 Logo */
46
+ showTitle: boolean
47
+ /** 标题下的副标题,默认使用 site.subtitle */
48
+ subtitle: string
49
+ /** 标题后的随机表情 */
50
+ emojiTail: string[]
51
+ }
52
+ link: {
53
+ /** 无订阅源展示静音图标 */
54
+ remindNoFeed: boolean
55
+ /** 友链分组内随机排序 */
56
+ randomInGroup: boolean
57
+ }
58
58
  /** 左侧栏导航 */
59
59
  nav: NavGroup[]
60
- pagination: {
61
- perPage: number
62
- /** 默认排序方式,需为 article.order 中的键名 */
63
- sortOrder: string
64
- /** 允许文章列表正序,开启后排序方式左侧图标可切换顺序 */
65
- allowAscending: boolean
66
- }
67
- themes: Record<'light' | 'system' | 'dark', {
68
- icon: string
69
- tip: string
70
- }>
71
- }
72
-
60
+ pagination: {
61
+ perPage: number
62
+ /** 默认排序方式,需为 article.order 中的键名 */
63
+ sortOrder: string
64
+ /** 允许文章列表正序,开启后排序方式左侧图标可切换顺序 */
65
+ allowAscending: boolean
66
+ }
67
+ themes: Record<'light' | 'system' | 'dark', {
68
+ icon: string
69
+ tip: string
70
+ }>
71
+ }
72
+
73
73
  export type ClarityAppConfig = ClarityPublicConfig & ClarityUiConfig
74
74
 
75
75
  /**