starlight-obsidian 0.1.1 → 0.3.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/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/types'
2
+ import type { AstroIntegrationLogger } from 'astro'
2
3
  import { z } from 'astro/zod'
3
4
 
4
5
  import { starlightObsidianIntegration } from './libs/integration'
@@ -7,6 +8,12 @@ import { throwUserError } from './libs/plugin'
7
8
  import { addObsidianFiles, getSidebarFromConfig, getSidebarGroupPlaceholder } from './libs/starlight'
8
9
 
9
10
  const starlightObsidianConfigSchema = z.object({
11
+ /**
12
+ * Add links to Starlight headings to make it easier to share a link to a specific section of a page.
13
+ *
14
+ * @default false
15
+ */
16
+ autoLinkHeadings: z.boolean().default(false),
10
17
  /**
11
18
  * The name of the Obsidian vault configuration folder if different from the default one.
12
19
  *
@@ -30,6 +37,15 @@ const starlightObsidianConfigSchema = z.object({
30
37
  * @default 'notes'
31
38
  */
32
39
  output: z.string().default('notes'),
40
+ /**
41
+ * Whether the Starlight Obsidian plugin should skip the generation of the Obsidian vault pages.
42
+ *
43
+ * This is useful to disable generating the Obsidian vault pages when deploying on platforms that do not have access
44
+ * to the Obsidian vault. This will require you to build and commit the pages locally ahead of time.
45
+ *
46
+ * @default false
47
+ */
48
+ skipGeneration: z.boolean().default(false),
33
49
  /**
34
50
  * The generated vault pages sidebar group configuration.
35
51
  */
@@ -55,6 +71,14 @@ const starlightObsidianConfigSchema = z.object({
55
71
  label: z.string().default('Notes'),
56
72
  })
57
73
  .default({}),
74
+ /**
75
+ * Determines if the table of contents top-level heading should be the Starlight default one ("Overview") or the page
76
+ * title.
77
+ * This option is useful when the Obsidian vault pages already have a top-level heading named "Overview".
78
+ *
79
+ * @default 'title'
80
+ */
81
+ tableOfContentsOverview: z.union([z.literal('default'), z.literal('title')]).default('default'),
58
82
  /**
59
83
  * The absolute or relative path to the Obsidian vault to publish.
60
84
  */
@@ -82,44 +106,68 @@ export default function starlightObsidianPlugin(userConfig: StarlightObsidianUse
82
106
  return
83
107
  }
84
108
 
109
+ const customCss = [...(starlightConfig.customCss ?? []), 'starlight-obsidian/styles/common']
110
+
111
+ if (config.autoLinkHeadings) {
112
+ customCss.push('starlight-obsidian/styles/autolinks-headings')
113
+ }
114
+
85
115
  const updatedStarlightConfig: Partial<StarlightUserConfig> = {
86
- customCss: [...(starlightConfig.customCss ?? []), 'starlight-obsidian/styles'],
116
+ customCss,
87
117
  sidebar: getSidebarFromConfig(config, starlightConfig.sidebar),
88
118
  }
89
119
 
120
+ if (!updatedStarlightConfig.components) {
121
+ updatedStarlightConfig.components = {}
122
+ }
123
+
90
124
  if (starlightConfig.components?.PageTitle) {
91
- logger.warn(
92
- 'It looks like you already have a `PageTitle` component override in your Starlight configuration.',
93
- )
94
- logger.warn('To use `starlight-obsidian`, remove the override for the `PageTitle` component.\n')
125
+ logComponentOverrideWarning(logger, 'PageTitle')
95
126
  } else {
96
- updatedStarlightConfig.components = {
97
- PageTitle: 'starlight-obsidian/overrides/PageTitle.astro',
127
+ updatedStarlightConfig.components.PageTitle = 'starlight-obsidian/overrides/PageTitle.astro'
128
+ }
129
+
130
+ if (config.tableOfContentsOverview === 'title') {
131
+ if (starlightConfig.components?.PageSidebar) {
132
+ logComponentOverrideWarning(logger, 'PageSidebar')
133
+ } else {
134
+ updatedStarlightConfig.components.PageSidebar = 'starlight-obsidian/overrides/PageSidebar.astro'
98
135
  }
99
136
  }
100
137
 
101
- try {
102
- const start = performance.now()
103
- logger.info('Generating Starlight pages from Obsidian vault')
138
+ if (config.skipGeneration) {
139
+ logger.warn(
140
+ `Skipping generation of Starlight pages from Obsidian vault as the 'skipGeneration' option is enabled.`,
141
+ )
142
+ } else {
143
+ try {
144
+ const start = performance.now()
145
+ logger.info('Generating Starlight pages from Obsidian vault…')
104
146
 
105
- const vault = await getVault(config)
106
- const obsidianPaths = await getObsidianPaths(vault, config.ignore)
107
- await addObsidianFiles(config, vault, obsidianPaths, logger)
147
+ const vault = await getVault(config)
148
+ const obsidianPaths = await getObsidianPaths(vault, config.ignore)
149
+ await addObsidianFiles(config, vault, obsidianPaths, logger)
108
150
 
109
- const duration = Math.round(performance.now() - start)
110
- logger.info(`Starlight pages generated from Obsidian vault in ${duration}ms.`)
111
- } catch (error) {
112
- logger.error(error instanceof Error ? error.message : String(error))
151
+ const duration = Math.round(performance.now() - start)
152
+ logger.info(`Starlight pages generated from Obsidian vault in ${duration}ms.`)
153
+ } catch (error) {
154
+ logger.error(error instanceof Error ? error.message : String(error))
113
155
 
114
- throwUserError('Failed to generate Starlight pages from Obsidian vault.')
156
+ throwUserError('Failed to generate Starlight pages from Obsidian vault.')
157
+ }
115
158
  }
116
159
 
117
- addIntegration(starlightObsidianIntegration())
160
+ addIntegration(starlightObsidianIntegration(config))
118
161
  updateConfig(updatedStarlightConfig)
119
162
  },
120
163
  },
121
164
  }
122
165
  }
