starlight-theme-nova 0.2.1 → 0.4.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/lib/styles.gen.css +5 -1
- package/package.json +3 -1
- package/src/components/CodeFile.astro +64 -0
- package/src/components/CodeTabs.astro +10 -4
- package/src/components/index.ts +2 -1
- package/src/components/rehype-code-tabs.ts +56 -0
- package/src/constants.ts +4 -0
- package/src/index.ts +7 -2
- package/src/shiki-config.ts +4 -1
- package/src/shiki-meta-title.ts +6 -0
- package/src/shiki-transformer-container.ts +70 -0
- package/src/shiki-transformer-copy-button.ts +18 -36
- package/src/user-options.ts +8 -0
- package/src/virtual-internal.d.ts +2 -2
- package/src/virtual-user-config.ts +3 -3
package/lib/styles.gen.css
CHANGED
|
@@ -99,7 +99,8 @@
|
|
|
99
99
|
.nova-pagination-link-icon-right{--nova-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m9 18l6-6l-6-6'/%3E%3C/svg%3E");-webkit-mask:var(--nova-icon) no-repeat;mask:var(--nova-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1em;height:1em;display:block;min-width:1.25rem;min-height:1.25rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
|
|
100
100
|
.nova-theme-select-icon-dark{--nova-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M12 1.992a10 10 0 1 0 9.236 13.838c.341-.82-.476-1.644-1.298-1.31a6.5 6.5 0 0 1-6.864-10.787l.077-.08c.551-.63.113-1.653-.758-1.653h-.266l-.068-.006z'/%3E%3C/svg%3E");-webkit-mask:var(--nova-icon) no-repeat;mask:var(--nova-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1em;height:1em;}
|
|
101
101
|
.nova-theme-select-icon-light{--nova-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M12 19a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1m-4.95-2.05a1 1 0 0 1 0 1.414l-1.414 1.414a1 1 0 1 1-1.414-1.414l1.414-1.414a1 1 0 0 1 1.414 0m11.314 0l1.414 1.414a1 1 0 0 1-1.414 1.414l-1.414-1.414a1 1 0 0 1 1.414-1.414m-5.049-9.836a5 5 0 1 1-2.532 9.674a5 5 0 0 1 2.532-9.674M4 11a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2zm18 0a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zM5.636 4.222L7.05 5.636A1 1 0 0 1 5.636 7.05L4.222 5.636a1 1 0 0 1 1.414-1.414m14.142 0a1 1 0 0 1 0 1.414L18.364 7.05a1 1 0 0 1-1.414-1.414l1.414-1.414a1 1 0 0 1 1.414 0M12 1a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0V2a1 1 0 0 1 1-1'/%3E%3C/svg%3E");-webkit-mask:var(--nova-icon) no-repeat;mask:var(--nova-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1em;height:1em;}
|
|
102
|
-
.nova-code-container{position:relative;}
|
|
102
|
+
.nova-code-container{position:relative;overflow:hidden;border-width:1px;border-color:var(--sl-color-gray-5);border-radius:0.375rem;border-style:solid;}
|
|
103
|
+
.nova-code-container pre{position:relative;}
|
|
103
104
|
.nova-code-copy-button{position:absolute;right:0.5rem;top:0.5rem;margin:0;width:1.5rem;height:1.5rem;border-width:1px;border-color:var(--sl-color-gray-5);border-radius:0.25rem;border-style:solid;background-color:rgb(243 244 246 / 0.3) /* #f3f4f6 */;padding:0.25rem;--nova-text-opacity:1;color:rgb(0 0 0 / var(--nova-text-opacity)) /* #000 */;--nova-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--nova-backdrop-blur) var(--nova-backdrop-brightness) var(--nova-backdrop-contrast) var(--nova-backdrop-grayscale) var(--nova-backdrop-hue-rotate) var(--nova-backdrop-invert) var(--nova-backdrop-opacity) var(--nova-backdrop-saturate) var(--nova-backdrop-sepia);backdrop-filter:var(--nova-backdrop-blur) var(--nova-backdrop-brightness) var(--nova-backdrop-contrast) var(--nova-backdrop-grayscale) var(--nova-backdrop-hue-rotate) var(--nova-backdrop-invert) var(--nova-backdrop-opacity) var(--nova-backdrop-saturate) var(--nova-backdrop-sepia);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
|
|
104
105
|
.nova-link-card{position:relative;display:flex;flex-direction:column;gap:0.5rem;border-width:1px;border-color:var(--sl-color-gray-5);border-radius:0.75rem;border-style:solid;--nova-bg-opacity:1;background-color:rgb(255 255 255 / var(--nova-bg-opacity)) /* #fff */;padding-left:1.25rem;padding-right:1.25rem;padding-top:1rem;padding-bottom:1rem;--nova-text-opacity:1;color:rgb(55 65 81 / var(--nova-text-opacity)) /* #374151 */;--nova-shadow:var(--nova-shadow-inset) 0 1px 2px 0 var(--nova-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--nova-ring-offset-shadow), var(--nova-ring-shadow), var(--nova-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;transition-duration:200ms;}
|
|
105
106
|
.nova-page-frame-header{position:fixed;inset:0;z-index:var(--sl-z-index-navbar);box-sizing:border-box;width:100%;height:var(--sl-nav-height);border-width:0px;border-bottom-width:1px;border-color:var(--sl-color-hairline);border-style:solid;background-color:var(--sl-color-bg-nav) /* var(--sl-color-bg-nav) */;padding-top:var(--sl-nav-pad-y);padding-bottom:var(--sl-nav-pad-y);padding-left:var(--sl-nav-pad-x);padding-right:var(--sl-nav-pad-x);--nova-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--nova-backdrop-blur) var(--nova-backdrop-brightness) var(--nova-backdrop-contrast) var(--nova-backdrop-grayscale) var(--nova-backdrop-hue-rotate) var(--nova-backdrop-invert) var(--nova-backdrop-opacity) var(--nova-backdrop-saturate) var(--nova-backdrop-sepia);backdrop-filter:var(--nova-backdrop-blur) var(--nova-backdrop-brightness) var(--nova-backdrop-contrast) var(--nova-backdrop-grayscale) var(--nova-backdrop-hue-rotate) var(--nova-backdrop-invert) var(--nova-backdrop-opacity) var(--nova-backdrop-saturate) var(--nova-backdrop-sepia);}
|
|
@@ -108,6 +109,8 @@
|
|
|
108
109
|
.nova-pagination-link{margin:0;display:flex;align-items:center;justify-content:flex-end;gap:0.5rem;border-radius:0.75rem;padding:0.5rem;color:var(--sl-color-gray-2) /* var(--sl-color-gray-2) */;font-weight:500;text-decoration:none;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;transition-duration:100ms;}
|
|
109
110
|
.nova-link-button{margin-inline-end:0.5rem;margin-top:0.5rem;margin-bottom:0.5rem;display:inline-flex;align-items:center;justify-content:space-between;gap:0.5rem;border-width:1px;border-color:transparent;border-radius:0.75rem;border-style:solid;padding-left:1.5rem;padding-right:1.5rem;padding-top:0.75rem;padding-bottom:0.75rem;font-weight:500;text-decoration:none;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;transition-duration:200ms;}
|
|
110
111
|
.nova-header{box-sizing:border-box;height:100%;display:flex;align-items:center;gap:0.5rem;}
|
|
112
|
+
div[data-nova-code-container][data-nova-code-title] .nova-code-title{display:block;}
|
|
113
|
+
.nova-code-title{display:none;border-bottom-width:1px;border-bottom-color:var(--sl-color-gray-5);border-bottom-style:solid;background-color:var(--sl-color-bg) /* var(--sl-color-bg) */;padding-left:1rem;padding-right:1rem;padding-top:0.5rem;padding-bottom:0.5rem;font-size:0.875rem;line-height:1.25rem;color:var(--sl-color-gray-3) /* var(--sl-color-gray-3) */;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;}
|
|
111
114
|
.nova-header-actions{display:none;align-items:center;gap:0.5rem;}
|
|
112
115
|
.nova-social-icons-link{width:2rem;height:2rem;display:flex;border-radius:0.375rem;padding:0.5rem;color:var(--sl-color-text) /* var(--sl-color-text) */;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
|
|
113
116
|
.nova-theme-select{width:2rem;height:2rem;border-radius:0.375rem;padding:0.5rem;color:var(--sl-color-text) /* var(--sl-color-text) */;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
|
|
@@ -126,6 +129,7 @@
|
|
|
126
129
|
.nova-pagination-link:active[data-side="left"]{--nova-translate-x:-0.25rem;transform:translateX(var(--nova-translate-x)) translateY(var(--nova-translate-y)) translateZ(var(--nova-translate-z)) rotate(var(--nova-rotate)) rotateX(var(--nova-rotate-x)) rotateY(var(--nova-rotate-y)) rotateZ(var(--nova-rotate-z)) skewX(var(--nova-skew-x)) skewY(var(--nova-skew-y)) scaleX(var(--nova-scale-x)) scaleY(var(--nova-scale-y)) scaleZ(var(--nova-scale-z));}
|
|
127
130
|
.nova-pagination-link:active[data-side="right"]{--nova-translate-x:0.25rem;transform:translateX(var(--nova-translate-x)) translateY(var(--nova-translate-y)) translateZ(var(--nova-translate-z)) rotate(var(--nova-rotate)) rotateX(var(--nova-rotate-x)) rotateY(var(--nova-rotate-y)) rotateZ(var(--nova-rotate-z)) skewX(var(--nova-skew-x)) skewY(var(--nova-skew-y)) scaleX(var(--nova-scale-x)) scaleY(var(--nova-scale-y)) scaleZ(var(--nova-scale-z));}
|
|
128
131
|
.nova-code-copy-button:active{--nova-scale-x:0.9;--nova-scale-y:0.9;transform:translateX(var(--nova-translate-x)) translateY(var(--nova-translate-y)) translateZ(var(--nova-translate-z)) rotate(var(--nova-rotate)) rotateX(var(--nova-rotate-x)) rotateY(var(--nova-rotate-y)) rotateZ(var(--nova-rotate-z)) skewX(var(--nova-skew-x)) skewY(var(--nova-skew-y)) scaleX(var(--nova-scale-x)) scaleY(var(--nova-scale-y)) scaleZ(var(--nova-scale-z));}
|
|
132
|
+
.sl-markdown-content .nova-code-container pre.astro-code{border-width:0px;border-color:transparent;border-radius:0;}
|
|
129
133
|
.nova-page-frame-sidebar-pane{border-width:0px;border-color:var(--sl-color-hairline);border-style:solid;}
|
|
130
134
|
.nova-link-button-secondary{border-color:var(--sl-color-gray-5);--nova-bg-opacity:1;background-color:rgb(255 255 255 / var(--nova-bg-opacity)) /* #fff */;--nova-text-opacity:1;color:rgb(55 65 81 / var(--nova-text-opacity)) /* #374151 */;--nova-shadow:var(--nova-shadow-inset) 0 1px 2px 0 var(--nova-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--nova-ring-offset-shadow), var(--nova-ring-shadow), var(--nova-shadow);}
|
|
131
135
|
.nova-mobile-table-of-contents summary{border-bottom-color:var(--sl-color-hairline);}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-theme-nova",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "ocavue <ocavue@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
"@shikijs/transformers": "^3.2.1",
|
|
30
30
|
"@shikijs/twoslash": "^3.2.1",
|
|
31
31
|
"@shikijs/types": "^3.2.1",
|
|
32
|
+
"@types/hast": "^3.0.4",
|
|
32
33
|
"astro-theme-toggle": "^0.6.0",
|
|
33
34
|
"hast-util-is-element": "^3.0.0",
|
|
35
|
+
"rehype": "^13.0.2",
|
|
34
36
|
"shiki-twoslash-renderer": "0.0.2"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import fs from 'node:fs/promises'
|
|
5
|
+
import { basename } from 'node:path'
|
|
6
|
+
import Code from './Code.astro'
|
|
7
|
+
import config from 'virtual:starlight-theme-nova/user-config'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
type CodeProps = ComponentProps<typeof Code>
|
|
11
|
+
|
|
12
|
+
interface Props extends Omit<CodeProps, 'code' | 'lang'> {
|
|
13
|
+
/**
|
|
14
|
+
* The file path relative to the project root.
|
|
15
|
+
*/
|
|
16
|
+
path: string
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The title of the file. If not provided, the file name will be used.
|
|
20
|
+
*/
|
|
21
|
+
title?: string
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The language of the file. If not provided, the file extension will be used.
|
|
25
|
+
*/
|
|
26
|
+
lang?: CodeProps['lang']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { path, title, lang, meta, ...rest } = Astro.props
|
|
30
|
+
|
|
31
|
+
if (!path) {
|
|
32
|
+
throw new Error('path is required for <CodeFile> component')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const root = new URL(config.rootHref)
|
|
36
|
+
const url = new URL(path, root)
|
|
37
|
+
const absolutePath = fileURLToPath(url)
|
|
38
|
+
if (!existsSync(absolutePath)) {
|
|
39
|
+
throw new Error(`Unable to find file ${absolutePath}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let code: string
|
|
43
|
+
try {
|
|
44
|
+
code = await fs.readFile(absolutePath, 'utf-8')
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`Unable to read file ${absolutePath}: ${error}`, {
|
|
47
|
+
cause: error,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let fileName = basename(path)
|
|
52
|
+
let defaultLang = (fileName.split('.').pop() || 'plaintext') as 'plaintext'
|
|
53
|
+
let defaultTitle = fileName
|
|
54
|
+
let titleMeta = `title="${title || defaultTitle}"`
|
|
55
|
+
|
|
56
|
+
const codeProps: CodeProps = {
|
|
57
|
+
...rest,
|
|
58
|
+
lang: lang ?? defaultLang,
|
|
59
|
+
meta: meta ? `${meta} ${titleMeta}` : titleMeta,
|
|
60
|
+
code,
|
|
61
|
+
}
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
<Code {...codeProps} />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Tabs } from '@astrojs/starlight/components'
|
|
3
|
+
import { processCodeTabs } from './rehype-code-tabs'
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
border?: boolean
|
|
@@ -7,11 +8,14 @@ interface Props {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
const { border = true, syncKey } = Astro.props
|
|
11
|
+
|
|
12
|
+
const originalHtml = await Astro.slots.render('default')
|
|
13
|
+
const processedHtml = processCodeTabs(originalHtml)
|
|
10
14
|
---
|
|
11
15
|
|
|
12
16
|
<div class:list={['code-tabs', border ? 'code-tabs-border' : undefined]}>
|
|
13
17
|
<Tabs syncKey={syncKey}>
|
|
14
|
-
<
|
|
18
|
+
<Fragment set:html={processedHtml} />
|
|
15
19
|
</Tabs>
|
|
16
20
|
</div>
|
|
17
21
|
|
|
@@ -59,12 +63,14 @@ const { border = true, syncKey } = Astro.props
|
|
|
59
63
|
margin: 0;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
border-
|
|
66
|
+
.astro-code {
|
|
67
|
+
border-radius: 0;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
.
|
|
70
|
+
.nova-code-container {
|
|
67
71
|
border-radius: 0;
|
|
72
|
+
border-width: 0;
|
|
73
|
+
border-color: transparent;
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
76
|
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as Code } from './Code.astro'
|
|
2
|
+
export { default as CodeFile } from './CodeFile.astro'
|
|
2
3
|
export { default as CodePackageManagers } from './CodePackageManagers.astro'
|
|
3
|
-
export { default as CodeTabs } from './CodeTabs.astro'
|
|
4
4
|
export { default as CodeTabItem } from './CodeTabItem.astro'
|
|
5
|
+
export { default as CodeTabs } from './CodeTabs.astro'
|
|
5
6
|
export { default as LinkButton } from './LinkButton.astro'
|
|
6
7
|
export { default as LinkCard } from './LinkCard.astro'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Element, ElementContent } from 'hast'
|
|
2
|
+
import { rehype } from 'rehype'
|
|
3
|
+
|
|
4
|
+
import { CODE_PROPERTY_CONTAINER, CODE_PROPERTY_TITLE } from '../constants'
|
|
5
|
+
|
|
6
|
+
// https://github.com/withastro/starlight/blob/bdb05e704a6cf06a029367f99b6bf2f575e5a5f3/packages/starlight/user-components/rehype-tabs.ts#L20
|
|
7
|
+
export const TabItemTagname = 'starlight-tab-item'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rehype processor to extract tab panel data and turn each
|
|
11
|
+
* `<starlight-tab-item>` into a `<div>` with the necessary
|
|
12
|
+
* attributes.
|
|
13
|
+
*/
|
|
14
|
+
const tabsProcessor = rehype()
|
|
15
|
+
.data('settings', { fragment: true })
|
|
16
|
+
.use(() => {
|
|
17
|
+
return (tree: Element) => {
|
|
18
|
+
return {
|
|
19
|
+
...tree,
|
|
20
|
+
children: tree.children.map(replacePre),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function replacePre(node: ElementContent): ElementContent {
|
|
26
|
+
if (
|
|
27
|
+
node.type === 'element' &&
|
|
28
|
+
node.tagName === 'div' &&
|
|
29
|
+
node.properties[CODE_PROPERTY_CONTAINER] != null
|
|
30
|
+
) {
|
|
31
|
+
const title = node.properties[CODE_PROPERTY_TITLE]
|
|
32
|
+
|
|
33
|
+
if (title) {
|
|
34
|
+
node.properties[CODE_PROPERTY_TITLE] = undefined
|
|
35
|
+
return {
|
|
36
|
+
type: 'element',
|
|
37
|
+
tagName: TabItemTagname,
|
|
38
|
+
properties: {
|
|
39
|
+
dataLabel: title,
|
|
40
|
+
},
|
|
41
|
+
children: [node],
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return node
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Processes the children of the `<CodeTabs>` component to transform
|
|
51
|
+
* any code blocks with "title" meta information into a <TabItem> component.
|
|
52
|
+
*/
|
|
53
|
+
export function processCodeTabs(html: string): string {
|
|
54
|
+
const file = tabsProcessor.processSync({ value: html })
|
|
55
|
+
return file.toString()
|
|
56
|
+
}
|
package/src/constants.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export default function starlightThemeNova(
|
|
|
32
32
|
return {
|
|
33
33
|
name: 'starlight-theme-nova',
|
|
34
34
|
hooks: {
|
|
35
|
-
setup({ config, updateConfig, addIntegration }) {
|
|
35
|
+
setup({ config, updateConfig, addIntegration, astroConfig }) {
|
|
36
36
|
const newConfig = {
|
|
37
37
|
customCss: [
|
|
38
38
|
// Including any user CSS *after* our own.
|
|
@@ -58,7 +58,12 @@ export default function starlightThemeNova(
|
|
|
58
58
|
shikiConfig: createShikiConfig({ twoslash: true }),
|
|
59
59
|
},
|
|
60
60
|
vite: {
|
|
61
|
-
plugins: [
|
|
61
|
+
plugins: [
|
|
62
|
+
vitePluginUserConfig({
|
|
63
|
+
nav: options.nav,
|
|
64
|
+
rootHref: astroConfig.root.toString(),
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
62
67
|
},
|
|
63
68
|
})
|
|
64
69
|
},
|
package/src/shiki-config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
transformerMetaHighlight,
|
|
3
3
|
transformerMetaWordHighlight,
|
|
4
|
-
transformerNotationDiff,
|
|
4
|
+
transformerNotationDiff,transformerRemoveNotationEscape,
|
|
5
5
|
transformerNotationHighlight,
|
|
6
6
|
transformerNotationWordHighlight,
|
|
7
7
|
} from '@shikijs/transformers'
|
|
@@ -10,6 +10,7 @@ import type { ShikiTransformer } from '@shikijs/types'
|
|
|
10
10
|
import type { ShikiConfig } from 'astro'
|
|
11
11
|
import { createRenderer } from 'shiki-twoslash-renderer'
|
|
12
12
|
|
|
13
|
+
import { transformerContainer } from './shiki-transformer-container'
|
|
13
14
|
import { transformerCopyButton } from './shiki-transformer-copy-button'
|
|
14
15
|
|
|
15
16
|
export function createShikiConfig(options: { twoslash: boolean }): ShikiConfig {
|
|
@@ -19,8 +20,10 @@ export function createShikiConfig(options: { twoslash: boolean }): ShikiConfig {
|
|
|
19
20
|
transformerNotationDiff(),
|
|
20
21
|
transformerNotationHighlight(),
|
|
21
22
|
transformerNotationWordHighlight(),
|
|
23
|
+
transformerRemoveNotationEscape(),
|
|
22
24
|
|
|
23
25
|
transformerCopyButton(),
|
|
26
|
+
transformerContainer(),
|
|
24
27
|
options.twoslash
|
|
25
28
|
? transformerTwoslash({
|
|
26
29
|
renderer: createRenderer(),
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ShikiTransformer } from '@shikijs/types'
|
|
2
|
+
import type { ElementContent, RootContent } from 'hast'
|
|
3
|
+
import { isElement } from 'hast-util-is-element'
|
|
4
|
+
|
|
5
|
+
import { CODE_PROPERTY_CONTAINER, CODE_PROPERTY_TITLE } from './constants'
|
|
6
|
+
import { parseTitleString } from './shiki-meta-title'
|
|
7
|
+
|
|
8
|
+
const name = 'starlight-theme-nova-shiki-transformer-container'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A transformer to wrap code blocks in a container.
|
|
12
|
+
*/
|
|
13
|
+
export function transformerContainer(): ShikiTransformer {
|
|
14
|
+
return {
|
|
15
|
+
name: name,
|
|
16
|
+
root(node) {
|
|
17
|
+
if (node.children.length === 0) {
|
|
18
|
+
throw new Error(`[${name}] Expected at least one child`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pre = node.children.find((child) => isElement(child, 'pre'))
|
|
22
|
+
|
|
23
|
+
if (!pre) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`[${name}] Expected a <pre> element in the root node but got ${JSON.stringify(node)}`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const title = parseTitleString(this.options.meta?.__raw || '')
|
|
30
|
+
|
|
31
|
+
const children: ElementContent[] = normalizeContent(node.children)
|
|
32
|
+
|
|
33
|
+
if (title) {
|
|
34
|
+
children.unshift({
|
|
35
|
+
type: 'element',
|
|
36
|
+
tagName: 'div',
|
|
37
|
+
properties: {
|
|
38
|
+
class: 'nova-code-title',
|
|
39
|
+
},
|
|
40
|
+
children: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
value: title,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
node.children = [
|
|
50
|
+
{
|
|
51
|
+
type: 'element',
|
|
52
|
+
tagName: 'div',
|
|
53
|
+
properties: {
|
|
54
|
+
[CODE_PROPERTY_CONTAINER]: '',
|
|
55
|
+
[CODE_PROPERTY_TITLE]: title || undefined,
|
|
56
|
+
class: 'nova-code-container not-content',
|
|
57
|
+
},
|
|
58
|
+
children,
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
return node
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A simple function to make typescript happy
|
|
68
|
+
function normalizeContent(children: RootContent[]): ElementContent[] {
|
|
69
|
+
return children.filter((child) => child.type !== 'doctype')
|
|
70
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ShikiTransformer } from '@shikijs/types'
|
|
2
|
-
import { isElement } from 'hast-util-is-element'
|
|
3
2
|
|
|
4
3
|
const name = 'starlight-theme-nova-shiki-transformer-copy-button'
|
|
5
4
|
|
|
@@ -9,42 +8,25 @@ const name = 'starlight-theme-nova-shiki-transformer-copy-button'
|
|
|
9
8
|
export function transformerCopyButton(): ShikiTransformer {
|
|
10
9
|
return {
|
|
11
10
|
name: name,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type: 'element',
|
|
27
|
-
tagName: 'div',
|
|
28
|
-
properties: {
|
|
29
|
-
class: 'nova-code-container not-content',
|
|
30
|
-
},
|
|
31
|
-
children: [
|
|
32
|
-
pre,
|
|
33
|
-
{
|
|
34
|
-
type: 'element',
|
|
35
|
-
tagName: 'nova-code-copy-button',
|
|
36
|
-
properties: {
|
|
37
|
-
type: 'button',
|
|
38
|
-
'data-code': this.source,
|
|
39
|
-
title: 'Copy code',
|
|
40
|
-
'aria-label': 'Copy code',
|
|
41
|
-
class: 'nova-code-copy-button',
|
|
42
|
-
},
|
|
43
|
-
children: [],
|
|
11
|
+
pre(node) {
|
|
12
|
+
return {
|
|
13
|
+
...node,
|
|
14
|
+
children: [
|
|
15
|
+
...node.children,
|
|
16
|
+
{
|
|
17
|
+
type: 'element',
|
|
18
|
+
tagName: 'nova-code-copy-button',
|
|
19
|
+
properties: {
|
|
20
|
+
type: 'button',
|
|
21
|
+
'data-code': this.source,
|
|
22
|
+
title: 'Copy code',
|
|
23
|
+
'aria-label': 'Copy code',
|
|
24
|
+
class: 'nova-code-copy-button',
|
|
44
25
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
children: [],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}
|
|
48
30
|
},
|
|
49
31
|
}
|
|
50
32
|
}
|
package/src/user-options.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
|
2
2
|
|
|
3
3
|
declare module 'virtual:starlight-theme-nova/user-config' {
|
|
4
|
-
const
|
|
5
|
-
export default
|
|
4
|
+
const config: import('./user-options').ConfigSerialized
|
|
5
|
+
export default config
|
|
6
6
|
}
|
|
7
7
|
declare module 'virtual:starlight/components/Search' {
|
|
8
8
|
const Search: typeof import('./components/Search.astro').default
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ViteUserConfig } from 'astro'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ConfigSerialized } from './user-options'
|
|
4
4
|
|
|
5
5
|
function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
|
|
6
6
|
return `\0${id}`
|
|
@@ -8,11 +8,11 @@ function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
|
|
|
8
8
|
|
|
9
9
|
/** Vite plugin that exposes user config via virtual modules. */
|
|
10
10
|
export function vitePluginUserConfig(
|
|
11
|
-
|
|
11
|
+
config: ConfigSerialized,
|
|
12
12
|
): NonNullable<ViteUserConfig['plugins']>[number] {
|
|
13
13
|
/** Map of virtual module names to their code contents as strings. */
|
|
14
14
|
const modules = {
|
|
15
|
-
'virtual:starlight-theme-nova/user-config': `export default ${JSON.stringify(
|
|
15
|
+
'virtual:starlight-theme-nova/user-config': `export default ${JSON.stringify(config)}`,
|
|
16
16
|
} satisfies Record<string, string>
|
|
17
17
|
|
|
18
18
|
/** Mapping names prefixed with `\0` to their original form. */
|