@jaamd/astro 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +63 -2
- package/dist/default.css +1 -0
- package/dist/shiki-dual.css +1 -0
- package/dist/styles.css +1 -0
- package/dist/themes/catppuccin/index.css +1 -0
- package/dist/themes/dracula/dark.css +1 -0
- package/dist/themes/dracula/index.css +1 -0
- package/dist/themes/gruvbox/index.css +1 -0
- package/dist/themes/nord/dark.css +1 -0
- package/dist/themes/nord/index.css +1 -0
- package/dist/themes/one-dark/dark.css +1 -0
- package/dist/themes/one-dark/index.css +1 -0
- package/dist/themes/rose-pine/index.css +1 -0
- package/dist/themes/rose-pine-moon/index.css +1 -0
- package/dist/themes/tokyo-night/dark.css +1 -0
- package/dist/themes/tokyo-night/index.css +1 -0
- package/docs/00-overview.md +33 -0
- package/docs/01-getting-started/01-installation.md +109 -0
- package/docs/01-getting-started/02-options.md +81 -0
- package/docs/02-markdown.md +156 -0
- package/docs/03-usage/01-markdown-content.md +45 -0
- package/docs/03-usage/02-client.md +70 -0
- package/docs/04-styling/01-variables.md +87 -0
- package/docs/04-styling/02-themes.md +90 -0
- package/docs/04-styling/03-markup.md +29 -0
- package/docs/05-advanced/01-manual-setup.md +72 -0
- package/docs/05-advanced/02-frontmatter.md +20 -0
- package/docs/05-advanced/03-other-frameworks.md +111 -0
- package/docs/06-upgrading.md +156 -0
- package/index.ts +6 -0
- package/package.json +73 -3
- package/src/astro-satteri.ts +10 -0
- package/src/client.ts +1 -0
- package/src/components/MarkdownContent.astro +21 -0
- package/src/components/MarkdownContent.astro.d.ts +12 -0
- package/src/frontmatter.ts +27 -0
- package/src/integration.ts +96 -0
- package/src/processors.ts +135 -0
- package/src/rehype-heading-ids.js +5 -0
- package/src/shiki.ts +29 -0
- package/src/themes.ts +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLTag } from 'astro/types'
|
|
3
|
+
import '../../dist/default.css'
|
|
4
|
+
import '../../dist/styles.css'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Kept deliberately non-generic: Astro's parser reads `<Tag extends HTMLTag>` as
|
|
8
|
+
* an HTML tag. The real generic type lives in MarkdownContent.astro.d.ts.
|
|
9
|
+
*/
|
|
10
|
+
type Props = {
|
|
11
|
+
as?: HTMLTag
|
|
12
|
+
class?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { as: Tag = 'div', class: extraClass, ...attrs } = Astro.props
|
|
16
|
+
const className = extraClass ? `jaamd-content ${extraClass}` : 'jaamd-content'
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<Tag class={className} {...attrs}>
|
|
20
|
+
<slot />
|
|
21
|
+
</Tag>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HTMLTag, Polymorphic } from 'astro/types'
|
|
2
|
+
|
|
3
|
+
export type Props<Tag extends HTMLTag = 'div'> = Polymorphic<{
|
|
4
|
+
as: Tag
|
|
5
|
+
/** Appended to the always-present `jaamd-content` class, which the JS
|
|
6
|
+
* enhancements select on. */
|
|
7
|
+
class?: string
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
declare const MarkdownContent: <Tag extends HTMLTag = 'div'>(_props: Props<Tag>) => any
|
|
12
|
+
export default MarkdownContent
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { z as Zod } from 'astro/zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Frontmatter fields shared by JAAMD sites, to spread into a collection schema:
|
|
5
|
+
* `z.object({ ...frontmatterFields(z), order: z.number().optional() })`.
|
|
6
|
+
*/
|
|
7
|
+
export function frontmatterFields(z: typeof Zod) {
|
|
8
|
+
return {
|
|
9
|
+
title: z.string().optional(),
|
|
10
|
+
description: z.string().optional(),
|
|
11
|
+
/** Left out of production builds. */
|
|
12
|
+
draft: z.boolean().default(false),
|
|
13
|
+
/** Metadata and search weight; never rendered. */
|
|
14
|
+
keywords: z.array(z.string()).default([]),
|
|
15
|
+
author: z.string().optional(),
|
|
16
|
+
/** Public path or URL; false drops the site's. */
|
|
17
|
+
ogImage: z
|
|
18
|
+
.union([
|
|
19
|
+
z.string().refine((v) => v.startsWith('/') || /^https?:\/\//.test(v), {
|
|
20
|
+
message: 'ogImage must be a public url, such as "/og/page.png"',
|
|
21
|
+
}),
|
|
22
|
+
z.literal(false),
|
|
23
|
+
])
|
|
24
|
+
.optional(),
|
|
25
|
+
lastUpdated: z.coerce.date().optional(),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { DEFAULT_SELECTOR } from '@jaamd/core/client'
|
|
4
|
+
import { registerPlugins, type PluginOptions } from './processors.ts'
|
|
5
|
+
import { withTheme, type ShikiTheme } from './shiki.ts'
|
|
6
|
+
|
|
7
|
+
export interface JaamdOptions extends PluginOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Elements the client script enhances. `<MarkdownContent>` renders
|
|
10
|
+
* `.jaamd-content`; a wrapper of your own needs its selector here.
|
|
11
|
+
* @default ".jaamd-content"
|
|
12
|
+
*/
|
|
13
|
+
selector?: string
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shiki theme. `{ light, dark }` switches on a `.dark` class on `<html>`.
|
|
17
|
+
* Unset, `markdown.shikiConfig` is left as configured; "github-light"
|
|
18
|
+
* replaces only Astro's own default.
|
|
19
|
+
* @default "github-light"
|
|
20
|
+
*/
|
|
21
|
+
theme?: ShikiTheme
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Injects the default `--jaamd-*` values on every page. `<MarkdownContent>`
|
|
25
|
+
* imports them anyway.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
defaultVariables?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Absolute path of a stylesheet of this package, as a quoted specifier: injected
|
|
32
|
+
* scripts resolve from the project, where `@jaamd/astro` may be unreachable. */
|
|
33
|
+
const own = (relative: string) => JSON.stringify(fileURLToPath(new URL(relative, import.meta.url)))
|
|
34
|
+
|
|
35
|
+
const STYLES = {
|
|
36
|
+
variables: own('../dist/default.css'),
|
|
37
|
+
shikiDual: own('../dist/shiki-dual.css'),
|
|
38
|
+
markdown: own('../dist/styles.css'),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CLIENT = own('./client.ts')
|
|
42
|
+
|
|
43
|
+
/** Registers JAAMD's plugins on Astro's markdown processor and injects its styles and client script. */
|
|
44
|
+
export default function jaamd(options: JaamdOptions = {}): AstroIntegration {
|
|
45
|
+
const { selector = DEFAULT_SELECTOR, theme, defaultVariables = true, features, labels } = options
|
|
46
|
+
const client = JSON.stringify({ features, labels })
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name: '@jaamd/astro',
|
|
50
|
+
hooks: {
|
|
51
|
+
'astro:config:setup': ({ config, updateConfig, injectScript, logger }) => {
|
|
52
|
+
const { shikiConfig, dual } = withTheme(config.markdown.shikiConfig, theme)
|
|
53
|
+
const { processor } = config.markdown
|
|
54
|
+
const registration = registerPlugins(processor, { features, labels })
|
|
55
|
+
|
|
56
|
+
if (!registration) {
|
|
57
|
+
logger.warn(
|
|
58
|
+
`markdown processor "${processor.name}" is neither Sätteri nor unified: JAAMD's plugins were not added. Pass them to your processor yourself.`,
|
|
59
|
+
)
|
|
60
|
+
} else if (registration.total > 0) {
|
|
61
|
+
logger.info(
|
|
62
|
+
`markdown processor: ${processor.name}, registered ${registration.added}/${registration.total} plugin(s)`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
updateConfig({
|
|
67
|
+
// Ships TypeScript and an Astro component, compiled by Vite.
|
|
68
|
+
vite: { ssr: { noExternal: ['@jaamd/astro'] } },
|
|
69
|
+
markdown: { shikiConfig },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// "page-ssr": the stylesheets render as <link> tags in every page's <head>.
|
|
73
|
+
injectScript(
|
|
74
|
+
'page-ssr',
|
|
75
|
+
[
|
|
76
|
+
...(defaultVariables ? [`import ${STYLES.variables};`] : []),
|
|
77
|
+
...(dual ? [`import ${STYLES.shikiDual};`] : []),
|
|
78
|
+
`import ${STYLES.markdown};`,
|
|
79
|
+
].join('\n'),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// Again on every View Transitions navigation, releasing the previous run.
|
|
83
|
+
injectScript(
|
|
84
|
+
'page',
|
|
85
|
+
[
|
|
86
|
+
`import { enhance } from ${CLIENT};`,
|
|
87
|
+
`let dispose;`,
|
|
88
|
+
`const run = () => { dispose?.(); dispose = enhance(${JSON.stringify(selector)}, ${client}); };`,
|
|
89
|
+
`run();`,
|
|
90
|
+
`document.addEventListener("astro:page-load", run);`,
|
|
91
|
+
].join('\n'),
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Features, Labels } from '@jaamd/core/client'
|
|
2
|
+
import type { HeadingText } from '@jaamd/core/satteri'
|
|
3
|
+
import type { AstroConfig, RehypePlugins, RemarkPlugins } from 'astro'
|
|
4
|
+
import { satteriPlugins as coreSatteriPlugins } from '@jaamd/core/satteri'
|
|
5
|
+
import { rehypeHeadingLinks, unifiedPlugins as coreUnifiedPlugins } from '@jaamd/core/unified'
|
|
6
|
+
import { satteriCollectHastText, satteriHeadingIdsPlugin } from './astro-satteri.ts'
|
|
7
|
+
|
|
8
|
+
export interface PluginOptions {
|
|
9
|
+
/** Features to turn off. All on by default. */
|
|
10
|
+
features?: Features
|
|
11
|
+
/** Text JAAMD writes into the page. English by default. */
|
|
12
|
+
labels?: Partial<Labels>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Astro's heading id plugin for unified, as a module URL: `@astrojs/markdown-remark`,
|
|
16
|
+
* an optional peer, imports it only when unified runs. */
|
|
17
|
+
const REHYPE_HEADING_IDS = new URL('./rehype-heading-ids.js', import.meta.url).href
|
|
18
|
+
|
|
19
|
+
/** The heading text Astro records, frontmatter expressions resolved. */
|
|
20
|
+
const headingText: HeadingText = (node, ctx) => {
|
|
21
|
+
const raw = ctx.textContent(node)
|
|
22
|
+
return raw.includes('frontmatter') ? satteriCollectHastText(node, ctx.data.astro?.frontmatter ?? {}) : raw
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isHeadingLinks = (plugin: unknown) => (Array.isArray(plugin) ? plugin[0] : plugin) === rehypeHeadingLinks
|
|
26
|
+
|
|
27
|
+
/** JAAMD's plugins for Astro's `unified()`, Astro's heading ids included. */
|
|
28
|
+
export function unifiedPlugins(options: PluginOptions = {}): {
|
|
29
|
+
remarkPlugins: RemarkPlugins
|
|
30
|
+
rehypePlugins: RehypePlugins
|
|
31
|
+
} {
|
|
32
|
+
const preset = coreUnifiedPlugins(options)
|
|
33
|
+
return {
|
|
34
|
+
// unified's `[plugin, ...options]` is Astro's `[plugin, options]`.
|
|
35
|
+
remarkPlugins: preset.remarkPlugins as RemarkPlugins,
|
|
36
|
+
rehypePlugins: preset.rehypePlugins.flatMap((plugin) =>
|
|
37
|
+
isHeadingLinks(plugin) ? [REHYPE_HEADING_IDS, plugin] : [plugin],
|
|
38
|
+
) as RehypePlugins,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** JAAMD's plugins for Astro's `satteri()`, Astro's heading ids included. */
|
|
43
|
+
export function satteriPlugins(options: PluginOptions = {}): ReturnType<typeof coreSatteriPlugins> {
|
|
44
|
+
return coreSatteriPlugins({ ...options, headingIds: satteriHeadingIdsPlugin(), headingText })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Processor = AstroConfig['markdown']['processor']
|
|
48
|
+
|
|
49
|
+
interface SatteriProcessorOptions {
|
|
50
|
+
features?: Record<string, unknown>
|
|
51
|
+
mdastPlugins: unknown[]
|
|
52
|
+
hastPlugins: unknown[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface UnifiedProcessorOptions {
|
|
56
|
+
remarkPlugins: unknown[]
|
|
57
|
+
rehypePlugins: unknown[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Registration {
|
|
61
|
+
/** Plugins added. */
|
|
62
|
+
added: number
|
|
63
|
+
/** Plugins JAAMD would add to an empty processor. */
|
|
64
|
+
total: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Names of a Sätteri entry: definitions carry `name`, groups nest, factories have none. */
|
|
68
|
+
const satteriNames = (entry: unknown): string[] =>
|
|
69
|
+
Array.isArray(entry)
|
|
70
|
+
? entry.flatMap(satteriNames)
|
|
71
|
+
: entry && typeof entry === 'object' && 'name' in entry
|
|
72
|
+
? [String(entry.name)]
|
|
73
|
+
: []
|
|
74
|
+
|
|
75
|
+
/** Name of a unified entry: the plugin function or module, alone or as `[plugin, options]`. */
|
|
76
|
+
const unifiedNames = (entry: unknown): string[] => {
|
|
77
|
+
const plugin = Array.isArray(entry) ? entry[0] : entry
|
|
78
|
+
if (typeof plugin === 'string') return [plugin]
|
|
79
|
+
return typeof plugin === 'function' && plugin.name ? [plugin.name] : []
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Plugins whose names are not registered yet. Registering one twice would run it twice. */
|
|
83
|
+
function missing<T>(existing: readonly unknown[], plugins: readonly T[], names: (entry: unknown) => string[]): T[] {
|
|
84
|
+
const taken = new Set(existing.flatMap(names))
|
|
85
|
+
return plugins.filter((plugin) => !names(plugin).some((name) => taken.has(name)))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isFallback = (plugin: { name: string }) => plugin.name === 'jaamd-directive-fallback'
|
|
89
|
+
|
|
90
|
+
/** Ahead of the user's mdast plugins, the directive fallback after them; hast plugins after the user's. */
|
|
91
|
+
function registerSatteri(processor: SatteriProcessorOptions, options: PluginOptions): Registration {
|
|
92
|
+
const preset = satteriPlugins(options)
|
|
93
|
+
const mdast = missing(processor.mdastPlugins, preset.mdastPlugins, satteriNames)
|
|
94
|
+
const hast = missing(processor.hastPlugins, preset.hastPlugins, satteriNames)
|
|
95
|
+
|
|
96
|
+
processor.mdastPlugins = [
|
|
97
|
+
...mdast.filter((plugin) => !isFallback(plugin)),
|
|
98
|
+
...processor.mdastPlugins,
|
|
99
|
+
...mdast.filter(isFallback),
|
|
100
|
+
]
|
|
101
|
+
processor.hastPlugins = [...processor.hastPlugins, ...hast]
|
|
102
|
+
if (preset.features.directive) processor.features = { ...processor.features, directive: true }
|
|
103
|
+
|
|
104
|
+
return { added: mdast.length + hast.length, total: preset.mdastPlugins.length + preset.hastPlugins.length }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** remark plugins ahead of the user's, rehype plugins after them. */
|
|
108
|
+
function registerUnified(processor: UnifiedProcessorOptions, options: PluginOptions): Registration {
|
|
109
|
+
const preset = unifiedPlugins(options)
|
|
110
|
+
const remark = missing(processor.remarkPlugins, preset.remarkPlugins, unifiedNames)
|
|
111
|
+
const missingRehype = missing(processor.rehypePlugins, preset.rehypePlugins, unifiedNames)
|
|
112
|
+
// Astro's ids come along only with the links that read them.
|
|
113
|
+
const links = missingRehype.some(isHeadingLinks)
|
|
114
|
+
const rehype = missingRehype.filter((plugin) => links || plugin !== REHYPE_HEADING_IDS)
|
|
115
|
+
|
|
116
|
+
processor.remarkPlugins = [...remark, ...processor.remarkPlugins]
|
|
117
|
+
processor.rehypePlugins = [...processor.rehypePlugins, ...rehype]
|
|
118
|
+
|
|
119
|
+
return { added: remark.length + rehype.length, total: preset.remarkPlugins.length + preset.rehypePlugins.length }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Adds JAAMD's plugins to Astro's processor, in place, skipping those already
|
|
124
|
+
* there. New arrays each time: a processor shared across setup runs keeps no
|
|
125
|
+
* duplicates. `null` for a processor other than Sätteri and unified.
|
|
126
|
+
*/
|
|
127
|
+
export function registerPlugins(processor: Processor, options: PluginOptions = {}): Registration | null {
|
|
128
|
+
if (processor.name === 'satteri') {
|
|
129
|
+
return registerSatteri(processor.options as SatteriProcessorOptions, options)
|
|
130
|
+
}
|
|
131
|
+
if (processor.name === 'unified') {
|
|
132
|
+
return registerUnified(processor.options as UnifiedProcessorOptions, options)
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
package/src/shiki.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ShikiConfig } from 'astro'
|
|
2
|
+
|
|
3
|
+
/** A Shiki theme name, or one for each colour scheme. */
|
|
4
|
+
export type ShikiTheme = string | { light: string; dark: string }
|
|
5
|
+
|
|
6
|
+
/** Astro's default `shikiConfig.theme`, filled in before integrations run. */
|
|
7
|
+
const ASTRO_DEFAULT_THEME = 'github-dark'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `markdown.shikiConfig` with `theme` applied. Without one, the config stays
|
|
11
|
+
* as it is, except Astro's own default, which becomes "github-light".
|
|
12
|
+
* `{ light, dark }` sets Shiki's `themes` with no default colour: `dual`.
|
|
13
|
+
*/
|
|
14
|
+
export function withTheme(current: ShikiConfig, theme?: ShikiTheme): { shikiConfig: ShikiConfig; dual: boolean } {
|
|
15
|
+
const shikiConfig: ShikiConfig = { ...current }
|
|
16
|
+
const configured = Object.keys(shikiConfig.themes ?? {}).length > 0 || shikiConfig.theme !== ASTRO_DEFAULT_THEME
|
|
17
|
+
const resolved = theme ?? (configured ? undefined : 'github-light')
|
|
18
|
+
|
|
19
|
+
if (typeof resolved === 'object') {
|
|
20
|
+
if (!resolved.light || !resolved.dark) return { shikiConfig, dual: false }
|
|
21
|
+
delete shikiConfig.theme
|
|
22
|
+
shikiConfig.themes = { light: resolved.light, dark: resolved.dark } as ShikiConfig['themes']
|
|
23
|
+
shikiConfig.defaultColor = false
|
|
24
|
+
return { shikiConfig, dual: true }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (resolved) shikiConfig.theme = resolved as ShikiConfig['theme']
|
|
28
|
+
return { shikiConfig, dual: false }
|
|
29
|
+
}
|
package/src/themes.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@jaamd/core/themes'
|