starlight-obsidian 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/components/Tags.astro +36 -0
- package/components/Twitter.astro +11 -0
- package/components/Youtube.astro +11 -0
- package/index.ts +125 -0
- package/libs/fs.ts +37 -0
- package/libs/html.ts +18 -0
- package/libs/integration.ts +21 -0
- package/libs/markdown.ts +43 -0
- package/libs/obsidian.ts +225 -0
- package/libs/path.ts +39 -0
- package/libs/plugin.ts +8 -0
- package/libs/rehype.ts +74 -0
- package/libs/remark.ts +670 -0
- package/libs/starlight.ts +263 -0
- package/overrides/PageTitle.astro +14 -0
- package/package.json +77 -0
- package/schema.ts +8 -0
- package/styles.css +42 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { StarlightUserConfig } from '@astrojs/starlight/types'
|
|
5
|
+
import type { AstroIntegrationLogger } from 'astro'
|
|
6
|
+
|
|
7
|
+
import type { StarlightObsidianConfig } from '..'
|
|
8
|
+
|
|
9
|
+
import { copyFile, ensureDirectory, removeDirectory } from './fs'
|
|
10
|
+
import { transformMarkdownToString } from './markdown'
|
|
11
|
+
import { getObsidianVaultFiles, isObsidianFile, type Vault, type VaultFile } from './obsidian'
|
|
12
|
+
import { getExtension } from './path'
|
|
13
|
+
|
|
14
|
+
const assetsPath = 'src/assets'
|
|
15
|
+
const docsPath = 'src/content/docs'
|
|
16
|
+
const publicPath = 'public'
|
|
17
|
+
|
|
18
|
+
const starlightObsidianSidebarGroupLabel = Symbol('StarlightObsidianSidebarGroupLabel')
|
|
19
|
+
|
|
20
|
+
const obsidianToStarlightCalloutTypeMap: Record<string, string> = {
|
|
21
|
+
note: 'note',
|
|
22
|
+
abstract: 'tip',
|
|
23
|
+
summary: 'tip',
|
|
24
|
+
tldr: 'tip',
|
|
25
|
+
info: 'note',
|
|
26
|
+
todo: 'note',
|
|
27
|
+
tip: 'tip',
|
|
28
|
+
hint: 'tip',
|
|
29
|
+
important: 'tip',
|
|
30
|
+
success: 'note',
|
|
31
|
+
check: 'note',
|
|
32
|
+
done: 'note',
|
|
33
|
+
question: 'caution',
|
|
34
|
+
help: 'caution',
|
|
35
|
+
faq: 'caution',
|
|
36
|
+
warning: 'caution',
|
|
37
|
+
caution: 'caution',
|
|
38
|
+
attention: 'caution',
|
|
39
|
+
failure: 'danger',
|
|
40
|
+
fail: 'danger',
|
|
41
|
+
missing: 'danger',
|
|
42
|
+
danger: 'danger',
|
|
43
|
+
error: 'danger',
|
|
44
|
+
bug: 'danger',
|
|
45
|
+
example: 'tip',
|
|
46
|
+
quote: 'note',
|
|
47
|
+
cite: 'note',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getSidebarGroupPlaceholder(): SidebarManualGroup {
|
|
51
|
+
return {
|
|
52
|
+
items: [],
|
|
53
|
+
label: starlightObsidianSidebarGroupLabel.toString(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getSidebarFromConfig(
|
|
58
|
+
config: StarlightObsidianConfig,
|
|
59
|
+
sidebar: StarlightUserConfig['sidebar'],
|
|
60
|
+
): StarlightUserConfig['sidebar'] {
|
|
61
|
+
if (!sidebar || sidebar.length === 0) {
|
|
62
|
+
return sidebar
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function replaceSidebarGroupPlaceholder(group: SidebarManualGroup): SidebarGroup {
|
|
66
|
+
if (group.label === starlightObsidianSidebarGroupLabel.toString()) {
|
|
67
|
+
return {
|
|
68
|
+
autogenerate: {
|
|
69
|
+
collapsed: config.sidebar.collapsedFolders ?? config.sidebar.collapsed,
|
|
70
|
+
directory: config.output,
|
|
71
|
+
},
|
|
72
|
+
collapsed: config.sidebar.collapsed,
|
|
73
|
+
label: config.sidebar.label,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isSidebarGroup(group)) {
|
|
78
|
+
return {
|
|
79
|
+
...group,
|
|
80
|
+
items: group.items.map((item) => {
|
|
81
|
+
return isSidebarGroup(item) ? replaceSidebarGroupPlaceholder(item) : item
|
|
82
|
+
}),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return group
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return sidebar.map((item) => {
|
|
90
|
+
return isSidebarGroup(item) ? replaceSidebarGroupPlaceholder(item) : item
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function addObsidianFiles(
|
|
95
|
+
config: StarlightObsidianConfig,
|
|
96
|
+
vault: Vault,
|
|
97
|
+
obsidianPaths: string[],
|
|
98
|
+
logger: AstroIntegrationLogger,
|
|
99
|
+
) {
|
|
100
|
+
const outputPaths = getOutputPaths(config)
|
|
101
|
+
|
|
102
|
+
await cleanOutputPaths(outputPaths)
|
|
103
|
+
|
|
104
|
+
const vaultFiles = getObsidianVaultFiles(vault, obsidianPaths)
|
|
105
|
+
|
|
106
|
+
const results = await Promise.allSettled(
|
|
107
|
+
vaultFiles.map(async (vaultFile) => {
|
|
108
|
+
await (vaultFile.type === 'asset'
|
|
109
|
+
? addAsset(outputPaths, vaultFile)
|
|
110
|
+
: vaultFile.type === 'file'
|
|
111
|
+
? addFile(outputPaths, vaultFile)
|
|
112
|
+
: addContent(config, vault, outputPaths, vaultFiles, vaultFile))
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
let didFail = false
|
|
117
|
+
|
|
118
|
+
for (const result of results) {
|
|
119
|
+
if (result.status === 'rejected') {
|
|
120
|
+
didFail = true
|
|
121
|
+
logger.error(result.reason instanceof Error ? result.reason.message : String(result.reason))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (didFail) {
|
|
126
|
+
throw new Error('Failed to generate some Starlight pages. See the error(s) above for more information.')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getStarlightCalloutType(obsidianCalloutType: string): string {
|
|
131
|
+
return obsidianToStarlightCalloutTypeMap[obsidianCalloutType] ?? 'note'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isAssetFile(filePath: string): boolean {
|
|
135
|
+
return getExtension(filePath) !== '.bmp' && isObsidianFile(filePath, 'image')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function addContent(
|
|
139
|
+
config: StarlightObsidianConfig,
|
|
140
|
+
vault: Vault,
|
|
141
|
+
outputPaths: OutputPaths,
|
|
142
|
+
vaultFiles: VaultFile[],
|
|
143
|
+
vaultFile: VaultFile,
|
|
144
|
+
) {
|
|
145
|
+
try {
|
|
146
|
+
const obsidianContent = await fs.readFile(vaultFile.fsPath, 'utf8')
|
|
147
|
+
const {
|
|
148
|
+
content: starlightContent,
|
|
149
|
+
aliases,
|
|
150
|
+
skip,
|
|
151
|
+
type,
|
|
152
|
+
} = await transformMarkdownToString(vaultFile.fsPath, obsidianContent, {
|
|
153
|
+
files: vaultFiles,
|
|
154
|
+
output: config.output,
|
|
155
|
+
vault,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
if (skip) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const starlightPath = path.join(
|
|
163
|
+
outputPaths.content,
|
|
164
|
+
type === 'markdown' ? vaultFile.path : vaultFile.path.replace(/\.md$/, '.mdx'),
|
|
165
|
+
)
|
|
166
|
+
const starlightDirPath = path.dirname(starlightPath)
|
|
167
|
+
|
|
168
|
+
await ensureDirectory(starlightDirPath)
|
|
169
|
+
await fs.writeFile(starlightPath, starlightContent)
|
|
170
|
+
|
|
171
|
+
if (aliases) {
|
|
172
|
+
for (const alias of aliases) {
|
|
173
|
+
await addAlias(config, outputPaths, vaultFile, alias)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throwVaultFileError(error, vaultFile)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function addFile(outputPaths: OutputPaths, vaultFile: VaultFile) {
|
|
182
|
+
try {
|
|
183
|
+
await copyFile(vaultFile.fsPath, path.join(outputPaths.file, vaultFile.slug))
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throwVaultFileError(error, vaultFile)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function addAsset(outputPaths: OutputPaths, vaultFile: VaultFile) {
|
|
190
|
+
try {
|
|
191
|
+
await copyFile(vaultFile.fsPath, path.join(outputPaths.asset, vaultFile.slug))
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throwVaultFileError(error, vaultFile)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function addAlias(
|
|
198
|
+
config: StarlightObsidianConfig,
|
|
199
|
+
outputPaths: OutputPaths,
|
|
200
|
+
vaultFile: VaultFile,
|
|
201
|
+
alias: string,
|
|
202
|
+
) {
|
|
203
|
+
const starlightPath = path.join(outputPaths.file, path.dirname(vaultFile.path), alias, 'index.html')
|
|
204
|
+
const starlightDirPath = path.dirname(starlightPath)
|
|
205
|
+
|
|
206
|
+
const to = path.posix.join(path.posix.sep, config.output, vaultFile.slug)
|
|
207
|
+
const from = path.posix.join(path.dirname(to), alias)
|
|
208
|
+
|
|
209
|
+
await ensureDirectory(starlightDirPath)
|
|
210
|
+
|
|
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.
|
|
213
|
+
await fs.writeFile(
|
|
214
|
+
starlightPath,
|
|
215
|
+
`<!doctype html>
|
|
216
|
+
<html>
|
|
217
|
+
<head>
|
|
218
|
+
<title>Redirecting to: ${to}</title>
|
|
219
|
+
<meta http-equiv="refresh" content="0;url=${to}">
|
|
220
|
+
<meta name="robots" content="noindex">
|
|
221
|
+
<link rel="canonical" href="${to}">
|
|
222
|
+
</head>
|
|
223
|
+
<body>
|
|
224
|
+
<a href="${to}">Redirecting from <code>${from}</code> to "<code>${to}</code>"</a>
|
|
225
|
+
</body>
|
|
226
|
+
</html>`,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getOutputPaths(config: StarlightObsidianConfig): OutputPaths {
|
|
231
|
+
return {
|
|
232
|
+
asset: path.join(assetsPath, config.output),
|
|
233
|
+
content: path.join(docsPath, config.output),
|
|
234
|
+
file: path.join(publicPath, config.output),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function cleanOutputPaths(outputPaths: OutputPaths) {
|
|
239
|
+
await removeDirectory(outputPaths.asset)
|
|
240
|
+
await removeDirectory(outputPaths.content)
|
|
241
|
+
await removeDirectory(outputPaths.file)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function throwVaultFileError(error: unknown, vaultFile: VaultFile): never {
|
|
245
|
+
throw new Error(`${vaultFile.path} — ${error instanceof Error ? error.message : String(error)}`, { cause: error })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isSidebarGroup(item: SidebarGroup): item is SidebarManualGroup {
|
|
249
|
+
return 'items' in item
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface OutputPaths {
|
|
253
|
+
asset: string
|
|
254
|
+
content: string
|
|
255
|
+
file: string
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface SidebarManualGroup {
|
|
259
|
+
items: SidebarManualGroup[]
|
|
260
|
+
label: string
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
type SidebarGroup = NonNullable<StarlightUserConfig['sidebar']>[number]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Default from '@astrojs/starlight/components/PageTitle.astro'
|
|
3
|
+
import type { Props } from '@astrojs/starlight/props'
|
|
4
|
+
|
|
5
|
+
import Tags from '../components/Tags.astro'
|
|
6
|
+
|
|
7
|
+
const { entry } = Astro.props
|
|
8
|
+
|
|
9
|
+
const tags = entry['data']['tags']
|
|
10
|
+
const showTags = tags !== undefined && tags.length > 0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<Default {...Astro.props}><slot /></Default>
|
|
14
|
+
{showTags && <Tags {tags} />}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starlight-obsidian",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Starlight plugin to publish Obsidian vaults.",
|
|
6
|
+
"author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.ts",
|
|
10
|
+
"./components/Twitter.astro": "./components/Twitter.astro",
|
|
11
|
+
"./components/Youtube.astro": "./components/Youtube.astro",
|
|
12
|
+
"./overrides/PageTitle.astro": "./overrides/PageTitle.astro",
|
|
13
|
+
"./schema": "./schema.ts",
|
|
14
|
+
"./styles": "./styles.css",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@astro-community/astro-embed-twitter": "0.5.3",
|
|
19
|
+
"@astro-community/astro-embed-youtube": "0.4.4",
|
|
20
|
+
"github-slugger": "2.0.0",
|
|
21
|
+
"globby": "14.0.0",
|
|
22
|
+
"hast-util-to-html": "9.0.0",
|
|
23
|
+
"is-absolute-url": "4.0.1",
|
|
24
|
+
"mdast-util-find-and-replace": "3.0.1",
|
|
25
|
+
"mdast-util-to-hast": "13.1.0",
|
|
26
|
+
"nanoid": "5.0.4",
|
|
27
|
+
"rehype": "13.0.1",
|
|
28
|
+
"rehype-katex": "7.0.0",
|
|
29
|
+
"rehype-mermaid": "2.1.0",
|
|
30
|
+
"remark": "15.0.1",
|
|
31
|
+
"remark-frontmatter": "5.0.0",
|
|
32
|
+
"remark-gfm": "4.0.0",
|
|
33
|
+
"remark-math": "6.0.0",
|
|
34
|
+
"unist-util-visit": "5.0.0",
|
|
35
|
+
"vfile": "6.0.1",
|
|
36
|
+
"yaml": "2.3.4"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@astrojs/starlight": "0.16.0",
|
|
40
|
+
"@types/hast": "3.0.3",
|
|
41
|
+
"@types/mdast": "4.0.3",
|
|
42
|
+
"@types/unist": "3.0.2",
|
|
43
|
+
"astro": "4.2.1",
|
|
44
|
+
"typescript": "5.3.3",
|
|
45
|
+
"vitest": "1.2.1"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@astrojs/starlight": ">=0.15.0",
|
|
49
|
+
"astro": ">=4.0.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.14.1"
|
|
53
|
+
},
|
|
54
|
+
"packageManager": "pnpm@8.14.1",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"sideEffects": false,
|
|
59
|
+
"keywords": [
|
|
60
|
+
"starlight",
|
|
61
|
+
"plugin",
|
|
62
|
+
"obsidian",
|
|
63
|
+
"vault",
|
|
64
|
+
"documentation",
|
|
65
|
+
"astro"
|
|
66
|
+
],
|
|
67
|
+
"homepage": "https://github.com/HiDeoo/starlight-obsidian",
|
|
68
|
+
"repository": {
|
|
69
|
+
"type": "git",
|
|
70
|
+
"url": "https://github.com/HiDeoo/starlight-obsidian.git"
|
|
71
|
+
},
|
|
72
|
+
"bugs": "https://github.com/HiDeoo/starlight-obsidian/issues",
|
|
73
|
+
"scripts": {
|
|
74
|
+
"test": "vitest",
|
|
75
|
+
"lint": "prettier -c --cache . && eslint . --cache --max-warnings=0"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/schema.ts
ADDED
package/styles.css
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--sl--bs-hue-yellow: 51;
|
|
3
|
+
|
|
4
|
+
--sl-obs-highlight: hsl(var(--sl--bs-hue-yellow), 71%, 56%);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:root[data-theme='light'] {
|
|
8
|
+
--sl-obs-highlight: hsl(var(--sl--bs-hue-yellow), 80%, 56%);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.sl-obs-highlight {
|
|
12
|
+
background-color: var(--sl-obs-highlight);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.sl-markdown-content .sl-obs-embed-audio {
|
|
16
|
+
width: 100%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.sl-markdown-content .sl-obs-embed-pdf {
|
|
20
|
+
border: none;
|
|
21
|
+
height: 600px;
|
|
22
|
+
width: 100%;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.sl-obs-tag {
|
|
26
|
+
background-color: var(--sl-color-accent-low);
|
|
27
|
+
border-radius: 0.25rem;
|
|
28
|
+
border: 1px solid var(--sl-color-accent);
|
|
29
|
+
color: #fff;
|
|
30
|
+
font-family: var(--sl-font-system-mono);
|
|
31
|
+
font-size: var(--sl-text-xs);
|
|
32
|
+
padding: 0.25rem 0.375rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[data-theme='light'] .sl-obs-tag {
|
|
36
|
+
background-color: var(--sl-color-accent-high);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
blockquote.twitter-tweet:not(.twitter-tweet-rendered) {
|
|
40
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
41
|
+
margin-block: 1rem;
|
|
42
|
+
}
|