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 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 { throwUserError } from './libs/plugin'
11
- import { addObsidianFiles, getSidebarFromConfig, getSidebarGroupPlaceholder, type SidebarGroup } from './libs/starlight'
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 group configuration.
96
+ * The generated vault pages sidebar entries configuration.
92
97
  */
93
98
  sidebar: z
94
99
  .object({
95
100
  /**
96
- * Whether the generated vault pages root sidebar group should be collapsed by default.
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.boolean().default(false),
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
- * Defaults to the value of the `collapsed` option.
107
+ * @default false
105
108
  */
106
- collapsedFolders: z.boolean().optional(),
109
+ collapsedFolders: z.boolean().default(false),
107
110
  /**
108
- * The generated vault pages sidebar group label.
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.union([z.string(), z.record(z.string(), z.string())]).default('Notes'),
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 obsidianSidebarGroup = getSidebarGroupPlaceholder()
132
+ export const obsidianSidebarEntries = getSidebarEntriesPlaceholder()
135
133
 
136
134
  export default function starlightObsidianPlugin(userConfig: StarlightObsidianUserConfig): StarlightPlugin {
137
- return makeStarlightObsidianPlugin(obsidianSidebarGroup)(userConfig)
135
+ return makeStarlightObsidianPlugin(obsidianSidebarEntries)(userConfig)
138
136
  }
139
137
 
140
- export function createStarlightObsidianPlugin(): [plugin: typeof starlightObsidianPlugin, sidebarGroup: SidebarGroup] {
141
- const sidebarGroup = getSidebarGroupPlaceholder(Symbol(randomBytes(24).toString('base64url')))
142
- return [makeStarlightObsidianPlugin(sidebarGroup), sidebarGroup]
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
- sidebarGroup: SidebarGroup,
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
- throwUserError(
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
- throwUserError(
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, sidebarGroup),
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
- throwUserError(`Failed to generate Starlight pages from Obsidian vault at '${config.vault}'.`)
234
+ throwPluginError(`Failed to generate Starlight pages from Obsidian vault at '${config.vault}'.`)
222
235
  }
223
236
  }
224
237
 
@@ -1,6 +1,6 @@
1
1
  import { AstroError } from 'astro/errors'
2
2
 
3
- export function throwUserError(message: string, hint?: string): never {
3
+ export function throwPluginError(message: string, hint?: string): never {
4
4
  throw new AstroError(
5
5
  message,
6
6
  hint ??
@@ -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 { rehypeStarlightObsidian } from './rehype'
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
- updateConfig({
16
- markdown: {
17
- rehypePlugins: [rehypeStarlightObsidian, rehypeKatex],
12
+ 'astro:config:setup': ({ config: astroConfig, updateConfig }) => {
13
+ applyMarkdownPlugin(astroConfig.markdown.processor, config)
18
14
 
19
- remarkPlugins: [[remarkMath, { singleDollarTextMath: config.math.singleDollarTextMath }]],
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
- throwUserError(`The provided vault path is not a directory.\n> Provided path: ${vaultPath}`)
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
- throwUserError(
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
- const blockIdentifierRegex = /(?<identifier> *\^(?<name>[\w-]+))$/
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.at(-1)
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.at(-1)
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.at(-1))
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.at(-1))
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 = getBlockIdentifer(node)
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
@@ -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 starlightObsidianSidebarGroupLabel = Symbol('StarlightObsidianSidebarGroupLabel')
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 getSidebarGroupPlaceholder(label = starlightObsidianSidebarGroupLabel): SidebarManualGroup {
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
- sidebarGroupPlaceholder: SidebarGroup,
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 replaceSidebarGroupPlaceholder(group: SidebarManualGroup): SidebarItem {
86
- if (group.label === sidebarGroupPlaceholder.label) {
87
- const defaultLocaleConfig = starlightConfig.locales?.[starlightConfig.defaultLocale ?? 'root']
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(group)) {
92
+ if (isSidebarGroup(item)) {
114
93
  return {
115
- ...group,
116
- items: group.items.map((item) => {
117
- return isSidebarGroup(item) ? replaceSidebarGroupPlaceholder(item) : item
118
- }),
94
+ ...item,
95
+ items: item.items.map(replaceSidebarEntriesPlaceholder),
119
96
  }
120
97
  }
121
98
 
122
- return group
99
+ return item
123
100
  }
124
101
 
125
- return starlightConfig.sidebar.map((item) => {
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 SidebarManualGroup {
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
- export type SidebarGroup = Exclude<SidebarItem, string>
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.12.1",
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.34.0"
53
+ "@astrojs/starlight": ">=0.41.0"
52
54
  },
53
55
  "engines": {
54
56
  "node": ">=22.12.0"