@@ -24,14 +28,18 @@ defineProps<{
{{ member.title }}
-
- @
-
-
+ @
+
{{ member.org }}
-
+
@@ -155,12 +163,12 @@ defineProps<{
}
.name {
- margin: 0;
+ margin: 0;
font-weight: 600;
}
.affiliation {
- margin: 0;
+ margin: 0;
font-weight: 500;
color: var(--vp-c-text-2);
}
@@ -171,7 +179,7 @@ defineProps<{
}
.org.link:hover {
- color: var(--vp-c-brand);
+ color: var(--vp-c-brand-1);
}
.desc {
@@ -180,7 +188,7 @@ defineProps<{
.desc :deep(a) {
font-weight: 500;
- color: var(--vp-c-brand);
+ color: var(--vp-c-brand-1);
text-decoration-style: dotted;
transition: color 0.25s;
}
diff --git a/src/client/theme-default/composables/outline.ts b/src/client/theme-default/composables/outline.ts
index c59234cb..b4c9fb4a 100644
--- a/src/client/theme-default/composables/outline.ts
+++ b/src/client/theme-default/composables/outline.ts
@@ -22,7 +22,9 @@ export function resolveTitle(theme: DefaultTheme.Config) {
}
export function getHeaders(range: DefaultTheme.Config['outline']) {
- const headers = [...document.querySelectorAll('.VPDoc h2,h3,h4,h5,h6')]
+ const headers = [
+ ...document.querySelectorAll('.VPDoc :where(h1,h2,h3,h4,h5,h6)')
+ ]
.filter((el) => el.id && el.hasChildNodes())
.map((el) => {
const level = Number(el.tagName[1])
@@ -166,7 +168,9 @@ export function useActiveAnchor(
prevActiveLink.classList.remove('active')
}
- if (hash !== null) {
+ if (hash == null) {
+ prevActiveLink = null
+ } else {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`
)
diff --git a/src/client/theme-default/composables/prev-next.ts b/src/client/theme-default/composables/prev-next.ts
index 6f967f73..300e7984 100644
--- a/src/client/theme-default/composables/prev-next.ts
+++ b/src/client/theme-default/composables/prev-next.ts
@@ -31,7 +31,9 @@ export function usePrevNext() {
? frontmatter.value.prev
: typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.text
- : undefined) ?? candidates[index - 1]?.text,
+ : undefined) ??
+ candidates[index - 1]?.docFooterText ??
+ candidates[index - 1]?.text,
link:
(typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.link
@@ -45,7 +47,9 @@ export function usePrevNext() {
? frontmatter.value.next
: typeof frontmatter.value.next === 'object'
? frontmatter.value.next.text
- : undefined) ?? candidates[index + 1]?.text,
+ : undefined) ??
+ candidates[index + 1]?.docFooterText ??
+ candidates[index + 1]?.text,
link:
(typeof frontmatter.value.next === 'object'
? frontmatter.value.next.link
diff --git a/src/client/theme-default/composables/sidebar.ts b/src/client/theme-default/composables/sidebar.ts
index a8bb6df1..ddc9c073 100644
--- a/src/client/theme-default/composables/sidebar.ts
+++ b/src/client/theme-default/composables/sidebar.ts
@@ -1,16 +1,17 @@
+import { useMediaQuery } from '@vueuse/core'
+import type { DefaultTheme } from 'vitepress/theme'
import {
- type ComputedRef,
- type Ref,
computed,
onMounted,
onUnmounted,
ref,
- watchEffect
+ watch,
+ watchEffect,
+ watchPostEffect,
+ type ComputedRef,
+ type Ref
} from 'vue'
-import { useMediaQuery } from '@vueuse/core'
-import { useRoute } from 'vitepress'
-import type { DefaultTheme } from 'vitepress/theme'
-import { isActive } from '../../shared'
+import { inBrowser, isActive } from '../../shared'
import {
hasActiveLink as containsActiveLink,
getSidebar,
@@ -22,25 +23,31 @@ export interface SidebarControl {
collapsed: Ref
collapsible: ComputedRef
isLink: ComputedRef
- isActiveLink: ComputedRef
+ isActiveLink: Ref
hasActiveLink: ComputedRef
hasChildren: ComputedRef
toggle(): void
}
export function useSidebar() {
- const route = useRoute()
- const { theme, frontmatter } = useData()
+ const { frontmatter, page, theme } = useData()
const is960 = useMediaQuery('(min-width: 960px)')
const isOpen = ref(false)
- const sidebar = computed(() => {
+ const _sidebar = computed(() => {
const sidebarConfig = theme.value.sidebar
- const relativePath = route.data.relativePath
+ const relativePath = page.value.relativePath
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : []
})
+ const sidebar = ref(_sidebar.value)
+
+ watch(_sidebar, (next, prev) => {
+ if (JSON.stringify(next) !== JSON.stringify(prev))
+ sidebar.value = _sidebar.value
+ })
+
const hasSidebar = computed(() => {
return (
frontmatter.value.sidebar !== false &&
@@ -127,6 +134,13 @@ export function useCloseSidebarOnEscape(
}
}
+const hashRef = ref(inBrowser ? location.hash : '')
+if (inBrowser) {
+ window.addEventListener('hashchange', () => {
+ hashRef.value = location.hash
+ })
+}
+
export function useSidebarControl(
item: ComputedRef
): SidebarControl {
@@ -142,9 +156,13 @@ export function useSidebarControl(
return !!item.value.link
})
- const isActiveLink = computed(() => {
- return isActive(page.value.relativePath, item.value.link)
- })
+ const isActiveLink = ref(false)
+ const updateIsActiveLink = () => {
+ isActiveLink.value = isActive(page.value.relativePath, item.value.link)
+ }
+
+ watch([page, item, hashRef], updateIsActiveLink)
+ onMounted(updateIsActiveLink)
const hasActiveLink = computed(() => {
if (isActiveLink.value) {
@@ -164,7 +182,7 @@ export function useSidebarControl(
collapsed.value = !!(collapsible.value && item.value.collapsed)
})
- watchEffect(() => {
+ watchPostEffect(() => {
;(isActiveLink.value || hasActiveLink.value) && (collapsed.value = false)
})
diff --git a/src/client/theme-default/styles/base.css b/src/client/theme-default/styles/base.css
index 5e45af7c..5fb3a3cc 100644
--- a/src/client/theme-default/styles/base.css
+++ b/src/client/theme-default/styles/base.css
@@ -1,3 +1,17 @@
+@media (prefers-reduced-motion: reduce) {
+ *,
+ ::before,
+ ::after {
+ animation-delay: -1ms !important;
+ animation-duration: 1ms !important;
+ animation-iteration-count: 1 !important;
+ background-attachment: initial !important;
+ scroll-behavior: auto !important;
+ transition-duration: 0s !important;
+ transition-delay: 0s !important;
+ }
+}
+
*,
::before,
::after {
@@ -227,3 +241,12 @@ p {
vite-error-overlay {
z-index: 9999;
}
+
+mjx-container {
+ display: inline-block;
+ margin: auto 2px -2px;
+}
+
+mjx-container > svg {
+ margin: auto;
+}
diff --git a/src/client/theme-default/styles/components/custom-block.css b/src/client/theme-default/styles/components/custom-block.css
index 373b3a3a..655bb34b 100644
--- a/src/client/theme-default/styles/components/custom-block.css
+++ b/src/client/theme-default/styles/components/custom-block.css
@@ -13,10 +13,13 @@
background-color: var(--vp-custom-block-info-bg);
}
-.custom-block.custom-block th,
-.custom-block.custom-block blockquote > p {
- font-size: var(--vp-custom-block-font-size);
- color: inherit;
+.custom-block.info a,
+.custom-block.info code {
+ color: var(--vp-c-brand-1);
+}
+
+.custom-block.info a:hover {
+ color: var(--vp-c-brand-2);
}
.custom-block.info code {
@@ -29,6 +32,15 @@
background-color: var(--vp-custom-block-tip-bg);
}
+.custom-block.tip a,
+.custom-block.tip code {
+ color: var(--vp-c-brand-1);
+}
+
+.custom-block.tip a:hover {
+ color: var(--vp-c-brand-2);
+}
+
.custom-block.tip code {
background-color: var(--vp-custom-block-tip-code-bg);
}
@@ -39,6 +51,15 @@
background-color: var(--vp-custom-block-warning-bg);
}
+.custom-block.warning a,
+.custom-block.warning code {
+ color: var(--vp-c-warning-1);
+}
+
+.custom-block.warning a:hover {
+ color: var(--vp-c-warning-2);
+}
+
.custom-block.warning code {
background-color: var(--vp-custom-block-warning-code-bg);
}
@@ -49,6 +70,15 @@
background-color: var(--vp-custom-block-danger-bg);
}
+.custom-block.danger a,
+.custom-block.danger code {
+ color: var(--vp-c-danger-1);
+}
+
+.custom-block.danger a:hover {
+ color: var(--vp-c-danger-2);
+}
+
.custom-block.danger code {
background-color: var(--vp-custom-block-danger-code-bg);
}
@@ -59,6 +89,14 @@
background-color: var(--vp-custom-block-details-bg);
}
+.custom-block.details a {
+ color: var(--vp-c-brand-1);
+}
+
+.custom-block.details a:hover {
+ color: var(--vp-c-brand-2);
+}
+
.custom-block.details code {
background-color: var(--vp-custom-block-details-code-bg);
}
@@ -84,12 +122,21 @@
.custom-block a {
color: inherit;
font-weight: 600;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ transition: opacity 0.25s;
}
.custom-block a:hover {
- text-decoration: underline;
+ opacity: 0.75;
}
.custom-block code {
font-size: var(--vp-custom-block-code-font-size);
}
+
+.custom-block.custom-block th,
+.custom-block.custom-block blockquote > p {
+ font-size: var(--vp-custom-block-font-size);
+ color: inherit;
+}
diff --git a/src/client/theme-default/styles/components/vp-code-group.css b/src/client/theme-default/styles/components/vp-code-group.css
index 0b591872..4f23a416 100644
--- a/src/client/theme-default/styles/components/vp-code-group.css
+++ b/src/client/theme-default/styles/components/vp-code-group.css
@@ -23,7 +23,7 @@
}
.vp-code-group .tabs input {
- position: absolute;
+ position: fixed;
opacity: 0;
pointer-events: none;
}
@@ -48,7 +48,8 @@
bottom: -1px;
left: 8px;
z-index: 1;
- height: 1px;
+ height: 2px;
+ border-radius: 2px;
content: '';
background-color: transparent;
transition: background-color 0.25s;
diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css
index 23bc6852..332982df 100644
--- a/src/client/theme-default/styles/components/vp-doc.css
+++ b/src/client/theme-default/styles/components/vp-doc.css
@@ -43,6 +43,7 @@
font-weight: 500;
user-select: none;
opacity: 0;
+ text-decoration: none;
transition:
color 0.25s,
opacity 0.25s;
@@ -108,14 +109,16 @@
.vp-doc a {
font-weight: 500;
- color: var(--vp-c-brand);
- text-decoration-style: dotted;
- transition: color 0.25s;
+ color: var(--vp-c-brand-1);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ transition:
+ color 0.25s,
+ opacity 0.25s;
}
.vp-doc a:hover {
- /* color: var(--vp-c-brand-dark); */
- text-decoration: underline;
+ color: var(--vp-c-brand-2);
}
.vp-doc strong {
@@ -214,21 +217,6 @@
margin: 0;
}
-.vp-doc .custom-block a {
- color: inherit;
- font-weight: 600;
-}
-
-.vp-doc .custom-block a:hover {
- text-decoration: underline;
-}
-
-.vp-doc .custom-block code {
- font-size: var(--vp-custom-block-code-font-size);
- font-weight: 700;
- color: inherit;
-}
-
.vp-doc .custom-block div[class*='language-'] {
margin: 8px 0;
border-radius: 8px;
@@ -251,31 +239,30 @@
/* inline code */
.vp-doc :not(pre, h1, h2, h3, h4, h5, h6) > code {
font-size: var(--vp-code-font-size);
+ color: var(--vp-code-color);
}
.vp-doc :not(pre) > code {
border-radius: 4px;
padding: 3px 6px;
- color: var(--vp-c-text-code);
- background-color: var(--vp-c-mute);
+ background-color: var(--vp-code-bg);
transition:
- color 0.5s,
+ color 0.25s,
background-color 0.5s;
}
-.vp-doc h1 > code,
-.vp-doc h2 > code,
-.vp-doc h3 > code {
- font-size: 0.9em;
-}
-
.vp-doc a > code {
- color: var(--vp-c-brand);
- transition: color 0.25s;
+ color: var(--vp-code-link-color);
}
.vp-doc a:hover > code {
- color: var(--vp-c-brand-dark);
+ color: var(--vp-code-link-hover-color);
+}
+
+.vp-doc h1 > code,
+.vp-doc h2 > code,
+.vp-doc h3 > code {
+ font-size: 0.9em;
}
.vp-doc div[class*='language-'],
@@ -490,7 +477,7 @@
position: relative;
top: -1px;
/*rtl:ignore*/
- left: -65px;
+ transform: translateX(calc(-100% - 1px));
display: flex;
justify-content: center;
align-items: center;
@@ -498,7 +485,8 @@
/*rtl:ignore*/
border-right: 0;
border-radius: 4px 0 0 4px;
- width: 64px;
+ padding: 0 10px;
+ width: fit-content;
height: 40px;
text-align: center;
font-size: 12px;
@@ -506,7 +494,7 @@
color: var(--vp-code-copy-code-active-text);
background-color: var(--vp-code-copy-code-hover-bg);
white-space: nowrap;
- content: 'Copied';
+ content: var(--vp-code-copy-copied-text-content);
}
.vp-doc [class*='language-'] > span.lang {
@@ -517,7 +505,7 @@
z-index: 2;
font-size: 12px;
font-weight: 500;
- color: var(--vp-c-code-dimm);
+ color: var(--vp-code-lang-color);
transition:
color 0.4s,
opacity 0.4s;
diff --git a/src/client/theme-default/styles/components/vp-sponsor.css b/src/client/theme-default/styles/components/vp-sponsor.css
index da2160bb..9d7a2a5c 100644
--- a/src/client/theme-default/styles/components/vp-sponsor.css
+++ b/src/client/theme-default/styles/components/vp-sponsor.css
@@ -114,7 +114,7 @@
}
.vp-sponsor-grid-item:hover {
- background-color: var(--vp-c-bg-soft-down);
+ background-color: var(--vp-c-default-soft);
}
.vp-sponsor-grid-item:hover .vp-sponsor-grid-image {
diff --git a/src/client/theme-default/styles/vars.css b/src/client/theme-default/styles/vars.css
index f1c0e1c6..3ebcac17 100644
--- a/src/client/theme-default/styles/vars.css
+++ b/src/client/theme-default/styles/vars.css
@@ -1,141 +1,229 @@
/**
- * Colors Base
- *
- * These are the pure base color presets. Most of the time, you should not be
- * using these colors directly in the theme but rather use "Colors Theme"
- * instead because those are "Theme (light or dark)" dependant.
+ * Colors: Solid
* -------------------------------------------------------------------------- */
:root {
--vp-c-white: #ffffff;
--vp-c-black: #000000;
- --vp-c-gray: #8e8e93;
-
- --vp-c-text-light-1: rgba(60, 60, 67);
- --vp-c-text-light-2: rgba(60, 60, 67, 0.75);
- --vp-c-text-light-3: rgba(60, 60, 67, 0.33);
-
- --vp-c-text-dark-1: rgba(255, 255, 245, 0.86);
- --vp-c-text-dark-2: rgba(235, 235, 245, 0.6);
- --vp-c-text-dark-3: rgba(235, 235, 245, 0.38);
-
- --vp-c-green: #10b981;
- --vp-c-green-light: #34d399;
- --vp-c-green-lighter: #6ee7b7;
- --vp-c-green-dark: #059669;
- --vp-c-green-darker: #047857;
- --vp-c-green-dimm-1: rgba(16, 185, 129, 0.05);
- --vp-c-green-dimm-2: rgba(16, 185, 129, 0.2);
- --vp-c-green-dimm-3: rgba(16, 185, 129, 0.5);
-
- --vp-c-yellow: #d97706;
- --vp-c-yellow-light: #f59e0b;
- --vp-c-yellow-lighter: #fbbf24;
- --vp-c-yellow-dark: #b45309;
- --vp-c-yellow-darker: #92400e;
- --vp-c-yellow-dimm-1: rgba(234, 179, 8, 0.05);
- --vp-c-yellow-dimm-2: rgba(234, 179, 8, 0.2);
- --vp-c-yellow-dimm-3: rgba(234, 179, 8, 0.5);
-
- --vp-c-red: #f43f5e;
- --vp-c-red-light: #fb7185;
- --vp-c-red-lighter: #fda4af;
- --vp-c-red-dark: #e11d48;
- --vp-c-red-darker: #be123c;
- --vp-c-red-dimm-1: rgba(244, 63, 94, 0.05);
- --vp-c-red-dimm-2: rgba(244, 63, 94, 0.2);
- --vp-c-red-dimm-3: rgba(244, 63, 94, 0.5);
+ --vp-c-neutral: var(--vp-c-black);
+ --vp-c-neutral-inverse: var(--vp-c-white);
+}
- --vp-c-sponsor: #db2777;
+.dark {
+ --vp-c-neutral: var(--vp-c-white);
+ --vp-c-neutral-inverse: var(--vp-c-black);
}
/**
- * Colors Theme
+ * Colors: Palette
+ *
+ * The primitive colors used for accent colors. These colors are referenced
+ * by functional colors such as "Text", "Background", or "Brand".
+ *
+ * Each colors have exact same color scale system with 3 levels of solid
+ * colors with different brightness, and 1 soft color.
+ *
+ * - `XXX-1`: The most solid color used mainly for colored text. It must
+ * satisfy the contrast ratio against when used on top of `XXX-soft`.
+ *
+ * - `XXX-2`: The color used mainly for hover state of the button.
+ *
+ * - `XXX-3`: The color for solid background, such as bg color of the button.
+ * It must satisfy the contrast ratio with pure white (#ffffff) text on
+ * top of it.
+ *
+ * - `XXX-soft`: The color used for subtle background such as custom container
+ * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
+ * on top of it.
+ *
+ * The soft color must be semi transparent alpha channel. This is crucial
+ * because it allows adding multiple "soft" colors on top of each other
+ * to create a accent, such as when having inline code block inside
+ * custom containers.
* -------------------------------------------------------------------------- */
:root {
- --vp-c-bg: #ffffff;
-
- --vp-c-bg-elv: #ffffff;
- --vp-c-bg-elv-up: #ffffff;
- --vp-c-bg-elv-down: #f6f6f7;
- --vp-c-bg-elv-mute: #f6f6f7;
-
- --vp-c-bg-soft: #f6f6f7;
- --vp-c-bg-soft-up: #f9f9fa;
- --vp-c-bg-soft-down: #e3e3e5;
- --vp-c-bg-soft-mute: #e3e3e5;
-
- --vp-c-bg-alt: #f6f6f7;
-
- --vp-c-border: rgba(60, 60, 67, 0.29);
- --vp-c-divider: rgba(60, 60, 67, 0.12);
- --vp-c-gutter: rgba(60, 60, 67, 0.12);
-
- --vp-c-neutral: var(--vp-c-black);
- --vp-c-neutral-inverse: var(--vp-c-white);
-
- --vp-c-text-1: var(--vp-c-text-light-1);
- --vp-c-text-2: var(--vp-c-text-light-2);
- --vp-c-text-3: var(--vp-c-text-light-3);
+ --vp-c-gray-1: #dddde3;
+ --vp-c-gray-2: #e4e4e9;
+ --vp-c-gray-3: #ebebef;
+ --vp-c-gray-soft: rgba(142, 150, 170, 0.14);
+
+ --vp-c-indigo-1: #3451b2;
+ --vp-c-indigo-2: #3a5ccc;
+ --vp-c-indigo-3: #5672cd;
+ --vp-c-indigo-soft: rgba(100, 108, 255, 0.14);
+
+ --vp-c-green-1: #18794e;
+ --vp-c-green-2: #299764;
+ --vp-c-green-3: #30a46c;
+ --vp-c-green-soft: rgba(16, 185, 129, 0.14);
+
+ --vp-c-yellow-1: #915930;
+ --vp-c-yellow-2: #946300;
+ --vp-c-yellow-3: #9f6a00;
+ --vp-c-yellow-soft: rgba(234, 179, 8, 0.14);
+
+ --vp-c-red-1: #b8272c;
+ --vp-c-red-2: #d5393e;
+ --vp-c-red-3: #e0575b;
+ --vp-c-red-soft: rgba(244, 63, 94, 0.14);
- --vp-c-text-inverse-1: var(--vp-c-text-dark-1);
- --vp-c-text-inverse-2: var(--vp-c-text-dark-2);
- --vp-c-text-inverse-3: var(--vp-c-text-dark-3);
+ --vp-c-sponsor: #db2777;
+}
- --vp-c-text-code: #476582;
+.dark {
+ --vp-c-gray-1: #515c67;
+ --vp-c-gray-2: #414853;
+ --vp-c-gray-3: #32363f;
+ --vp-c-gray-soft: rgba(101, 117, 133, 0.16);
+
+ --vp-c-indigo-1: #a8b1ff;
+ --vp-c-indigo-2: #5c73e7;
+ --vp-c-indigo-3: #3e63dd;
+ --vp-c-indigo-soft: rgba(100, 108, 255, 0.16);
+
+ --vp-c-green-1: #3dd68c;
+ --vp-c-green-2: #30a46c;
+ --vp-c-green-3: #298459;
+ --vp-c-green-soft: rgba(16, 185, 129, 0.16);
+
+ --vp-c-yellow-1: #f9b44e;
+ --vp-c-yellow-2: #da8b17;
+ --vp-c-yellow-3: #a46a0a;
+ --vp-c-yellow-soft: rgba(234, 179, 8, 0.16);
+
+ --vp-c-red-1: #f66f81;
+ --vp-c-red-2: #f14158;
+ --vp-c-red-3: #b62a3c;
+ --vp-c-red-soft: rgba(244, 63, 94, 0.16);
+}
- --vp-c-brand: var(--vp-c-green);
- --vp-c-brand-light: var(--vp-c-green-light);
- --vp-c-brand-lighter: var(--vp-c-green-lighter);
- --vp-c-brand-dark: var(--vp-c-green-dark);
- --vp-c-brand-darker: var(--vp-c-green-darker);
+/**
+ * Colors: Background
+ *
+ * - `bg`: The bg color used for main screen.
+ *
+ * - `bg-alt`: The alternative bg color used in places such as "sidebar",
+ * or "code block".
+ *
+ * - `bg-elv`: The elevated bg color. This is used at parts where it "floats",
+ * such as "dialog".
+ *
+ * - `bg-soft`: The bg color to slightly ditinguish some components from
+ * the page. Used for things like "carbon ads" or "table".
+ * -------------------------------------------------------------------------- */
- --vp-c-mute: #f6f6f7;
- --vp-c-mute-light: #f9f9fc;
- --vp-c-mute-lighter: #ffffff;
- --vp-c-mute-dark: #e3e3e5;
- --vp-c-mute-darker: #d7d7d9;
+:root {
+ --vp-c-bg: #ffffff;
+ --vp-c-bg-alt: #f6f6f7;
+ --vp-c-bg-elv: #ffffff;
+ --vp-c-bg-soft: #f6f6f7;
}
.dark {
- --vp-c-bg: #1e1e20;
-
- --vp-c-bg-elv: #252529;
- --vp-c-bg-elv-up: #313136;
- --vp-c-bg-elv-down: #1e1e20;
- --vp-c-bg-elv-mute: #313136;
+ --vp-c-bg: #1b1b1f;
+ --vp-c-bg-alt: #161618;
+ --vp-c-bg-elv: #202127;
+ --vp-c-bg-soft: #202127;
+}
- --vp-c-bg-soft: #252529;
- --vp-c-bg-soft-up: #313136;
- --vp-c-bg-soft-down: #1e1e20;
- --vp-c-bg-soft-mute: #313136;
+/**
+ * Colors: Borders
+ *
+ * - `divider`: This is used for separators. This is used to divide sections
+ * within the same components, such as having separator on "h2" heading.
+ *
+ * - `border`: This is designed for borders on interactive components.
+ * For example this should be used for a button outline.
+ *
+ * - `gutter`: This is used to divide components in the page. For example
+ * the header and the lest of the page.
+ * -------------------------------------------------------------------------- */
- --vp-c-bg-alt: #161618;
+:root {
+ --vp-c-border: #c2c2c4;
+ --vp-c-divider: #e2e2e3;
+ --vp-c-gutter: #e2e2e3;
+}
- --vp-c-border: rgba(82, 82, 89, 0.68);
- --vp-c-divider: rgba(82, 82, 89, 0.32);
+.dark {
+ --vp-c-border: #3c3f44;
+ --vp-c-divider: #2e2e32;
--vp-c-gutter: #000000;
+}
- --vp-c-neutral: var(--vp-c-white);
- --vp-c-neutral-inverse: var(--vp-c-black);
+/**
+ * Colors: Text
+ *
+ * - `text-1`: Used for primary text.
+ *
+ * - `text-2`: Used for muted texts, such as "inactive menu" or "info texts".
+ *
+ * - `text-3`: Used for subtle texts, such as "placeholders" or "caret icon".
+ * -------------------------------------------------------------------------- */
- --vp-c-text-1: var(--vp-c-text-dark-1);
- --vp-c-text-2: var(--vp-c-text-dark-2);
- --vp-c-text-3: var(--vp-c-text-dark-3);
+:root {
+ --vp-c-text-1: rgba(60, 60, 67);
+ --vp-c-text-2: rgba(60, 60, 67, 0.78);
+ --vp-c-text-3: rgba(60, 60, 67, 0.56);
+}
- --vp-c-text-inverse-1: var(--vp-c-text-light-1);
- --vp-c-text-inverse-2: var(--vp-c-text-light-2);
- --vp-c-text-inverse-3: var(--vp-c-text-light-3);
+.dark {
+ --vp-c-text-1: rgba(255, 255, 245, 0.86);
+ --vp-c-text-2: rgba(235, 235, 245, 0.6);
+ --vp-c-text-3: rgba(235, 235, 245, 0.38);
+}
- --vp-c-text-code: #c9def1;
+/**
+ * Colors: Function
+ *
+ * - `default`: The color used purely for subtle indication without any
+ * special meanings attched to it such as bg color for menu hover state.
+ *
+ * - `brand`: Used for primary brand colors, such as link text, button with
+ * brand theme, etc.
+ *
+ * - `tip`: Used to indicate useful information. The default theme uses the
+ * brand color for this by default.
+ *
+ * - `warning`: Used to indicate warning to the users. Used in custom
+ * container, badges, etc.
+ *
+ * - `danger`: Used to show error, or dangerous message to the users. Used
+ * in custom container, badges, etc.
+ *
+ * To understand the scaling system, refer to "Colors: Palette" section.
+ * -------------------------------------------------------------------------- */
- --vp-c-mute: #313136;
- --vp-c-mute-light: #3a3a3c;
- --vp-c-mute-lighter: #505053;
- --vp-c-mute-dark: #2c2c30;
- --vp-c-mute-darker: #252529;
+:root {
+ --vp-c-default-1: var(--vp-c-gray-1);
+ --vp-c-default-2: var(--vp-c-gray-2);
+ --vp-c-default-3: var(--vp-c-gray-3);
+ --vp-c-default-soft: var(--vp-c-gray-soft);
+
+ --vp-c-brand-1: var(--vp-c-indigo-1);
+ --vp-c-brand-2: var(--vp-c-indigo-2);
+ --vp-c-brand-3: var(--vp-c-indigo-3);
+ --vp-c-brand-soft: var(--vp-c-indigo-soft);
+
+ /* DEPRECATED: Use `--vp-c-brand-1` instead. */
+ --vp-c-brand: var(--vp-c-brand-1);
+
+ --vp-c-tip-1: var(--vp-c-brand-1);
+ --vp-c-tip-2: var(--vp-c-brand-2);
+ --vp-c-tip-3: var(--vp-c-brand-3);
+ --vp-c-tip-soft: var(--vp-c-brand-soft);
+
+ --vp-c-warning-1: var(--vp-c-yellow-1);
+ --vp-c-warning-2: var(--vp-c-yellow-2);
+ --vp-c-warning-3: var(--vp-c-yellow-3);
+ --vp-c-warning-soft: var(--vp-c-yellow-soft);
+
+ --vp-c-danger-1: var(--vp-c-red-1);
+ --vp-c-danger-2: var(--vp-c-red-2);
+ --vp-c-danger-3: var(--vp-c-red-3);
+ --vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
@@ -168,12 +256,12 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-z-index-local-nav: 10;
- --vp-z-index-nav: 20;
- --vp-z-index-layout-top: 30;
- --vp-z-index-backdrop: 40;
- --vp-z-index-sidebar: 50;
- --vp-z-index-footer: 60;
+ --vp-z-index-footer: 10;
+ --vp-z-index-local-nav: 20;
+ --vp-z-index-nav: 30;
+ --vp-z-index-layout-top: 40;
+ --vp-z-index-backdrop: 50;
+ --vp-z-index-sidebar: 60;
}
/**
@@ -208,62 +296,42 @@
:root {
--vp-code-line-height: 1.7;
--vp-code-font-size: 0.875em;
- --vp-c-code-dimm: var(--vp-c-text-dark-3);
-
- --vp-code-block-color: var(--vp-c-text-dark-1);
- --vp-code-block-bg: #292b30;
- --vp-code-block-bg-light: #1e1e20;
- --vp-code-block-divider-color: #000000;
-
- --vp-code-line-highlight-color: rgba(0, 0, 0, 0.5);
- --vp-code-line-number-color: var(--vp-c-code-dimm);
-
- --vp-code-line-diff-add-color: var(--vp-c-green-dimm-2);
- --vp-code-line-diff-add-symbol-color: var(--vp-c-green);
-
- --vp-code-line-diff-remove-color: var(--vp-c-red-dimm-2);
- --vp-code-line-diff-remove-symbol-color: var(--vp-c-red);
+ --vp-code-color: var(--vp-c-brand-1);
+ --vp-code-link-color: var(--vp-c-brand-1);
+ --vp-code-link-hover-color: var(--vp-c-brand-2);
+ --vp-code-bg: var(--vp-c-default-soft);
- --vp-code-line-warning-color: var(--vp-c-yellow-dimm-2);
- --vp-code-line-error-color: var(--vp-c-red-dimm-2);
+ --vp-code-block-color: var(--vp-c-text-2);
+ --vp-code-block-bg: var(--vp-c-bg-alt);
+ --vp-code-block-divider-color: var(--vp-c-gutter);
- --vp-code-copy-code-border-color: transparent;
- --vp-code-copy-code-bg: var(--vp-code-block-bg-light);
- --vp-code-copy-code-hover-border-color: var(--vp-c-divider);
- --vp-code-copy-code-hover-bg: var(--vp-code-block-bg-light);
- --vp-code-copy-code-active-text: var(--vp-c-text-dark-2);
+ --vp-code-lang-color: var(--vp-c-text-3);
- --vp-code-tab-divider: var(--vp-code-block-divider-color);
- --vp-code-tab-text-color: var(--vp-c-text-dark-2);
- --vp-code-tab-bg: var(--vp-code-block-bg);
- --vp-code-tab-hover-text-color: var(--vp-c-text-dark-1);
- --vp-code-tab-active-text-color: var(--vp-c-text-dark-1);
- --vp-code-tab-active-bar-color: var(--vp-c-brand);
-}
+ --vp-code-line-highlight-color: var(--vp-c-default-soft);
+ --vp-code-line-number-color: var(--vp-c-text-3);
-.dark {
- --vp-code-block-bg: #161618;
-}
+ --vp-code-line-diff-add-color: var(--vp-c-green-soft);
+ --vp-code-line-diff-add-symbol-color: var(--vp-c-green-1);
-:root:not(.dark) .vp-adaptive-theme {
- --vp-c-code-dimm: var(--vp-c-text-2);
+ --vp-code-line-diff-remove-color: var(--vp-c-red-soft);
+ --vp-code-line-diff-remove-symbol-color: var(--vp-c-red-1);
- --vp-code-block-color: var(--vp-c-text-1);
- --vp-code-block-bg: #f8f8f8;
- --vp-code-block-divider-color: var(--vp-c-divider);
+ --vp-code-line-warning-color: var(--vp-c-yellow-soft);
+ --vp-code-line-error-color: var(--vp-c-red-soft);
- --vp-code-line-highlight-color: #ececec;
- --vp-code-line-number-color: var(--vp-c-code-dimm);
-
- --vp-code-copy-code-bg: #e2e2e2;
- --vp-code-copy-code-hover-bg: #dcdcdc;
+ --vp-code-copy-code-border-color: var(--vp-c-divider);
+ --vp-code-copy-code-bg: var(--vp-c-bg-soft);
+ --vp-code-copy-code-hover-border-color: var(--vp-c-divider);
+ --vp-code-copy-code-hover-bg: var(--vp-c-bg);
--vp-code-copy-code-active-text: var(--vp-c-text-2);
+ --vp-code-copy-copied-text-content: 'Copied';
- --vp-code-tab-divider: var(--vp-c-divider);
+ --vp-code-tab-divider: var(--vp-code-block-divider-color);
--vp-code-tab-text-color: var(--vp-c-text-2);
--vp-code-tab-bg: var(--vp-code-block-bg);
--vp-code-tab-hover-text-color: var(--vp-c-text-1);
--vp-code-tab-active-text-color: var(--vp-c-text-1);
+ --vp-code-tab-active-bar-color: var(--vp-c-brand-1);
}
/**
@@ -271,28 +339,28 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-button-brand-border: var(--vp-c-brand-lighter);
+ --vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
- --vp-button-brand-bg: var(--vp-c-brand);
- --vp-button-brand-hover-border: var(--vp-c-brand-lighter);
+ --vp-button-brand-bg: var(--vp-c-brand-3);
+ --vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
- --vp-button-brand-hover-bg: var(--vp-c-brand-dark);
- --vp-button-brand-active-border: var(--vp-c-brand-lighter);
+ --vp-button-brand-hover-bg: var(--vp-c-brand-2);
+ --vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
- --vp-button-brand-active-bg: var(--vp-c-brand-darker);
-
- --vp-button-alt-border: var(--vp-c-border);
- --vp-button-alt-text: var(--vp-c-neutral);
- --vp-button-alt-bg: var(--vp-c-mute);
- --vp-button-alt-hover-border: var(--vp-c-border);
- --vp-button-alt-hover-text: var(--vp-c-neutral);
- --vp-button-alt-hover-bg: var(--vp-c-mute-dark);
- --vp-button-alt-active-border: var(--vp-c-border);
- --vp-button-alt-active-text: var(--vp-c-neutral);
- --vp-button-alt-active-bg: var(--vp-c-mute-darker);
-
- --vp-button-sponsor-border: var(--vp-c-gray-light-3);
- --vp-button-sponsor-text: var(--vp-c-text-light-2);
+ --vp-button-brand-active-bg: var(--vp-c-brand-1);
+
+ --vp-button-alt-border: transparent;
+ --vp-button-alt-text: var(--vp-c-text-1);
+ --vp-button-alt-bg: var(--vp-c-default-3);
+ --vp-button-alt-hover-border: transparent;
+ --vp-button-alt-hover-text: var(--vp-c-text-1);
+ --vp-button-alt-hover-bg: var(--vp-c-default-2);
+ --vp-button-alt-active-border: transparent;
+ --vp-button-alt-active-text: var(--vp-c-text-1);
+ --vp-button-alt-active-bg: var(--vp-c-default-1);
+
+ --vp-button-sponsor-border: var(--vp-c-text-2);
+ --vp-button-sponsor-text: var(--vp-c-text-2);
--vp-button-sponsor-bg: transparent;
--vp-button-sponsor-hover-border: var(--vp-c-sponsor);
--vp-button-sponsor-hover-text: var(--vp-c-sponsor);
@@ -302,11 +370,6 @@
--vp-button-sponsor-active-bg: transparent;
}
-.dark {
- --vp-button-sponsor-border: var(--vp-c-gray-dark-1);
- --vp-button-sponsor-text: var(--vp-c-text-dark-2);
-}
-
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
@@ -315,30 +378,30 @@
--vp-custom-block-font-size: 14px;
--vp-custom-block-code-font-size: 13px;
- --vp-custom-block-info-border: var(--vp-c-border);
- --vp-custom-block-info-text: var(--vp-c-text-2);
- --vp-custom-block-info-bg: var(--vp-c-bg-soft-up);
- --vp-custom-block-info-code-bg: var(--vp-c-bg-soft);
+ --vp-custom-block-info-border: transparent;
+ --vp-custom-block-info-text: var(--vp-c-text-1);
+ --vp-custom-block-info-bg: var(--vp-c-default-soft);
+ --vp-custom-block-info-code-bg: var(--vp-c-default-soft);
- --vp-custom-block-tip-border: var(--vp-c-green);
- --vp-custom-block-tip-text: var(--vp-c-green-dark);
- --vp-custom-block-tip-bg: var(--vp-c-bg-soft-up);
- --vp-custom-block-tip-code-bg: var(--vp-c-bg-soft);
+ --vp-custom-block-tip-border: transparent;
+ --vp-custom-block-tip-text: var(--vp-c-text-1);
+ --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
+ --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
- --vp-custom-block-warning-border: var(--vp-c-yellow);
- --vp-custom-block-warning-text: var(--vp-c-yellow);
- --vp-custom-block-warning-bg: var(--vp-c-bg-soft-up);
- --vp-custom-block-warning-code-bg: var(--vp-c-bg-soft);
+ --vp-custom-block-warning-border: transparent;
+ --vp-custom-block-warning-text: var(--vp-c-text-1);
+ --vp-custom-block-warning-bg: var(--vp-c-warning-soft);
+ --vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);
- --vp-custom-block-danger-border: var(--vp-c-red);
- --vp-custom-block-danger-text: var(--vp-c-red);
- --vp-custom-block-danger-bg: var(--vp-c-bg-soft-up);
- --vp-custom-block-danger-code-bg: var(--vp-c-bg-soft);
+ --vp-custom-block-danger-border: transparent;
+ --vp-custom-block-danger-text: var(--vp-c-text-1);
+ --vp-custom-block-danger-bg: var(--vp-c-danger-soft);
+ --vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);
--vp-custom-block-details-border: var(--vp-custom-block-info-border);
--vp-custom-block-details-text: var(--vp-custom-block-info-text);
--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);
- --vp-custom-block-details-code-bg: var(--vp-custom-block-details-bg);
+ --vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg);
}
/**
@@ -348,9 +411,8 @@
:root {
--vp-input-border-color: var(--vp-c-border);
--vp-input-bg-color: var(--vp-c-bg-alt);
- --vp-input-hover-border-color: var(--vp-c-gray);
- --vp-input-switch-bg-color: var(--vp-c-mute);
+ --vp-input-switch-bg-color: var(--vp-c-gray-soft);
}
/**
@@ -364,6 +426,14 @@
--vp-nav-logo-height: 24px;
}
+.hide-nav {
+ --vp-nav-height: 0px;
+}
+
+.hide-nav .VPSidebar {
+ --vp-nav-height: 22px;
+}
+
/**
* Component: Local Nav
* -------------------------------------------------------------------------- */
@@ -394,7 +464,7 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-home-hero-name-color: var(--vp-c-brand);
+ --vp-home-hero-name-color: var(--vp-c-brand-1);
--vp-home-hero-name-background: transparent;
--vp-home-hero-image-background-image: none;
@@ -406,21 +476,21 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-badge-info-border: var(--vp-c-border);
+ --vp-badge-info-border: transparent;
--vp-badge-info-text: var(--vp-c-text-2);
- --vp-badge-info-bg: var(--vp-c-bg-soft-up);
+ --vp-badge-info-bg: var(--vp-c-default-soft);
- --vp-badge-tip-border: var(--vp-c-green-dark);
- --vp-badge-tip-text: var(--vp-c-green);
- --vp-badge-tip-bg: var(--vp-c-green-dimm-1);
+ --vp-badge-tip-border: transparent;
+ --vp-badge-tip-text: var(--vp-c-brand-1);
+ --vp-badge-tip-bg: var(--vp-c-brand-soft);
- --vp-badge-warning-border: var(--vp-c-yellow-dark);
- --vp-badge-warning-text: var(--vp-c-yellow);
- --vp-badge-warning-bg: var(--vp-c-yellow-dimm-1);
+ --vp-badge-warning-border: transparent;
+ --vp-badge-warning-text: var(--vp-c-warning-1);
+ --vp-badge-warning-bg: var(--vp-c-warning-soft);
- --vp-badge-danger-border: var(--vp-c-red-dark);
- --vp-badge-danger-text: var(--vp-c-red);
- --vp-badge-danger-bg: var(--vp-c-red-dimm-1);
+ --vp-badge-danger-border: transparent;
+ --vp-badge-danger-text: var(--vp-c-danger-1);
+ --vp-badge-danger-bg: var(--vp-c-danger-soft);
}
/**
@@ -431,19 +501,20 @@
--vp-carbon-ads-text-color: var(--vp-c-text-1);
--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);
--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);
- --vp-carbon-ads-hover-text-color: var(--vp-c-brand);
+ --vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);
--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1);
}
/**
* Component: Local Search
* -------------------------------------------------------------------------- */
+
:root {
--vp-local-search-bg: var(--vp-c-bg);
--vp-local-search-result-bg: var(--vp-c-bg);
--vp-local-search-result-border: var(--vp-c-divider);
--vp-local-search-result-selected-bg: var(--vp-c-bg);
- --vp-local-search-result-selected-border: var(--vp-c-brand);
- --vp-local-search-highlight-bg: var(--vp-c-green-lighter);
- --vp-local-search-highlight-text: var(--vp-c-black);
+ --vp-local-search-result-selected-border: var(--vp-c-brand-1);
+ --vp-local-search-highlight-bg: var(--vp-c-brand-1);
+ --vp-local-search-highlight-text: var(--vp-c-neutral-inverse);
}
diff --git a/src/client/theme-default/support/sidebar.ts b/src/client/theme-default/support/sidebar.ts
index 5c2e646b..955eb23e 100644
--- a/src/client/theme-default/support/sidebar.ts
+++ b/src/client/theme-default/support/sidebar.ts
@@ -5,8 +5,11 @@ import { isActive } from '../../shared'
export interface SidebarLink {
text: string
link: string
+ docFooterText?: string
}
+type SidebarItem = DefaultTheme.SidebarItem
+
/**
* Get the `Sidebar` from sidebar option. This method will ensure to get correct
* sidebar config from `MultiSideBarConfig` with various path combinations such
@@ -14,20 +17,15 @@ export interface SidebarLink {
* return empty array.
*/
export function getSidebar(
- sidebar: DefaultTheme.Sidebar | undefined,
+ _sidebar: DefaultTheme.Sidebar | undefined,
path: string
-): DefaultTheme.SidebarItem[] {
- if (Array.isArray(sidebar)) {
- return sidebar
- }
-
- if (sidebar == null) {
- return []
- }
+): SidebarItem[] {
+ if (Array.isArray(_sidebar)) return addBase(_sidebar)
+ if (_sidebar == null) return []
path = ensureStartingSlash(path)
- const dir = Object.keys(sidebar)
+ const dir = Object.keys(_sidebar)
.sort((a, b) => {
return b.split('/').length - a.split('/').length
})
@@ -36,16 +34,17 @@ export function getSidebar(
return path.startsWith(ensureStartingSlash(dir))
})
- return dir ? sidebar[dir] : []
+ const sidebar = dir ? _sidebar[dir] : []
+ return Array.isArray(sidebar)
+ ? addBase(sidebar)
+ : addBase(sidebar.items, sidebar.base)
}
/**
* Get or generate sidebar group from the given sidebar items.
*/
-export function getSidebarGroups(
- sidebar: DefaultTheme.SidebarItem[]
-): DefaultTheme.SidebarItem[] {
- const groups: DefaultTheme.SidebarItem[] = []
+export function getSidebarGroups(sidebar: SidebarItem[]): SidebarItem[] {
+ const groups: SidebarItem[] = []
let lastGroupIndex: number = 0
@@ -67,15 +66,17 @@ export function getSidebarGroups(
return groups
}
-export function getFlatSideBarLinks(
- sidebar: DefaultTheme.SidebarItem[]
-): SidebarLink[] {
+export function getFlatSideBarLinks(sidebar: SidebarItem[]): SidebarLink[] {
const links: SidebarLink[] = []
- function recursivelyExtractLinks(items: DefaultTheme.SidebarItem[]) {
+ function recursivelyExtractLinks(items: SidebarItem[]) {
for (const item of items) {
if (item.text && item.link) {
- links.push({ text: item.text, link: item.link })
+ links.push({
+ text: item.text,
+ link: item.link,
+ docFooterText: item.docFooterText
+ })
}
if (item.items) {
@@ -94,7 +95,7 @@ export function getFlatSideBarLinks(
*/
export function hasActiveLink(
path: string,
- items: DefaultTheme.SidebarItem | DefaultTheme.SidebarItem[]
+ items: SidebarItem | SidebarItem[]
): boolean {
if (Array.isArray(items)) {
return items.some((item) => hasActiveLink(path, item))
@@ -106,3 +107,13 @@ export function hasActiveLink(
? hasActiveLink(path, items.items)
: false
}
+
+function addBase(items: SidebarItem[], _base?: string): SidebarItem[] {
+ return [...items].map((_item) => {
+ const item = { ..._item }
+ const base = item.base || _base
+ if (base && item.link) item.link = base + item.link
+ if (item.items) item.items = addBase(item.items, base)
+ return item
+ })
+}
diff --git a/src/client/theme-default/support/socialIcons.ts b/src/client/theme-default/support/socialIcons.ts
index 05d538d7..df427040 100644
--- a/src/client/theme-default/support/socialIcons.ts
+++ b/src/client/theme-default/support/socialIcons.ts
@@ -1,5 +1,4 @@
-// Used under CC0 1.0 from https://simpleicons.org/
-
+// used under CC0 1.0 from https://simpleicons.org/
export const icons = {
discord:
'',
@@ -16,7 +15,8 @@ export const icons = {
slack:
'',
twitter:
- '',
+ '',
+ x: '',
youtube:
''
} as const
diff --git a/src/client/theme-default/support/utils.ts b/src/client/theme-default/support/utils.ts
index e5fa8bca..1cd168cd 100644
--- a/src/client/theme-default/support/utils.ts
+++ b/src/client/theme-default/support/utils.ts
@@ -1,25 +1,18 @@
import { withBase } from 'vitepress'
import { useData } from '../composables/data'
-import { isExternal, PATHNAME_PROTOCOL_RE } from '../../shared'
+import { isExternal } from '../../shared'
export function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeoutId: NodeJS.Timeout
let called = false
return () => {
- if (timeoutId) {
- clearTimeout(timeoutId)
- }
+ if (timeoutId) clearTimeout(timeoutId)
if (!called) {
fn()
- called = true
- setTimeout(() => {
- called = false
- }, delay)
- } else {
- timeoutId = setTimeout(fn, delay)
- }
+ ;(called = true) && setTimeout(() => (called = false), delay)
+ } else timeoutId = setTimeout(fn, delay)
}
}
@@ -28,12 +21,17 @@ export function ensureStartingSlash(path: string): string {
}
export function normalizeLink(url: string): string {
- if (isExternal(url)) {
- return url.replace(PATHNAME_PROTOCOL_RE, '')
- }
+ const { pathname, search, hash, protocol } = new URL(url, 'http://a.com')
+
+ if (
+ isExternal(url) ||
+ url.startsWith('#') ||
+ !protocol.startsWith('http') ||
+ /\.(?!html|md)\w+($|\?)/i.test(url)
+ )
+ return url
const { site } = useData()
- const { pathname, search, hash } = new URL(url, 'http://a.com')
const normalizedPath =
pathname.endsWith('/') || pathname.endsWith('.html')
diff --git a/src/client/theme-default/without-fonts.ts b/src/client/theme-default/without-fonts.ts
index f1f1c9f4..f44d7205 100644
--- a/src/client/theme-default/without-fonts.ts
+++ b/src/client/theme-default/without-fonts.ts
@@ -14,10 +14,13 @@ import Layout from './Layout.vue'
// Note: if we add more optional components here, i.e. components that are not
// used in the theme by default unless the user imports them, make sure to update
// the `lazyDefaultThemeComponentsRE` regex in src/node/build/bundle.ts.
+export { default as VPImage } from './components/VPImage.vue'
+export { default as VPButton } from './components/VPButton.vue'
export { default as VPHomeHero } from './components/VPHomeHero.vue'
export { default as VPHomeFeatures } from './components/VPHomeFeatures.vue'
export { default as VPHomeSponsors } from './components/VPHomeSponsors.vue'
export { default as VPDocAsideSponsors } from './components/VPDocAsideSponsors.vue'
+export { default as VPSponsors } from './components/VPSponsors.vue'
export { default as VPTeamPage } from './components/VPTeamPage.vue'
export { default as VPTeamPageTitle } from './components/VPTeamPageTitle.vue'
export { default as VPTeamPageSection } from './components/VPTeamPageSection.vue'
diff --git a/src/node/build/build.ts b/src/node/build/build.ts
index fea62860..b5c50978 100644
--- a/src/node/build/build.ts
+++ b/src/node/build/build.ts
@@ -45,6 +45,10 @@ export async function build(
buildOptions
)
+ if (process.env.BUNDLE_ONLY) {
+ return
+ }
+
const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).toString())
@@ -176,7 +180,7 @@ function generateMetadataScript(
const metadataContent = `window.__VP_HASH_MAP__=JSON.parse(${hashMapString});${
siteDataString.includes('_vp-fn_')
- ? `${deserializeFunctions.toString()};window.__VP_SITE_DATA__=deserializeFunctions(JSON.parse(${siteDataString}));`
+ ? `${deserializeFunctions};window.__VP_SITE_DATA__=deserializeFunctions(JSON.parse(${siteDataString}));`
: `window.__VP_SITE_DATA__=JSON.parse(${siteDataString});`
}`
diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts
index c8e58142..d96cf75d 100644
--- a/src/node/build/bundle.ts
+++ b/src/node/build/bundle.ts
@@ -6,7 +6,7 @@ import {
normalizePath,
type BuildOptions,
type Rollup,
- type UserConfig as ViteUserConfig
+ type InlineConfig as ViteInlineConfig
} from 'vite'
import { APP_PATH } from '../alias'
import type { SiteConfig } from '../config'
@@ -17,7 +17,7 @@ import { buildMPAClient } from './buildMPAClient'
// A list of default theme components that should only be loaded on demand.
const lazyDefaultThemeComponentsRE =
- /VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|LocalSearchBox|AlgoliaSearchBox|CarbonAds|DocAsideCarbonAds)/
+ /VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|LocalSearchBox|AlgoliaSearchBox|CarbonAds|DocAsideCarbonAds|Sponsors)/
const clientDir = normalizePath(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client')
@@ -50,7 +50,9 @@ export async function bundle(
// resolve options to pass to vite
const { rollupOptions } = options
- const resolveViteConfig = async (ssr: boolean): Promise => ({
+ const resolveViteConfig = async (
+ ssr: boolean
+ ): Promise => ({
root: config.srcDir,
cacheDir: config.cacheDir,
base: config.site.base,
@@ -68,6 +70,7 @@ export async function bundle(
...options,
emptyOutDir: true,
ssr,
+ ssrEmitAssets: config.mpa,
// minify with esbuild in MPA mode (for CSS)
minify: ssr
? config.mpa
@@ -136,7 +139,8 @@ export async function bundle(
})
}
}
- }
+ },
+ configFile: config.vite?.configFile
})
let clientResult!: Rollup.RollupOutput | null
diff --git a/src/node/build/generateSitemap.ts b/src/node/build/generateSitemap.ts
index f94980b3..579141bf 100644
--- a/src/node/build/generateSitemap.ts
+++ b/src/node/build/generateSitemap.ts
@@ -17,11 +17,12 @@ export async function generateSitemap(siteConfig: SiteConfig) {
const getLastmod = async (url: string) => {
if (!siteConfig.lastUpdated) return undefined
- let path = url.replace(/(^|\/)$/, '$1index')
- path = path.replace(/(\.html)?$/, '.md')
- path = siteConfig.rewrites.inv[path] || path
+ let file = url.replace(/(^|\/)$/, '$1index')
+ file = file.replace(/(\.html)?$/, '.md')
+ file = siteConfig.rewrites.inv[file] || file
+ file = path.join(siteConfig.srcDir, file)
- return (await getGitTimestamp(path)) || undefined
+ return (await getGitTimestamp(file)) || undefined
}
await task('generating sitemap', async () => {
diff --git a/src/node/build/render.ts b/src/node/build/render.ts
index 4988449d..e5d8a324 100644
--- a/src/node/build/render.ts
+++ b/src/node/build/render.ts
@@ -205,7 +205,9 @@ function resolvePageImports(
// they start fetching as early as possible
let srcPath = path.resolve(config.srcDir, page)
try {
- srcPath = fs.realpathSync(srcPath)
+ if (!config.vite?.resolve?.preserveSymlinks) {
+ srcPath = fs.realpathSync(srcPath)
+ }
} catch (e) {
// if the page is a virtual page generated by a dynamic route this would
// fail, which is expected
diff --git a/src/node/cli.ts b/src/node/cli.ts
index c2bd9e4a..71eb4c0f 100644
--- a/src/node/cli.ts
+++ b/src/node/cli.ts
@@ -42,6 +42,9 @@ if (!command || command === 'dev') {
)
process.exit(1)
})
+} else if (command === 'init') {
+ createLogger().info('', { clear: true })
+ init()
} else {
logVersion()
if (command === 'build') {
@@ -56,8 +59,6 @@ if (!command || command === 'dev') {
)
process.exit(1)
})
- } else if (command === 'init') {
- init()
} else {
createLogger().error(c.red(`unknown command "${command}".`))
process.exit(1)
diff --git a/src/node/config.ts b/src/node/config.ts
index a6591adf..b2425c3e 100644
--- a/src/node/config.ts
+++ b/src/node/config.ts
@@ -138,7 +138,7 @@ export async function resolveConfig(
return config
}
-const supportedConfigExtensions = ['js', 'ts', 'cjs', 'mjs', 'cts', 'mts']
+const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts']
export async function resolveUserConfig(
root: string,
@@ -249,22 +249,31 @@ function resolveSiteDataHead(userConfig?: UserConfig): HeadConfig[] {
// if appearance mode set to light or dark, default to the defined mode
// in case the user didn't specify a preference - otherwise, default to auto
const fallbackPreference =
- userConfig?.appearance !== true ? userConfig?.appearance ?? '' : 'auto'
+ typeof userConfig?.appearance === 'string'
+ ? userConfig?.appearance
+ : typeof userConfig?.appearance === 'object'
+ ? userConfig.appearance.initialValue ?? 'auto'
+ : 'auto'
head.push([
'script',
- { id: 'check-dark-light' },
- `
- ;(() => {
- const preference = localStorage.getItem('${APPEARANCE_KEY}') || '${fallbackPreference}'
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
- if (!preference || preference === 'auto' ? prefersDark : preference === 'dark') {
- document.documentElement.classList.add('dark')
- }
- })()
- `
+ { id: 'check-dark-mode' },
+ fallbackPreference === 'force-dark'
+ ? `document.documentElement.classList.add('dark')`
+ : `;(() => {
+ const preference = localStorage.getItem('${APPEARANCE_KEY}') || '${fallbackPreference}'
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+ if (!preference || preference === 'auto' ? prefersDark : preference === 'dark')
+ document.documentElement.classList.add('dark')
+ })()`
])
}
+ head.push([
+ 'script',
+ { id: 'check-mac-os' },
+ `document.documentElement.classList.toggle('mac', /Mac|iPhone|iPod|iPad/i.test(navigator.platform))`
+ ])
+
return head
}
diff --git a/src/node/contentLoader.ts b/src/node/contentLoader.ts
index 4aa7b813..c213f794 100644
--- a/src/node/contentLoader.ts
+++ b/src/node/contentLoader.ts
@@ -4,7 +4,10 @@ import glob from 'fast-glob'
import type { SiteConfig } from './config'
import matter from 'gray-matter'
import { normalizePath } from 'vite'
-import { createMarkdownRenderer, type MarkdownRenderer } from './markdown'
+import {
+ createMarkdownRenderer,
+ type MarkdownRenderer
+} from './markdown/markdown'
export interface ContentOptions {
/**
@@ -49,6 +52,13 @@ export interface ContentOptions {
* bundle if imported from components or markdown files.
*/
transform?: (data: ContentData[]) => T | Promise
+
+ /**
+ * Options to pass to `fast-glob`.
+ * You'll need to manually specify `node_modules` and `dist` in
+ * `globOptions.ignore` if you've overridden it.
+ */
+ globOptions?: glob.Options
}
export interface ContentData {
@@ -72,7 +82,8 @@ export function createContentLoader(
includeSrc,
render,
excerpt: renderExcerpt,
- transform
+ transform,
+ globOptions
}: ContentOptions = {}
): {
watch: string | string[]
@@ -106,7 +117,8 @@ export function createContentLoader(
// the loader is being called directly, do a fresh glob
files = (
await glob(pattern, {
- ignore: ['**/node_modules/**', '**/dist/**']
+ ignore: ['**/node_modules/**', '**/dist/**'],
+ ...globOptions
})
).sort()
}
@@ -140,7 +152,7 @@ export function createContentLoader(
: { excerpt: renderExcerpt }
)
const url =
- config.site.base +
+ '/' +
normalizePath(path.relative(config.srcDir, file))
.replace(/(^|\/)index\.md$/, '$1')
.replace(/\.md$/, config.cleanUrls ? '' : '.html')
diff --git a/src/node/index.ts b/src/node/index.ts
index 35c3a912..90c0956a 100644
--- a/src/node/index.ts
+++ b/src/node/index.ts
@@ -1,17 +1,18 @@
-export * from './config'
-export * from './server'
-export * from './markdown'
+export { loadEnv, type Plugin } from 'vite'
export * from './build/build'
-export * from './serve/serve'
-export * from './init/init'
+export * from './config'
export * from './contentLoader'
+export * from './init/init'
+export * from './markdown/markdown'
export { defineLoader, type LoaderModule } from './plugins/staticDataPlugin'
-export { loadEnv } from 'vite'
+export * from './postcss/isolateStyles'
+export * from './serve/serve'
+export * from './server'
// shared types
export type {
- SiteData,
+ DefaultTheme,
HeadConfig,
Header,
- DefaultTheme
+ SiteData
} from '../../types/shared'
diff --git a/src/node/init/init.ts b/src/node/init/init.ts
index 792a3540..774e528b 100644
--- a/src/node/init/init.ts
+++ b/src/node/init/init.ts
@@ -9,9 +9,8 @@ import {
} from '@clack/prompts'
import fs from 'fs-extra'
import path from 'path'
-import { black, cyan, bgCyan, bold, yellow } from 'picocolors'
+import { cyan, bold, yellow } from 'picocolors'
import { fileURLToPath } from 'url'
-// @ts-ignore
import template from 'lodash.template'
export enum ScaffoldThemeType {
@@ -29,14 +28,22 @@ export interface ScaffoldOptions {
injectNpmScripts: boolean
}
+const getPackageManger = () => {
+ const name = process.env?.npm_config_user_agent || 'npm'
+ if (name === 'npm') {
+ return 'npm'
+ }
+ return name.split('/')[0]
+}
+
export async function init() {
- intro(bgCyan(bold(black(` Welcome to VitePress! `))))
+ intro(bold(cyan('Welcome to VitePress!')))
const options: ScaffoldOptions = await group(
{
root: () =>
text({
- message: `Where should VitePress initialize the config?`,
+ message: 'Where should VitePress initialize the config?',
initialValue: './',
validate(value) {
// TODO make sure directory is inside
@@ -45,13 +52,13 @@ export async function init() {
title: () =>
text({
- message: `Site title:`,
+ message: 'Site title:',
placeholder: 'My Awesome Project'
}),
description: () =>
text({
- message: `Site description:`,
+ message: 'Site description:',
placeholder: 'A VitePress Site'
}),
@@ -62,20 +69,20 @@ export async function init() {
{
// @ts-ignore
value: ScaffoldThemeType.Default,
- label: `Default Theme`,
- hint: `Out of the box, good-looking docs`
+ label: 'Default Theme',
+ hint: 'Out of the box, good-looking docs'
},
{
// @ts-ignore
value: ScaffoldThemeType.DefaultCustom,
- label: `Default Theme + Customization`,
- hint: `Add custom CSS and layout slots`
+ label: 'Default Theme + Customization',
+ hint: 'Add custom CSS and layout slots'
},
{
// @ts-ignore
value: ScaffoldThemeType.Custom,
- label: `Custom Theme`,
- hint: `Build your own or use external`
+ label: 'Custom Theme',
+ hint: 'Build your own or use external'
}
]
}),
@@ -85,7 +92,7 @@ export async function init() {
injectNpmScripts: () =>
confirm({
- message: `Add VitePress npm scripts to package.json?`
+ message: 'Add VitePress npm scripts to package.json?'
})
},
{
@@ -122,11 +129,21 @@ export function scaffold({
theme === ScaffoldThemeType.DefaultCustom
}
+ const pkgPath = path.resolve('package.json')
+ const userPkg = fs.existsSync(pkgPath)
+ ? JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
+ : {}
+
+ const useMjs = userPkg.type !== 'module'
+
const renderFile = (file: string) => {
const filePath = path.resolve(templateDir, file)
let targetPath = path.resolve(resolvedRoot, file)
+ if (useMjs && file === '.vitepress/config.js') {
+ targetPath = targetPath.replace(/\.js$/, '.mjs')
+ }
if (useTs) {
- targetPath = targetPath.replace(/\.js$/, '.ts')
+ targetPath = targetPath.replace(/\.(m?)js$/, '.$1ts')
}
const src = fs.readFileSync(filePath, 'utf-8')
const compiled = template(src)(data)
@@ -137,19 +154,19 @@ export function scaffold({
'index.md',
'api-examples.md',
'markdown-examples.md',
- `.vitepress/config.js`
+ '.vitepress/config.js'
]
if (theme === ScaffoldThemeType.DefaultCustom) {
filesToScaffold.push(
- `.vitepress/theme/index.js`,
- `.vitepress/theme/style.css`
+ '.vitepress/theme/index.js',
+ '.vitepress/theme/style.css'
)
} else if (theme === ScaffoldThemeType.Custom) {
filesToScaffold.push(
- `.vitepress/theme/index.js`,
- `.vitepress/theme/style.css`,
- `.vitepress/theme/Layout.vue`
+ '.vitepress/theme/index.js',
+ '.vitepress/theme/style.css',
+ '.vitepress/theme/Layout.vue'
)
}
@@ -158,18 +175,15 @@ export function scaffold({
}
const dir =
- root === './' ? `` : ` ${root.replace(/^\.\//, '').replace(/[/\\]$/, '')}`
-
- const pkgPath = path.resolve('package.json')
- const userPkg = fs.existsSync(pkgPath)
- ? JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
- : {}
+ root === './' ? '' : ` ${root.replace(/^\.\//, '').replace(/[/\\]$/, '')}`
+ const gitignorePrefix = dir ? `${dir}/.vitepress` : '.vitepress'
const tips = []
if (fs.existsSync('.git')) {
tips.push(
- `Make sure to add ${cyan(`.vitepress/dist`)} and ` +
- `${cyan(`.vitepress/cache`)} to your ${cyan(`.gitignore`)} file.`
+ `Make sure to add ${cyan(`${gitignorePrefix}/dist`)} and ` +
+ `${cyan(`${gitignorePrefix}/cache`)} to your ` +
+ `${cyan(`.gitignore`)} file.`
)
}
if (
@@ -193,10 +207,13 @@ export function scaffold({
}
Object.assign(userPkg.scripts || (userPkg.scripts = {}), scripts)
fs.writeFileSync(pkgPath, JSON.stringify(userPkg, null, 2))
- return `Done! Now run ${cyan(`npm run docs:dev`)} and start writing.${tip}`
+ return `Done! Now run ${cyan(
+ `${getPackageManger()} run docs:dev`
+ )} and start writing.${tip}`
} else {
+ const execCommand = getPackageManger() === 'bun' ? 'bunx' : 'npx'
return `You're all set! Now run ${cyan(
- `npx vitepress dev${dir}`
+ `${execCommand} vitepress dev${dir}`
)} and start writing.${tip}`
}
}
diff --git a/src/node/markdown/env.ts b/src/node/markdown/env.ts
deleted file mode 100644
index 7ed29a31..00000000
--- a/src/node/markdown/env.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { MarkdownSfcBlocks } from '@mdit-vue/plugin-sfc'
-import type { Header } from '../shared'
-
-// Manually declaring all properties as rollup-plugin-dts
-// is unable to merge augmented module declarations
-
-export interface MarkdownEnv {
- /**
- * The raw Markdown content without frontmatter
- */
- content?: string
- /**
- * The excerpt that extracted by `@mdit-vue/plugin-frontmatter`
- *
- * - Would be the rendered HTML when `renderExcerpt` is enabled
- * - Would be the raw Markdown when `renderExcerpt` is disabled
- */
- excerpt?: string
- /**
- * The frontmatter that extracted by `@mdit-vue/plugin-frontmatter`
- */
- frontmatter?: Record
- /**
- * The headers that extracted by `@mdit-vue/plugin-headers`
- */
- headers?: Header[]
- /**
- * SFC blocks that extracted by `@mdit-vue/plugin-sfc`
- */
- sfcBlocks?: MarkdownSfcBlocks
- /**
- * The title that extracted by `@mdit-vue/plugin-title`
- */
- title?: string
- path: string
- relativePath: string
- cleanUrls: boolean
- links?: string[]
- includes?: string[]
-}
diff --git a/src/node/markdown/index.ts b/src/node/markdown/index.ts
deleted file mode 100644
index 8d85b7bb..00000000
--- a/src/node/markdown/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './env'
-export * from './markdown'
diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts
index 22298c8e..3ec80b9c 100644
--- a/src/node/markdown/markdown.ts
+++ b/src/node/markdown/markdown.ts
@@ -1,4 +1,7 @@
-import { componentPlugin } from '@mdit-vue/plugin-component'
+import {
+ componentPlugin,
+ type ComponentPluginOptions
+} from '@mdit-vue/plugin-component'
import {
frontmatterPlugin,
type FrontmatterPluginOptions
@@ -17,7 +20,7 @@ import attrsPlugin from 'markdown-it-attrs'
import emojiPlugin from 'markdown-it-emoji'
import type { ILanguageRegistration, IThemeRegistration } from 'shiki'
import type { Logger } from 'vite'
-import { containerPlugin } from './plugins/containers'
+import { containerPlugin, type ContainerOptions } from './plugins/containers'
import { highlight } from './plugins/highlight'
import { highlightLinePlugin } from './plugins/highlightLines'
import { imagePlugin } from './plugins/image'
@@ -34,12 +37,13 @@ export type ThemeOptions =
export interface MarkdownOptions extends MarkdownIt.Options {
lineNumbers?: boolean
+ preConfig?: (md: MarkdownIt) => void
config?: (md: MarkdownIt) => void
anchor?: anchorPlugin.AnchorOptions
attrs?: {
leftDelimiter?: string
rightDelimiter?: string
- allowedAttributes?: string[]
+ allowedAttributes?: Array
disable?: boolean
}
defaultHighlightLang?: string
@@ -51,6 +55,9 @@ export interface MarkdownOptions extends MarkdownIt.Options {
toc?: TocPluginOptions
externalLinks?: Record
cache?: boolean
+ component?: ComponentPluginOptions
+ math?: boolean | any
+ container?: ContainerOptions
}
export type MarkdownRenderer = MarkdownIt
@@ -61,7 +68,7 @@ export const createMarkdownRenderer = async (
base = '/',
logger: Pick = console
): Promise => {
- const theme = options.theme ?? 'material-theme-palenight'
+ const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
const md = MarkdownIt({
@@ -76,16 +83,20 @@ export const createMarkdownRenderer = async (
logger
)),
...options
- }) as MarkdownRenderer
+ })
md.linkify.set({ fuzzyLink: false })
+ if (options.preConfig) {
+ options.preConfig(md)
+ }
+
// custom plugins
- md.use(componentPlugin)
+ md.use(componentPlugin, { ...options.component })
.use(highlightLinePlugin)
.use(preWrapperPlugin, { hasSingleTheme })
.use(snippetPlugin, srcDir)
- .use(containerPlugin, { hasSingleTheme })
+ .use(containerPlugin, { hasSingleTheme }, options.container)
.use(imagePlugin)
.use(
linkPlugin,
@@ -140,6 +151,19 @@ export const createMarkdownRenderer = async (
...options.toc
} as TocPluginOptions)
+ if (options.math) {
+ try {
+ const mathPlugin = await import('markdown-it-mathjax3')
+ md.use(mathPlugin.default ?? mathPlugin, {
+ ...(typeof options.math === 'boolean' ? {} : options.math)
+ })
+ } catch (error) {
+ throw new Error(
+ 'You need to install `markdown-it-mathjax3` to use math support.'
+ )
+ }
+ }
+
// apply user config
if (options.config) {
options.config(md)
diff --git a/src/node/markdown/math.ts b/src/node/markdown/math.ts
new file mode 100644
index 00000000..268a9353
--- /dev/null
+++ b/src/node/markdown/math.ts
@@ -0,0 +1,89 @@
+export const mathjaxElements = [
+ 'mjx-container',
+ 'mjx-assistive-mml',
+ 'math',
+ 'maction',
+ 'maligngroup',
+ 'malignmark',
+ 'menclose',
+ 'merror',
+ 'mfenced',
+ 'mfrac',
+ 'mi',
+ 'mlongdiv',
+ 'mmultiscripts',
+ 'mn',
+ 'mo',
+ 'mover',
+ 'mpadded',
+ 'mphantom',
+ 'mroot',
+ 'mrow',
+ 'ms',
+ 'mscarries',
+ 'mscarry',
+ 'mscarries',
+ 'msgroup',
+ 'mstack',
+ 'mlongdiv',
+ 'msline',
+ 'mstack',
+ 'mspace',
+ 'msqrt',
+ 'msrow',
+ 'mstack',
+ 'mstack',
+ 'mstyle',
+ 'msub',
+ 'msup',
+ 'msubsup',
+ 'mtable',
+ 'mtd',
+ 'mtext',
+ 'mtr',
+ 'munder',
+ 'munderover',
+ 'semantics',
+ 'math',
+ 'mi',
+ 'mn',
+ 'mo',
+ 'ms',
+ 'mspace',
+ 'mtext',
+ 'menclose',
+ 'merror',
+ 'mfenced',
+ 'mfrac',
+ 'mpadded',
+ 'mphantom',
+ 'mroot',
+ 'mrow',
+ 'msqrt',
+ 'mstyle',
+ 'mmultiscripts',
+ 'mover',
+ 'mprescripts',
+ 'msub',
+ 'msubsup',
+ 'msup',
+ 'munder',
+ 'munderover',
+ 'none',
+ 'maligngroup',
+ 'malignmark',
+ 'mtable',
+ 'mtd',
+ 'mtr',
+ 'mlongdiv',
+ 'mscarries',
+ 'mscarry',
+ 'msgroup',
+ 'msline',
+ 'msrow',
+ 'mstack',
+ 'maction',
+ 'semantics',
+ 'annotation',
+ 'annotation-xml'
+]
diff --git a/src/node/markdown/plugins/containers.ts b/src/node/markdown/plugins/containers.ts
index b9397581..2a70e217 100644
--- a/src/node/markdown/plugins/containers.ts
+++ b/src/node/markdown/plugins/containers.ts
@@ -9,12 +9,34 @@ import {
type Options
} from './preWrapper'
-export const containerPlugin = (md: MarkdownIt, options: Options) => {
- md.use(...createContainer('tip', 'TIP', md))
- .use(...createContainer('info', 'INFO', md))
- .use(...createContainer('warning', 'WARNING', md))
- .use(...createContainer('danger', 'DANGER', md))
- .use(...createContainer('details', 'Details', md))
+export const containerPlugin = (
+ md: MarkdownIt,
+ options: Options,
+ containerOptions?: ContainerOptions
+) => {
+ md.use(...createContainer('tip', containerOptions?.tipLabel || 'TIP', md))
+ .use(...createContainer('info', containerOptions?.infoLabel || 'INFO', md))
+ .use(
+ ...createContainer(
+ 'warning',
+ containerOptions?.warningLabel || 'WARNING',
+ md
+ )
+ )
+ .use(
+ ...createContainer(
+ 'danger',
+ containerOptions?.dangerLabel || 'DANGER',
+ md
+ )
+ )
+ .use(
+ ...createContainer(
+ 'details',
+ containerOptions?.detailsLabel || 'Details',
+ md
+ )
+ )
// explicitly escape Vue syntax
.use(container, 'v-pre', {
render: (tokens: Token[], idx: number) =>
@@ -38,18 +60,18 @@ function createContainer(
container,
klass,
{
- render(tokens, idx) {
+ render(tokens, idx, _options, env) {
const token = tokens[idx]
const info = token.info.trim().slice(klass.length).trim()
+ const attrs = md.renderer.renderAttrs(token)
if (token.nesting === 1) {
- const title = md.renderInline(info || defaultTitle)
- if (klass === 'details') {
- return `${title}
\n`
- }
- return `${title}
\n`
- } else {
- return klass === 'details' ? `\n` : `\n`
- }
+ const title = md.renderInline(info || defaultTitle, {
+ references: env.references
+ })
+ if (klass === 'details')
+ return `${title}
\n`
+ return `${title}
\n`
+ } else return klass === 'details' ? `\n` : `\n`
}
}
]
@@ -104,3 +126,11 @@ function createCodeGroup(options: Options): ContainerArgs {
}
]
}
+
+export interface ContainerOptions {
+ infoLabel?: string
+ tipLabel?: string
+ warningLabel?: string
+ dangerLabel?: string
+ detailsLabel?: string
+}
diff --git a/src/node/markdown/plugins/highlight.ts b/src/node/markdown/plugins/highlight.ts
index fa44ae11..52463286 100644
--- a/src/node/markdown/plugins/highlight.ts
+++ b/src/node/markdown/plugins/highlight.ts
@@ -87,17 +87,22 @@ export async function highlight(
const styleRE = /]*(style=".*?")/
const preRE = /^/
const vueRE = /-vue$/
- const lineNoRE = /:(no-)?line-numbers$/
+ const lineNoStartRE = /=(\d*)/
+ const lineNoRE = /:(no-)?line-numbers(=\d*)?$/
const mustacheRE = /\{\{.*?\}\}/g
return (str: string, lang: string, attrs: string) => {
const vPre = vueRE.test(lang) ? '' : 'v-pre'
lang =
- lang.replace(lineNoRE, '').replace(vueRE, '').toLowerCase() || defaultLang
+ lang
+ .replace(lineNoStartRE, '')
+ .replace(lineNoRE, '')
+ .replace(vueRE, '')
+ .toLowerCase() || defaultLang
if (lang) {
const langLoaded = highlighter.getLoadedLanguages().includes(lang as any)
- if (!langLoaded && lang !== 'ansi' && lang !== 'txt') {
+ if (!langLoaded && !['ansi', 'plaintext', 'txt', 'text'].includes(lang)) {
logger.warn(
c.yellow(
`\nThe language '${lang}' is not loaded, falling back to '${
@@ -148,7 +153,7 @@ export async function highlight(
)
}
- str = removeMustache(str).trim()
+ str = removeMustache(str).trimEnd()
const codeToHtml = (theme: IThemeRegistration) => {
const res =
diff --git a/src/node/markdown/plugins/lineNumbers.ts b/src/node/markdown/plugins/lineNumbers.ts
index ac4f3ece..15fff082 100644
--- a/src/node/markdown/plugins/lineNumbers.ts
+++ b/src/node/markdown/plugins/lineNumbers.ts
@@ -12,12 +12,18 @@ export const lineNumberPlugin = (md: MarkdownIt, enable = false) => {
const info = tokens[idx].info
if (
- (!enable && !/:line-numbers($| )/.test(info)) ||
+ (!enable && !/:line-numbers($| |=)/.test(info)) ||
(enable && /:no-line-numbers($| )/.test(info))
) {
return rawCode
}
+ let startLineNumber = 1
+ const matchStartLineNumber = info.match(/=(\d*)/)
+ if (matchStartLineNumber && matchStartLineNumber[1]) {
+ startLineNumber = parseInt(matchStartLineNumber[1])
+ }
+
const code = rawCode.slice(
rawCode.indexOf(''),
rawCode.indexOf('')
@@ -26,7 +32,10 @@ export const lineNumberPlugin = (md: MarkdownIt, enable = false) => {
const lines = code.split('\n')
const lineNumbersCode = [...Array(lines.length)]
- .map((_, index) => `${index + 1}
`)
+ .map(
+ (_, index) =>
+ `${index + startLineNumber}
`
+ )
.join('')
const lineNumbersWrapperCode = ``
diff --git a/src/node/markdown/plugins/link.ts b/src/node/markdown/plugins/link.ts
index d742f678..841cd4cd 100644
--- a/src/node/markdown/plugins/link.ts
+++ b/src/node/markdown/plugins/link.ts
@@ -3,9 +3,8 @@
// 2. normalize internal links to end with `.html`
import type MarkdownIt from 'markdown-it'
-import type { MarkdownEnv } from '../env'
import { URL } from 'url'
-import { EXTERNAL_URL_RE, PATHNAME_PROTOCOL_RE, isExternal } from '../../shared'
+import { EXTERNAL_URL_RE, isExternal, type MarkdownEnv } from '../../shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i
@@ -34,13 +33,13 @@ export const linkPlugin = (
if (url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost:')) {
pushLink(url, env)
}
- hrefAttr[1] = url.replace(PATHNAME_PROTOCOL_RE, '')
+ hrefAttr[1] = url
} else {
if (
// internal anchor links
!url.startsWith('#') &&
- // mail links
- !url.startsWith('mailto:') &&
+ // mail/custom protocol links
+ new URL(url, 'http://a.com').protocol.startsWith('http') &&
// links to files (other than html/md)
!/\.(?!html|md)\w+($|\?)/i.test(url)
) {
diff --git a/src/node/markdown/plugins/preWrapper.ts b/src/node/markdown/plugins/preWrapper.ts
index db237dbf..9e79a4e1 100644
--- a/src/node/markdown/plugins/preWrapper.ts
+++ b/src/node/markdown/plugins/preWrapper.ts
@@ -40,7 +40,9 @@ export function extractTitle(info: string, html = false) {
function extractLang(info: string) {
return info
.trim()
- .replace(/:(no-)?line-numbers({| |$).*/, '')
+ .replace(/=(\d*)/, '')
+ .replace(/:(no-)?line-numbers({| |$|=\d*).*/, '')
.replace(/(-vue|{| ).*$/, '')
.replace(/^vue-html$/, 'template')
+ .replace(/^ansi$/, '')
}
diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts
index b558f35b..d1a9e8a7 100644
--- a/src/node/markdown/plugins/snippet.ts
+++ b/src/node/markdown/plugins/snippet.ts
@@ -1,8 +1,37 @@
import fs from 'fs-extra'
-import path from 'path'
import type MarkdownIt from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block'
-import type { MarkdownEnv } from '../env'
+import path from 'path'
+import type { MarkdownEnv } from '../../shared'
+
+/**
+ * raw path format: "/path/to/file.extension#region {meta} [title]"
+ * where #region, {meta} and [title] are optional
+ * meta can be like '1,2,4-6 lang', 'lang' or '1,2,4-6'
+ * lang can contain special characters like C++, C#, F#, etc.
+ * path can be relative to the current file or absolute
+ * file extension is optional
+ * path can contain spaces and dots
+ *
+ * captures: ['/path/to/file.extension', 'extension', '#region', '{meta}', '[title]']
+ */
+export const rawPathRegexp =
+ /^(.+?(?:(?:\.([a-z0-9]+))?))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)?}))? ?(?:\[(.+)\])?$/
+
+export function rawPathToToken(rawPath: string) {
+ const [
+ filepath = '',
+ extension = '',
+ region = '',
+ lines = '',
+ lang = '',
+ rawTitle = ''
+ ] = (rawPathRegexp.exec(rawPath) || []).slice(1)
+
+ const title = rawTitle || filepath.split('/').pop() || ''
+
+ return { filepath, extension, region, lines, lang, title }
+}
export function dedent(text: string): string {
const lines = text.split('\n')
@@ -91,32 +120,14 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
const start = pos + 3
const end = state.skipSpacesBack(max, pos)
- /**
- * raw path format: "/path/to/file.extension#region {meta}"
- * where #region and {meta} are optional
- * and meta can be like '1,2,4-6 lang', 'lang' or '1,2,4-6'
- *
- * captures: ['/path/to/file.extension', 'extension', '#region', '{meta}', '[title]']
- */
- const rawPathRegexp =
- /^(.+(?:\.([a-z0-9]+)))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)?}))? ?(?:\[(.+)\])?$/
-
const rawPath = state.src
.slice(start, end)
.trim()
.replace(/^@/, srcDir)
.trim()
- const [
- filepath = '',
- extension = '',
- region = '',
- lines = '',
- lang = '',
- rawTitle = ''
- ] = (rawPathRegexp.exec(rawPath) || []).slice(1)
-
- const title = rawTitle || filepath.split('/').pop() || ''
+ const { filepath, extension, region, lines, lang, title } =
+ rawPathToToken(rawPath)
state.line = startLine + 1
@@ -125,10 +136,9 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
title ? `[${title}]` : ''
}`
- const resolvedPath = path.resolve(
- path.dirname((state.env as MarkdownEnv).path),
- filepath
- )
+ const { realPath, path: _path } = state.env as MarkdownEnv
+ const resolvedPath = path.resolve(path.dirname(realPath ?? _path), filepath)
+
// @ts-ignore
token.src = [resolvedPath, region.slice(1)]
token.markup = '```'
@@ -151,7 +161,7 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
includes.push(src)
}
- const isAFile = fs.lstatSync(src).isFile()
+ const isAFile = fs.statSync(src).isFile()
if (!fs.existsSync(src) || !isAFile) {
token.content = isAFile
? `Code snippet path not found: ${src}`
diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts
index 7559cb11..0648f456 100644
--- a/src/node/markdownToVue.ts
+++ b/src/node/markdownToVue.ts
@@ -6,22 +6,21 @@ import path from 'path'
import type { SiteConfig } from './config'
import {
createMarkdownRenderer,
- type MarkdownEnv,
type MarkdownOptions,
type MarkdownRenderer
-} from './markdown'
+} from './markdown/markdown'
import {
EXTERNAL_URL_RE,
slash,
type HeadConfig,
+ type MarkdownEnv,
type PageData
} from './shared'
import { getGitTimestamp } from './utils/getGitTimestamp'
+import { processIncludes } from './utils/processIncludes'
const debug = _debug('vitepress:md')
const cache = new LRUCache({ max: 1024 })
-const includesRE = //g
-const rangeRE = /\{(\d*),(\d*)\}$/
export interface MarkdownCompileResult {
vueSrc: string
@@ -30,8 +29,14 @@ export interface MarkdownCompileResult {
includes: string[]
}
-export function clearCache() {
- cache.clear()
+export function clearCache(file?: string) {
+ if (!file) {
+ cache.clear()
+ return
+ }
+
+ file = JSON.stringify({ file }).slice(1)
+ cache.find((_, key) => key.endsWith(file!) && cache.delete(key))
}
export async function createMarkdownToVueRenderFn(
@@ -65,7 +70,7 @@ export async function createMarkdownToVueRenderFn(
siteConfig?.rewrites.map[file.slice(srcDir.length + 1)]
file = alias ? path.join(srcDir, alias) : file
const relativePath = slash(path.relative(srcDir, file))
- const cacheKey = JSON.stringify({ src, file })
+ const cacheKey = JSON.stringify({ src, file: fileOrig })
if (isBuild || options.cache !== false) {
const cached = cache.get(cacheKey)
@@ -89,46 +94,15 @@ export async function createMarkdownToVueRenderFn(
// resolve includes
let includes: string[] = []
-
- function processIncludes(src: string, file: string): string {
- return src.replace(includesRE, (m: string, m1: string) => {
- if (!m1.length) return m
-
- const range = m1.match(rangeRE)
- range && (m1 = m1.slice(0, -range[0].length))
- const atPresent = m1[0] === '@'
- try {
- const includePath = atPresent
- ? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1))
- : path.join(path.dirname(file), m1)
- let content = fs.readFileSync(includePath, 'utf-8')
- if (range) {
- const [, startLine, endLine] = range
- const lines = content.split(/\r?\n/)
- content = lines
- .slice(
- startLine ? parseInt(startLine, 10) - 1 : undefined,
- endLine ? parseInt(endLine, 10) : undefined
- )
- .join('\n')
- }
- includes.push(slash(includePath))
- // recursively process includes in the content
- return processIncludes(content, includePath)
- } catch (error) {
- return m // silently ignore error if file is not present
- }
- })
- }
-
- src = processIncludes(src, fileOrig)
+ src = processIncludes(srcDir, src, fileOrig, includes)
// reset env before render
const env: MarkdownEnv = {
path: file,
relativePath,
cleanUrls,
- includes
+ includes,
+ realPath: fileOrig
}
const html = md.render(src, env)
const {
diff --git a/src/node/plugin.ts b/src/node/plugin.ts
index 0aa75334..6de976ec 100644
--- a/src/node/plugin.ts
+++ b/src/node/plugin.ts
@@ -3,6 +3,7 @@ import c from 'picocolors'
import {
mergeConfig,
searchForWorkspaceRoot,
+ type ModuleNode,
type Plugin,
type ResolvedConfig,
type Rollup,
@@ -14,19 +15,20 @@ import {
SITE_DATA_REQUEST_PATH,
resolveAliases
} from './alias'
-import { resolveUserConfig, resolvePages, type SiteConfig } from './config'
+import { resolvePages, resolveUserConfig, type SiteConfig } from './config'
+import { mathjaxElements } from './markdown/math'
import {
clearCache,
createMarkdownToVueRenderFn,
type MarkdownCompileResult
} from './markdownToVue'
-import { slash, type PageDataPayload } from './shared'
-import { staticDataPlugin } from './plugins/staticDataPlugin'
-import { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
-import { rewritesPlugin } from './plugins/rewritesPlugin'
import { localSearchPlugin } from './plugins/localSearchPlugin'
-import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize'
+import { rewritesPlugin } from './plugins/rewritesPlugin'
+import { staticDataPlugin } from './plugins/staticDataPlugin'
+import { webFontsPlugin } from './plugins/webFontsPlugin'
+import { slash, type PageDataPayload } from './shared'
+import { deserializeFunctions, serializeFunctions } from './utils/fnSerialize'
declare module 'vite' {
interface UserConfig {
@@ -34,7 +36,8 @@ declare module 'vite' {
}
}
-const hashRE = /\.(\w+)\.js$/
+const themeRE = /\/\.vitepress\/theme\/index\.(m|c)?(j|t)s$/
+const hashRE = /\.([-\w]+)\.js$/
const staticInjectMarkerRE =
/\b(const _hoisted_\d+ = \/\*(?:#|@)__PURE__\*\/\s*createStaticVNode)\("(.*)", (\d+)\)/g
const staticStripRE = /['"`]__VP_STATIC_START__[^]*?__VP_STATIC_END__['"`]/g
@@ -79,12 +82,31 @@ export async function createVitePressPlugin(
} = siteConfig
let markdownToVue: Awaited>
+ const userCustomElementChecker =
+ userVuePluginOptions?.template?.compilerOptions?.isCustomElement
+ let isCustomElement = userCustomElementChecker
+
+ if (markdown?.math) {
+ isCustomElement = (tag) => {
+ if (mathjaxElements.includes(tag)) {
+ return true
+ }
+ return userCustomElementChecker?.(tag) ?? false
+ }
+ }
// lazy require plugin-vue to respect NODE_ENV in @vue/compiler-x
const vuePlugin = await import('@vitejs/plugin-vue').then((r) =>
r.default({
include: [/\.vue$/, /\.md$/],
- ...userVuePluginOptions
+ ...userVuePluginOptions,
+ template: {
+ ...userVuePluginOptions?.template,
+ compilerOptions: {
+ ...userVuePluginOptions?.template?.compilerOptions,
+ isCustomElement
+ }
+ }
})
)
@@ -100,6 +122,7 @@ export async function createVitePressPlugin(
let siteData = site
let allDeadLinks: MarkdownCompileResult['deadLinks'] = []
let config: ResolvedConfig
+ let importerMap: Record | undefined> = {}
const vitePressPlugin: Plugin = {
name: 'vitepress',
@@ -172,8 +195,7 @@ export async function createVitePressPlugin(
}
}
data = serializeFunctions(data)
- return `${deserializeFunctions.toString()}
- export default deserializeFunctions(JSON.parse(${JSON.stringify(
+ return `${deserializeFunctions};export default deserializeFunctions(JSON.parse(${JSON.stringify(
JSON.stringify(data)
)}))`
}
@@ -192,6 +214,7 @@ export async function createVitePressPlugin(
allDeadLinks.push(...deadLinks)
if (includes.length) {
includes.forEach((i) => {
+ ;(importerMap[slash(i)] ??= new Set()).add(id)
this.addWatchFile(i)
})
}
@@ -225,16 +248,37 @@ export async function createVitePressPlugin(
configDeps.forEach((file) => server.watcher.add(file))
}
- // update pages, dynamicRoutes and rewrites on md file add / deletion
- const onFileAddDelete = async (file: string) => {
+ const onFileAddDelete = async (added: boolean, _file: string) => {
+ const file = slash(_file)
+ // restart server on theme file creation / deletion
+ if (themeRE.test(file)) {
+ siteConfig.logger.info(
+ c.green(
+ `${path.relative(process.cwd(), _file)} ${
+ added ? 'created' : 'deleted'
+ }, restarting server...\n`
+ ),
+ { clear: true, timestamp: true }
+ )
+
+ await recreateServer?.()
+ }
+
+ // update pages, dynamicRoutes and rewrites on md file creation / deletion
if (file.endsWith('.md')) {
Object.assign(
siteConfig,
await resolvePages(siteConfig.srcDir, siteConfig.userConfig)
)
}
+
+ if (!added && importerMap[file]) {
+ delete importerMap[file]
+ }
}
- server.watcher.on('add', onFileAddDelete).on('unlink', onFileAddDelete)
+ server.watcher
+ .on('add', onFileAddDelete.bind(null, true))
+ .on('unlink', onFileAddDelete.bind(null, false))
// serve our index.html after vite history fallback
return () => {
@@ -284,15 +328,8 @@ export async function createVitePressPlugin(
generateBundle(_options, bundle) {
if (ssr) {
- // ssr build:
- // delete all asset chunks
- for (const name in bundle) {
- if (bundle[name].type === 'asset') {
- delete bundle[name]
- }
- }
-
- if (config.ssr?.format === 'esm') {
+ // @ts-ignore will be removed in vite 5
+ if (config.ssr?.format !== 'cjs') {
this.emitFile({
type: 'asset',
fileName: 'package.json',
@@ -374,10 +411,27 @@ export async function createVitePressPlugin(
}
}
+ const hmrFix: Plugin = {
+ name: 'vitepress:hmr-fix',
+ async handleHotUpdate({ file, server, modules }) {
+ const importers = [...(importerMap[slash(file)] || [])]
+ if (importers.length > 0) {
+ return [
+ ...modules,
+ ...importers.map((id) => {
+ clearCache(id)
+ return server.moduleGraph.getModuleById(id)
+ })
+ ].filter(Boolean) as ModuleNode[]
+ }
+ }
+ }
+
return [
vitePressPlugin,
rewritesPlugin(siteConfig),
vuePlugin,
+ hmrFix,
webFontsPlugin(siteConfig.useWebFonts),
...(userViteConfig?.plugins || []),
await localSearchPlugin(siteConfig),
diff --git a/src/node/plugins/dynamicRoutesPlugin.ts b/src/node/plugins/dynamicRoutesPlugin.ts
index 5003af23..2807d3ff 100644
--- a/src/node/plugins/dynamicRoutesPlugin.ts
+++ b/src/node/plugins/dynamicRoutesPlugin.ts
@@ -7,7 +7,7 @@ import {
import fs from 'fs-extra'
import c from 'picocolors'
import path from 'path'
-import fg from 'fast-glob'
+import glob from 'fast-glob'
import { type SiteConfig, type UserConfig } from '../siteConfig'
import { resolveRewrites } from './rewritesPlugin'
@@ -21,9 +21,13 @@ export async function resolvePages(srcDir: string, userConfig: UserConfig) {
// JavaScript built-in sort() is mandated to be stable as of ES2019 and
// supported in Node 12+, which is required by Vite.
const allMarkdownFiles = (
- await fg(['**.md'], {
+ await glob(['**.md'], {
cwd: srcDir,
- ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
+ ignore: [
+ '**/node_modules/**',
+ '**/dist/**',
+ ...(userConfig.srcExclude || [])
+ ]
})
).sort()
@@ -158,19 +162,21 @@ export async function resolveDynamicRoutes(
for (const route of routes) {
// locate corresponding route paths file
const fullPath = normalizePath(path.resolve(srcDir, route))
- const jsPathsFile = fullPath.replace(/\.md$/, '.paths.js')
- let pathsFile = jsPathsFile
- if (!fs.existsSync(jsPathsFile)) {
- pathsFile = fullPath.replace(/\.md$/, '.paths.ts')
- if (!fs.existsSync(pathsFile)) {
- console.warn(
- c.yellow(
- `Missing paths file for dynamic route ${route}: ` +
- `a corresponding ${jsPathsFile} or ${pathsFile} is needed.`
- )
+
+ const paths = ['js', 'ts', 'mjs', 'mts'].map((ext) =>
+ fullPath.replace(/\.md$/, `.paths.${ext}`)
+ )
+
+ const pathsFile = paths.find((p) => fs.existsSync(p))
+
+ if (pathsFile == null) {
+ console.warn(
+ c.yellow(
+ `Missing paths file for dynamic route ${route}: ` +
+ `a corresponding ${paths[0]} (or .ts/.mjs/.mts) file is needed.`
)
- continue
- }
+ )
+ continue
}
// load the paths loader module
diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts
index e6cabcb4..dc7ca8da 100644
--- a/src/node/plugins/localSearchPlugin.ts
+++ b/src/node/plugins/localSearchPlugin.ts
@@ -4,8 +4,14 @@ import MiniSearch from 'minisearch'
import path from 'path'
import type { Plugin, ViteDevServer } from 'vite'
import type { SiteConfig } from '../config'
-import { createMarkdownRenderer, type MarkdownEnv } from '../markdown'
-import { resolveSiteDataByRoute, slash, type DefaultTheme } from '../shared'
+import { createMarkdownRenderer } from '../markdown/markdown'
+import {
+ resolveSiteDataByRoute,
+ slash,
+ type DefaultTheme,
+ type MarkdownEnv
+} from '../shared'
+import { processIncludes } from '../utils/processIncludes'
const debug = _debug('vitepress:local-search')
@@ -45,23 +51,17 @@ export async function localSearchPlugin(
siteConfig.logger
)
+ const options = siteConfig.site.themeConfig.search.options || {}
+
function render(file: string) {
- const { srcDir, cleanUrls = false, site } = siteConfig
+ const { srcDir, cleanUrls = false } = siteConfig
const relativePath = slash(path.relative(srcDir, file))
- const env: MarkdownEnv = {
- path: file,
- relativePath,
- cleanUrls
- }
- const html = md.render(fs.readFileSync(file, 'utf-8'), env)
- if (
- env.frontmatter?.search === false ||
- (site.themeConfig.search?.provider === 'local' &&
- site.themeConfig.search.options?.exclude?.(relativePath))
- ) {
- return ''
- }
- return html
+ const env: MarkdownEnv = { path: file, relativePath, cleanUrls }
+ let src = fs.readFileSync(file, 'utf-8')
+ src = processIncludes(srcDir, src, file, [])
+ if (options._render) return options._render(src, env, md)
+ const html = md.render(src, env)
+ return env.frontmatter?.search === false ? '' : html
}
const indexByLocales = new Map>()
@@ -72,8 +72,7 @@ export async function localSearchPlugin(
index = new MiniSearch({
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
- ...(siteConfig.site.themeConfig?.search?.provider === 'local' &&
- siteConfig.site.themeConfig.search.options?.miniSearch?.options)
+ ...options.miniSearch?.options
})
indexByLocales.set(locale, index)
}
diff --git a/src/node/plugins/rewritesPlugin.ts b/src/node/plugins/rewritesPlugin.ts
index 2388a70a..51794a1d 100644
--- a/src/node/plugins/rewritesPlugin.ts
+++ b/src/node/plugins/rewritesPlugin.ts
@@ -7,7 +7,7 @@ export function resolveRewrites(
userRewrites: UserConfig['rewrites']
) {
const rewriteRules = Object.entries(userRewrites || {}).map(([from, to]) => ({
- toPath: compile(to, { validate: false }),
+ toPath: compile(`/${to}`, { validate: false }),
matchUrl: match(from.startsWith('^') ? new RegExp(from) : from)
}))
@@ -18,7 +18,7 @@ export function resolveRewrites(
for (const { matchUrl, toPath } of rewriteRules) {
const res = matchUrl(page)
if (res) {
- const dest = toPath(res.params)
+ const dest = toPath(res.params).slice(1)
pageToRewrite[page] = dest
rewriteToPage[dest] = page
break
diff --git a/src/node/plugins/staticDataPlugin.ts b/src/node/plugins/staticDataPlugin.ts
index efa6fe47..283a5138 100644
--- a/src/node/plugins/staticDataPlugin.ts
+++ b/src/node/plugins/staticDataPlugin.ts
@@ -8,7 +8,7 @@ import path, { dirname, resolve } from 'path'
import { isMatch } from 'micromatch'
import glob from 'fast-glob'
-const loaderMatch = /\.data\.(j|t)s($|\?)/
+const loaderMatch = /\.data\.m?(j|t)s($|\?)/
let server: ViteDevServer
diff --git a/src/node/postcss/isolateStyles.ts b/src/node/postcss/isolateStyles.ts
new file mode 100644
index 00000000..dadd5cbd
--- /dev/null
+++ b/src/node/postcss/isolateStyles.ts
@@ -0,0 +1,15 @@
+import postcssPrefixSelector from 'postcss-prefix-selector'
+
+export function postcssIsolateStyles(
+ options: Parameters[0] = {}
+): ReturnType {
+ return postcssPrefixSelector({
+ prefix: ':not(:where(.vp-raw, .vp-raw *))',
+ includeFiles: [/base\.css/],
+ transform(prefix, _selector) {
+ const [selector, pseudo = ''] = _selector.split(/(:\S*)$/)
+ return selector + prefix + pseudo
+ },
+ ...options
+ })
+}
diff --git a/src/node/server.ts b/src/node/server.ts
index ebf0aedf..4105edfe 100644
--- a/src/node/server.ts
+++ b/src/node/server.ts
@@ -1,4 +1,3 @@
-import dns from 'dns'
import { createServer as createViteServer, type ServerOptions } from 'vite'
import { resolveConfig } from './config'
import { createVitePressPlugin } from './plugin'
@@ -15,14 +14,13 @@ export async function createServer(
delete serverOptions.base
}
- dns.setDefaultResultOrder('verbatim')
-
return createViteServer({
root: config.srcDir,
base: config.site.base,
cacheDir: config.cacheDir,
plugins: await createVitePressPlugin(config, false, {}, {}, recreateServer),
server: serverOptions,
- customLogger: config.logger
+ customLogger: config.logger,
+ configFile: config.vite?.configFile
})
}
diff --git a/src/node/shortcuts.ts b/src/node/shortcuts.ts
index 11f88565..84aa03ec 100644
--- a/src/node/shortcuts.ts
+++ b/src/node/shortcuts.ts
@@ -68,6 +68,7 @@ export function bindShortcuts(
server.httpServer.on('close', () => {
process.stdin.off('data', onInput).pause()
+ process.stdin.setRawMode(false)
})
}
diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts
index ae6d976f..10cd3967 100644
--- a/src/node/siteConfig.ts
+++ b/src/node/siteConfig.ts
@@ -1,8 +1,9 @@
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
+import type { UseDarkOptions } from '@vueuse/core'
import type { SitemapStreamOptions } from 'sitemap'
import type { Logger, UserConfig as ViteConfig } from 'vite'
import type { SitemapItem } from './build/generateSitemap'
-import type { MarkdownOptions } from './markdown'
+import type { MarkdownOptions } from './markdown/markdown'
import type {
Awaitable,
HeadConfig,
@@ -68,7 +69,11 @@ export interface UserConfig
locales?: LocaleConfig
- appearance?: boolean | 'dark'
+ appearance?:
+ | boolean
+ | 'dark'
+ | 'force-dark'
+ | (Omit & { initialValue?: 'dark' })
lastUpdated?: boolean
contentProps?: Record
@@ -83,7 +88,7 @@ export interface UserConfig
/**
* Vite config
*/
- vite?: ViteConfig
+ vite?: ViteConfig & { configFile?: string | false }
/**
* Configure the scroll offset when the theme has a sticky header.
@@ -93,7 +98,11 @@ export interface UserConfig
* selector if a selector fails to match, or the matched element is not
* currently visible in viewport.
*/
- scrollOffset?: number | string | string[]
+ scrollOffset?:
+ | number
+ | string
+ | string[]
+ | { selector: string | string[]; padding: number }
/**
* Enable MPA / zero-JS mode.
diff --git a/src/node/tsconfig.json b/src/node/tsconfig.json
index 26db4671..44906f44 100644
--- a/src/node/tsconfig.json
+++ b/src/node/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
- "target": "es2020",
+ "target": "es2022",
"baseUrl": ".",
"outDir": "../../dist/node",
"module": "esnext",
diff --git a/src/node/utils/fnSerialize.ts b/src/node/utils/fnSerialize.ts
index f3022eec..242ca90d 100644
--- a/src/node/utils/fnSerialize.ts
+++ b/src/node/utils/fnSerialize.ts
@@ -3,6 +3,7 @@ export function serializeFunctions(value: any, key?: string): any {
return value.map((v) => serializeFunctions(v))
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc, key) => {
+ if (key[0] === '_') return acc
acc[key] = serializeFunctions(value[key], key)
return acc
}, {} as any)
@@ -20,6 +21,7 @@ export function serializeFunctions(value: any, key?: string): any {
}
}
+/*
export function deserializeFunctions(value: any): any {
if (Array.isArray(value)) {
return value.map(deserializeFunctions)
@@ -34,3 +36,7 @@ export function deserializeFunctions(value: any): any {
return value
}
}
+*/
+
+export const deserializeFunctions =
+ 'function deserializeFunctions(r){return Array.isArray(r)?r.map(deserializeFunctions):typeof r=="object"&&r!==null?Object.keys(r).reduce((t,n)=>(t[n]=deserializeFunctions(r[n]),t),{}):typeof r=="string"&&r.startsWith("_vp-fn_")?new Function(`return ${r.slice(7)}`)():r}'
diff --git a/src/node/utils/processIncludes.ts b/src/node/utils/processIncludes.ts
new file mode 100644
index 00000000..43808ba4
--- /dev/null
+++ b/src/node/utils/processIncludes.ts
@@ -0,0 +1,41 @@
+import path from 'path'
+import fs from 'fs-extra'
+import { slash } from '../shared'
+
+export function processIncludes(
+ srcDir: string,
+ src: string,
+ file: string,
+ includes: string[]
+): string {
+ const includesRE = //g
+ const rangeRE = /\{(\d*),(\d*)\}$/
+ return src.replace(includesRE, (m: string, m1: string) => {
+ if (!m1.length) return m
+
+ const range = m1.match(rangeRE)
+ range && (m1 = m1.slice(0, -range[0].length))
+ const atPresent = m1[0] === '@'
+ try {
+ const includePath = atPresent
+ ? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1))
+ : path.join(path.dirname(file), m1)
+ let content = fs.readFileSync(includePath, 'utf-8')
+ if (range) {
+ const [, startLine, endLine] = range
+ const lines = content.split(/\r?\n/)
+ content = lines
+ .slice(
+ startLine ? parseInt(startLine, 10) - 1 : undefined,
+ endLine ? parseInt(endLine, 10) : undefined
+ )
+ .join('\n')
+ }
+ includes.push(slash(includePath))
+ // recursively process includes in the content
+ return processIncludes(srcDir, content, includePath, includes)
+ } catch (error) {
+ return m // silently ignore error if file is not present
+ }
+ })
+}
diff --git a/src/shared/shared.ts b/src/shared/shared.ts
index b110b3a1..9bc2de00 100644
--- a/src/shared/shared.ts
+++ b/src/shared/shared.ts
@@ -7,14 +7,14 @@ export type {
Header,
LocaleConfig,
LocaleSpecificConfig,
+ MarkdownEnv,
PageData,
PageDataPayload,
- SiteData,
- SSGContext
+ SSGContext,
+ SiteData
} from '../../types/shared'
export const EXTERNAL_URL_RE = /^[a-z]+:/i
-export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//
export const APPEARANCE_KEY = 'vitepress-theme-appearance'
export const HASH_RE = /#.*$/
export const EXT_RE = /(index)?\.(md|html)$/
diff --git a/template/.vitepress/theme/index.js b/template/.vitepress/theme/index.js
index 8ea44ecb..3e14542a 100644
--- a/template/.vitepress/theme/index.js
+++ b/template/.vitepress/theme/index.js
@@ -1,25 +1,29 @@
// https://vitepress.dev/guide/custom-theme
-<% if (!defaultTheme) { %>import Layout from './Layout.vue'
+<% if (!defaultTheme) { %>import Layout from './Layout.vue'<% if (useTs) { %>
+import type { Theme } from 'vitepress'<% } %>
import './style.css'
-export default {
+<% if (!useTs) { %>/** @type {import('vitepress').Theme} */
+<% } %>export default {
Layout,
enhanceApp({ app, router, siteData }) {
// ...
}
-}
-<% } else { %>import { h } from 'vue'
-import Theme from 'vitepress/theme'
+}<% if (useTs) { %> satisfies Theme<% } %>
+<% } else { %>import { h } from 'vue'<% if (useTs) { %>
+import type { Theme } from 'vitepress'<% } %>
+import DefaultTheme from 'vitepress/theme'
import './style.css'
-export default {
- extends: Theme,
+<% if (!useTs) { %>/** @type {import('vitepress').Theme} */
+<% } %>export default {
+ extends: DefaultTheme,
Layout: () => {
- return h(Theme.Layout, null, {
+ return h(DefaultTheme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
},
enhanceApp({ app, router, siteData }) {
// ...
}
-}<% } %>
+}<% if (useTs) { %> satisfies Theme<% } %><% } %>
diff --git a/template/.vitepress/theme/style.css b/template/.vitepress/theme/style.css
index 84c26fa6..2a518312 100644
--- a/template/.vitepress/theme/style.css
+++ b/template/.vitepress/theme/style.css
@@ -5,16 +5,69 @@
/**
* Colors
+ *
+ * Each colors have exact same color scale system with 3 levels of solid
+ * colors with different brightness, and 1 soft color.
+ *
+ * - `XXX-1`: The most solid color used mainly for colored text. It must
+ * satisfy the contrast ratio against when used on top of `XXX-soft`.
+ *
+ * - `XXX-2`: The color used mainly for hover state of the button.
+ *
+ * - `XXX-3`: The color for solid background, such as bg color of the button.
+ * It must satisfy the contrast ratio with pure white (#ffffff) text on
+ * top of it.
+ *
+ * - `XXX-soft`: The color used for subtle background such as custom container
+ * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
+ * on top of it.
+ *
+ * The soft color must be semi transparent alpha channel. This is crucial
+ * because it allows adding multiple "soft" colors on top of each other
+ * to create a accent, such as when having inline code block inside
+ * custom containers.
+ *
+ * - `default`: The color used purely for subtle indication without any
+ * special meanings attched to it such as bg color for menu hover state.
+ *
+ * - `brand`: Used for primary brand colors, such as link text, button with
+ * brand theme, etc.
+ *
+ * - `tip`: Used to indicate useful information. The default theme uses the
+ * brand color for this by default.
+ *
+ * - `warning`: Used to indicate warning to the users. Used in custom
+ * container, badges, etc.
+ *
+ * - `danger`: Used to show error, or dangerous message to the users. Used
+ * in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root {
- --vp-c-brand: #646cff;
- --vp-c-brand-light: #747bff;
- --vp-c-brand-lighter: #9499ff;
- --vp-c-brand-lightest: #bcc0ff;
- --vp-c-brand-dark: #535bf2;
- --vp-c-brand-darker: #454ce1;
- --vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
+ --vp-c-default-1: var(--vp-c-gray-1);
+ --vp-c-default-2: var(--vp-c-gray-2);
+ --vp-c-default-3: var(--vp-c-gray-3);
+ --vp-c-default-soft: var(--vp-c-gray-soft);
+
+ --vp-c-brand-1: var(--vp-c-indigo-1);
+ --vp-c-brand-2: var(--vp-c-indigo-2);
+ --vp-c-brand-3: var(--vp-c-indigo-3);
+ --vp-c-brand-soft: var(--vp-c-indigo-soft);
+
+ --vp-c-tip-1: var(--vp-c-brand-1);
+ --vp-c-tip-2: var(--vp-c-brand-2);
+ --vp-c-tip-3: var(--vp-c-brand-3);
+ --vp-c-tip-soft: var(--vp-c-brand-soft);
+
+ --vp-c-warning-1: var(--vp-c-yellow-1);
+ --vp-c-warning-2: var(--vp-c-yellow-2);
+ --vp-c-warning-3: var(--vp-c-yellow-3);
+ --vp-c-warning-soft: var(--vp-c-yellow-soft);
+
+ --vp-c-danger-1: var(--vp-c-red-1);
+ --vp-c-danger-2: var(--vp-c-red-2);
+ --vp-c-danger-3: var(--vp-c-red-3);
+ --vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
@@ -22,15 +75,15 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-button-brand-border: var(--vp-c-brand-light);
+ --vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
- --vp-button-brand-bg: var(--vp-c-brand);
- --vp-button-brand-hover-border: var(--vp-c-brand-light);
+ --vp-button-brand-bg: var(--vp-c-brand-3);
+ --vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
- --vp-button-brand-hover-bg: var(--vp-c-brand-light);
- --vp-button-brand-active-border: var(--vp-c-brand-light);
+ --vp-button-brand-hover-bg: var(--vp-c-brand-2);
+ --vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
- --vp-button-brand-active-bg: var(--vp-button-brand-bg);
+ --vp-button-brand-active-bg: var(--vp-c-brand-1);
}
/**
@@ -50,7 +103,7 @@
#bd34fe 50%,
#47caff 50%
);
- --vp-home-hero-image-filter: blur(40px);
+ --vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
@@ -61,7 +114,7 @@
@media (min-width: 960px) {
:root {
- --vp-home-hero-image-filter: blur(72px);
+ --vp-home-hero-image-filter: blur(68px);
}
}
@@ -70,15 +123,10 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-custom-block-tip-border: var(--vp-c-brand);
- --vp-custom-block-tip-text: var(--vp-c-brand-darker);
- --vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
-}
-
-.dark {
- --vp-custom-block-tip-border: var(--vp-c-brand);
- --vp-custom-block-tip-text: var(--vp-c-brand-lightest);
- --vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
+ --vp-custom-block-tip-border: transparent;
+ --vp-custom-block-tip-text: var(--vp-c-text-1);
+ --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
+ --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
@@ -86,7 +134,7 @@
* -------------------------------------------------------------------------- */
.DocSearch {
- --docsearch-primary-color: var(--vp-c-brand) !important;
+ --docsearch-primary-color: var(--vp-c-brand-1) !important;
}
<% } else { %>
html {
diff --git a/theme-without-fonts.d.ts b/theme-without-fonts.d.ts
index 7a71fe6d..531dd12e 100644
--- a/theme-without-fonts.d.ts
+++ b/theme-without-fonts.d.ts
@@ -1,2 +1,2 @@
-export * from './theme'
-export { default } from './theme'
+export * from './theme.js'
+export { default } from './theme.js'
diff --git a/theme.d.ts b/theme.d.ts
index 4b1076b5..446a9d65 100644
--- a/theme.d.ts
+++ b/theme.d.ts
@@ -1,16 +1,10 @@
// so that users can do `import DefaultTheme from 'vitepress/theme'`
+
import type { DefineComponent } from 'vue'
-import { EnhanceAppContext } from './dist/client/index.js'
+import type { EnhanceAppContext } from './dist/client/index.js'
+import type { DefaultTheme } from './types/default-theme.js'
-// TODO: add props for these
-export const VPHomeHero: DefineComponent
-export const VPHomeFeatures: DefineComponent
-export const VPHomeSponsors: DefineComponent
-export const VPDocAsideSponsors: DefineComponent
-export const VPTeamPage: DefineComponent
-export const VPTeamPageTitle: DefineComponent
-export const VPTeamPageSection: DefineComponent
-export const VPTeamMembers: DefineComponent
+export type { DefaultTheme } from './types/default-theme.js'
declare const theme: {
Layout: DefineComponent
@@ -18,6 +12,17 @@ declare const theme: {
}
export default theme
-export type { DefaultTheme } from './types/default-theme.js'
+export declare const useSidebar: () => DefaultTheme.DocSidebar
-export const useSidebar: () => DefaultTheme.DocSidebar
+// TODO: add props for these
+export declare const VPButton: DefineComponent
+export declare const VPDocAsideSponsors: DefineComponent
+export declare const VPHomeFeatures: DefineComponent
+export declare const VPHomeHero: DefineComponent
+export declare const VPHomeSponsors: DefineComponent
+export declare const VPImage: DefineComponent
+export declare const VPSponsors: DefineComponent
+export declare const VPTeamMembers: DefineComponent
+export declare const VPTeamPage: DefineComponent
+export declare const VPTeamPageSection: DefineComponent
+export declare const VPTeamPageTitle: DefineComponent
diff --git a/types/default-theme.d.ts b/types/default-theme.d.ts
index 8e350a33..94dfb95c 100644
--- a/types/default-theme.d.ts
+++ b/types/default-theme.d.ts
@@ -1,8 +1,9 @@
+import type MarkdownIt from 'markdown-it'
import type { Options as MiniSearchOptions } from 'minisearch'
import type { ComputedRef, Ref } from 'vue'
import type { DocSearchProps } from './docsearch.js'
import type { LocalSearchTranslations } from './local-search.js'
-import type { PageData } from './shared.js'
+import type { MarkdownEnv, PageData } from './shared.js'
export namespace DefaultTheme {
export interface Config {
@@ -13,6 +14,11 @@ export namespace DefaultTheme {
*/
logo?: ThemeableImage
+ /**
+ * Overrides the link of the site logo.
+ */
+ logoLink?: string
+
/**
* Custom site title in navbar. If the value is undefined,
* `config.title` will be used.
@@ -148,6 +154,7 @@ export namespace DefaultTheme {
export interface NavItemWithLink {
text: string
link: string
+ items?: never
/**
* `activeMatch` is expected to be a regex string. We can't use actual
@@ -178,18 +185,25 @@ export namespace DefaultTheme {
export type ThemeableImage =
| string
- | { src: string; alt?: string }
- | { light: string; dark: string; alt?: string }
+ | { src: string; alt?: string; [prop: string]: any }
+ | { light: string; dark: string; alt?: string; [prop: string]: any }
export type FeatureIcon =
| string
- | { src: string; alt?: string; width?: string; height: string }
+ | {
+ src: string
+ alt?: string
+ width?: string
+ height?: string
+ wrap?: boolean
+ }
| {
light: string
dark: string
alt?: string
width?: string
- height: string
+ height?: string
+ wrap?: boolean
}
// sidebar -------------------------------------------------------------------
@@ -197,7 +211,7 @@ export namespace DefaultTheme {
export type Sidebar = SidebarItem[] | SidebarMulti
export interface SidebarMulti {
- [path: string]: SidebarItem[]
+ [path: string]: SidebarItem[] | { items: SidebarItem[]; base: string }
}
export type SidebarItem = {
@@ -224,6 +238,19 @@ export namespace DefaultTheme {
* If `false`, group is collapsible but expanded by default
*/
collapsed?: boolean
+
+ /**
+ * Base path for the children items.
+ */
+ base?: string
+
+ /**
+ * Customize text that appears on the footer of previous/next page.
+ */
+ docFooterText?: string
+
+ rel?: string
+ target?: string
}
/**
@@ -296,6 +323,7 @@ export namespace DefaultTheme {
| 'mastodon'
| 'slack'
| 'twitter'
+ | 'x'
| 'youtube'
| { svg: string }
@@ -367,15 +395,16 @@ export namespace DefaultTheme {
}
/**
- * exclude content from search results
+ * Allows transformation of content before indexing (node only)
+ * Return empty string to skip indexing
*/
- exclude?: (relativePath: string) => boolean
+ _render?: (src: string, env: MarkdownEnv, md: MarkdownIt) => string
}
// algolia -------------------------------------------------------------------
/**
- * The Algolia search options. Partially copied from
+ * Algolia search options. Partially copied from
* `@docsearch/react/dist/esm/DocSearch.d.ts`
*/
export interface AlgoliaSearchOptions extends DocSearchProps {
@@ -406,7 +435,7 @@ export namespace DefaultTheme {
* @default
* { dateStyle: 'short', timeStyle: 'short' }
*/
- formatOptions?: Intl.DateTimeFormatOptions
+ formatOptions?: Intl.DateTimeFormatOptions & { forceLocale?: boolean }
}
// not found -----------------------------------------------------------------
diff --git a/types/docsearch.d.ts b/types/docsearch.d.ts
index 4f0b5588..76542015 100644
--- a/types/docsearch.d.ts
+++ b/types/docsearch.d.ts
@@ -6,6 +6,7 @@ export interface DocSearchProps {
searchParameters?: SearchOptions
disableUserPersonalization?: boolean
initialQuery?: string
+ insights?: boolean
translations?: DocSearchTranslations
}
diff --git a/types/shared.d.ts b/types/shared.d.ts
index 3cb48065..99a9b57a 100644
--- a/types/shared.d.ts
+++ b/types/shared.d.ts
@@ -1,4 +1,5 @@
// types shared between server and client
+import type { UseDarkOptions } from '@vueuse/core'
import type { SSRContext } from 'vue/server-renderer'
export type { DefaultTheme } from './default-theme.js'
@@ -17,6 +18,62 @@ export interface PageData {
lastUpdated?: number
}
+/**
+ * SFC block extracted from markdown
+ */
+export interface SfcBlock {
+ /**
+ * The type of the block
+ */
+ type: string
+ /**
+ * The content, including open-tag and close-tag
+ */
+ content: string
+ /**
+ * The content that stripped open-tag and close-tag off
+ */
+ contentStripped: string
+ /**
+ * The open-tag
+ */
+ tagOpen: string
+ /**
+ * The close-tag
+ */
+ tagClose: string
+}
+
+export interface MarkdownSfcBlocks {
+ /**
+ * The `` block
+ */
+ template: SfcBlock | null
+ /**
+ * The common `
${title}
\n` - } - return `${title}
\n` - } else { - return klass === 'details' ? `\n` : `${title}
\n` + return `${title}
\n` + } else return klass === 'details' ? `\n` : `]*(style=".*?")/
const preRE = /^/
const vueRE = /-vue$/
- const lineNoRE = /:(no-)?line-numbers$/
+ const lineNoStartRE = /=(\d*)/
+ const lineNoRE = /:(no-)?line-numbers(=\d*)?$/
const mustacheRE = /\{\{.*?\}\}/g
return (str: string, lang: string, attrs: string) => {
const vPre = vueRE.test(lang) ? '' : 'v-pre'
lang =
- lang.replace(lineNoRE, '').replace(vueRE, '').toLowerCase() || defaultLang
+ lang
+ .replace(lineNoStartRE, '')
+ .replace(lineNoRE, '')
+ .replace(vueRE, '')
+ .toLowerCase() || defaultLang
if (lang) {
const langLoaded = highlighter.getLoadedLanguages().includes(lang as any)
- if (!langLoaded && lang !== 'ansi' && lang !== 'txt') {
+ if (!langLoaded && !['ansi', 'plaintext', 'txt', 'text'].includes(lang)) {
logger.warn(
c.yellow(
`\nThe language '${lang}' is not loaded, falling back to '${
@@ -148,7 +153,7 @@ export async function highlight(
)
}
- str = removeMustache(str).trim()
+ str = removeMustache(str).trimEnd()
const codeToHtml = (theme: IThemeRegistration) => {
const res =
diff --git a/src/node/markdown/plugins/lineNumbers.ts b/src/node/markdown/plugins/lineNumbers.ts
index ac4f3ece..15fff082 100644
--- a/src/node/markdown/plugins/lineNumbers.ts
+++ b/src/node/markdown/plugins/lineNumbers.ts
@@ -12,12 +12,18 @@ export const lineNumberPlugin = (md: MarkdownIt, enable = false) => {
const info = tokens[idx].info
if (
- (!enable && !/:line-numbers($| )/.test(info)) ||
+ (!enable && !/:line-numbers($| |=)/.test(info)) ||
(enable && /:no-line-numbers($| )/.test(info))
) {
return rawCode
}
+ let startLineNumber = 1
+ const matchStartLineNumber = info.match(/=(\d*)/)
+ if (matchStartLineNumber && matchStartLineNumber[1]) {
+ startLineNumber = parseInt(matchStartLineNumber[1])
+ }
+
const code = rawCode.slice(
rawCode.indexOf(''),
rawCode.indexOf('')
@@ -26,7 +32,10 @@ export const lineNumberPlugin = (md: MarkdownIt, enable = false) => {
const lines = code.split('\n')
const lineNumbersCode = [...Array(lines.length)]
- .map((_, index) => `${index + 1}
`)
+ .map(
+ (_, index) =>
+ `${index + startLineNumber}
`
+ )
.join('')
const lineNumbersWrapperCode = ``
diff --git a/src/node/markdown/plugins/link.ts b/src/node/markdown/plugins/link.ts
index d742f678..841cd4cd 100644
--- a/src/node/markdown/plugins/link.ts
+++ b/src/node/markdown/plugins/link.ts
@@ -3,9 +3,8 @@
// 2. normalize internal links to end with `.html`
import type MarkdownIt from 'markdown-it'
-import type { MarkdownEnv } from '../env'
import { URL } from 'url'
-import { EXTERNAL_URL_RE, PATHNAME_PROTOCOL_RE, isExternal } from '../../shared'
+import { EXTERNAL_URL_RE, isExternal, type MarkdownEnv } from '../../shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i
@@ -34,13 +33,13 @@ export const linkPlugin = (
if (url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost:')) {
pushLink(url, env)
}
- hrefAttr[1] = url.replace(PATHNAME_PROTOCOL_RE, '')
+ hrefAttr[1] = url
} else {
if (
// internal anchor links
!url.startsWith('#') &&
- // mail links
- !url.startsWith('mailto:') &&
+ // mail/custom protocol links
+ new URL(url, 'http://a.com').protocol.startsWith('http') &&
// links to files (other than html/md)
!/\.(?!html|md)\w+($|\?)/i.test(url)
) {
diff --git a/src/node/markdown/plugins/preWrapper.ts b/src/node/markdown/plugins/preWrapper.ts
index db237dbf..9e79a4e1 100644
--- a/src/node/markdown/plugins/preWrapper.ts
+++ b/src/node/markdown/plugins/preWrapper.ts
@@ -40,7 +40,9 @@ export function extractTitle(info: string, html = false) {
function extractLang(info: string) {
return info
.trim()
- .replace(/:(no-)?line-numbers({| |$).*/, '')
+ .replace(/=(\d*)/, '')
+ .replace(/:(no-)?line-numbers({| |$|=\d*).*/, '')
.replace(/(-vue|{| ).*$/, '')
.replace(/^vue-html$/, 'template')
+ .replace(/^ansi$/, '')
}
diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts
index b558f35b..d1a9e8a7 100644
--- a/src/node/markdown/plugins/snippet.ts
+++ b/src/node/markdown/plugins/snippet.ts
@@ -1,8 +1,37 @@
import fs from 'fs-extra'
-import path from 'path'
import type MarkdownIt from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block'
-import type { MarkdownEnv } from '../env'
+import path from 'path'
+import type { MarkdownEnv } from '../../shared'
+
+/**
+ * raw path format: "/path/to/file.extension#region {meta} [title]"
+ * where #region, {meta} and [title] are optional
+ * meta can be like '1,2,4-6 lang', 'lang' or '1,2,4-6'
+ * lang can contain special characters like C++, C#, F#, etc.
+ * path can be relative to the current file or absolute
+ * file extension is optional
+ * path can contain spaces and dots
+ *
+ * captures: ['/path/to/file.extension', 'extension', '#region', '{meta}', '[title]']
+ */
+export const rawPathRegexp =
+ /^(.+?(?:(?:\.([a-z0-9]+))?))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)?}))? ?(?:\[(.+)\])?$/
+
+export function rawPathToToken(rawPath: string) {
+ const [
+ filepath = '',
+ extension = '',
+ region = '',
+ lines = '',
+ lang = '',
+ rawTitle = ''
+ ] = (rawPathRegexp.exec(rawPath) || []).slice(1)
+
+ const title = rawTitle || filepath.split('/').pop() || ''
+
+ return { filepath, extension, region, lines, lang, title }
+}
export function dedent(text: string): string {
const lines = text.split('\n')
@@ -91,32 +120,14 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
const start = pos + 3
const end = state.skipSpacesBack(max, pos)
- /**
- * raw path format: "/path/to/file.extension#region {meta}"
- * where #region and {meta} are optional
- * and meta can be like '1,2,4-6 lang', 'lang' or '1,2,4-6'
- *
- * captures: ['/path/to/file.extension', 'extension', '#region', '{meta}', '[title]']
- */
- const rawPathRegexp =
- /^(.+(?:\.([a-z0-9]+)))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)?}))? ?(?:\[(.+)\])?$/
-
const rawPath = state.src
.slice(start, end)
.trim()
.replace(/^@/, srcDir)
.trim()
- const [
- filepath = '',
- extension = '',
- region = '',
- lines = '',
- lang = '',
- rawTitle = ''
- ] = (rawPathRegexp.exec(rawPath) || []).slice(1)
-
- const title = rawTitle || filepath.split('/').pop() || ''
+ const { filepath, extension, region, lines, lang, title } =
+ rawPathToToken(rawPath)
state.line = startLine + 1
@@ -125,10 +136,9 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
title ? `[${title}]` : ''
}`
- const resolvedPath = path.resolve(
- path.dirname((state.env as MarkdownEnv).path),
- filepath
- )
+ const { realPath, path: _path } = state.env as MarkdownEnv
+ const resolvedPath = path.resolve(path.dirname(realPath ?? _path), filepath)
+
// @ts-ignore
token.src = [resolvedPath, region.slice(1)]
token.markup = '```'
@@ -151,7 +161,7 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
includes.push(src)
}
- const isAFile = fs.lstatSync(src).isFile()
+ const isAFile = fs.statSync(src).isFile()
if (!fs.existsSync(src) || !isAFile) {
token.content = isAFile
? `Code snippet path not found: ${src}`
diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts
index 7559cb11..0648f456 100644
--- a/src/node/markdownToVue.ts
+++ b/src/node/markdownToVue.ts
@@ -6,22 +6,21 @@ import path from 'path'
import type { SiteConfig } from './config'
import {
createMarkdownRenderer,
- type MarkdownEnv,
type MarkdownOptions,
type MarkdownRenderer
-} from './markdown'
+} from './markdown/markdown'
import {
EXTERNAL_URL_RE,
slash,
type HeadConfig,
+ type MarkdownEnv,
type PageData
} from './shared'
import { getGitTimestamp } from './utils/getGitTimestamp'
+import { processIncludes } from './utils/processIncludes'
const debug = _debug('vitepress:md')
const cache = new LRUCache({ max: 1024 })
-const includesRE = //g
-const rangeRE = /\{(\d*),(\d*)\}$/
export interface MarkdownCompileResult {
vueSrc: string
@@ -30,8 +29,14 @@ export interface MarkdownCompileResult {
includes: string[]
}
-export function clearCache() {
- cache.clear()
+export function clearCache(file?: string) {
+ if (!file) {
+ cache.clear()
+ return
+ }
+
+ file = JSON.stringify({ file }).slice(1)
+ cache.find((_, key) => key.endsWith(file!) && cache.delete(key))
}
export async function createMarkdownToVueRenderFn(
@@ -65,7 +70,7 @@ export async function createMarkdownToVueRenderFn(
siteConfig?.rewrites.map[file.slice(srcDir.length + 1)]
file = alias ? path.join(srcDir, alias) : file
const relativePath = slash(path.relative(srcDir, file))
- const cacheKey = JSON.stringify({ src, file })
+ const cacheKey = JSON.stringify({ src, file: fileOrig })
if (isBuild || options.cache !== false) {
const cached = cache.get(cacheKey)
@@ -89,46 +94,15 @@ export async function createMarkdownToVueRenderFn(
// resolve includes
let includes: string[] = []
-
- function processIncludes(src: string, file: string): string {
- return src.replace(includesRE, (m: string, m1: string) => {
- if (!m1.length) return m
-
- const range = m1.match(rangeRE)
- range && (m1 = m1.slice(0, -range[0].length))
- const atPresent = m1[0] === '@'
- try {
- const includePath = atPresent
- ? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1))
- : path.join(path.dirname(file), m1)
- let content = fs.readFileSync(includePath, 'utf-8')
- if (range) {
- const [, startLine, endLine] = range
- const lines = content.split(/\r?\n/)
- content = lines
- .slice(
- startLine ? parseInt(startLine, 10) - 1 : undefined,
- endLine ? parseInt(endLine, 10) : undefined
- )
- .join('\n')
- }
- includes.push(slash(includePath))
- // recursively process includes in the content
- return processIncludes(content, includePath)
- } catch (error) {
- return m // silently ignore error if file is not present
- }
- })
- }
-
- src = processIncludes(src, fileOrig)
+ src = processIncludes(srcDir, src, fileOrig, includes)
// reset env before render
const env: MarkdownEnv = {
path: file,
relativePath,
cleanUrls,
- includes
+ includes,
+ realPath: fileOrig
}
const html = md.render(src, env)
const {
diff --git a/src/node/plugin.ts b/src/node/plugin.ts
index 0aa75334..6de976ec 100644
--- a/src/node/plugin.ts
+++ b/src/node/plugin.ts
@@ -3,6 +3,7 @@ import c from 'picocolors'
import {
mergeConfig,
searchForWorkspaceRoot,
+ type ModuleNode,
type Plugin,
type ResolvedConfig,
type Rollup,
@@ -14,19 +15,20 @@ import {
SITE_DATA_REQUEST_PATH,
resolveAliases
} from './alias'
-import { resolveUserConfig, resolvePages, type SiteConfig } from './config'
+import { resolvePages, resolveUserConfig, type SiteConfig } from './config'
+import { mathjaxElements } from './markdown/math'
import {
clearCache,
createMarkdownToVueRenderFn,
type MarkdownCompileResult
} from './markdownToVue'
-import { slash, type PageDataPayload } from './shared'
-import { staticDataPlugin } from './plugins/staticDataPlugin'
-import { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
-import { rewritesPlugin } from './plugins/rewritesPlugin'
import { localSearchPlugin } from './plugins/localSearchPlugin'
-import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize'
+import { rewritesPlugin } from './plugins/rewritesPlugin'
+import { staticDataPlugin } from './plugins/staticDataPlugin'
+import { webFontsPlugin } from './plugins/webFontsPlugin'
+import { slash, type PageDataPayload } from './shared'
+import { deserializeFunctions, serializeFunctions } from './utils/fnSerialize'
declare module 'vite' {
interface UserConfig {
@@ -34,7 +36,8 @@ declare module 'vite' {
}
}
-const hashRE = /\.(\w+)\.js$/
+const themeRE = /\/\.vitepress\/theme\/index\.(m|c)?(j|t)s$/
+const hashRE = /\.([-\w]+)\.js$/
const staticInjectMarkerRE =
/\b(const _hoisted_\d+ = \/\*(?:#|@)__PURE__\*\/\s*createStaticVNode)\("(.*)", (\d+)\)/g
const staticStripRE = /['"`]__VP_STATIC_START__[^]*?__VP_STATIC_END__['"`]/g
@@ -79,12 +82,31 @@ export async function createVitePressPlugin(
} = siteConfig
let markdownToVue: Awaited>
+ const userCustomElementChecker =
+ userVuePluginOptions?.template?.compilerOptions?.isCustomElement
+ let isCustomElement = userCustomElementChecker
+
+ if (markdown?.math) {
+ isCustomElement = (tag) => {
+ if (mathjaxElements.includes(tag)) {
+ return true
+ }
+ return userCustomElementChecker?.(tag) ?? false
+ }
+ }
// lazy require plugin-vue to respect NODE_ENV in @vue/compiler-x
const vuePlugin = await import('@vitejs/plugin-vue').then((r) =>
r.default({
include: [/\.vue$/, /\.md$/],
- ...userVuePluginOptions
+ ...userVuePluginOptions,
+ template: {
+ ...userVuePluginOptions?.template,
+ compilerOptions: {
+ ...userVuePluginOptions?.template?.compilerOptions,
+ isCustomElement
+ }
+ }
})
)
@@ -100,6 +122,7 @@ export async function createVitePressPlugin(
let siteData = site
let allDeadLinks: MarkdownCompileResult['deadLinks'] = []
let config: ResolvedConfig
+ let importerMap: Record | undefined> = {}
const vitePressPlugin: Plugin = {
name: 'vitepress',
@@ -172,8 +195,7 @@ export async function createVitePressPlugin(
}
}
data = serializeFunctions(data)
- return `${deserializeFunctions.toString()}
- export default deserializeFunctions(JSON.parse(${JSON.stringify(
+ return `${deserializeFunctions};export default deserializeFunctions(JSON.parse(${JSON.stringify(
JSON.stringify(data)
)}))`
}
@@ -192,6 +214,7 @@ export async function createVitePressPlugin(
allDeadLinks.push(...deadLinks)
if (includes.length) {
includes.forEach((i) => {
+ ;(importerMap[slash(i)] ??= new Set()).add(id)
this.addWatchFile(i)
})
}
@@ -225,16 +248,37 @@ export async function createVitePressPlugin(
configDeps.forEach((file) => server.watcher.add(file))
}
- // update pages, dynamicRoutes and rewrites on md file add / deletion
- const onFileAddDelete = async (file: string) => {
+ const onFileAddDelete = async (added: boolean, _file: string) => {
+ const file = slash(_file)
+ // restart server on theme file creation / deletion
+ if (themeRE.test(file)) {
+ siteConfig.logger.info(
+ c.green(
+ `${path.relative(process.cwd(), _file)} ${
+ added ? 'created' : 'deleted'
+ }, restarting server...\n`
+ ),
+ { clear: true, timestamp: true }
+ )
+
+ await recreateServer?.()
+ }
+
+ // update pages, dynamicRoutes and rewrites on md file creation / deletion
if (file.endsWith('.md')) {
Object.assign(
siteConfig,
await resolvePages(siteConfig.srcDir, siteConfig.userConfig)
)
}
+
+ if (!added && importerMap[file]) {
+ delete importerMap[file]
+ }
}
- server.watcher.on('add', onFileAddDelete).on('unlink', onFileAddDelete)
+ server.watcher
+ .on('add', onFileAddDelete.bind(null, true))
+ .on('unlink', onFileAddDelete.bind(null, false))
// serve our index.html after vite history fallback
return () => {
@@ -284,15 +328,8 @@ export async function createVitePressPlugin(
generateBundle(_options, bundle) {
if (ssr) {
- // ssr build:
- // delete all asset chunks
- for (const name in bundle) {
- if (bundle[name].type === 'asset') {
- delete bundle[name]
- }
- }
-
- if (config.ssr?.format === 'esm') {
+ // @ts-ignore will be removed in vite 5
+ if (config.ssr?.format !== 'cjs') {
this.emitFile({
type: 'asset',
fileName: 'package.json',
@@ -374,10 +411,27 @@ export async function createVitePressPlugin(
}
}
+ const hmrFix: Plugin = {
+ name: 'vitepress:hmr-fix',
+ async handleHotUpdate({ file, server, modules }) {
+ const importers = [...(importerMap[slash(file)] || [])]
+ if (importers.length > 0) {
+ return [
+ ...modules,
+ ...importers.map((id) => {
+ clearCache(id)
+ return server.moduleGraph.getModuleById(id)
+ })
+ ].filter(Boolean) as ModuleNode[]
+ }
+ }
+ }
+
return [
vitePressPlugin,
rewritesPlugin(siteConfig),
vuePlugin,
+ hmrFix,
webFontsPlugin(siteConfig.useWebFonts),
...(userViteConfig?.plugins || []),
await localSearchPlugin(siteConfig),
diff --git a/src/node/plugins/dynamicRoutesPlugin.ts b/src/node/plugins/dynamicRoutesPlugin.ts
index 5003af23..2807d3ff 100644
--- a/src/node/plugins/dynamicRoutesPlugin.ts
+++ b/src/node/plugins/dynamicRoutesPlugin.ts
@@ -7,7 +7,7 @@ import {
import fs from 'fs-extra'
import c from 'picocolors'
import path from 'path'
-import fg from 'fast-glob'
+import glob from 'fast-glob'
import { type SiteConfig, type UserConfig } from '../siteConfig'
import { resolveRewrites } from './rewritesPlugin'
@@ -21,9 +21,13 @@ export async function resolvePages(srcDir: string, userConfig: UserConfig) {
// JavaScript built-in sort() is mandated to be stable as of ES2019 and
// supported in Node 12+, which is required by Vite.
const allMarkdownFiles = (
- await fg(['**.md'], {
+ await glob(['**.md'], {
cwd: srcDir,
- ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
+ ignore: [
+ '**/node_modules/**',
+ '**/dist/**',
+ ...(userConfig.srcExclude || [])
+ ]
})
).sort()
@@ -158,19 +162,21 @@ export async function resolveDynamicRoutes(
for (const route of routes) {
// locate corresponding route paths file
const fullPath = normalizePath(path.resolve(srcDir, route))
- const jsPathsFile = fullPath.replace(/\.md$/, '.paths.js')
- let pathsFile = jsPathsFile
- if (!fs.existsSync(jsPathsFile)) {
- pathsFile = fullPath.replace(/\.md$/, '.paths.ts')
- if (!fs.existsSync(pathsFile)) {
- console.warn(
- c.yellow(
- `Missing paths file for dynamic route ${route}: ` +
- `a corresponding ${jsPathsFile} or ${pathsFile} is needed.`
- )
+
+ const paths = ['js', 'ts', 'mjs', 'mts'].map((ext) =>
+ fullPath.replace(/\.md$/, `.paths.${ext}`)
+ )
+
+ const pathsFile = paths.find((p) => fs.existsSync(p))
+
+ if (pathsFile == null) {
+ console.warn(
+ c.yellow(
+ `Missing paths file for dynamic route ${route}: ` +
+ `a corresponding ${paths[0]} (or .ts/.mjs/.mts) file is needed.`
)
- continue
- }
+ )
+ continue
}
// load the paths loader module
diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts
index e6cabcb4..dc7ca8da 100644
--- a/src/node/plugins/localSearchPlugin.ts
+++ b/src/node/plugins/localSearchPlugin.ts
@@ -4,8 +4,14 @@ import MiniSearch from 'minisearch'
import path from 'path'
import type { Plugin, ViteDevServer } from 'vite'
import type { SiteConfig } from '../config'
-import { createMarkdownRenderer, type MarkdownEnv } from '../markdown'
-import { resolveSiteDataByRoute, slash, type DefaultTheme } from '../shared'
+import { createMarkdownRenderer } from '../markdown/markdown'
+import {
+ resolveSiteDataByRoute,
+ slash,
+ type DefaultTheme,
+ type MarkdownEnv
+} from '../shared'
+import { processIncludes } from '../utils/processIncludes'
const debug = _debug('vitepress:local-search')
@@ -45,23 +51,17 @@ export async function localSearchPlugin(
siteConfig.logger
)
+ const options = siteConfig.site.themeConfig.search.options || {}
+
function render(file: string) {
- const { srcDir, cleanUrls = false, site } = siteConfig
+ const { srcDir, cleanUrls = false } = siteConfig
const relativePath = slash(path.relative(srcDir, file))
- const env: MarkdownEnv = {
- path: file,
- relativePath,
- cleanUrls
- }
- const html = md.render(fs.readFileSync(file, 'utf-8'), env)
- if (
- env.frontmatter?.search === false ||
- (site.themeConfig.search?.provider === 'local' &&
- site.themeConfig.search.options?.exclude?.(relativePath))
- ) {
- return ''
- }
- return html
+ const env: MarkdownEnv = { path: file, relativePath, cleanUrls }
+ let src = fs.readFileSync(file, 'utf-8')
+ src = processIncludes(srcDir, src, file, [])
+ if (options._render) return options._render(src, env, md)
+ const html = md.render(src, env)
+ return env.frontmatter?.search === false ? '' : html
}
const indexByLocales = new Map>()
@@ -72,8 +72,7 @@ export async function localSearchPlugin(
index = new MiniSearch({
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
- ...(siteConfig.site.themeConfig?.search?.provider === 'local' &&
- siteConfig.site.themeConfig.search.options?.miniSearch?.options)
+ ...options.miniSearch?.options
})
indexByLocales.set(locale, index)
}
diff --git a/src/node/plugins/rewritesPlugin.ts b/src/node/plugins/rewritesPlugin.ts
index 2388a70a..51794a1d 100644
--- a/src/node/plugins/rewritesPlugin.ts
+++ b/src/node/plugins/rewritesPlugin.ts
@@ -7,7 +7,7 @@ export function resolveRewrites(
userRewrites: UserConfig['rewrites']
) {
const rewriteRules = Object.entries(userRewrites || {}).map(([from, to]) => ({
- toPath: compile(to, { validate: false }),
+ toPath: compile(`/${to}`, { validate: false }),
matchUrl: match(from.startsWith('^') ? new RegExp(from) : from)
}))
@@ -18,7 +18,7 @@ export function resolveRewrites(
for (const { matchUrl, toPath } of rewriteRules) {
const res = matchUrl(page)
if (res) {
- const dest = toPath(res.params)
+ const dest = toPath(res.params).slice(1)
pageToRewrite[page] = dest
rewriteToPage[dest] = page
break
diff --git a/src/node/plugins/staticDataPlugin.ts b/src/node/plugins/staticDataPlugin.ts
index efa6fe47..283a5138 100644
--- a/src/node/plugins/staticDataPlugin.ts
+++ b/src/node/plugins/staticDataPlugin.ts
@@ -8,7 +8,7 @@ import path, { dirname, resolve } from 'path'
import { isMatch } from 'micromatch'
import glob from 'fast-glob'
-const loaderMatch = /\.data\.(j|t)s($|\?)/
+const loaderMatch = /\.data\.m?(j|t)s($|\?)/
let server: ViteDevServer
diff --git a/src/node/postcss/isolateStyles.ts b/src/node/postcss/isolateStyles.ts
new file mode 100644
index 00000000..dadd5cbd
--- /dev/null
+++ b/src/node/postcss/isolateStyles.ts
@@ -0,0 +1,15 @@
+import postcssPrefixSelector from 'postcss-prefix-selector'
+
+export function postcssIsolateStyles(
+ options: Parameters[0] = {}
+): ReturnType {
+ return postcssPrefixSelector({
+ prefix: ':not(:where(.vp-raw, .vp-raw *))',
+ includeFiles: [/base\.css/],
+ transform(prefix, _selector) {
+ const [selector, pseudo = ''] = _selector.split(/(:\S*)$/)
+ return selector + prefix + pseudo
+ },
+ ...options
+ })
+}
diff --git a/src/node/server.ts b/src/node/server.ts
index ebf0aedf..4105edfe 100644
--- a/src/node/server.ts
+++ b/src/node/server.ts
@@ -1,4 +1,3 @@
-import dns from 'dns'
import { createServer as createViteServer, type ServerOptions } from 'vite'
import { resolveConfig } from './config'
import { createVitePressPlugin } from './plugin'
@@ -15,14 +14,13 @@ export async function createServer(
delete serverOptions.base
}
- dns.setDefaultResultOrder('verbatim')
-
return createViteServer({
root: config.srcDir,
base: config.site.base,
cacheDir: config.cacheDir,
plugins: await createVitePressPlugin(config, false, {}, {}, recreateServer),
server: serverOptions,
- customLogger: config.logger
+ customLogger: config.logger,
+ configFile: config.vite?.configFile
})
}
diff --git a/src/node/shortcuts.ts b/src/node/shortcuts.ts
index 11f88565..84aa03ec 100644
--- a/src/node/shortcuts.ts
+++ b/src/node/shortcuts.ts
@@ -68,6 +68,7 @@ export function bindShortcuts(
server.httpServer.on('close', () => {
process.stdin.off('data', onInput).pause()
+ process.stdin.setRawMode(false)
})
}
diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts
index ae6d976f..10cd3967 100644
--- a/src/node/siteConfig.ts
+++ b/src/node/siteConfig.ts
@@ -1,8 +1,9 @@
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
+import type { UseDarkOptions } from '@vueuse/core'
import type { SitemapStreamOptions } from 'sitemap'
import type { Logger, UserConfig as ViteConfig } from 'vite'
import type { SitemapItem } from './build/generateSitemap'
-import type { MarkdownOptions } from './markdown'
+import type { MarkdownOptions } from './markdown/markdown'
import type {
Awaitable,
HeadConfig,
@@ -68,7 +69,11 @@ export interface UserConfig
locales?: LocaleConfig
- appearance?: boolean | 'dark'
+ appearance?:
+ | boolean
+ | 'dark'
+ | 'force-dark'
+ | (Omit & { initialValue?: 'dark' })
lastUpdated?: boolean
contentProps?: Record
@@ -83,7 +88,7 @@ export interface UserConfig
/**
* Vite config
*/
- vite?: ViteConfig
+ vite?: ViteConfig & { configFile?: string | false }
/**
* Configure the scroll offset when the theme has a sticky header.
@@ -93,7 +98,11 @@ export interface UserConfig
* selector if a selector fails to match, or the matched element is not
* currently visible in viewport.
*/
- scrollOffset?: number | string | string[]
+ scrollOffset?:
+ | number
+ | string
+ | string[]
+ | { selector: string | string[]; padding: number }
/**
* Enable MPA / zero-JS mode.
diff --git a/src/node/tsconfig.json b/src/node/tsconfig.json
index 26db4671..44906f44 100644
--- a/src/node/tsconfig.json
+++ b/src/node/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
- "target": "es2020",
+ "target": "es2022",
"baseUrl": ".",
"outDir": "../../dist/node",
"module": "esnext",
diff --git a/src/node/utils/fnSerialize.ts b/src/node/utils/fnSerialize.ts
index f3022eec..242ca90d 100644
--- a/src/node/utils/fnSerialize.ts
+++ b/src/node/utils/fnSerialize.ts
@@ -3,6 +3,7 @@ export function serializeFunctions(value: any, key?: string): any {
return value.map((v) => serializeFunctions(v))
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc, key) => {
+ if (key[0] === '_') return acc
acc[key] = serializeFunctions(value[key], key)
return acc
}, {} as any)
@@ -20,6 +21,7 @@ export function serializeFunctions(value: any, key?: string): any {
}
}
+/*
export function deserializeFunctions(value: any): any {
if (Array.isArray(value)) {
return value.map(deserializeFunctions)
@@ -34,3 +36,7 @@ export function deserializeFunctions(value: any): any {
return value
}
}
+*/
+
+export const deserializeFunctions =
+ 'function deserializeFunctions(r){return Array.isArray(r)?r.map(deserializeFunctions):typeof r=="object"&&r!==null?Object.keys(r).reduce((t,n)=>(t[n]=deserializeFunctions(r[n]),t),{}):typeof r=="string"&&r.startsWith("_vp-fn_")?new Function(`return ${r.slice(7)}`)():r}'
diff --git a/src/node/utils/processIncludes.ts b/src/node/utils/processIncludes.ts
new file mode 100644
index 00000000..43808ba4
--- /dev/null
+++ b/src/node/utils/processIncludes.ts
@@ -0,0 +1,41 @@
+import path from 'path'
+import fs from 'fs-extra'
+import { slash } from '../shared'
+
+export function processIncludes(
+ srcDir: string,
+ src: string,
+ file: string,
+ includes: string[]
+): string {
+ const includesRE = //g
+ const rangeRE = /\{(\d*),(\d*)\}$/
+ return src.replace(includesRE, (m: string, m1: string) => {
+ if (!m1.length) return m
+
+ const range = m1.match(rangeRE)
+ range && (m1 = m1.slice(0, -range[0].length))
+ const atPresent = m1[0] === '@'
+ try {
+ const includePath = atPresent
+ ? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1))
+ : path.join(path.dirname(file), m1)
+ let content = fs.readFileSync(includePath, 'utf-8')
+ if (range) {
+ const [, startLine, endLine] = range
+ const lines = content.split(/\r?\n/)
+ content = lines
+ .slice(
+ startLine ? parseInt(startLine, 10) - 1 : undefined,
+ endLine ? parseInt(endLine, 10) : undefined
+ )
+ .join('\n')
+ }
+ includes.push(slash(includePath))
+ // recursively process includes in the content
+ return processIncludes(srcDir, content, includePath, includes)
+ } catch (error) {
+ return m // silently ignore error if file is not present
+ }
+ })
+}
diff --git a/src/shared/shared.ts b/src/shared/shared.ts
index b110b3a1..9bc2de00 100644
--- a/src/shared/shared.ts
+++ b/src/shared/shared.ts
@@ -7,14 +7,14 @@ export type {
Header,
LocaleConfig,
LocaleSpecificConfig,
+ MarkdownEnv,
PageData,
PageDataPayload,
- SiteData,
- SSGContext
+ SSGContext,
+ SiteData
} from '../../types/shared'
export const EXTERNAL_URL_RE = /^[a-z]+:/i
-export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//
export const APPEARANCE_KEY = 'vitepress-theme-appearance'
export const HASH_RE = /#.*$/
export const EXT_RE = /(index)?\.(md|html)$/
diff --git a/template/.vitepress/theme/index.js b/template/.vitepress/theme/index.js
index 8ea44ecb..3e14542a 100644
--- a/template/.vitepress/theme/index.js
+++ b/template/.vitepress/theme/index.js
@@ -1,25 +1,29 @@
// https://vitepress.dev/guide/custom-theme
-<% if (!defaultTheme) { %>import Layout from './Layout.vue'
+<% if (!defaultTheme) { %>import Layout from './Layout.vue'<% if (useTs) { %>
+import type { Theme } from 'vitepress'<% } %>
import './style.css'
-export default {
+<% if (!useTs) { %>/** @type {import('vitepress').Theme} */
+<% } %>export default {
Layout,
enhanceApp({ app, router, siteData }) {
// ...
}
-}
-<% } else { %>import { h } from 'vue'
-import Theme from 'vitepress/theme'
+}<% if (useTs) { %> satisfies Theme<% } %>
+<% } else { %>import { h } from 'vue'<% if (useTs) { %>
+import type { Theme } from 'vitepress'<% } %>
+import DefaultTheme from 'vitepress/theme'
import './style.css'
-export default {
- extends: Theme,
+<% if (!useTs) { %>/** @type {import('vitepress').Theme} */
+<% } %>export default {
+ extends: DefaultTheme,
Layout: () => {
- return h(Theme.Layout, null, {
+ return h(DefaultTheme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
},
enhanceApp({ app, router, siteData }) {
// ...
}
-}<% } %>
+}<% if (useTs) { %> satisfies Theme<% } %><% } %>
diff --git a/template/.vitepress/theme/style.css b/template/.vitepress/theme/style.css
index 84c26fa6..2a518312 100644
--- a/template/.vitepress/theme/style.css
+++ b/template/.vitepress/theme/style.css
@@ -5,16 +5,69 @@
/**
* Colors
+ *
+ * Each colors have exact same color scale system with 3 levels of solid
+ * colors with different brightness, and 1 soft color.
+ *
+ * - `XXX-1`: The most solid color used mainly for colored text. It must
+ * satisfy the contrast ratio against when used on top of `XXX-soft`.
+ *
+ * - `XXX-2`: The color used mainly for hover state of the button.
+ *
+ * - `XXX-3`: The color for solid background, such as bg color of the button.
+ * It must satisfy the contrast ratio with pure white (#ffffff) text on
+ * top of it.
+ *
+ * - `XXX-soft`: The color used for subtle background such as custom container
+ * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
+ * on top of it.
+ *
+ * The soft color must be semi transparent alpha channel. This is crucial
+ * because it allows adding multiple "soft" colors on top of each other
+ * to create a accent, such as when having inline code block inside
+ * custom containers.
+ *
+ * - `default`: The color used purely for subtle indication without any
+ * special meanings attched to it such as bg color for menu hover state.
+ *
+ * - `brand`: Used for primary brand colors, such as link text, button with
+ * brand theme, etc.
+ *
+ * - `tip`: Used to indicate useful information. The default theme uses the
+ * brand color for this by default.
+ *
+ * - `warning`: Used to indicate warning to the users. Used in custom
+ * container, badges, etc.
+ *
+ * - `danger`: Used to show error, or dangerous message to the users. Used
+ * in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root {
- --vp-c-brand: #646cff;
- --vp-c-brand-light: #747bff;
- --vp-c-brand-lighter: #9499ff;
- --vp-c-brand-lightest: #bcc0ff;
- --vp-c-brand-dark: #535bf2;
- --vp-c-brand-darker: #454ce1;
- --vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
+ --vp-c-default-1: var(--vp-c-gray-1);
+ --vp-c-default-2: var(--vp-c-gray-2);
+ --vp-c-default-3: var(--vp-c-gray-3);
+ --vp-c-default-soft: var(--vp-c-gray-soft);
+
+ --vp-c-brand-1: var(--vp-c-indigo-1);
+ --vp-c-brand-2: var(--vp-c-indigo-2);
+ --vp-c-brand-3: var(--vp-c-indigo-3);
+ --vp-c-brand-soft: var(--vp-c-indigo-soft);
+
+ --vp-c-tip-1: var(--vp-c-brand-1);
+ --vp-c-tip-2: var(--vp-c-brand-2);
+ --vp-c-tip-3: var(--vp-c-brand-3);
+ --vp-c-tip-soft: var(--vp-c-brand-soft);
+
+ --vp-c-warning-1: var(--vp-c-yellow-1);
+ --vp-c-warning-2: var(--vp-c-yellow-2);
+ --vp-c-warning-3: var(--vp-c-yellow-3);
+ --vp-c-warning-soft: var(--vp-c-yellow-soft);
+
+ --vp-c-danger-1: var(--vp-c-red-1);
+ --vp-c-danger-2: var(--vp-c-red-2);
+ --vp-c-danger-3: var(--vp-c-red-3);
+ --vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
@@ -22,15 +75,15 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-button-brand-border: var(--vp-c-brand-light);
+ --vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
- --vp-button-brand-bg: var(--vp-c-brand);
- --vp-button-brand-hover-border: var(--vp-c-brand-light);
+ --vp-button-brand-bg: var(--vp-c-brand-3);
+ --vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
- --vp-button-brand-hover-bg: var(--vp-c-brand-light);
- --vp-button-brand-active-border: var(--vp-c-brand-light);
+ --vp-button-brand-hover-bg: var(--vp-c-brand-2);
+ --vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
- --vp-button-brand-active-bg: var(--vp-button-brand-bg);
+ --vp-button-brand-active-bg: var(--vp-c-brand-1);
}
/**
@@ -50,7 +103,7 @@
#bd34fe 50%,
#47caff 50%
);
- --vp-home-hero-image-filter: blur(40px);
+ --vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
@@ -61,7 +114,7 @@
@media (min-width: 960px) {
:root {
- --vp-home-hero-image-filter: blur(72px);
+ --vp-home-hero-image-filter: blur(68px);
}
}
@@ -70,15 +123,10 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-custom-block-tip-border: var(--vp-c-brand);
- --vp-custom-block-tip-text: var(--vp-c-brand-darker);
- --vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
-}
-
-.dark {
- --vp-custom-block-tip-border: var(--vp-c-brand);
- --vp-custom-block-tip-text: var(--vp-c-brand-lightest);
- --vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
+ --vp-custom-block-tip-border: transparent;
+ --vp-custom-block-tip-text: var(--vp-c-text-1);
+ --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
+ --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
@@ -86,7 +134,7 @@
* -------------------------------------------------------------------------- */
.DocSearch {
- --docsearch-primary-color: var(--vp-c-brand) !important;
+ --docsearch-primary-color: var(--vp-c-brand-1) !important;
}
<% } else { %>
html {
diff --git a/theme-without-fonts.d.ts b/theme-without-fonts.d.ts
index 7a71fe6d..531dd12e 100644
--- a/theme-without-fonts.d.ts
+++ b/theme-without-fonts.d.ts
@@ -1,2 +1,2 @@
-export * from './theme'
-export { default } from './theme'
+export * from './theme.js'
+export { default } from './theme.js'
diff --git a/theme.d.ts b/theme.d.ts
index 4b1076b5..446a9d65 100644
--- a/theme.d.ts
+++ b/theme.d.ts
@@ -1,16 +1,10 @@
// so that users can do `import DefaultTheme from 'vitepress/theme'`
+
import type { DefineComponent } from 'vue'
-import { EnhanceAppContext } from './dist/client/index.js'
+import type { EnhanceAppContext } from './dist/client/index.js'
+import type { DefaultTheme } from './types/default-theme.js'
-// TODO: add props for these
-export const VPHomeHero: DefineComponent
-export const VPHomeFeatures: DefineComponent
-export const VPHomeSponsors: DefineComponent
-export const VPDocAsideSponsors: DefineComponent
-export const VPTeamPage: DefineComponent
-export const VPTeamPageTitle: DefineComponent
-export const VPTeamPageSection: DefineComponent
-export const VPTeamMembers: DefineComponent
+export type { DefaultTheme } from './types/default-theme.js'
declare const theme: {
Layout: DefineComponent
@@ -18,6 +12,17 @@ declare const theme: {
}
export default theme
-export type { DefaultTheme } from './types/default-theme.js'
+export declare const useSidebar: () => DefaultTheme.DocSidebar
-export const useSidebar: () => DefaultTheme.DocSidebar
+// TODO: add props for these
+export declare const VPButton: DefineComponent
+export declare const VPDocAsideSponsors: DefineComponent
+export declare const VPHomeFeatures: DefineComponent
+export declare const VPHomeHero: DefineComponent
+export declare const VPHomeSponsors: DefineComponent
+export declare const VPImage: DefineComponent
+export declare const VPSponsors: DefineComponent
+export declare const VPTeamMembers: DefineComponent
+export declare const VPTeamPage: DefineComponent
+export declare const VPTeamPageSection: DefineComponent
+export declare const VPTeamPageTitle: DefineComponent
diff --git a/types/default-theme.d.ts b/types/default-theme.d.ts
index 8e350a33..94dfb95c 100644
--- a/types/default-theme.d.ts
+++ b/types/default-theme.d.ts
@@ -1,8 +1,9 @@
+import type MarkdownIt from 'markdown-it'
import type { Options as MiniSearchOptions } from 'minisearch'
import type { ComputedRef, Ref } from 'vue'
import type { DocSearchProps } from './docsearch.js'
import type { LocalSearchTranslations } from './local-search.js'
-import type { PageData } from './shared.js'
+import type { MarkdownEnv, PageData } from './shared.js'
export namespace DefaultTheme {
export interface Config {
@@ -13,6 +14,11 @@ export namespace DefaultTheme {
*/
logo?: ThemeableImage
+ /**
+ * Overrides the link of the site logo.
+ */
+ logoLink?: string
+
/**
* Custom site title in navbar. If the value is undefined,
* `config.title` will be used.
@@ -148,6 +154,7 @@ export namespace DefaultTheme {
export interface NavItemWithLink {
text: string
link: string
+ items?: never
/**
* `activeMatch` is expected to be a regex string. We can't use actual
@@ -178,18 +185,25 @@ export namespace DefaultTheme {
export type ThemeableImage =
| string
- | { src: string; alt?: string }
- | { light: string; dark: string; alt?: string }
+ | { src: string; alt?: string; [prop: string]: any }
+ | { light: string; dark: string; alt?: string; [prop: string]: any }
export type FeatureIcon =
| string
- | { src: string; alt?: string; width?: string; height: string }
+ | {
+ src: string
+ alt?: string
+ width?: string
+ height?: string
+ wrap?: boolean
+ }
| {
light: string
dark: string
alt?: string
width?: string
- height: string
+ height?: string
+ wrap?: boolean
}
// sidebar -------------------------------------------------------------------
@@ -197,7 +211,7 @@ export namespace DefaultTheme {
export type Sidebar = SidebarItem[] | SidebarMulti
export interface SidebarMulti {
- [path: string]: SidebarItem[]
+ [path: string]: SidebarItem[] | { items: SidebarItem[]; base: string }
}
export type SidebarItem = {
@@ -224,6 +238,19 @@ export namespace DefaultTheme {
* If `false`, group is collapsible but expanded by default
*/
collapsed?: boolean
+
+ /**
+ * Base path for the children items.
+ */
+ base?: string
+
+ /**
+ * Customize text that appears on the footer of previous/next page.
+ */
+ docFooterText?: string
+
+ rel?: string
+ target?: string
}
/**
@@ -296,6 +323,7 @@ export namespace DefaultTheme {
| 'mastodon'
| 'slack'
| 'twitter'
+ | 'x'
| 'youtube'
| { svg: string }
@@ -367,15 +395,16 @@ export namespace DefaultTheme {
}
/**
- * exclude content from search results
+ * Allows transformation of content before indexing (node only)
+ * Return empty string to skip indexing
*/
- exclude?: (relativePath: string) => boolean
+ _render?: (src: string, env: MarkdownEnv, md: MarkdownIt) => string
}
// algolia -------------------------------------------------------------------
/**
- * The Algolia search options. Partially copied from
+ * Algolia search options. Partially copied from
* `@docsearch/react/dist/esm/DocSearch.d.ts`
*/
export interface AlgoliaSearchOptions extends DocSearchProps {
@@ -406,7 +435,7 @@ export namespace DefaultTheme {
* @default
* { dateStyle: 'short', timeStyle: 'short' }
*/
- formatOptions?: Intl.DateTimeFormatOptions
+ formatOptions?: Intl.DateTimeFormatOptions & { forceLocale?: boolean }
}
// not found -----------------------------------------------------------------
diff --git a/types/docsearch.d.ts b/types/docsearch.d.ts
index 4f0b5588..76542015 100644
--- a/types/docsearch.d.ts
+++ b/types/docsearch.d.ts
@@ -6,6 +6,7 @@ export interface DocSearchProps {
searchParameters?: SearchOptions
disableUserPersonalization?: boolean
initialQuery?: string
+ insights?: boolean
translations?: DocSearchTranslations
}
diff --git a/types/shared.d.ts b/types/shared.d.ts
index 3cb48065..99a9b57a 100644
--- a/types/shared.d.ts
+++ b/types/shared.d.ts
@@ -1,4 +1,5 @@
// types shared between server and client
+import type { UseDarkOptions } from '@vueuse/core'
import type { SSRContext } from 'vue/server-renderer'
export type { DefaultTheme } from './default-theme.js'
@@ -17,6 +18,62 @@ export interface PageData {
lastUpdated?: number
}
+/**
+ * SFC block extracted from markdown
+ */
+export interface SfcBlock {
+ /**
+ * The type of the block
+ */
+ type: string
+ /**
+ * The content, including open-tag and close-tag
+ */
+ content: string
+ /**
+ * The content that stripped open-tag and close-tag off
+ */
+ contentStripped: string
+ /**
+ * The open-tag
+ */
+ tagOpen: string
+ /**
+ * The close-tag
+ */
+ tagClose: string
+}
+
+export interface MarkdownSfcBlocks {
+ /**
+ * The `` block
+ */
+ template: SfcBlock | null
+ /**
+ * The common `
-