starlight-obsidian 0.1.1 → 0.2.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
  *
@@ -55,6 +62,14 @@ const starlightObsidianConfigSchema = z.object({
55
62
  label: z.string().default('Notes'),
56
63
  })
57
64
  .default({}),
65
+ /**
66
+ * Determines if the table of contents top-level heading should be the Starlight default one ("Overview") or the page
67
+ * title.
68
+ * This option is useful when the Obsidian vault pages already have a top-level heading named "Overview".
69
+ *
70
+ * @default 'title'
71
+ */
72
+ tableOfContentsOverview: z.union([z.literal('default'), z.literal('title')]).default('default'),
58
73
  /**
59
74
  * The absolute or relative path to the Obsidian vault to publish.
60
75
  */
@@ -82,19 +97,32 @@ export default function starlightObsidianPlugin(userConfig: StarlightObsidianUse
82
97
  return
83
98
  }
84
99
 
100
+ const customCss = [...(starlightConfig.customCss ?? []), 'starlight-obsidian/styles/common']
101
+
102
+ if (config.autoLinkHeadings) {
103
+ customCss.push('starlight-obsidian/styles/autolinks-headings')
104
+ }
105
+
85
106
  const updatedStarlightConfig: Partial<StarlightUserConfig> = {
86
- customCss: [...(starlightConfig.customCss ?? []), 'starlight-obsidian/styles'],
107
+ customCss,
87
108
  sidebar: getSidebarFromConfig(config, starlightConfig.sidebar),
88
109
  }
89
110
 
111
+ if (!updatedStarlightConfig.components) {
112
+ updatedStarlightConfig.components = {}
113
+ }
114
+
90
115
  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')
116
+ logComponentOverrideWarning(logger, 'PageTitle')
95
117
  } else {
96
- updatedStarlightConfig.components = {
97
- PageTitle: 'starlight-obsidian/overrides/PageTitle.astro',
118
+ updatedStarlightConfig.components.PageTitle = 'starlight-obsidian/overrides/PageTitle.astro'
119
+ }
120
+
121
+ if (config.tableOfContentsOverview === 'title') {
122
+ if (starlightConfig.components?.PageSidebar) {
123
+ logComponentOverrideWarning(logger, 'PageSidebar')
124
+ } else {
125
+ updatedStarlightConfig.components.PageSidebar = 'starlight-obsidian/overrides/PageSidebar.astro'
98
126
  }
99
127
  }
100
128
 
@@ -114,12 +142,17 @@ export default function starlightObsidianPlugin(userConfig: StarlightObsidianUse
114
142
  throwUserError('Failed to generate Starlight pages from Obsidian vault.')
115
143
  }
116
144
 
117
- addIntegration(starlightObsidianIntegration())
145
+ addIntegration(starlightObsidianIntegration(config))
118
146
  updateConfig(updatedStarlightConfig)
119
147
  },
120
148
  },
121
149
  }
122
150
  }
123
151
 
152
+ function logComponentOverrideWarning(logger: AstroIntegrationLogger, component: string) {
153
+ logger.warn(`It looks like you already have a \`${component}\` component override in your Starlight configuration.`)
154
+ logger.warn(`To use \`starlight-obsidian\`, remove the override for the \`${component}\` component.\n`)
155
+ }
156
+
124
157
  export type StarlightObsidianUserConfig = z.input<typeof starlightObsidianConfigSchema>
125
158
  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.2.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