astro-pure 1.0.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/bun.lockb +0 -0
- package/bunfig.toml +2 -0
- package/components/advanced/Comment.astro +148 -0
- package/components/advanced/GithubCard.astro +148 -0
- package/components/advanced/LinkPreview.astro +82 -0
- package/components/advanced/MediumZoom.astro +50 -0
- package/components/advanced/QRCode.astro +35 -0
- package/components/advanced/Quote.astro +44 -0
- package/components/advanced/index.ts +11 -0
- package/components/user/Aside.astro +74 -0
- package/components/user/Button.astro +79 -0
- package/components/user/Card.astro +23 -0
- package/components/user/CardList.astro +28 -0
- package/components/user/CardListChildren.astro +24 -0
- package/components/user/Collapse.astro +84 -0
- package/components/user/FormattedDate.astro +21 -0
- package/components/user/Label.astro +18 -0
- package/components/user/Spoiler.astro +11 -0
- package/components/user/Steps.astro +84 -0
- package/components/user/TabItem.astro +18 -0
- package/components/user/Tabs.astro +266 -0
- package/components/user/Timeline.astro +38 -0
- package/components/user/index.ts +17 -0
- package/index.ts +74 -0
- package/package.json +38 -0
- package/plugins/link-preview.ts +110 -0
- package/plugins/rehype-steps.ts +98 -0
- package/plugins/rehype-tabs.ts +112 -0
- package/plugins/virtual-user-config.ts +83 -0
- package/schemas/favicon.ts +42 -0
- package/schemas/head.ts +18 -0
- package/schemas/logo.ts +28 -0
- package/schemas/social.ts +51 -0
- package/types/common.d.ts +48 -0
- package/types/index.d.ts +6 -0
- package/types/integrations-config.ts +43 -0
- package/types/theme-config.ts +125 -0
- package/types/user-config.ts +24 -0
- package/utils/clsx.ts +24 -0
- package/utils/collections.ts +48 -0
- package/utils/date.ts +17 -0
- package/utils/docsContents.ts +36 -0
- package/utils/index.ts +23 -0
- package/utils/module.d.ts +25 -0
- package/utils/server.ts +11 -0
- package/utils/tailwind.ts +7 -0
- package/utils/theme.ts +40 -0
- package/utils/toast.ts +3 -0
- package/utils/toc.ts +41 -0
- package/virtual.d.ts +2 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
declare module 'virtual:types' {
|
|
2
|
+
export type MenuLinks = { link: string; label: string }[]
|
|
3
|
+
|
|
4
|
+
export interface PaginationLink {
|
|
5
|
+
url: string
|
|
6
|
+
text?: string
|
|
7
|
+
srLabel?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SiteMeta {
|
|
11
|
+
title: string
|
|
12
|
+
description?: string
|
|
13
|
+
ogImage?: string | undefined
|
|
14
|
+
articleDate?: string | undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SocialLink {
|
|
18
|
+
name:
|
|
19
|
+
| 'coolapk'
|
|
20
|
+
| 'telegram'
|
|
21
|
+
| 'github'
|
|
22
|
+
| 'bilibili'
|
|
23
|
+
| 'twitter'
|
|
24
|
+
| 'zhihu'
|
|
25
|
+
| 'steam'
|
|
26
|
+
| 'netease_music'
|
|
27
|
+
| 'mail'
|
|
28
|
+
url: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ShareItem = 'weibo' | 'x' | 'bluesky'
|
|
32
|
+
|
|
33
|
+
export type CardListData = {
|
|
34
|
+
title: string
|
|
35
|
+
list: CardList
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type CardList = {
|
|
39
|
+
title: string
|
|
40
|
+
link?: string
|
|
41
|
+
children?: CardList
|
|
42
|
+
}[]
|
|
43
|
+
|
|
44
|
+
export type TimelineEvent = {
|
|
45
|
+
date: string
|
|
46
|
+
content: string
|
|
47
|
+
}
|
|
48
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
|
|
3
|
+
export const IntegrationConfigSchema = () =>
|
|
4
|
+
z.object({
|
|
5
|
+
/**
|
|
6
|
+
* Define whether Starlight’s default site search provider Pagefind is enabled.
|
|
7
|
+
* Set to `false` to disable indexing your site with Pagefind.
|
|
8
|
+
* This will also hide the default search UI if in use.
|
|
9
|
+
*/
|
|
10
|
+
pagefind: z.boolean().optional(),
|
|
11
|
+
quote: z.object({
|
|
12
|
+
/** The server to fetch the quote from. */
|
|
13
|
+
server: z.string(),
|
|
14
|
+
// target: (data: unknown) => string
|
|
15
|
+
target: z.function().args(z.unknown()).returns(z.string())
|
|
16
|
+
}),
|
|
17
|
+
typography: z.object({
|
|
18
|
+
/** The class to apply to the typography. */
|
|
19
|
+
class: z
|
|
20
|
+
.string()
|
|
21
|
+
.default('prose prose-pure dark:prose-invert dark:prose-pure prose-headings:font-medium')
|
|
22
|
+
}),
|
|
23
|
+
mediumZoom: z.object({
|
|
24
|
+
/** Enable the medium zoom library. */
|
|
25
|
+
enable: z.boolean().default(true),
|
|
26
|
+
/** The selector to apply the zoom effect to. */
|
|
27
|
+
selector: z.string().default('.prose .zoomable'),
|
|
28
|
+
/** Options to pass to the medium zoom library. */
|
|
29
|
+
options: z.record(z.string(), z.any()).default({ className: 'zoomable' })
|
|
30
|
+
}),
|
|
31
|
+
waline: z.object({
|
|
32
|
+
/** Enable the Waline comment system. */
|
|
33
|
+
enable: z.boolean().default(false),
|
|
34
|
+
/** The server to use for the Waline comment system. */
|
|
35
|
+
server: z.string().optional(),
|
|
36
|
+
/** The emoji to use for the Waline comment system. */
|
|
37
|
+
emoji: z.array(z.string()).optional(),
|
|
38
|
+
/** Additional configurations for the Waline comment system. */
|
|
39
|
+
additionalConfigs: z.record(z.string(), z.any()).default({})
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export type IntegrationConfig = z.infer<ReturnType<typeof IntegrationConfigSchema>>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
|
|
3
|
+
import { FaviconSchema } from '../schemas/favicon'
|
|
4
|
+
import { HeadConfigSchema } from '../schemas/head'
|
|
5
|
+
import { LogoConfigSchema } from '../schemas/logo'
|
|
6
|
+
import { SocialLinksSchema } from '../schemas/social'
|
|
7
|
+
|
|
8
|
+
export const ThemeConfigSchema = z.object({
|
|
9
|
+
/** Title for your website. Will be used in metadata and as browser tab title. */
|
|
10
|
+
title: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('Title for your website. Will be used in metadata and as browser tab title.'),
|
|
13
|
+
|
|
14
|
+
/** Description metadata for your website. Can be used in page metadata. */
|
|
15
|
+
description: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Description metadata for your website. Can be used in page metadata.'),
|
|
19
|
+
|
|
20
|
+
/** Set a logo image to show in the homepage. */
|
|
21
|
+
logo: LogoConfigSchema(),
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Optional details about the social media accounts for this site.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* social: {
|
|
28
|
+
* codeberg: 'https://codeberg.org/knut/examples',
|
|
29
|
+
* discord: 'https://astro.build/chat',
|
|
30
|
+
* github: 'https://github.com/withastro/starlight',
|
|
31
|
+
* gitlab: 'https://gitlab.com/delucis',
|
|
32
|
+
* linkedin: 'https://www.linkedin.com/company/astroinc',
|
|
33
|
+
* mastodon: 'https://m.webtoo.ls/@astro',
|
|
34
|
+
* threads: 'https://www.threads.net/@nmoodev',
|
|
35
|
+
* twitch: 'https://www.twitch.tv/bholmesdev',
|
|
36
|
+
* twitter: 'https://twitter.com/astrodotbuild',
|
|
37
|
+
* youtube: 'https://youtube.com/@astrodotbuild',
|
|
38
|
+
* }
|
|
39
|
+
*/
|
|
40
|
+
social: SocialLinksSchema(),
|
|
41
|
+
|
|
42
|
+
/** The tagline for your website. */
|
|
43
|
+
tagline: z.string().optional().describe('The tagline for your website.'),
|
|
44
|
+
|
|
45
|
+
/** Configure the defaults for the table of contents on each page. */
|
|
46
|
+
// tableOfContents: TableOfContentsSchema(),
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Specify the default language for this site.
|
|
50
|
+
*
|
|
51
|
+
* The default locale will be used to provide fallback content where translations are missing.
|
|
52
|
+
*/
|
|
53
|
+
locale: z.string().optional(),
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add extra tags to your site’s `<head>`.
|
|
57
|
+
*
|
|
58
|
+
* Can also be set for a single page in a page’s frontmatter.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Add Fathom analytics to your site
|
|
62
|
+
* starlight({
|
|
63
|
+
* head: [
|
|
64
|
+
* {
|
|
65
|
+
* tag: 'script',
|
|
66
|
+
* attrs: {
|
|
67
|
+
* src: 'https://cdn.usefathom.com/script.js',
|
|
68
|
+
* 'data-site': 'MY-FATHOM-ID',
|
|
69
|
+
* defer: true,
|
|
70
|
+
* },
|
|
71
|
+
* },
|
|
72
|
+
* ],
|
|
73
|
+
* })
|
|
74
|
+
*/
|
|
75
|
+
head: HeadConfigSchema(),
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Provide CSS files to customize the look and feel of your Starlight site.
|
|
79
|
+
*
|
|
80
|
+
* Supports local CSS files relative to the root of your project,
|
|
81
|
+
* e.g. `'/src/custom.css'`, and CSS you installed as an npm
|
|
82
|
+
* module, e.g. `'@fontsource/roboto'`.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* starlight({
|
|
86
|
+
* customCss: ['/src/custom-styles.css', '@fontsource/roboto'],
|
|
87
|
+
* })
|
|
88
|
+
*/
|
|
89
|
+
customCss: z.string().array().optional().default([]),
|
|
90
|
+
|
|
91
|
+
/** The default favicon for your site which should be a path to an image in the `public/` directory. */
|
|
92
|
+
favicon: FaviconSchema(),
|
|
93
|
+
|
|
94
|
+
/** Will be used as title delimiter in the generated `<title>` tag. */
|
|
95
|
+
titleDelimiter: z
|
|
96
|
+
.string()
|
|
97
|
+
.default('|')
|
|
98
|
+
.describe('Will be used as title delimiter in the generated `<title>` tag.'),
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Define whether Starlight pages should be prerendered or not.
|
|
102
|
+
* Defaults to always prerender Starlight pages, even when the project is
|
|
103
|
+
* set to "server" output mode.
|
|
104
|
+
*/
|
|
105
|
+
prerender: z.boolean().default(true),
|
|
106
|
+
|
|
107
|
+
/** Enable displaying a “Built with Starlight” link in your site’s footer. */
|
|
108
|
+
credits: z
|
|
109
|
+
.boolean()
|
|
110
|
+
.default(false)
|
|
111
|
+
.describe('Enable displaying a “Built with Starlight” link in your site’s footer.'),
|
|
112
|
+
|
|
113
|
+
/** The npm CDN to use for loading npm packages.
|
|
114
|
+
* @example
|
|
115
|
+
* npmCDN: 'https://cdn.jsdelivr.net/npm'
|
|
116
|
+
* npmCDN: 'https://cdn.smartcis.cn/npm'
|
|
117
|
+
* npmCDN: 'https://unkpg.com'
|
|
118
|
+
* npmCDN: 'https://cdn.cbd.int'
|
|
119
|
+
* npmCDN: 'https://esm.sh'
|
|
120
|
+
*/
|
|
121
|
+
npmCDN: z
|
|
122
|
+
.string()
|
|
123
|
+
.default('https://esm.sh')
|
|
124
|
+
.describe('The npm CDN to use for loading npm packages.')
|
|
125
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { IntegrationConfigSchema } from './integrations-config'
|
|
4
|
+
import { ThemeConfigSchema } from './theme-config'
|
|
5
|
+
|
|
6
|
+
export const UserConfigSchema = ThemeConfigSchema.strict()
|
|
7
|
+
.merge(
|
|
8
|
+
z.object({
|
|
9
|
+
integ: IntegrationConfigSchema()
|
|
10
|
+
})
|
|
11
|
+
)
|
|
12
|
+
.transform((config) => ({
|
|
13
|
+
...config,
|
|
14
|
+
// Pagefind only defaults to true if prerender is also true.
|
|
15
|
+
integ: {
|
|
16
|
+
...config.integ,
|
|
17
|
+
pagefind: config.integ.pagefind ?? config.prerender
|
|
18
|
+
}
|
|
19
|
+
}))
|
|
20
|
+
.refine((config) => !(config.integ.pagefind && !config.prerender), {
|
|
21
|
+
message: 'Pagefind search is not supported with prerendering disabled.'
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export type UserConfig = z.infer<typeof UserConfigSchema>
|
package/utils/clsx.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ClassValue = ClassArray | ClassDictionary | string | number | null | boolean | undefined
|
|
2
|
+
export type ClassDictionary = Record<string, unknown>
|
|
3
|
+
export type ClassArray = ClassValue[]
|
|
4
|
+
|
|
5
|
+
export function clsx(...inputs: ClassValue[]): string {
|
|
6
|
+
let str = ''
|
|
7
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
8
|
+
const input = inputs[i]
|
|
9
|
+
if (typeof input === 'string' || typeof input === 'number') {
|
|
10
|
+
str += (str && ' ') + input
|
|
11
|
+
} else if (Array.isArray(input)) {
|
|
12
|
+
str += (str && ' ') + clsx(...input)
|
|
13
|
+
} else if (typeof input === 'object') {
|
|
14
|
+
for (const key in input) {
|
|
15
|
+
if (input[key]) {
|
|
16
|
+
str += (str && ' ') + key
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return str
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default clsx
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type CollectionEntry, type CollectionKey } from 'astro:content'
|
|
2
|
+
|
|
3
|
+
type Collections = CollectionEntry<CollectionKey>[]
|
|
4
|
+
|
|
5
|
+
export function groupCollectionsByYear<T extends CollectionKey>(
|
|
6
|
+
collections: Collections
|
|
7
|
+
): [number, CollectionEntry<T>[]][] {
|
|
8
|
+
const collectionsByYear = collections.reduce((acc, collection) => {
|
|
9
|
+
const year = new Date(collection.data.updatedDate ?? collection.data.publishDate).getFullYear()
|
|
10
|
+
if (!acc.has(year)) {
|
|
11
|
+
acc.set(year, [])
|
|
12
|
+
}
|
|
13
|
+
acc.get(year)!.push(collection)
|
|
14
|
+
return acc
|
|
15
|
+
}, new Map<number, Collections>())
|
|
16
|
+
|
|
17
|
+
return Array.from(
|
|
18
|
+
collectionsByYear.entries() as IterableIterator<[number, CollectionEntry<T>[]]>
|
|
19
|
+
).sort((a, b) => b[0] - a[0])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sortMDByDate(collections: Collections) {
|
|
23
|
+
return collections.sort((a, b) => {
|
|
24
|
+
const aDate = new Date(a.data.updatedDate ?? a.data.publishDate).valueOf()
|
|
25
|
+
const bDate = new Date(b.data.updatedDate ?? b.data.publishDate).valueOf()
|
|
26
|
+
return bDate - aDate
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */
|
|
31
|
+
export function getAllTags(collections: Collections) {
|
|
32
|
+
return collections.flatMap((collection) => [...collection.data.tags])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */
|
|
36
|
+
export function getUniqueTags(collections: Collections) {
|
|
37
|
+
return [...new Set(getAllTags(collections))]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */
|
|
41
|
+
export function getUniqueTagsWithCount(collections: Collections): [string, number][] {
|
|
42
|
+
return [
|
|
43
|
+
...getAllTags(collections).reduce(
|
|
44
|
+
(acc, t) => acc.set(t, (acc.get(t) || 0) + 1),
|
|
45
|
+
new Map<string, number>()
|
|
46
|
+
)
|
|
47
|
+
].sort((a, b) => b[1] - a[1])
|
|
48
|
+
}
|
package/utils/date.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { siteConfig } from '@/site-config'
|
|
2
|
+
|
|
3
|
+
const dateFormat = new Intl.DateTimeFormat(siteConfig.date.locale, siteConfig.date.options)
|
|
4
|
+
|
|
5
|
+
export function getFormattedDate(
|
|
6
|
+
date: string | number | Date,
|
|
7
|
+
options?: Intl.DateTimeFormatOptions
|
|
8
|
+
) {
|
|
9
|
+
if (typeof options !== 'undefined') {
|
|
10
|
+
return new Date(date).toLocaleDateString(siteConfig.date.locale, {
|
|
11
|
+
...(siteConfig.date.options as Intl.DateTimeFormatOptions),
|
|
12
|
+
...options
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return dateFormat.format(new Date(date))
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CardListData } from '@/types'
|
|
2
|
+
|
|
3
|
+
// Docs content declaration
|
|
4
|
+
export const docs: CardListData = {
|
|
5
|
+
title: 'Docs content',
|
|
6
|
+
list: [
|
|
7
|
+
{
|
|
8
|
+
title: 'Setup',
|
|
9
|
+
children: [
|
|
10
|
+
{ title: 'Getting Started', link: '/docs/setup/getting-started' },
|
|
11
|
+
{ title: 'Configuration', link: '/docs/setup/configuration' },
|
|
12
|
+
{ title: 'Authoring Content', link: '/docs/setup/content' },
|
|
13
|
+
{ title: 'Deployment', link: '/docs/setup/deployment' }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: 'Integrations',
|
|
18
|
+
children: [
|
|
19
|
+
{ title: 'Comment System', link: '/docs/integrations/comment' },
|
|
20
|
+
{ title: 'Friend Links', link: '/docs/integrations/links' },
|
|
21
|
+
{ title: 'Shiki Code', link: '/docs/integrations/code' },
|
|
22
|
+
{ title: 'User Components', link: '/docs/integrations/components' },
|
|
23
|
+
{ title: 'Advanced Components', link: '/docs/integrations/advanced' },
|
|
24
|
+
{ title: 'Other Integrations', link: '/docs/integrations/others' }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: 'Advanced',
|
|
29
|
+
children: [
|
|
30
|
+
{ title: 'Update Theme', link: '/docs/advanced/update' },
|
|
31
|
+
{ title: 'Optimize Your Site', link: '/docs/advanced/optimize' },
|
|
32
|
+
{ title: 'Acknowledgements', link: '/docs/advanced/thanks' }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
package/utils/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Tailwind
|
|
2
|
+
export { cn } from './tailwind'
|
|
3
|
+
|
|
4
|
+
// Collections
|
|
5
|
+
export {
|
|
6
|
+
groupCollectionsByYear,
|
|
7
|
+
sortMDByDate,
|
|
8
|
+
getUniqueTags,
|
|
9
|
+
getUniqueTagsWithCount
|
|
10
|
+
} from './collections'
|
|
11
|
+
|
|
12
|
+
// Date
|
|
13
|
+
export { getFormattedDate } from './date'
|
|
14
|
+
|
|
15
|
+
// Toc
|
|
16
|
+
export { generateToc } from './toc'
|
|
17
|
+
export type { TocItem } from './toc'
|
|
18
|
+
|
|
19
|
+
// Theme
|
|
20
|
+
export { getTheme, listenThemeChange, setTheme } from './theme'
|
|
21
|
+
|
|
22
|
+
// Toast
|
|
23
|
+
export { showToast } from './toast'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare module '../../utils' {
|
|
2
|
+
// Tailwind
|
|
3
|
+
export { cn } from './tailwind'
|
|
4
|
+
|
|
5
|
+
// Collections
|
|
6
|
+
export {
|
|
7
|
+
groupCollectionsByYear,
|
|
8
|
+
sortMDByDate,
|
|
9
|
+
getUniqueTags,
|
|
10
|
+
getUniqueTagsWithCount
|
|
11
|
+
} from './collections'
|
|
12
|
+
|
|
13
|
+
// Date
|
|
14
|
+
export { getFormattedDate } from './date'
|
|
15
|
+
|
|
16
|
+
// Toc
|
|
17
|
+
export { generateToc } from './toc'
|
|
18
|
+
export type { TocItem } from './toc'
|
|
19
|
+
|
|
20
|
+
// Theme
|
|
21
|
+
export { getTheme, listenThemeChange, setTheme } from './theme'
|
|
22
|
+
|
|
23
|
+
// Toast
|
|
24
|
+
export { showToast } from './toast'
|
|
25
|
+
}
|
package/utils/server.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getCollection, type CollectionEntry, type CollectionKey } from 'astro:content'
|
|
2
|
+
|
|
3
|
+
export const prod = import.meta.env.PROD
|
|
4
|
+
|
|
5
|
+
/** Note: this function filters out draft posts based on the environment */
|
|
6
|
+
export async function getBlogCollection(contentType: CollectionKey = 'blog') {
|
|
7
|
+
return await getCollection(contentType, ({ data }: CollectionEntry<typeof contentType>) => {
|
|
8
|
+
// Not in production & draft is not false
|
|
9
|
+
return !prod || data.draft !== false
|
|
10
|
+
})
|
|
11
|
+
}
|
package/utils/theme.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function getTheme() {
|
|
2
|
+
return localStorage.getItem('theme')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function listenThemeChange(theme?: string) {
|
|
6
|
+
if (!theme || theme === 'system') return // if theme is specified, no need to listen window theme change
|
|
7
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
8
|
+
setTheme(e.matches ? 'dark' : 'light')
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setTheme(theme?: string, save = false) {
|
|
13
|
+
const themes = ['system', 'dark', 'light']
|
|
14
|
+
if (theme) {
|
|
15
|
+
if (!themes.includes(theme)) return
|
|
16
|
+
if (save) localStorage.setItem('theme', theme)
|
|
17
|
+
} else {
|
|
18
|
+
theme = getTheme() ?? undefined
|
|
19
|
+
if (save) {
|
|
20
|
+
// Set theme equals undefined, switch cycle in ['system', 'dark', 'light']
|
|
21
|
+
const currentIndex = themes.indexOf(theme ?? 'system')
|
|
22
|
+
theme = themes[(currentIndex + 1) % themes.length]
|
|
23
|
+
localStorage.setItem('theme', theme) // save theme
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let targetTheme = theme
|
|
27
|
+
if (theme === 'system') {
|
|
28
|
+
targetTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
29
|
+
// Listen theme change
|
|
30
|
+
listenThemeChange(theme)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Set theme
|
|
34
|
+
document.documentElement.classList.toggle('dark', targetTheme === 'dark')
|
|
35
|
+
document
|
|
36
|
+
.querySelector('meta[name="theme-color"]')
|
|
37
|
+
?.setAttribute('content', targetTheme === 'dark' ? '#0B0B10' : '#FCFCFD')
|
|
38
|
+
|
|
39
|
+
return theme
|
|
40
|
+
}
|
package/utils/toast.ts
ADDED
package/utils/toc.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MarkdownHeading } from 'astro'
|
|
2
|
+
|
|
3
|
+
export interface TocItem extends MarkdownHeading {
|
|
4
|
+
subheadings: TocItem[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function diveChildren(item: TocItem, depth: number): TocItem[] {
|
|
8
|
+
if (depth === 1 || !item.subheadings.length) {
|
|
9
|
+
return item.subheadings
|
|
10
|
+
} else {
|
|
11
|
+
// e.g., 2
|
|
12
|
+
return diveChildren(item.subheadings[item.subheadings.length - 1] as TocItem, depth - 1)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function generateToc(headings: readonly MarkdownHeading[]) {
|
|
17
|
+
// this ignores/filters out h1 element(s)
|
|
18
|
+
const bodyHeadings = [...headings.filter(({ depth }) => depth > 1)]
|
|
19
|
+
const toc: TocItem[] = []
|
|
20
|
+
|
|
21
|
+
bodyHeadings.forEach((h) => {
|
|
22
|
+
const heading: TocItem = { ...h, subheadings: [] }
|
|
23
|
+
|
|
24
|
+
// add h2 elements into the top level
|
|
25
|
+
if (heading.depth === 2) {
|
|
26
|
+
toc.push(heading)
|
|
27
|
+
} else {
|
|
28
|
+
const lastItemInToc = toc[toc.length - 1]!
|
|
29
|
+
if (heading.depth < lastItemInToc.depth) {
|
|
30
|
+
throw new Error(`Orphan heading found: ${heading.text}.`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// higher depth
|
|
34
|
+
// push into children, or children's children
|
|
35
|
+
const gap = heading.depth - lastItemInToc.depth
|
|
36
|
+
const target = diveChildren(lastItemInToc, gap)
|
|
37
|
+
target.push(heading)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
return toc
|
|
41
|
+
}
|
package/virtual.d.ts
ADDED