starlight-obsidian 0.12.1 → 0.14.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/CHANGELOG.md +57 -0
- package/index.ts +39 -26
- package/libs/{plugin.ts → error.ts} +1 -1
- package/libs/integration.ts +4 -9
- package/libs/markdown.ts +29 -0
- package/libs/obsidian.ts +3 -3
- package/libs/processor.ts +68 -0
- package/libs/rehype.ts +6 -25
- package/libs/satteri.ts +86 -0
- package/libs/starlight.ts +27 -45
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# starlight-obsidian
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#77](https://github.com/HiDeoo/starlight-obsidian/pull/77) [`5818330`](https://github.com/HiDeoo/starlight-obsidian/commit/5818330456dd11d03f9787f4d9b6a4e68359d02d) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for Astro v7, drops support for Astro v6.
|
|
8
|
+
|
|
9
|
+
⚠️ **BREAKING CHANGE:** The minimum supported version of Starlight is now `0.41.0`.
|
|
10
|
+
|
|
11
|
+
Please follow the [upgrade guide](https://github.com/withastro/starlight/releases/tag/%40astrojs%2Fstarlight%400.41.0) to update your project.
|
|
12
|
+
|
|
13
|
+
- [#77](https://github.com/HiDeoo/starlight-obsidian/pull/77) [`5818330`](https://github.com/HiDeoo/starlight-obsidian/commit/5818330456dd11d03f9787f4d9b6a4e68359d02d) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for the Sätteri Markdown processor.
|
|
14
|
+
|
|
15
|
+
## 0.13.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [#74](https://github.com/HiDeoo/starlight-obsidian/pull/74) [`175ab3f`](https://github.com/HiDeoo/starlight-obsidian/commit/175ab3f553eb77ee4004d30330c05b6f416393b4) Thanks [@HiDeoo](https://github.com/HiDeoo)! - ⚠️ **BREAKING CHANGE:** The minimum supported version of Starlight is now `0.39.0`.
|
|
20
|
+
|
|
21
|
+
Please use the `@astrojs/upgrade` command to upgrade your project:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx @astrojs/upgrade
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- [#74](https://github.com/HiDeoo/starlight-obsidian/pull/74) [`175ab3f`](https://github.com/HiDeoo/starlight-obsidian/commit/175ab3f553eb77ee4004d30330c05b6f416393b4) Thanks [@HiDeoo](https://github.com/HiDeoo)! - ⚠️ **BREAKING CHANGE:** Updates sidebar generation to support Starlight `0.39` autogenerated sidebar entries.
|
|
28
|
+
|
|
29
|
+
The previously exported `obsidianSidebarGroup` has been replaced by `obsidianSidebarEntries`, which now expands to the generated Obsidian sidebar entries.
|
|
30
|
+
The `sidebar.label` and `sidebar.collapsed` plugin options have been removed.
|
|
31
|
+
|
|
32
|
+
To migrate, create a [sidebar group](https://starlight.astro.build/guides/sidebar/#groups) in your Starlight configuration and place `obsidianSidebarEntries` in that group’s `items` array:
|
|
33
|
+
|
|
34
|
+
```diff
|
|
35
|
+
-import starlightObsidian, { obsidianSidebarGroup } from 'starlight-obsidian'
|
|
36
|
+
+import starlightObsidian, { obsidianSidebarEntries } from 'starlight-obsidian'
|
|
37
|
+
|
|
38
|
+
starlight({
|
|
39
|
+
plugins: [
|
|
40
|
+
starlightObsidian({
|
|
41
|
+
vault: '../path/to/vault',
|
|
42
|
+
sidebar: {
|
|
43
|
+
- label: 'Notes',
|
|
44
|
+
- collapsed: true,
|
|
45
|
+
collapsedFolders: true,
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
],
|
|
49
|
+
sidebar: [
|
|
50
|
+
- obsidianSidebarGroup,
|
|
51
|
+
+ {
|
|
52
|
+
+ label: 'Notes',
|
|
53
|
+
+ collapsed: true,
|
|
54
|
+
+ items: [obsidianSidebarEntries],
|
|
55
|
+
+ },
|
|
56
|
+
],
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
3
60
|
## 0.12.1
|
|
4
61
|
|
|
5
62
|
### Patch Changes
|
package/index.ts
CHANGED
|
@@ -4,11 +4,16 @@ import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/ty
|
|
|
4
4
|
import type { AstroIntegrationLogger } from 'astro'
|
|
5
5
|
import { z } from 'astro/zod'
|
|
6
6
|
|
|
7
|
+
import { throwPluginError } from './libs/error'
|
|
7
8
|
import { starlightObsidianIntegration } from './libs/integration'
|
|
8
9
|
import { getObsidianPaths, getVault } from './libs/obsidian'
|
|
9
10
|
import { stripLeadingAndTrailingSlashes } from './libs/path'
|
|
10
|
-
import {
|
|
11
|
-
|
|
11
|
+
import {
|
|
12
|
+
addObsidianFiles,
|
|
13
|
+
getSidebarEntriesPlaceholder,
|
|
14
|
+
getSidebarFromConfig,
|
|
15
|
+
type SidebarAutoEntries,
|
|
16
|
+
} from './libs/starlight'
|
|
12
17
|
|
|
13
18
|
const starlightObsidianConfigSchema = z.object({
|
|
14
19
|
/**
|
|
@@ -88,31 +93,24 @@ const starlightObsidianConfigSchema = z.object({
|
|
|
88
93
|
*/
|
|
89
94
|
skipGeneration: z.boolean().default(false),
|
|
90
95
|
/**
|
|
91
|
-
* The generated vault pages sidebar
|
|
96
|
+
* The generated vault pages sidebar entries configuration.
|
|
92
97
|
*/
|
|
93
98
|
sidebar: z
|
|
94
99
|
.object({
|
|
95
100
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* @default false
|
|
101
|
+
* @deprecated Use the `collapsed` option of the Starlight sidebar group that contains the vault pages instead.
|
|
99
102
|
*/
|
|
100
|
-
collapsed: z.
|
|
103
|
+
collapsed: z.never().optional(),
|
|
101
104
|
/**
|
|
102
105
|
* Whether the sidebar groups of your vault nested folders should be collapsed by default.
|
|
103
106
|
*
|
|
104
|
-
*
|
|
107
|
+
* @default false
|
|
105
108
|
*/
|
|
106
|
-
collapsedFolders: z.boolean().
|
|
109
|
+
collapsedFolders: z.boolean().default(false),
|
|
107
110
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* The value can be a string, or for multilingual sites, an object with values for each different locale.
|
|
111
|
-
* When using the object form, the keys must be BCP-47 tags (e.g. `en`, `ar`, or `zh-CN`).
|
|
112
|
-
*
|
|
113
|
-
* @default 'Notes'
|
|
111
|
+
* @deprecated Use the `label` option of the Starlight sidebar group that contains the vault pages instead.
|
|
114
112
|
*/
|
|
115
|
-
label: z.
|
|
113
|
+
label: z.never().optional(),
|
|
116
114
|
})
|
|
117
115
|
.prefault({}),
|
|
118
116
|
/**
|
|
@@ -131,19 +129,22 @@ const starlightObsidianConfigSchema = z.object({
|
|
|
131
129
|
|
|
132
130
|
let overridesInjected = false
|
|
133
131
|
|
|
134
|
-
export const
|
|
132
|
+
export const obsidianSidebarEntries = getSidebarEntriesPlaceholder()
|
|
135
133
|
|
|
136
134
|
export default function starlightObsidianPlugin(userConfig: StarlightObsidianUserConfig): StarlightPlugin {
|
|
137
|
-
return makeStarlightObsidianPlugin(
|
|
135
|
+
return makeStarlightObsidianPlugin(obsidianSidebarEntries)(userConfig)
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
export function createStarlightObsidianPlugin(): [
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
export function createStarlightObsidianPlugin(): [
|
|
139
|
+
plugin: typeof starlightObsidianPlugin,
|
|
140
|
+
sidebarEntries: SidebarAutoEntries,
|
|
141
|
+
] {
|
|
142
|
+
const sidebarEntries = getSidebarEntriesPlaceholder(Symbol(randomBytes(24).toString('base64url')))
|
|
143
|
+
return [makeStarlightObsidianPlugin(sidebarEntries), sidebarEntries]
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
function makeStarlightObsidianPlugin(
|
|
146
|
-
|
|
147
|
+
sidebarEntries: SidebarAutoEntries,
|
|
147
148
|
): (userConfig: StarlightObsidianUserConfig) => StarlightPlugin {
|
|
148
149
|
overridesInjected = true
|
|
149
150
|
|
|
@@ -151,18 +152,30 @@ function makeStarlightObsidianPlugin(
|
|
|
151
152
|
const parsedConfig = starlightObsidianConfigSchema.safeParse(userConfig)
|
|
152
153
|
|
|
153
154
|
if (!parsedConfig.success) {
|
|
155
|
+
const isUsingRemovedSidebarGroupConfig = parsedConfig.error.issues.some((issue) => {
|
|
156
|
+
const path = issue.path.join('.')
|
|
157
|
+
return path === 'sidebar.label' || path === 'sidebar.collapsed'
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (isUsingRemovedSidebarGroupConfig) {
|
|
161
|
+
throwPluginError(
|
|
162
|
+
'The `sidebar.label` and `sidebar.collapsed` options have been removed.',
|
|
163
|
+
'Create a group in your Starlight sidebar configuration, set the `label` and `collapsed` options on that group, and add `obsidianSidebarEntries` to its `items` array.\nFor more information see https://starlight.astro.build/guides/sidebar/#groups',
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
154
167
|
const isUsingDeprecatedCopyStarlightFrontmatter = parsedConfig.error.issues.some(
|
|
155
168
|
(issue) => issue.path.join('.') === 'copyStarlightFrontmatter',
|
|
156
169
|
)
|
|
157
170
|
|
|
158
171
|
if (isUsingDeprecatedCopyStarlightFrontmatter) {
|
|
159
|
-
|
|
172
|
+
throwPluginError(
|
|
160
173
|
'The `copyStarlightFrontmatter` option has been deprecated in favor of the `copyFrontmatter` option.',
|
|
161
174
|
'For more information see https://starlight-obsidian.vercel.app/configuration/#copyfrontmatter',
|
|
162
175
|
)
|
|
163
176
|
}
|
|
164
177
|
|
|
165
|
-
|
|
178
|
+
throwPluginError(
|
|
166
179
|
`Invalid starlight-obsidian configuration:
|
|
167
180
|
|
|
168
181
|
${z.prettifyError(parsedConfig.error)}
|
|
@@ -197,7 +210,7 @@ ${z.prettifyError(parsedConfig.error)}
|
|
|
197
210
|
...overrideStarlightComponent(starlightConfig.components, logger, 'PageTitle'),
|
|
198
211
|
},
|
|
199
212
|
customCss: [...(starlightConfig.customCss ?? []), 'starlight-obsidian/styles/common'],
|
|
200
|
-
sidebar: getSidebarFromConfig(config, starlightConfig,
|
|
213
|
+
sidebar: getSidebarFromConfig(config, starlightConfig, sidebarEntries),
|
|
201
214
|
}
|
|
202
215
|
|
|
203
216
|
if (config.skipGeneration) {
|
|
@@ -218,7 +231,7 @@ ${z.prettifyError(parsedConfig.error)}
|
|
|
218
231
|
} catch (error) {
|
|
219
232
|
logger.error(error instanceof Error ? error.message : String(error))
|
|
220
233
|
|
|
221
|
-
|
|
234
|
+
throwPluginError(`Failed to generate Starlight pages from Obsidian vault at '${config.vault}'.`)
|
|
222
235
|
}
|
|
223
236
|
}
|
|
224
237
|
|
package/libs/integration.ts
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro'
|
|
2
|
-
import rehypeKatex from 'rehype-katex'
|
|
3
|
-
import remarkMath from 'remark-math'
|
|
4
2
|
|
|
5
3
|
import type { StarlightObsidianConfig } from '..'
|
|
6
4
|
|
|
7
|
-
import {
|
|
5
|
+
import { applyMarkdownPlugin } from './processor'
|
|
8
6
|
import { vitePluginStarlightObsidianConfig } from './vite'
|
|
9
7
|
|
|
10
8
|
export function starlightObsidianIntegration(config: StarlightObsidianConfig): AstroIntegration {
|
|
11
9
|
return {
|
|
12
10
|
name: 'starlight-obsidian-integration',
|
|
13
11
|
hooks: {
|
|
14
|
-
'astro:config:setup': ({ updateConfig }) => {
|
|
15
|
-
|
|
16
|
-
markdown: {
|
|
17
|
-
rehypePlugins: [rehypeStarlightObsidian, rehypeKatex],
|
|
12
|
+
'astro:config:setup': ({ config: astroConfig, updateConfig }) => {
|
|
13
|
+
applyMarkdownPlugin(astroConfig.markdown.processor, config)
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
},
|
|
15
|
+
updateConfig({
|
|
21
16
|
vite: {
|
|
22
17
|
plugins: [vitePluginStarlightObsidianConfig(config)],
|
|
23
18
|
},
|
package/libs/markdown.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ElementContent } from 'hast'
|
|
2
|
+
import type { Literal } from 'mdast'
|
|
1
3
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
2
4
|
import { remark } from 'remark'
|
|
3
5
|
import remarkFrontmatter from 'remark-frontmatter'
|
|
@@ -7,6 +9,8 @@ import { VFile } from 'vfile'
|
|
|
7
9
|
|
|
8
10
|
import { remarkStarlightObsidian, type TransformContext } from './remark'
|
|
9
11
|
|
|
12
|
+
const blockIdentifierRegex = /(?<identifier> *\^(?<name>[\w-]+))$/
|
|
13
|
+
|
|
10
14
|
let processor: ReturnType<typeof remark> | undefined
|
|
11
15
|
|
|
12
16
|
export async function transformMarkdownToString(
|
|
@@ -49,9 +53,34 @@ function getVFile(filePath: string, markdown: string, context: TransformContext)
|
|
|
49
53
|
})
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
export function isNodeWithValue(node: ElementContent | undefined): node is NodeWithValue {
|
|
57
|
+
return node !== undefined && 'value' in node
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getBlockIdentifier(node: NodeWithValue): { length: number; name: string } | undefined {
|
|
61
|
+
const match = blockIdentifierRegex.exec(node.value)
|
|
62
|
+
const identifier = match?.groups?.['identifier']
|
|
63
|
+
const name = match?.groups?.['name']
|
|
64
|
+
|
|
65
|
+
if (!identifier || !name) {
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { length: identifier.length, name }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getLastContentChild(children: ElementContent[]) {
|
|
73
|
+
const index = children.findLastIndex((child) => child.type !== 'text' || child.value.trim().length > 0)
|
|
74
|
+
if (index === -1) return undefined
|
|
75
|
+
|
|
76
|
+
return { child: children[index], index }
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
interface TransformResult {
|
|
53
80
|
aliases: string[] | undefined
|
|
54
81
|
content: string
|
|
55
82
|
skip: boolean
|
|
56
83
|
type: 'markdown' | 'mdx'
|
|
57
84
|
}
|
|
85
|
+
|
|
86
|
+
export type NodeWithValue = ElementContent & Literal
|
package/libs/obsidian.ts
CHANGED
|
@@ -9,9 +9,9 @@ import yaml from 'yaml'
|
|
|
9
9
|
|
|
10
10
|
import type { StarlightObsidianConfig } from '..'
|
|
11
11
|
|
|
12
|
+
import { throwPluginError } from './error'
|
|
12
13
|
import { isDirectory, isFile } from './fs'
|
|
13
14
|
import { getExtension, isAnchor, slashify, slugifyPath, stripExtension } from './path'
|
|
14
|
-
import { throwUserError } from './plugin'
|
|
15
15
|
import { isAssetFile } from './starlight'
|
|
16
16
|
|
|
17
17
|
const obsidianAppConfigSchema = z.object({
|
|
@@ -48,11 +48,11 @@ export async function getVault(config: StarlightObsidianConfig): Promise<Vault>
|
|
|
48
48
|
const vaultPath = path.resolve(config.vault)
|
|
49
49
|
|
|
50
50
|
if (!(await isDirectory(vaultPath))) {
|
|
51
|
-
|
|
51
|
+
throwPluginError(`The provided vault path is not a directory.\n> Provided path: ${vaultPath}`)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (!(await isVaultDirectory(config, vaultPath))) {
|
|
55
|
-
|
|
55
|
+
throwPluginError(
|
|
56
56
|
`The provided vault path is not a valid Obsidian vault directory and does not include an '.obsidian/app.json' file.\n> Provided path: ${vaultPath}`,
|
|
57
57
|
)
|
|
58
58
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { katex as satteriKatex } from '@nullpinter/satteri-katex'
|
|
2
|
+
import type { AstroConfig } from 'astro'
|
|
3
|
+
import rehypeKatex from 'rehype-katex'
|
|
4
|
+
import remarkMath from 'remark-math'
|
|
5
|
+
|
|
6
|
+
import type { StarlightObsidianConfig } from '..'
|
|
7
|
+
|
|
8
|
+
import { throwPluginError } from './error'
|
|
9
|
+
import { rehypeStarlightObsidian } from './rehype'
|
|
10
|
+
import { satteriStarlightObsidian } from './satteri'
|
|
11
|
+
|
|
12
|
+
export function applyMarkdownPlugin(processor: MarkdownProcessor, config: StarlightObsidianConfig) {
|
|
13
|
+
if (isSatteriProcessor(processor)) {
|
|
14
|
+
processor.options.features ??= {}
|
|
15
|
+
processor.options.features.math = {
|
|
16
|
+
...(typeof processor.options.features.math === 'object' ? processor.options.features.math : {}),
|
|
17
|
+
singleDollarTextMath: config.math.singleDollarTextMath,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
processor.options.mdastPlugins.push(satteriKatex())
|
|
21
|
+
processor.options.hastPlugins.push(satteriStarlightObsidian)
|
|
22
|
+
} else if (isUnifiedProcessor(processor)) {
|
|
23
|
+
processor.options.remarkPlugins.push([remarkMath, { singleDollarTextMath: config.math.singleDollarTextMath }])
|
|
24
|
+
processor.options.rehypePlugins.push([rehypeStarlightObsidian], [rehypeKatex])
|
|
25
|
+
} else {
|
|
26
|
+
throwPluginError("The configured 'markdown.processor' is not supported by the starlight-obsidian plugin.")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isSatteriProcessor(processor: unknown): processor is SatteriMarkdownProcessor {
|
|
31
|
+
if (typeof processor !== 'object' || processor === null) return false
|
|
32
|
+
const candidate = processor as { name?: unknown; options?: { hastPlugins?: unknown; mdastPlugins?: unknown[] } }
|
|
33
|
+
return (
|
|
34
|
+
candidate.name === 'satteri' &&
|
|
35
|
+
Array.isArray(candidate.options?.hastPlugins) &&
|
|
36
|
+
Array.isArray(candidate.options.mdastPlugins)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isUnifiedProcessor(processor: unknown): processor is UnifiedMarkdownProcessor {
|
|
41
|
+
if (typeof processor !== 'object' || processor === null) return false
|
|
42
|
+
const candidate = processor as { name?: unknown; options?: { rehypePlugins?: unknown; remarkPlugins?: unknown[] } }
|
|
43
|
+
return (
|
|
44
|
+
candidate.name === 'unified' &&
|
|
45
|
+
Array.isArray(candidate.options?.rehypePlugins) &&
|
|
46
|
+
Array.isArray(candidate.options.remarkPlugins)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type MarkdownProcessor = NonNullable<AstroConfig['markdown']['processor']>
|
|
51
|
+
|
|
52
|
+
interface SatteriMarkdownProcessor {
|
|
53
|
+
name: string
|
|
54
|
+
options: { features?: SatteriFeatures; hastPlugins: unknown[]; mdastPlugins: unknown[] }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface SatteriFeatures {
|
|
58
|
+
math?:
|
|
59
|
+
| boolean
|
|
60
|
+
| {
|
|
61
|
+
singleDollarTextMath?: boolean
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface UnifiedMarkdownProcessor {
|
|
66
|
+
name: string
|
|
67
|
+
options: { rehypePlugins: unknown[]; remarkPlugins: unknown[] }
|
|
68
|
+
}
|
package/libs/rehype.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { Element, ElementContent, Root } from 'hast'
|
|
2
|
-
import type { Literal } from 'mdast'
|
|
3
2
|
import { CONTINUE, SKIP, visit } from 'unist-util-visit'
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
import { getBlockIdentifier, getLastContentChild, isNodeWithValue } from './markdown'
|
|
6
5
|
|
|
7
6
|
export function rehypeStarlightObsidian() {
|
|
8
7
|
return function transformer(tree: Root) {
|
|
@@ -12,7 +11,7 @@ export function rehypeStarlightObsidian() {
|
|
|
12
11
|
// Handle blockqoutes first as they are block which can contain paragraphs or list items and we want to hoist
|
|
13
12
|
// the IDs to the blockquote element.
|
|
14
13
|
if (node.tagName === 'blockquote') {
|
|
15
|
-
const lastChild = node.children
|
|
14
|
+
const lastChild = getLastContentChild(node.children)?.child
|
|
16
15
|
|
|
17
16
|
if (
|
|
18
17
|
lastChild?.type !== 'element' ||
|
|
@@ -21,15 +20,15 @@ export function rehypeStarlightObsidian() {
|
|
|
21
20
|
return CONTINUE
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
const lastGrandChild = lastChild.children
|
|
23
|
+
const lastGrandChild = getLastContentChild(lastChild.children)?.child
|
|
25
24
|
|
|
26
25
|
if (lastChild.tagName === 'p') {
|
|
27
26
|
return transformBlockIdentifier(node, lastGrandChild)
|
|
28
27
|
} else if (lastGrandChild?.type === 'element' && lastGrandChild.tagName === 'li') {
|
|
29
|
-
return transformBlockIdentifier(node, lastGrandChild.children
|
|
28
|
+
return transformBlockIdentifier(node, getLastContentChild(lastGrandChild.children)?.child)
|
|
30
29
|
}
|
|
31
30
|
} else if (node.tagName === 'p' || node.tagName === 'li') {
|
|
32
|
-
return transformBlockIdentifier(node, node.children
|
|
31
|
+
return transformBlockIdentifier(node, getLastContentChild(node.children)?.child)
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
return CONTINUE
|
|
@@ -42,7 +41,7 @@ function transformBlockIdentifier(reference: Element, node: ElementContent | und
|
|
|
42
41
|
return CONTINUE
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
const identifier =
|
|
44
|
+
const identifier = getBlockIdentifier(node)
|
|
46
45
|
|
|
47
46
|
if (!identifier) {
|
|
48
47
|
return CONTINUE
|
|
@@ -53,21 +52,3 @@ function transformBlockIdentifier(reference: Element, node: ElementContent | und
|
|
|
53
52
|
|
|
54
53
|
return SKIP
|
|
55
54
|
}
|
|
56
|
-
|
|
57
|
-
function isNodeWithValue(node: ElementContent | undefined): node is NodeWithValue {
|
|
58
|
-
return node !== undefined && 'value' in node
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function getBlockIdentifer(node: NodeWithValue): { length: number; name: string } | undefined {
|
|
62
|
-
const match = blockIdentifierRegex.exec(node.value)
|
|
63
|
-
const identifier = match?.groups?.['identifier']
|
|
64
|
-
const name = match?.groups?.['name']
|
|
65
|
-
|
|
66
|
-
if (!identifier || !name) {
|
|
67
|
-
return undefined
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return { length: identifier.length, name }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
type NodeWithValue = ElementContent & Literal
|
package/libs/satteri.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Element, ElementContent, Parents } from 'hast'
|
|
2
|
+
import { defineHastPlugin, type HastPluginDefinition, type HastVisitorContext } from 'satteri'
|
|
3
|
+
|
|
4
|
+
import { getBlockIdentifier, getLastContentChild, isNodeWithValue } from './markdown'
|
|
5
|
+
|
|
6
|
+
export function satteriStarlightObsidian(): HastPluginDefinition {
|
|
7
|
+
return defineHastPlugin({
|
|
8
|
+
name: 'starlight-obsidian',
|
|
9
|
+
element: {
|
|
10
|
+
filter: ['blockquote', 'p', 'li'],
|
|
11
|
+
visit(node, ctx) {
|
|
12
|
+
if (node.tagName === 'blockquote') {
|
|
13
|
+
const lastChild = getLastContentChild(node.children)?.child
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
lastChild?.type !== 'element' ||
|
|
17
|
+
!(lastChild.tagName === 'p' || lastChild.tagName === 'ul' || lastChild.tagName === 'ol')
|
|
18
|
+
) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lastGrandChild = getLastContentChild(lastChild.children)?.child
|
|
23
|
+
|
|
24
|
+
if (lastChild.tagName === 'p') {
|
|
25
|
+
transformBlockIdentifier(node, lastGrandChild, ctx)
|
|
26
|
+
} else if (lastGrandChild?.type === 'element' && lastGrandChild.tagName === 'li') {
|
|
27
|
+
transformBlockIdentifier(node, getLastContentChild(lastGrandChild.children)?.child, ctx)
|
|
28
|
+
}
|
|
29
|
+
} else if (node.tagName === 'p' || node.tagName === 'li') {
|
|
30
|
+
if (isInsideBlockquoteWithIdentifier(node, ctx)) return
|
|
31
|
+
|
|
32
|
+
transformBlockIdentifier(node, getLastContentChild(node.children)?.child, ctx)
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function transformBlockIdentifier(reference: Element, node: ElementContent | undefined, ctx: HastVisitorContext) {
|
|
40
|
+
if (!isNodeWithValue(node)) return
|
|
41
|
+
|
|
42
|
+
const identifier = getBlockIdentifier(node)
|
|
43
|
+
if (!identifier) return
|
|
44
|
+
|
|
45
|
+
ctx.setProperty(node, 'value', node.value.slice(0, identifier.length * -1))
|
|
46
|
+
ctx.setProperty(reference, 'id', `block-${identifier.name}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isInsideBlockquoteWithIdentifier(node: Element, ctx: HastVisitorContext) {
|
|
50
|
+
let parent: Parents | undefined = ctx.parent(node)
|
|
51
|
+
|
|
52
|
+
while (parent !== undefined) {
|
|
53
|
+
if (parent.type === 'element' && parent.tagName === 'blockquote') {
|
|
54
|
+
return blockquoteHasIdentifier(parent)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
parent = ctx.parent(parent)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function blockquoteHasIdentifier(blockquote: Element) {
|
|
64
|
+
const lastChild = getLastContentChild(blockquote.children)?.child
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
lastChild?.type !== 'element' ||
|
|
68
|
+
!(lastChild.tagName === 'p' || lastChild.tagName === 'ul' || lastChild.tagName === 'ol')
|
|
69
|
+
) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const lastGrandChild = getLastContentChild(lastChild.children)?.child
|
|
74
|
+
|
|
75
|
+
if (lastChild.tagName === 'p') {
|
|
76
|
+
return isNodeWithValue(lastGrandChild) && getBlockIdentifier(lastGrandChild) !== undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (lastGrandChild?.type !== 'element' || lastGrandChild.tagName !== 'li') {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lastListItemChild = getLastContentChild(lastGrandChild.children)?.child
|
|
84
|
+
|
|
85
|
+
return isNodeWithValue(lastListItemChild) && getBlockIdentifier(lastListItemChild) !== undefined
|
|
86
|
+
}
|
package/libs/starlight.ts
CHANGED
|
@@ -15,7 +15,7 @@ const assetsPath = 'src/assets'
|
|
|
15
15
|
const docsPath = 'src/content/docs'
|
|
16
16
|
const publicPath = 'public'
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const starlightObsidianSidebarEntriesDirectory = Symbol('StarlightObsidianSidebarEntriesDirectory')
|
|
19
19
|
|
|
20
20
|
const obsidianToStarlightCalloutTypeMap: Record<string, string> = {
|
|
21
21
|
note: 'note',
|
|
@@ -66,65 +66,40 @@ const starlightFrontmatterKeys = [
|
|
|
66
66
|
'sidebar',
|
|
67
67
|
]
|
|
68
68
|
|
|
69
|
-
export function
|
|
70
|
-
return {
|
|
71
|
-
items: [],
|
|
72
|
-
label: label.toString(),
|
|
73
|
-
}
|
|
69
|
+
export function getSidebarEntriesPlaceholder(directory = starlightObsidianSidebarEntriesDirectory): SidebarAutoEntries {
|
|
70
|
+
return { autogenerate: { directory: directory.toString() } }
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
export function getSidebarFromConfig(
|
|
77
74
|
config: StarlightObsidianConfig,
|
|
78
75
|
starlightConfig: HookParameters<'config:setup'>['config'],
|
|
79
|
-
|
|
76
|
+
sidebarEntriesPlaceholder: SidebarAutoEntries,
|
|
80
77
|
): StarlightUserConfig['sidebar'] {
|
|
81
78
|
if (!starlightConfig.sidebar || starlightConfig.sidebar.length === 0) {
|
|
82
79
|
return starlightConfig.sidebar
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
function
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
const label =
|
|
89
|
-
typeof config.sidebar.label === 'string'
|
|
90
|
-
? config.sidebar.label
|
|
91
|
-
: config.sidebar.label[defaultLocaleConfig?.lang ?? 'en']
|
|
92
|
-
|
|
93
|
-
if (!label || label.length === 0) {
|
|
94
|
-
throw new Error('The generated vault pages sidebar group label must have a key for the default language.')
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const group: SidebarGroup = {
|
|
82
|
+
function replaceSidebarEntriesPlaceholder(item: SidebarItem): SidebarItem {
|
|
83
|
+
if (isObsidianSidebarEntriesPlaceholder(item, sidebarEntriesPlaceholder)) {
|
|
84
|
+
return {
|
|
98
85
|
autogenerate: {
|
|
99
|
-
collapsed: config.sidebar.collapsedFolders ?? config.sidebar.collapsed,
|
|
100
86
|
directory: config.output,
|
|
87
|
+
collapsed: config.sidebar.collapsedFolders,
|
|
101
88
|
},
|
|
102
|
-
collapsed: config.sidebar.collapsed,
|
|
103
|
-
label,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (typeof config.sidebar.label !== 'string') {
|
|
107
|
-
group['translations'] = config.sidebar.label
|
|
108
89
|
}
|
|
109
|
-
|
|
110
|
-
return group
|
|
111
90
|
}
|
|
112
91
|
|
|
113
|
-
if (isSidebarGroup(
|
|
92
|
+
if (isSidebarGroup(item)) {
|
|
114
93
|
return {
|
|
115
|
-
...
|
|
116
|
-
items:
|
|
117
|
-
return isSidebarGroup(item) ? replaceSidebarGroupPlaceholder(item) : item
|
|
118
|
-
}),
|
|
94
|
+
...item,
|
|
95
|
+
items: item.items.map(replaceSidebarEntriesPlaceholder),
|
|
119
96
|
}
|
|
120
97
|
}
|
|
121
98
|
|
|
122
|
-
return
|
|
99
|
+
return item
|
|
123
100
|
}
|
|
124
101
|
|
|
125
|
-
return starlightConfig.sidebar.map(
|
|
126
|
-
return isSidebarGroup(item) ? replaceSidebarGroupPlaceholder(item) : item
|
|
127
|
-
})
|
|
102
|
+
return starlightConfig.sidebar.map(replaceSidebarEntriesPlaceholder)
|
|
128
103
|
}
|
|
129
104
|
|
|
130
105
|
export async function addObsidianFiles(
|
|
@@ -297,20 +272,27 @@ function throwVaultFileError(error: unknown, vaultFile: VaultFile): never {
|
|
|
297
272
|
throw new Error(`${vaultFile.path} — ${error instanceof Error ? error.message : String(error)}`, { cause: error })
|
|
298
273
|
}
|
|
299
274
|
|
|
300
|
-
function isSidebarGroup(item: SidebarItem): item is
|
|
275
|
+
function isSidebarGroup(item: SidebarItem): item is SidebarGroup {
|
|
301
276
|
return typeof item === 'object' && 'items' in item
|
|
302
277
|
}
|
|
303
278
|
|
|
279
|
+
function isObsidianSidebarEntriesPlaceholder(
|
|
280
|
+
item: SidebarItem,
|
|
281
|
+
placeholder: SidebarAutoEntries,
|
|
282
|
+
): item is SidebarAutoEntries {
|
|
283
|
+
return (
|
|
284
|
+
typeof item === 'object' &&
|
|
285
|
+
'autogenerate' in item &&
|
|
286
|
+
item.autogenerate.directory === placeholder.autogenerate.directory
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
304
290
|
interface OutputPaths {
|
|
305
291
|
asset: string
|
|
306
292
|
content: string
|
|
307
293
|
file: string
|
|
308
294
|
}
|
|
309
295
|
|
|
310
|
-
interface SidebarManualGroup {
|
|
311
|
-
items: SidebarManualGroup[]
|
|
312
|
-
label: string
|
|
313
|
-
}
|
|
314
|
-
|
|
315
296
|
type SidebarItem = NonNullable<StarlightUserConfig['sidebar']>[number]
|
|
316
|
-
|
|
297
|
+
type SidebarGroup = Extract<SidebarItem, { items: unknown[] }>
|
|
298
|
+
export type SidebarAutoEntries = Extract<SidebarItem, { autogenerate: unknown }>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-obsidian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Starlight plugin to publish Obsidian vaults.",
|
|
6
6
|
"author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@astro-community/astro-embed-twitter": "^0.5.11",
|
|
21
21
|
"@astro-community/astro-embed-youtube": "^0.5.10",
|
|
22
|
+
"@nullpinter/satteri-katex": "^0.1.2",
|
|
22
23
|
"decode-uri-component": "^0.4.1",
|
|
23
24
|
"github-slugger": "^2.0.0",
|
|
24
25
|
"globby": "^16.1.1",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"remark-frontmatter": "^5.0.0",
|
|
36
37
|
"remark-gfm": "^4.0.1",
|
|
37
38
|
"remark-math": "^6.0.0",
|
|
39
|
+
"satteri": "^0.9.3",
|
|
38
40
|
"unist-util-visit": "^5.1.0",
|
|
39
41
|
"vfile": "^6.0.3",
|
|
40
42
|
"yaml": "^2.8.2"
|
|
@@ -48,7 +50,7 @@
|
|
|
48
50
|
"vitest": "^4.0.18"
|
|
49
51
|
},
|
|
50
52
|
"peerDependencies": {
|
|
51
|
-
"@astrojs/starlight": ">=0.
|
|
53
|
+
"@astrojs/starlight": ">=0.41.0"
|
|
52
54
|
},
|
|
53
55
|
"engines": {
|
|
54
56
|
"node": ">=22.12.0"
|