@brillout/docpress 0.6.8 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MobileHeader.tsx +3 -3
- package/PageLayout.tsx +1 -1
- package/components/Link.tsx +61 -48
- package/config/resolveHeadingsData.ts +291 -0
- package/config/resolvePageContext.ts +14 -157
- package/dist/+config.js +4 -4
- package/dist/parsePageSections.d.ts +12 -0
- package/dist/{markdownHeadingsVitePlugin.js → parsePageSections.js} +24 -27
- package/dist/vite.config.js +6 -6
- package/installSectionUrlHashs.ts +2 -1
- package/navigation/Navigation-items.css +15 -15
- package/navigation/Navigation-layout.css +5 -5
- package/navigation/Navigation.client.ts +4 -40
- package/navigation/Navigation.tsx +83 -88
- package/navigation/initMobileNavigation.ts +32 -0
- package/navigation/initPressKit.ts +16 -0
- package/navigation/navigation-fullscreen/NavigationFullscreenButton.css +1 -1
- package/navigation/navigation-fullscreen/initNavigationFullscreen.ts +1 -1
- package/package.json +4 -3
- package/{markdownHeadingsVitePlugin.ts → parsePageSections.ts} +28 -32
- package/parseTitle.ts +2 -85
- package/renderer/onRenderHtml.tsx +10 -5
- package/types/Heading.ts +30 -28
- package/types.d.ts +1 -1
- package/vite.config.ts +2 -2
- package/dist/markdownHeadingsVitePlugin.d.ts +0 -13
package/MobileHeader.tsx
CHANGED
|
@@ -28,7 +28,7 @@ function MobileHeader() {
|
|
|
28
28
|
borderBottom: '1px solid #ddd',
|
|
29
29
|
}}
|
|
30
30
|
>
|
|
31
|
-
<
|
|
31
|
+
<MobileShowNavigationToggle />
|
|
32
32
|
<a
|
|
33
33
|
href="/"
|
|
34
34
|
style={{
|
|
@@ -47,9 +47,9 @@ function MobileHeader() {
|
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function
|
|
50
|
+
function MobileShowNavigationToggle() {
|
|
51
51
|
return (
|
|
52
|
-
<div style={{ padding: 20, lineHeight: 0, cursor: 'pointer' }} id="
|
|
52
|
+
<div style={{ padding: 20, lineHeight: 0, cursor: 'pointer' }} id="mobile-show-navigation-toggle">
|
|
53
53
|
<svg
|
|
54
54
|
style={{ width: 20 }}
|
|
55
55
|
className="icon"
|
package/PageLayout.tsx
CHANGED
|
@@ -17,7 +17,7 @@ function PageLayout({ pageContext, children }: { pageContext: PageContextResolve
|
|
|
17
17
|
<PageContextProvider pageContext={pageContext}>
|
|
18
18
|
<div id="page-layout" className={isLandingPage ? 'landing-page' : 'doc-page'}>
|
|
19
19
|
<div id="navigation-wrapper">
|
|
20
|
-
<Navigation
|
|
20
|
+
<Navigation {...pageContext.navigationData} />
|
|
21
21
|
</div>
|
|
22
22
|
<NavigationFullscreenButton />
|
|
23
23
|
<div id="page-wrapper">
|
package/components/Link.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export { Link }
|
|
2
|
+
export type { LinkData }
|
|
2
3
|
|
|
3
4
|
import React from 'react'
|
|
4
5
|
import { isRepoLink, RepoLink } from './RepoLink'
|
|
5
|
-
import type { Heading, HeadingDetached } from '../types/Heading'
|
|
6
6
|
import type { PageContextResolved } from '../config/resolvePageContext'
|
|
7
7
|
import { usePageContext } from '../renderer/usePageContext'
|
|
8
8
|
import { assert, assertUsage, determineSectionTitle, determineSectionUrlHash } from '../utils/server'
|
|
9
9
|
import { parseTitle } from '../parseTitle'
|
|
10
|
+
import pc from '@brillout/picocolors'
|
|
10
11
|
|
|
11
12
|
function Link({
|
|
12
13
|
href,
|
|
@@ -48,54 +49,27 @@ function getLinkText({
|
|
|
48
49
|
pageContext: PageContextResolved
|
|
49
50
|
doNotInferSectionTitle: true | undefined
|
|
50
51
|
}): string | JSX.Element {
|
|
51
|
-
|
|
52
|
-
let hrefWithoutHash: string = href
|
|
53
|
-
if (href.includes('#')) {
|
|
54
|
-
;[hrefWithoutHash, urlHash] = href.split('#')
|
|
55
|
-
assert(hrefWithoutHash || urlHash)
|
|
56
|
-
}
|
|
52
|
+
const { hrefPathname, hrefHash } = parseHref(href)
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
heading = findHeading(hrefWithoutHash, pageContext)
|
|
62
|
-
if (heading.url === pageContext.urlPathname) {
|
|
63
|
-
isLinkOnSamePage = true
|
|
64
|
-
// heading !== pageContext.activeHeading because activeHeading is a different object holding on-this-page subheadings
|
|
65
|
-
heading = pageContext.activeHeading
|
|
66
|
-
}
|
|
67
|
-
} else {
|
|
68
|
-
assert(urlHash)
|
|
69
|
-
isLinkOnSamePage = true
|
|
70
|
-
heading = pageContext.activeHeading
|
|
71
|
-
}
|
|
72
|
-
assert(heading)
|
|
73
|
-
assert(isLinkOnSamePage === (heading.url === pageContext.urlPathname))
|
|
74
|
-
assert(isLinkOnSamePage === (heading.url === pageContext.activeHeading.url))
|
|
75
|
-
assert(isLinkOnSamePage === (heading === pageContext.activeHeading))
|
|
54
|
+
const linkData = findLinkData(hrefPathname || pageContext.urlPathname, pageContext)
|
|
55
|
+
const isLinkOnSamePage = linkData.url === pageContext.urlPathname
|
|
56
|
+
if (!hrefPathname) assert(isLinkOnSamePage)
|
|
76
57
|
|
|
77
58
|
const breadcrumbParts: (string | JSX.Element)[] = []
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
breadcrumbParts.push(
|
|
81
|
-
...(heading.headingsBreadcrumb ?? [])
|
|
82
|
-
.slice()
|
|
83
|
-
.reverse()
|
|
84
|
-
.map(({ title }) => title),
|
|
85
|
-
)
|
|
59
|
+
if (linkData.linkBreadcrumb) {
|
|
60
|
+
breadcrumbParts.push(...(linkData.linkBreadcrumb ?? []).slice().reverse())
|
|
86
61
|
}
|
|
62
|
+
breadcrumbParts.push(linkData.title)
|
|
87
63
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (urlHash) {
|
|
64
|
+
if (hrefHash) {
|
|
91
65
|
let sectionTitle: string | JSX.Element | undefined = undefined
|
|
92
|
-
assert(!
|
|
66
|
+
assert(!hrefHash.startsWith('#'))
|
|
93
67
|
if (isLinkOnSamePage) {
|
|
94
|
-
const
|
|
95
|
-
sectionTitle =
|
|
96
|
-
} else if ('sectionTitles' in
|
|
97
|
-
|
|
98
|
-
if (determineSectionUrlHash(title) ===
|
|
68
|
+
const linkDataPageSection = findLinkData(`#${hrefHash}`, pageContext)
|
|
69
|
+
sectionTitle = linkDataPageSection.title
|
|
70
|
+
} else if ('sectionTitles' in linkData && linkData.sectionTitles) {
|
|
71
|
+
linkData.sectionTitles.forEach((title) => {
|
|
72
|
+
if (determineSectionUrlHash(title) === hrefHash) {
|
|
99
73
|
sectionTitle = parseTitle(title)
|
|
100
74
|
}
|
|
101
75
|
})
|
|
@@ -131,14 +105,53 @@ function getLinkText({
|
|
|
131
105
|
)
|
|
132
106
|
}
|
|
133
107
|
|
|
134
|
-
|
|
108
|
+
type LinkData = {
|
|
109
|
+
url?: null | string
|
|
110
|
+
title: JSX.Element
|
|
111
|
+
linkBreadcrumb: null | JSX.Element[]
|
|
112
|
+
sectionTitles?: string[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function findLinkData(href: string, pageContext: PageContextResolved): LinkData {
|
|
135
116
|
assert(href.startsWith('/') || href.startsWith('#'))
|
|
136
|
-
const {
|
|
137
|
-
const
|
|
117
|
+
const { linksAll } = pageContext
|
|
118
|
+
const linkData = linksAll.find(({ url }) => href === url)
|
|
138
119
|
if (href.startsWith('#')) {
|
|
139
|
-
assertUsage(
|
|
120
|
+
assertUsage(linkData, `Couldn't find ${href} in ${pageContext.urlPathname}, does it exist?`)
|
|
140
121
|
} else {
|
|
141
|
-
assertUsage(
|
|
122
|
+
assertUsage(
|
|
123
|
+
linkData,
|
|
124
|
+
[
|
|
125
|
+
`Couldn't find page with URL ${pc.bold(href)}`,
|
|
126
|
+
`— did you define it in`,
|
|
127
|
+
[
|
|
128
|
+
pc.cyan('docpress.config.js'),
|
|
129
|
+
pc.dim('#{'),
|
|
130
|
+
pc.cyan('headings'),
|
|
131
|
+
pc.dim(','),
|
|
132
|
+
pc.cyan('headingsDetached'),
|
|
133
|
+
pc.dim('}'),
|
|
134
|
+
'?',
|
|
135
|
+
].join(''),
|
|
136
|
+
].join(' '),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
return linkData
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseHref(href: string) {
|
|
143
|
+
let hrefHash: string | null = null
|
|
144
|
+
let hrefPathname: string | null = null
|
|
145
|
+
if (!href.includes('#')) {
|
|
146
|
+
hrefPathname = href
|
|
147
|
+
} else {
|
|
148
|
+
const [partsFirst, ...partsRest] = href.split('#')
|
|
149
|
+
if (partsFirst) {
|
|
150
|
+
hrefPathname = partsFirst
|
|
151
|
+
}
|
|
152
|
+
hrefHash = partsRest.join('#')
|
|
142
153
|
}
|
|
143
|
-
|
|
154
|
+
assert(hrefPathname !== null || hrefHash !== null)
|
|
155
|
+
assert(hrefPathname || hrefHash)
|
|
156
|
+
return { hrefPathname, hrefHash }
|
|
144
157
|
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
export { resolveHeadingsData }
|
|
2
|
+
|
|
3
|
+
import { assert, jsxToTextContent } from '../utils/server'
|
|
4
|
+
import type {
|
|
5
|
+
HeadingDefinition,
|
|
6
|
+
HeadingDetachedDefinition,
|
|
7
|
+
HeadingResolved,
|
|
8
|
+
HeadingDetachedResolved,
|
|
9
|
+
} from '../types/Heading'
|
|
10
|
+
import type { Config } from '../types/Config'
|
|
11
|
+
import { getConfig } from './getConfig'
|
|
12
|
+
import { parseTitle, withEmoji } from '../parseTitle'
|
|
13
|
+
import { NavigationData, NavItem } from '../navigation/Navigation'
|
|
14
|
+
import type { LinkData } from '../components'
|
|
15
|
+
import type { Exports, PageContextOriginal } from './resolvePageContext'
|
|
16
|
+
import pc from '@brillout/picocolors'
|
|
17
|
+
|
|
18
|
+
type PageSectionResolved = {
|
|
19
|
+
url: string | null
|
|
20
|
+
title: JSX.Element
|
|
21
|
+
titleInNav: JSX.Element
|
|
22
|
+
linkBreadcrumb: JSX.Element[]
|
|
23
|
+
pageSectionLevel: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveHeadingsData(pageContext: PageContextOriginal) {
|
|
27
|
+
const config = getConfig()
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
const { headings, headingsDetached } = config
|
|
31
|
+
assertHeadingsDefinition([...headings, ...headingsDetached])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolved = getHeadingsResolved(config)
|
|
35
|
+
const { headingsDetachedResolved } = resolved
|
|
36
|
+
let { headingsResolved } = resolved
|
|
37
|
+
|
|
38
|
+
const { activeHeading, isDetachedPage } = getActiveHeading(headingsResolved, headingsDetachedResolved, pageContext)
|
|
39
|
+
|
|
40
|
+
const { documentTitle, isLandingPage, pageTitle } = getTitles(activeHeading, pageContext, config)
|
|
41
|
+
|
|
42
|
+
const pageSectionsResolved = getPageSectionsResolved(pageContext, activeHeading)
|
|
43
|
+
|
|
44
|
+
const linksAll: LinkData[] = [
|
|
45
|
+
...pageSectionsResolved.map(pageSectionToLinkData),
|
|
46
|
+
...headingsResolved.map(headingToLinkData),
|
|
47
|
+
...headingsDetachedResolved.map(headingToLinkData),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
let navigationData: NavigationData
|
|
51
|
+
{
|
|
52
|
+
const currentUrl: string = pageContext.urlPathname
|
|
53
|
+
const navItemsPageSections = pageSectionsResolved
|
|
54
|
+
.filter((pageSection) => pageSection.pageSectionLevel === 2)
|
|
55
|
+
.map(pageSectionToNavItem)
|
|
56
|
+
if (isDetachedPage) {
|
|
57
|
+
const navItemsAll: NavItem[] = headingsResolved
|
|
58
|
+
const navItems: NavItem[] = [headingToNavItem(activeHeading), ...navItemsPageSections]
|
|
59
|
+
navigationData = {
|
|
60
|
+
isDetachedPage: true,
|
|
61
|
+
navItems,
|
|
62
|
+
navItemsAll,
|
|
63
|
+
currentUrl,
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
const navItemsAll: NavItem[] = headingsResolved.map(headingToNavItem)
|
|
67
|
+
const activeHeadingIndex = navItemsAll.findIndex((navItem) => navItem.url === currentUrl)
|
|
68
|
+
assert(activeHeadingIndex >= 0)
|
|
69
|
+
navItemsPageSections.forEach((navItem, i) => {
|
|
70
|
+
navItemsAll.splice(activeHeadingIndex + 1 + i, 0, navItem)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
navigationData = {
|
|
74
|
+
isDetachedPage: false,
|
|
75
|
+
navItems: navItemsAll,
|
|
76
|
+
navItemsAll,
|
|
77
|
+
currentUrl,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const pageContextAddendum = {
|
|
83
|
+
navigationData,
|
|
84
|
+
linksAll,
|
|
85
|
+
isLandingPage,
|
|
86
|
+
pageTitle,
|
|
87
|
+
documentTitle,
|
|
88
|
+
}
|
|
89
|
+
return pageContextAddendum
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function headingToNavItem(heading: HeadingResolved | HeadingDetachedResolved): NavItem {
|
|
93
|
+
return {
|
|
94
|
+
level: heading.level,
|
|
95
|
+
url: heading.url,
|
|
96
|
+
title: heading.title,
|
|
97
|
+
titleInNav: heading.titleInNav,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function headingToLinkData(heading: HeadingResolved | HeadingDetachedResolved): LinkData {
|
|
101
|
+
return {
|
|
102
|
+
url: heading.url,
|
|
103
|
+
title: heading.title,
|
|
104
|
+
linkBreadcrumb: heading.linkBreadcrumb,
|
|
105
|
+
sectionTitles: heading.sectionTitles,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function pageSectionToNavItem(pageSection: PageSectionResolved): NavItem {
|
|
109
|
+
return {
|
|
110
|
+
level: pageSection.pageSectionLevel + 1,
|
|
111
|
+
url: pageSection.url,
|
|
112
|
+
title: pageSection.title,
|
|
113
|
+
titleInNav: pageSection.titleInNav,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function pageSectionToLinkData(pageSection: PageSectionResolved): LinkData {
|
|
117
|
+
return {
|
|
118
|
+
url: pageSection.url,
|
|
119
|
+
title: pageSection.title,
|
|
120
|
+
linkBreadcrumb: pageSection.linkBreadcrumb,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getTitles(
|
|
125
|
+
activeHeading: HeadingResolved | HeadingDetachedResolved,
|
|
126
|
+
pageContext: { urlOriginal: string },
|
|
127
|
+
config: Config,
|
|
128
|
+
) {
|
|
129
|
+
const url = pageContext.urlOriginal
|
|
130
|
+
const isLandingPage = url === '/'
|
|
131
|
+
|
|
132
|
+
const { title } = activeHeading
|
|
133
|
+
let pageTitle = isLandingPage ? null : title
|
|
134
|
+
let documentTitle = activeHeading.titleDocument || jsxToTextContent(title)
|
|
135
|
+
|
|
136
|
+
if (!isLandingPage) {
|
|
137
|
+
documentTitle += ' | ' + config.projectInfo.projectName
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isLandingPage) {
|
|
141
|
+
pageTitle = null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { documentTitle, isLandingPage, pageTitle }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getActiveHeading(
|
|
148
|
+
headingsResolved: HeadingResolved[],
|
|
149
|
+
headingsDetachedResolved: HeadingDetachedResolved[],
|
|
150
|
+
pageContext: { urlOriginal: string; exports: Exports },
|
|
151
|
+
) {
|
|
152
|
+
let activeHeading: HeadingResolved | HeadingDetachedResolved | null = null
|
|
153
|
+
const { urlOriginal } = pageContext
|
|
154
|
+
assert(urlOriginal)
|
|
155
|
+
headingsResolved.forEach((heading) => {
|
|
156
|
+
if (heading.url === urlOriginal) {
|
|
157
|
+
activeHeading = heading
|
|
158
|
+
assert(heading.level === 2, { pageUrl: urlOriginal, heading })
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
const isDetachedPage = !activeHeading
|
|
162
|
+
if (!activeHeading) {
|
|
163
|
+
activeHeading = headingsDetachedResolved.find(({ url }) => urlOriginal === url) ?? null
|
|
164
|
+
}
|
|
165
|
+
if (!activeHeading) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
[
|
|
168
|
+
`URL ${pc.bold(urlOriginal)} not found in following URLs:`,
|
|
169
|
+
...headingsResolved
|
|
170
|
+
.map((h) => ` ${h.url}`)
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.sort(),
|
|
173
|
+
].join('\n'),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
return { activeHeading, isDetachedPage }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getPageSectionsResolved(
|
|
180
|
+
pageContext: { exports: Exports; urlOriginal: string },
|
|
181
|
+
activeHeading: HeadingResolved | HeadingDetachedResolved,
|
|
182
|
+
): PageSectionResolved[] {
|
|
183
|
+
const pageSections = pageContext.exports.pageSectionsExport ?? []
|
|
184
|
+
|
|
185
|
+
const pageSectionsResolved = pageSections.map((pageSection) => {
|
|
186
|
+
const pageSectionTitleJsx = parseTitle(pageSection.pageSectionTitle)
|
|
187
|
+
const url: null | string = pageSection.pageSectionId === null ? null : '#' + pageSection.pageSectionId
|
|
188
|
+
const pageSectionResolved: PageSectionResolved = {
|
|
189
|
+
url,
|
|
190
|
+
title: pageSectionTitleJsx,
|
|
191
|
+
linkBreadcrumb: [activeHeading.title, ...(activeHeading.linkBreadcrumb ?? [])],
|
|
192
|
+
titleInNav: pageSectionTitleJsx,
|
|
193
|
+
pageSectionLevel: pageSection.pageSectionLevel,
|
|
194
|
+
}
|
|
195
|
+
return pageSectionResolved
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if (activeHeading?.sectionTitles) {
|
|
199
|
+
activeHeading.sectionTitles.forEach((sectionTitle) => {
|
|
200
|
+
const pageSectionTitles = pageSections.map((h) => h.pageSectionTitle)
|
|
201
|
+
assert(pageSectionTitles.includes(sectionTitle), { pageHeadingTitles: pageSectionTitles, sectionTitle })
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return pageSectionsResolved
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* - Parse title (from `string` to `JSX.Element`)
|
|
210
|
+
* - Determine navigation breadcrumbs
|
|
211
|
+
*/
|
|
212
|
+
function getHeadingsResolved(config: {
|
|
213
|
+
headings: HeadingDefinition[]
|
|
214
|
+
headingsDetached: HeadingDetachedDefinition[]
|
|
215
|
+
}): {
|
|
216
|
+
headingsResolved: HeadingResolved[]
|
|
217
|
+
headingsDetachedResolved: HeadingDetachedResolved[]
|
|
218
|
+
} {
|
|
219
|
+
const headingsWithoutBreadcrumb: Omit<HeadingResolved, 'linkBreadcrumb'>[] = config.headings.map(
|
|
220
|
+
(heading: HeadingDefinition) => {
|
|
221
|
+
const titleParsed: JSX.Element = parseTitle(heading.title)
|
|
222
|
+
|
|
223
|
+
const titleInNav = heading.titleInNav || heading.title
|
|
224
|
+
let titleInNavParsed: JSX.Element
|
|
225
|
+
titleInNavParsed = parseTitle(titleInNav)
|
|
226
|
+
if ('titleEmoji' in heading) {
|
|
227
|
+
assert(heading.titleEmoji)
|
|
228
|
+
titleInNavParsed = withEmoji(heading.titleEmoji, titleInNavParsed)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const headingResolved: Omit<HeadingResolved, 'linkBreadcrumb'> = {
|
|
232
|
+
...heading,
|
|
233
|
+
title: titleParsed,
|
|
234
|
+
titleInNav: titleInNavParsed,
|
|
235
|
+
}
|
|
236
|
+
return headingResolved
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
const headingsResolved: HeadingResolved[] = []
|
|
241
|
+
headingsWithoutBreadcrumb.forEach((heading) => {
|
|
242
|
+
const linkBreadcrumb = getHeadingsBreadcrumb(heading, headingsResolved)
|
|
243
|
+
headingsResolved.push({
|
|
244
|
+
...heading,
|
|
245
|
+
linkBreadcrumb,
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const headingsDetachedResolved = config.headingsDetached.map((headingsDetached) => {
|
|
250
|
+
const { url, title } = headingsDetached
|
|
251
|
+
assert(
|
|
252
|
+
headingsResolved.find((heading) => heading.url === url) === undefined,
|
|
253
|
+
`remove ${headingsDetached.url} from headingsDetached`,
|
|
254
|
+
)
|
|
255
|
+
const titleParsed = typeof title === 'string' ? parseTitle(title) : title
|
|
256
|
+
return {
|
|
257
|
+
...headingsDetached,
|
|
258
|
+
level: 2 as const,
|
|
259
|
+
title: titleParsed,
|
|
260
|
+
titleInNav: titleParsed,
|
|
261
|
+
linkBreadcrumb: null,
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
return { headingsResolved, headingsDetachedResolved }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getHeadingsBreadcrumb(heading: Omit<HeadingResolved, 'linkBreadcrumb'>, headings: HeadingResolved[]) {
|
|
269
|
+
const linkBreadcrumb: JSX.Element[] = []
|
|
270
|
+
let levelCurrent = heading.level
|
|
271
|
+
headings
|
|
272
|
+
.slice()
|
|
273
|
+
.reverse()
|
|
274
|
+
.forEach((parentCandidate) => {
|
|
275
|
+
const isParent = parentCandidate.level < levelCurrent
|
|
276
|
+
if (isParent) {
|
|
277
|
+
levelCurrent = parentCandidate.level
|
|
278
|
+
linkBreadcrumb.push(parentCandidate.title)
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
return linkBreadcrumb
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function assertHeadingsDefinition(headings: { url?: null | string }[]) {
|
|
285
|
+
headings.forEach((heading) => {
|
|
286
|
+
if (heading.url) {
|
|
287
|
+
const { url } = heading
|
|
288
|
+
assert(url.startsWith('/'))
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
}
|
|
@@ -1,56 +1,34 @@
|
|
|
1
|
-
import { assert, jsxToTextContent, objectAssign } from '../utils/server'
|
|
2
|
-
import type { Heading, HeadingDetached } from '../types/Heading'
|
|
3
|
-
import type { PageContextBuiltInServer } from 'vike/types'
|
|
4
|
-
import type { MarkdownHeading } from '../markdownHeadingsVitePlugin'
|
|
5
|
-
import type { Config } from '../types/Config'
|
|
6
|
-
import { getConfig } from './getConfig'
|
|
7
|
-
import { getHeadingsWithProcessedTitle, parseTitle } from '../parseTitle'
|
|
8
|
-
|
|
9
1
|
export { resolvePageContext }
|
|
10
2
|
export type { PageContextOriginal }
|
|
11
3
|
export type { PageContextResolved }
|
|
12
|
-
export type {
|
|
4
|
+
export type { Exports }
|
|
5
|
+
|
|
6
|
+
import { objectAssign } from '../utils/server'
|
|
7
|
+
import type { PageContextServer } from 'vike/types'
|
|
8
|
+
import type { PageSection } from '../parsePageSections'
|
|
9
|
+
import { getConfig } from './getConfig'
|
|
10
|
+
import { resolveHeadingsData } from './resolveHeadingsData'
|
|
13
11
|
|
|
14
12
|
type ReactComponent = () => JSX.Element
|
|
15
13
|
type Exports = {
|
|
16
|
-
|
|
14
|
+
pageSectionsExport?: PageSection[]
|
|
17
15
|
}
|
|
18
|
-
type PageContextOriginal =
|
|
16
|
+
type PageContextOriginal = PageContextServer & {
|
|
19
17
|
Page: ReactComponent
|
|
20
18
|
exports: Exports
|
|
21
19
|
}
|
|
22
20
|
type PageContextResolved = ReturnType<typeof resolvePageContext>
|
|
23
21
|
|
|
24
22
|
function resolvePageContext(pageContext: PageContextOriginal) {
|
|
23
|
+
const pageContextResolved = {}
|
|
24
|
+
|
|
25
|
+
objectAssign(pageContextResolved, resolveHeadingsData(pageContext))
|
|
26
|
+
|
|
25
27
|
const config = getConfig()
|
|
26
|
-
const processed = getHeadingsWithProcessedTitle(config)
|
|
27
|
-
const { headingsDetachedProcessed } = processed
|
|
28
|
-
let { headingsProcessed } = processed
|
|
29
|
-
const { activeHeading, activeNavigationHeading } = findHeading(
|
|
30
|
-
headingsProcessed,
|
|
31
|
-
headingsDetachedProcessed,
|
|
32
|
-
pageContext,
|
|
33
|
-
)
|
|
34
|
-
let headingsOfDetachedPage: null | (Heading | HeadingDetached)[] = null
|
|
35
|
-
let headingsAll = [...headingsProcessed, ...headingsDetachedProcessed]
|
|
36
|
-
headingsAll = getHeadingsAll(headingsAll, pageContext, activeHeading)
|
|
37
|
-
if (activeNavigationHeading) {
|
|
38
|
-
headingsProcessed = getHeadingsAll(headingsProcessed, pageContext, activeNavigationHeading)
|
|
39
|
-
} else {
|
|
40
|
-
headingsOfDetachedPage = [activeHeading, ...getHeadingsOfTheCurrentPage(pageContext, activeHeading)]
|
|
41
|
-
}
|
|
42
|
-
const { title, isLandingPage, pageTitle } = getMetaData(
|
|
43
|
-
headingsDetachedProcessed,
|
|
44
|
-
activeNavigationHeading,
|
|
45
|
-
pageContext,
|
|
46
|
-
config,
|
|
47
|
-
)
|
|
48
28
|
const { faviconUrl, algolia, tagline, twitterHandle, bannerUrl, websiteUrl } = config
|
|
49
|
-
const pageContextResolved = {}
|
|
50
29
|
objectAssign(pageContextResolved, {
|
|
51
30
|
...pageContext,
|
|
52
31
|
meta: {
|
|
53
|
-
title,
|
|
54
32
|
faviconUrl,
|
|
55
33
|
twitterHandle,
|
|
56
34
|
bannerUrl,
|
|
@@ -58,129 +36,8 @@ function resolvePageContext(pageContext: PageContextOriginal) {
|
|
|
58
36
|
tagline,
|
|
59
37
|
algolia,
|
|
60
38
|
},
|
|
61
|
-
activeHeading,
|
|
62
|
-
headingsAll,
|
|
63
|
-
headingsProcessed,
|
|
64
|
-
headingsDetachedProcessed,
|
|
65
|
-
headingsOfDetachedPage,
|
|
66
|
-
isLandingPage,
|
|
67
|
-
pageTitle,
|
|
68
39
|
config,
|
|
69
40
|
})
|
|
70
|
-
return pageContextResolved
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function getMetaData(
|
|
74
|
-
headingsDetachedProcessed: HeadingDetached[],
|
|
75
|
-
activeNavigationHeading: Heading | null,
|
|
76
|
-
pageContext: { urlOriginal: string; exports: Exports },
|
|
77
|
-
config: Config,
|
|
78
|
-
) {
|
|
79
|
-
const url = pageContext.urlOriginal
|
|
80
|
-
|
|
81
|
-
let title: string
|
|
82
|
-
let pageTitle: string | JSX.Element | null
|
|
83
|
-
if (activeNavigationHeading) {
|
|
84
|
-
title = activeNavigationHeading.titleDocument || jsxToTextContent(activeNavigationHeading.title)
|
|
85
|
-
pageTitle = activeNavigationHeading.title
|
|
86
|
-
} else {
|
|
87
|
-
pageTitle = headingsDetachedProcessed.find((h) => h.url === url)!.title
|
|
88
|
-
title = jsxToTextContent(pageTitle)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const isLandingPage = url === '/'
|
|
92
|
-
if (!isLandingPage) {
|
|
93
|
-
title += ' | ' + config.projectInfo.projectName
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (isLandingPage) {
|
|
97
|
-
pageTitle = null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return { title, isLandingPage, pageTitle }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function findHeading(
|
|
104
|
-
headingsProcessed: Heading[],
|
|
105
|
-
headingsDetachedProcessed: HeadingDetached[],
|
|
106
|
-
pageContext: { urlOriginal: string; exports: Exports },
|
|
107
|
-
): { activeHeading: Heading | HeadingDetached; activeNavigationHeading: Heading | null } {
|
|
108
|
-
let activeNavigationHeading: Heading | null = null
|
|
109
|
-
let activeHeading: Heading | HeadingDetached | null = null
|
|
110
|
-
assert(pageContext.urlOriginal)
|
|
111
|
-
const pageUrl = pageContext.urlOriginal
|
|
112
|
-
headingsProcessed.forEach((heading) => {
|
|
113
|
-
if (heading.url === pageUrl) {
|
|
114
|
-
activeNavigationHeading = heading
|
|
115
|
-
activeHeading = heading
|
|
116
|
-
assert(heading.level === 2, { pageUrl, heading })
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
if (!activeHeading) {
|
|
120
|
-
activeHeading = headingsDetachedProcessed.find(({ url }) => pageUrl === url) ?? null
|
|
121
|
-
}
|
|
122
|
-
if (!activeHeading) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
[
|
|
125
|
-
`Heading not found for URL '${pageUrl}'`,
|
|
126
|
-
'Heading is defined for following URLs:',
|
|
127
|
-
...headingsProcessed
|
|
128
|
-
.map((h) => ` ${h.url}`)
|
|
129
|
-
.filter(Boolean)
|
|
130
|
-
.sort(),
|
|
131
|
-
].join('\n'),
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
return { activeHeading, activeNavigationHeading }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getHeadingsAll<T extends Heading | HeadingDetached>(
|
|
138
|
-
headingsProcessed: T[],
|
|
139
|
-
pageContext: { exports: Exports; urlOriginal: string },
|
|
140
|
-
activeHeading: T,
|
|
141
|
-
): T[] {
|
|
142
|
-
const headingsAll = headingsProcessed.slice()
|
|
143
|
-
|
|
144
|
-
const headingsOfTheCurrentPage = getHeadingsOfTheCurrentPage(pageContext, activeHeading)
|
|
145
|
-
|
|
146
|
-
const activeHeadingIdx = headingsAll.indexOf(activeHeading)
|
|
147
|
-
assert(activeHeadingIdx >= 0)
|
|
148
|
-
headingsOfTheCurrentPage.forEach((pageHeading, i) => {
|
|
149
|
-
headingsAll.splice(activeHeadingIdx + 1 + i, 0, pageHeading as T)
|
|
150
|
-
})
|
|
151
41
|
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function getHeadingsOfTheCurrentPage(
|
|
156
|
-
pageContext: { exports: Exports; urlOriginal: string },
|
|
157
|
-
currentHeading: Heading | HeadingDetached,
|
|
158
|
-
) {
|
|
159
|
-
const headingsOfCurrentPage: Heading[] = []
|
|
160
|
-
|
|
161
|
-
const headingsExport = pageContext.exports.headings ?? []
|
|
162
|
-
|
|
163
|
-
headingsExport.forEach((markdownHeading) => {
|
|
164
|
-
const title = parseTitle(markdownHeading.title)
|
|
165
|
-
const url: null | string = markdownHeading.headingId && '#' + markdownHeading.headingId
|
|
166
|
-
if (markdownHeading.headingLevel === 2) {
|
|
167
|
-
const heading: Heading = {
|
|
168
|
-
url,
|
|
169
|
-
title,
|
|
170
|
-
headingsBreadcrumb: [currentHeading, ...(currentHeading.headingsBreadcrumb ?? [])],
|
|
171
|
-
titleInNav: title,
|
|
172
|
-
level: 3,
|
|
173
|
-
}
|
|
174
|
-
headingsOfCurrentPage.push(heading)
|
|
175
|
-
}
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
if (currentHeading?.sectionTitles) {
|
|
179
|
-
currentHeading.sectionTitles.forEach((sectionTitle) => {
|
|
180
|
-
const pageHeadingTitles = headingsExport.map((h) => h.title)
|
|
181
|
-
assert(pageHeadingTitles.includes(sectionTitle), { pageHeadingTitles, sectionTitle })
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return headingsOfCurrentPage
|
|
42
|
+
return pageContextResolved
|
|
186
43
|
}
|
package/dist/+config.js
CHANGED
|
@@ -5,11 +5,11 @@ export default {
|
|
|
5
5
|
client: 'import:@brillout/docpress/renderer/client:doesNotExist',
|
|
6
6
|
meta: {
|
|
7
7
|
Page: {
|
|
8
|
-
env: { client: false, server: true }
|
|
8
|
+
env: { client: false, server: true },
|
|
9
9
|
},
|
|
10
10
|
// Vike already defines the setting 'name', but we redundantly define it here for older Vike versions (otherwise older Vike versions will complain that 'name` is an unknown config).
|
|
11
11
|
name: {
|
|
12
|
-
env: { config: true }
|
|
13
|
-
}
|
|
14
|
-
}
|
|
12
|
+
env: { config: true },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
15
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { parsePageSections };
|
|
2
|
+
export type { PageSection };
|
|
3
|
+
type PageSection = {
|
|
4
|
+
pageSectionTitle: string;
|
|
5
|
+
pageSectionId: string | null;
|
|
6
|
+
pageSectionLevel: number;
|
|
7
|
+
};
|
|
8
|
+
declare function parsePageSections(): {
|
|
9
|
+
name: string;
|
|
10
|
+
enforce: string;
|
|
11
|
+
transform: (code: string, id: string) => Promise<string | undefined>;
|
|
12
|
+
};
|