123
166
 
167
+ function logComponentOverrideWarning(logger: AstroIntegrationLogger, component: string) {
168
+ logger.warn(`It looks like you already have a \`${component}\` component override in your Starlight configuration.`)
169
+ logger.warn(`To use \`starlight-obsidian\`, remove the override for the \`${component}\` component.\n`)
170
+ }
171
+
124
172
  export type StarlightObsidianUserConfig = z.input<typeof starlightObsidianConfigSchema>
125
173
  export type StarlightObsidianConfig = z.output<typeof starlightObsidianConfigSchema>
@@ -1,19 +1,33 @@
1
+ import { rehypeHeadingIds } from '@astrojs/markdown-remark'
1
2
  import type { AstroIntegration } from 'astro'
3
+ import rehypeAutolinkHeadings from 'rehype-autolink-headings'
2
4
  import rehypeKatex from 'rehype-katex'
3
5
  import remarkMath from 'remark-math'
4
6
 
5
- import { rehypeStarlightObsidian } from './rehype'
7
+ import type { StarlightObsidianConfig } from '..'
6
8
 
7
- export function starlightObsidianIntegration(): AstroIntegration {
9
+ import { getRehypeAutolinkHeadingsOptions, rehypeStarlightObsidian } from './rehype'
10
+ import { vitePluginStarlightObsidianConfig } from './vite'
11
+
12
+ export function starlightObsidianIntegration(config: StarlightObsidianConfig): AstroIntegration {
8
13
  return {
9
14
  name: 'starlight-obsidian-integration',
10
15
  hooks: {
11
16
  'astro:config:setup': ({ updateConfig }) => {
12
17
  updateConfig({
13
18
  markdown: {
14
- rehypePlugins: [rehypeStarlightObsidian, rehypeKatex],
19
+ rehypePlugins: [
20
+ ...(config.autoLinkHeadings
21
+ ? [rehypeHeadingIds, [rehypeAutolinkHeadings, getRehypeAutolinkHeadingsOptions()]]
22
+ : []),
23
+ rehypeStarlightObsidian,
24
+ rehypeKatex,
25
+ ],
15
26
  remarkPlugins: [remarkMath],
16
27
  },
28
+ vite: {
29
+ plugins: [vitePluginStarlightObsidianConfig(config)],
30
+ },
17
31
  })
18
32
  },
19
33
  },
package/libs/rehype.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Element, ElementContent, Root } from 'hast'
2
+ import { toString } from 'hast-util-to-string'
3
+ import { h } from 'hastscript'
4
+ import { escape } from 'html-escaper'
2
5
  import type { Literal } from 'mdast'
6
+ import type { Options as RehypeAutolinkHeadingsOptions } from 'rehype-autolink-headings'
3
7
  import { CONTINUE, SKIP, visit } from 'unist-util-visit'
4
8
 
5
9
  const blockIdentifierRegex = /(?<identifier> *\^(?<name>[\w-]+))$/
@@ -25,12 +29,12 @@ export function rehypeStarlightObsidian() {
25
29
  const lastGrandChild = lastChild.children.at(-1)
26
30
 
27
31
  if (lastChild.tagName === 'p') {
28
- transformBlockIdentifier(node, lastGrandChild)
32
+ return transformBlockIdentifier(node, lastGrandChild)
29
33
  } else if (lastGrandChild?.type === 'element' && lastGrandChild.tagName === 'li') {
30
- transformBlockIdentifier(node, lastGrandChild.children.at(-1))
34
+ return transformBlockIdentifier(node, lastGrandChild.children.at(-1))
31
35
  }
32
36
  } else if (node.tagName === 'p' || node.tagName === 'li') {
33
- transformBlockIdentifier(node, node.children.at(-1))
37
+ return transformBlockIdentifier(node, node.children.at(-1))
34
38
  }
35
39
 
36
40
  return CONTINUE
@@ -38,6 +42,24 @@ export function rehypeStarlightObsidian() {
38
42
  }
39
43
  }
40
44
 
45
+ // https://hideoo.dev/notes/starlight-heading-links
46
+ // https://github.com/withastro/docs/blob/main/plugins/rehype-autolink.ts
47
+ // https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
48
+ export function getRehypeAutolinkHeadingsOptions(): RehypeAutolinkHeadingsOptions {
49
+ return {
50
+ behavior: 'after',
51
+ content: (heading) => {
52
+ return [
53
+ h('span', { ariaHidden: 'true' }, '§'),
54
+ h('span', { class: 'sr-only' }, `Section titled ${escape(toString(heading))}`),
55
+ ]
56
+ },
57
+ group: ({ tagName }) =>
58
+ h('div', { class: `sl-obs-section sl-obs-section-level-${tagName.slice(1)}`, tabIndex: -1 }),
59
+ properties: { class: 'sl-obs-heading-link' },
60
+ }
61
+ }
62
+
41
63
  function transformBlockIdentifier(reference: Element, node: ElementContent | undefined) {
42
64
  if (!isNodeWithValue(node)) {
43
65
  return CONTINUE
package/libs/starlight.ts CHANGED
@@ -209,19 +209,21 @@ async function addAlias(
209
209
  await ensureDirectory(starlightDirPath)
210
210
 
211
211
  // Based on https://github.com/withastro/astro/blob/57ab578bc7bdac6c65c2315365c0e94bc98af2b3/packages/astro/src/core/build/generate.ts#L584-L591
212
- // but tweaked to add an `<html>` element so that Pagefind does not emit a warning when ignoring the page.
212
+ // but tweaked to add an `<html>` element so that Pagefind does not emit a warning and ignore the page.
213
213
  await fs.writeFile(
214
214
  starlightPath,
215
215
  `<!doctype html>
216
- <html>
216
+ <html lang="en">
217
217
  <head>
218
- <title>Redirecting to: ${to}</title>
218
+ <title>${vaultFile.stem}</title>
219
219
  <meta http-equiv="refresh" content="0;url=${to}">
220
220
  <meta name="robots" content="noindex">
221
221
  <link rel="canonical" href="${to}">
222
222
  </head>
223
- <body>
224
- <a href="${to}">Redirecting from <code>${from}</code> to "<code>${to}</code>"</a>
223
+ <body data-pagefind-body>
224
+ <h2 id="alias">Alias</h2>
225
+ <code>(name: ${alias})</code>
226
+ <a href="${to}" data-pagefind-ignore>Redirecting from <code>${from}</code> to "<code>${to}</code>"</a>
225
227
  </body>
226
228
  </html>`,
227
229
  )
package/libs/vite.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { ViteUserConfig } from 'astro'
2
+
3
+ import type { StarlightObsidianConfig } from '..'
4
+
5
+ export function vitePluginStarlightObsidianConfig(config: StarlightObsidianConfig): VitePlugin {
6
+ const moduleId = 'virtual:starlight-obsidian-config'
7
+ const resolvedModuleId = `\0${moduleId}`
8
+ const moduleContent = `export default ${JSON.stringify(config)}`
9
+
10
+ return {
11
+ name: 'vite-plugin-starlight-obsidian-config',
12
+ load(id) {
13
+ return id === resolvedModuleId ? moduleContent : undefined
14
+ },
15
+ resolveId(id) {
16
+ return id === moduleId ? resolvedModuleId : undefined
17
+ },
18
+ }
19
+ }
20
+
21
+ type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number]
@@ -0,0 +1,67 @@
1
+ ---
2
+ import MobileTableOfContents from '@astrojs/starlight/components/MobileTableOfContents.astro'
3
+ import TableOfContents from '@astrojs/starlight/components/TableOfContents.astro'
4
+ import type { Props } from '@astrojs/starlight/props'
5
+ import config from 'virtual:starlight-obsidian-config'
6
+
7
+ const toc = Astro.props.toc
8
+
9
+ if (config.tableOfContentsOverview === 'title' && toc) {
10
+ const firstTocItem = toc.items.at(0)
11
+
12
+ if (firstTocItem) {
13
+ firstTocItem.text = Astro.props.entry['data'].title
14
+ }
15
+ }
16
+ ---
17
+
18
+ {
19
+ Astro.props.toc && (
20
+ <>
21
+ <div class="lg:sl-hidden">
22
+ <MobileTableOfContents {...Astro.props} {toc} />
23
+ </div>
24
+ <div class="right-sidebar-panel sl-hidden lg:sl-block">
25
+ <div class="sl-container">
26
+ <TableOfContents {...Astro.props} {toc} />
27
+ </div>
28
+ </div>
29
+ </>
30
+ )
31
+ }
32
+
33
+ <style>
34
+ .right-sidebar-panel {
35
+ padding: 1rem var(--sl-sidebar-pad-x);
36
+ }
37
+ .sl-container {
38
+ width: calc(var(--sl-sidebar-width) - 2 * var(--sl-sidebar-pad-x));
39
+ }
40
+ .right-sidebar-panel :global(h2) {
41
+ color: var(--sl-color-white);
42
+ font-size: var(--sl-text-h5);
43
+ font-weight: 600;
44
+ line-height: var(--sl-line-height-headings);
45
+ margin-bottom: 0.5rem;
46
+ }
47
+ .right-sidebar-panel :global(:where(a)) {
48
+ display: block;
49
+ font-size: var(--sl-text-xs);
50
+ text-decoration: none;
51
+ color: var(--sl-color-gray-3);
52
+ overflow-wrap: anywhere;
53
+ }
54
+ .right-sidebar-panel :global(:where(a):hover) {
55
+ color: var(--sl-color-white);
56
+ }
57
+ @media (min-width: 72rem) {
58
+ .sl-container {
59
+ max-width: calc(
60
+ (
61
+ (100vw - var(--sl-sidebar-width) - 2 * var(--sl-content-pad-x) - 2 * var(--sl-sidebar-pad-x)) * 0.25
62
+ /* MAGIC NUMBER 🥲 */
63
+ )
64
+ );
65
+ }
66
+ }
67
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-obsidian",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "Starlight plugin to publish Obsidian vaults.",
6
6
  "author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",
@@ -9,22 +9,29 @@
9
9
  ".": "./index.ts",
10
10
  "./components/Twitter.astro": "./components/Twitter.astro",
11
11
  "./components/Youtube.astro": "./components/Youtube.astro",
12
+ "./overrides/PageSidebar.astro": "./overrides/PageSidebar.astro",
12
13
  "./overrides/PageTitle.astro": "./overrides/PageTitle.astro",
13
14
  "./schema": "./schema.ts",
14
- "./styles": "./styles.css",
15
+ "./styles/common": "./styles/common.css",
16
+ "./styles/autolinks-headings": "./styles/autolinks-headings.css",
15
17
  "./package.json": "./package.json"
16
18
  },
17
19
  "dependencies": {
18
20
  "@astro-community/astro-embed-twitter": "0.5.3",
19
21
  "@astro-community/astro-embed-youtube": "0.4.4",
22
+ "@astrojs/markdown-remark": "4.2.1",
20
23
  "github-slugger": "2.0.0",
21
24
  "globby": "14.0.0",
22
25
  "hast-util-to-html": "9.0.0",
26
+ "hast-util-to-string": "3.0.0",
27
+ "hastscript": "9.0.0",
28
+ "html-escaper": "3.0.3",
23
29
  "is-absolute-url": "4.0.1",
24
30
  "mdast-util-find-and-replace": "3.0.1",
25
31
  "mdast-util-to-hast": "13.1.0",
26
32
  "nanoid": "5.0.4",
27
33
  "rehype": "13.0.1",
34
+ "rehype-autolink-headings": "7.1.0",
28
35
  "rehype-katex": "7.0.0",
29
36
  "rehype-mermaid": "2.1.0",
30
37
  "remark": "15.0.1",
@@ -38,6 +45,7 @@
38
45
  "devDependencies": {
39
46
  "@astrojs/starlight": "0.16.0",
40
47
  "@types/hast": "3.0.3",
48
+ "@types/html-escaper": "3.0.2",
41
49
  "@types/mdast": "4.0.3",
42
50
  "@types/unist": "3.0.2",
43
51
  "astro": "4.2.1",
@@ -0,0 +1,68 @@
1
+ .sl-markdown-content :not(.sl-obs-section) + :is(.sl-obs-section):not(:where(.not-content *)) {
2
+ margin-top: 1.5em;
3
+ }
4
+
5
+ .sl-markdown-content .sl-obs-section {
6
+ --sl-obs-heading-link-spacing: 0.25em;
7
+
8
+ line-height: var(--sl-line-height-headings);
9
+ }
10
+
11
+ .sl-markdown-content .sl-obs-section.sl-obs-section-level-1 {
12
+ font-size: var(--sl-text-h1);
13
+ }
14
+
15
+ .sl-markdown-content .sl-obs-section.sl-obs-section-level-2 {
16
+ font-size: var(--sl-text-h2);
17
+ }
18
+
19
+ .sl-markdown-content .sl-obs-section.sl-obs-section-level-3 {
20
+ font-size: var(--sl-text-h3);
21
+ }
22
+
23
+ .sl-markdown-content .sl-obs-section.sl-obs-section-level-4 {
24
+ font-size: var(--sl-text-h4);
25
+ }
26
+
27
+ .sl-markdown-content .sl-obs-section.sl-obs-section-level-5 {
28
+ font-size: var(--sl-text-h5);
29
+ }
30
+
31
+ .sl-markdown-content .sl-obs-section.sl-obs-section-level-6 {
32
+ font-size: var(--sl-text-h6);
33
+ }
34
+
35
+ .sl-markdown-content .sl-obs-section > :first-child {
36
+ display: inline;
37
+ margin-inline-end: var(--sl-obs-heading-link-spacing);
38
+ }
39
+
40
+ .sl-markdown-content .sl-obs-heading-link {
41
+ color: var(--sl-color-gray-3);
42
+ text-decoration: none;
43
+ }
44
+
45
+ .sl-markdown-content .sl-obs-heading-link:is(:hover, :focus) {
46
+ color: var(--sl-color-text-accent);
47
+ }
48
+
49
+ @media (hover: hover) {
50
+ .sl-markdown-content .sl-obs-heading-link {
51
+ opacity: 0;
52
+ }
53
+ }
54
+
55
+ .sl-markdown-content .sl-obs-section:hover > .sl-obs-heading-link,
56
+ .sl-markdown-content .sl-obs-heading-link:focus {
57
+ opacity: 1;
58
+ }
59
+
60
+ @media (min-width: 95em) {
61
+ .sl-markdown-content .sl-obs-section {
62
+ display: flex;
63
+ flex-direction: row-reverse;
64
+ gap: var(--sl-obs-heading-link-spacing);
65
+ justify-content: flex-end;
66
+ margin-inline-start: calc(-1 * (1ch + var(--sl-obs-heading-link-spacing)));
67
+ }
68
+ }
package/virtual.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module 'virtual:starlight-obsidian-config' {
2
+ const StarlightObsidianConfig: import('./index').StarlightObsidianConfig
3
+
4
+ export default StarlightObsidianConfig
5
+ }
File without changes