@docsector/docsector-reader 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/bin/docsector.js +13 -2
- package/docsector.config.js +8 -0
- package/package.json +1 -1
- package/src/components/DPageBar.vue +24 -2
- package/src/components/DPageMeta.vue +9 -1
- package/src/components/DPageSection.vue +24 -4
- package/src/css/app.sass +1 -1
- package/src/i18n/helpers.js +24 -1
- package/src/i18n/index.js +2 -1
- package/src/index.js +13 -0
- package/src/quasar.factory.js +195 -24
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
41
41
|
## ✨ Features
|
|
42
42
|
|
|
43
43
|
- 📝 **Markdown Rendering** — Write docs in Markdown, rendered with syntax highlighting (Prism.js)
|
|
44
|
+
- 🧱 **Raw HTML in Markdown** — Renders inline and block HTML tags inside markdown sections (including homepage remote README content)
|
|
44
45
|
- 🧩 **Mermaid Diagrams** — Native support for fenced ` ```mermaid ` blocks, with automatic dark/light theme switching
|
|
45
46
|
- 🚨 **GitHub-Style Alerts** — Native support for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]`
|
|
46
47
|
- 🌍 **Internationalization (i18n)** — Multi-language support with HJSON locale files and per-page translations
|
|
@@ -58,6 +59,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
58
59
|
- 🔐 **Web Bot Auth** — Can publish a signed HTTP message signatures directory and includes helpers to sign outbound bot requests
|
|
59
60
|
- 🧭 **Content Signals** — Injects `Content-Signal` policy in `robots.txt` with deterministic, idempotent build output
|
|
60
61
|
- 🏠 **Markdown Home at Root** — Homepage is rendered from `src/pages/Homepage.{lang}.md` directly at `/`
|
|
62
|
+
- 🌍 **Remote README as Home** — Optional build-time remote README source for homepage with automatic local fallback
|
|
63
|
+
- 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
|
|
61
64
|
- 🧭 **Quick Links Custom Element** — Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
|
|
62
65
|
- 🗂️ **API Catalog Well-Known** — Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
|
|
63
66
|
- ⚙️ **Single Config File** — Customize branding, links, and languages via `docsector.config.js`
|
|
@@ -333,6 +336,39 @@ Set any target to `null` or `false` to disable that relation.
|
|
|
333
336
|
|
|
334
337
|
---
|
|
335
338
|
|
|
339
|
+
## 🏠 Remote README as Home
|
|
340
|
+
|
|
341
|
+
You can configure Docsector Reader to use a remote README as homepage content.
|
|
342
|
+
|
|
343
|
+
- Fetch happens at build-time.
|
|
344
|
+
- The same README content is used for all configured languages.
|
|
345
|
+
- If fetch fails, it falls back to local `src/pages/Homepage.{lang}.md` by default.
|
|
346
|
+
|
|
347
|
+
### Configure
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
export default {
|
|
351
|
+
// ...other config
|
|
352
|
+
|
|
353
|
+
homePage: {
|
|
354
|
+
source: 'remote-readme',
|
|
355
|
+
remoteReadmeUrl: 'https://raw.githubusercontent.com/your-org/your-repo/main/README.md',
|
|
356
|
+
timeoutMs: 8000,
|
|
357
|
+
fallbackToLocal: true
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Validate
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
npx docsector build
|
|
366
|
+
cat dist/spa/homepage.md
|
|
367
|
+
cat dist/spa/homepage.en-US.md
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
336
372
|
## 🔐 Web Bot Auth
|
|
337
373
|
|
|
338
374
|
Docsector Reader can publish a signed Web Bot Auth directory at:
|
|
@@ -733,6 +769,7 @@ Consumer projects use the `buildMessages` helper from the engine:
|
|
|
733
769
|
|
|
734
770
|
```javascript
|
|
735
771
|
import { buildMessages } from '@docsector/docsector-reader/i18n'
|
|
772
|
+
import homePageOverride from 'virtual:docsector-homepage-override'
|
|
736
773
|
|
|
737
774
|
const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
|
|
738
775
|
const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
|
|
@@ -740,7 +777,7 @@ const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?r
|
|
|
740
777
|
import boot from 'pages/boot'
|
|
741
778
|
import pages from 'pages'
|
|
742
779
|
|
|
743
|
-
export default buildMessages({ langModules, mdModules, pages, boot })
|
|
780
|
+
export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
|
|
744
781
|
```
|
|
745
782
|
|
|
746
783
|
### Language files
|
package/bin/docsector.js
CHANGED
|
@@ -23,7 +23,7 @@ const packageRoot = resolve(__dirname, '..')
|
|
|
23
23
|
const args = process.argv.slice(2)
|
|
24
24
|
const command = args[0]
|
|
25
25
|
|
|
26
|
-
const VERSION = '1.
|
|
26
|
+
const VERSION = '1.7.1'
|
|
27
27
|
|
|
28
28
|
const HELP = `
|
|
29
29
|
Docsector Reader v${VERSION}
|
|
@@ -165,6 +165,16 @@ export default {
|
|
|
165
165
|
// }
|
|
166
166
|
// },
|
|
167
167
|
|
|
168
|
+
// @ Home page source (optional)
|
|
169
|
+
// Use a remote README.md as homepage content at build-time.
|
|
170
|
+
// Falls back to local src/pages/Homepage.{lang}.md on fetch failure by default.
|
|
171
|
+
// homePage: {
|
|
172
|
+
// source: 'remote-readme', // 'local' | 'remote-readme'
|
|
173
|
+
// remoteReadmeUrl: 'https://raw.githubusercontent.com/your-org/your-repo/main/README.md',
|
|
174
|
+
// timeoutMs: 8000,
|
|
175
|
+
// fallbackToLocal: true
|
|
176
|
+
// },
|
|
177
|
+
|
|
168
178
|
// @ Homepage Link headers for agent discovery (optional)
|
|
169
179
|
// linkHeaders: {
|
|
170
180
|
// enabled: true,
|
|
@@ -254,6 +264,7 @@ const TEMPLATE_CSS_STUB = `\
|
|
|
254
264
|
const TEMPLATE_I18N_INDEX = `\
|
|
255
265
|
// @ Import i18n message builder from Docsector Reader
|
|
256
266
|
import { buildMessages } from '@docsector/docsector-reader/i18n'
|
|
267
|
+
import homePageOverride from 'virtual:docsector-homepage-override'
|
|
257
268
|
|
|
258
269
|
// @ Import language HJSON files (Vite-compatible eager import)
|
|
259
270
|
const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
|
|
@@ -264,7 +275,7 @@ const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?r
|
|
|
264
275
|
import boot from 'pages/boot'
|
|
265
276
|
import pages from 'pages'
|
|
266
277
|
|
|
267
|
-
export default buildMessages({ langModules, mdModules, pages, boot })
|
|
278
|
+
export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
|
|
268
279
|
`
|
|
269
280
|
|
|
270
281
|
const TEMPLATE_I18N_HJSON = `\
|
package/docsector.config.js
CHANGED
|
@@ -43,6 +43,14 @@ export default {
|
|
|
43
43
|
toolSuffix: 'docsector'
|
|
44
44
|
},
|
|
45
45
|
|
|
46
|
+
// @ Home page source
|
|
47
|
+
homePage: {
|
|
48
|
+
source: 'remote-readme',
|
|
49
|
+
remoteReadmeUrl: 'https://raw.githubusercontent.com/docsector/docsector-reader/main/README.md',
|
|
50
|
+
timeoutMs: 8000,
|
|
51
|
+
fallbackToLocal: true
|
|
52
|
+
},
|
|
53
|
+
|
|
46
54
|
// @ Languages
|
|
47
55
|
languages: [
|
|
48
56
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
|
@@ -71,6 +71,13 @@ const rawMarkdown = computed(() => {
|
|
|
71
71
|
|
|
72
72
|
const markdownURL = computed(() => {
|
|
73
73
|
if (store.state.page.base === 'home') {
|
|
74
|
+
const homePage = docsectorConfig.homePage || {}
|
|
75
|
+
const isRemoteHome = homePage.source === 'remote-readme' && typeof homePage.remoteReadmeUrl === 'string' && homePage.remoteReadmeUrl.length > 0
|
|
76
|
+
|
|
77
|
+
if (isRemoteHome) {
|
|
78
|
+
return homePage.remoteReadmeUrl
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
return `/Homepage.${locale.value}.md`
|
|
75
82
|
}
|
|
76
83
|
|
|
@@ -87,12 +94,27 @@ const fullMarkdownURL = computed(() => {
|
|
|
87
94
|
return `${window.location.origin}${path}.md`
|
|
88
95
|
})
|
|
89
96
|
|
|
97
|
+
const chatSourceURL = computed(() => {
|
|
98
|
+
if (store.state.page.base !== 'home') {
|
|
99
|
+
return fullMarkdownURL.value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const homePage = docsectorConfig.homePage || {}
|
|
103
|
+
const isRemoteHome = homePage.source === 'remote-readme' && typeof homePage.remoteReadmeUrl === 'string' && homePage.remoteReadmeUrl.length > 0
|
|
104
|
+
|
|
105
|
+
if (isRemoteHome) {
|
|
106
|
+
return `${window.location.origin}/`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return fullMarkdownURL.value
|
|
110
|
+
})
|
|
111
|
+
|
|
90
112
|
const chatgptURL = computed(() => {
|
|
91
|
-
const prompt = `Read ${
|
|
113
|
+
const prompt = `Read ${chatSourceURL.value} and answer questions about the content.`
|
|
92
114
|
return `https://chat.openai.com/?q=${encodeURIComponent(prompt)}`
|
|
93
115
|
})
|
|
94
116
|
const claudeURL = computed(() => {
|
|
95
|
-
const prompt = `Read ${
|
|
117
|
+
const prompt = `Read ${chatSourceURL.value} and answer questions about the content.`
|
|
96
118
|
return `https://claude.ai/new?q=${encodeURIComponent(prompt)}`
|
|
97
119
|
})
|
|
98
120
|
|
|
@@ -157,11 +157,19 @@ const next = computed(() => {
|
|
|
157
157
|
|
|
158
158
|
return ''
|
|
159
159
|
})
|
|
160
|
+
|
|
161
|
+
const hideRemoteHomeFooterMeta = computed(() => {
|
|
162
|
+
const homePage = docsectorConfig.homePage || {}
|
|
163
|
+
const isRemoteHome = homePage.source === 'remote-readme' && typeof homePage.remoteReadmeUrl === 'string' && homePage.remoteReadmeUrl.length > 0
|
|
164
|
+
const isHomePage = route.path === '/' || store.state.page.base === 'home'
|
|
165
|
+
|
|
166
|
+
return isRemoteHome && isHomePage
|
|
167
|
+
})
|
|
160
168
|
</script>
|
|
161
169
|
|
|
162
170
|
<template>
|
|
163
171
|
<div id="d-page-meta">
|
|
164
|
-
<div class="row justify-between q-mt-lg">
|
|
172
|
+
<div v-if="!hideRemoteHomeFooterMeta" class="row justify-between q-mt-lg">
|
|
165
173
|
<div id="d-page-edit" class="col">
|
|
166
174
|
<q-btn dense no-caps text-color="black" :color="color" @click="openURL(URL)" aria-label="Edit page on Github">
|
|
167
175
|
<q-icon class="q-mr-xs" name="fab fa-github" size="20px" />
|
|
@@ -130,16 +130,21 @@ const tokenized = computed(() => {
|
|
|
130
130
|
|
|
131
131
|
const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(normalizedSource)
|
|
132
132
|
|
|
133
|
-
const Markdown = new MarkdownIt(
|
|
133
|
+
const Markdown = new MarkdownIt({
|
|
134
|
+
// Home remote README may contain raw HTML blocks (badges, centered headers, etc.)
|
|
135
|
+
html: true
|
|
136
|
+
})
|
|
134
137
|
Markdown.use(attrs, {
|
|
135
138
|
leftDelimiter: ':',
|
|
136
139
|
rightDelimiter: ';',
|
|
137
140
|
allowedAttributes: ['filename']
|
|
138
141
|
})
|
|
139
142
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
const MarkdownInline = new MarkdownIt(
|
|
143
|
+
// Keep inline rendering aligned with block parsing so raw HTML inline
|
|
144
|
+
// fragments (e.g. <b>, <a>) are rendered instead of escaped.
|
|
145
|
+
const MarkdownInline = new MarkdownIt({
|
|
146
|
+
html: true
|
|
147
|
+
})
|
|
143
148
|
|
|
144
149
|
const markdownEnv = {}
|
|
145
150
|
|
|
@@ -323,6 +328,12 @@ const tokenized = computed(() => {
|
|
|
323
328
|
})
|
|
324
329
|
break
|
|
325
330
|
}
|
|
331
|
+
case 'html_block':
|
|
332
|
+
tokens.push({
|
|
333
|
+
tag: 'html',
|
|
334
|
+
content: element.content
|
|
335
|
+
})
|
|
336
|
+
break
|
|
326
337
|
}
|
|
327
338
|
} else if (level === 1) {
|
|
328
339
|
const parent = tokens[tokens.length - 1]
|
|
@@ -370,6 +381,10 @@ const tokenized = computed(() => {
|
|
|
370
381
|
case 'inline':
|
|
371
382
|
parent.content += element.content
|
|
372
383
|
break
|
|
384
|
+
case 'html_inline':
|
|
385
|
+
case 'html_block':
|
|
386
|
+
parent.content += element.content
|
|
387
|
+
break
|
|
373
388
|
|
|
374
389
|
case 'list_item_close':
|
|
375
390
|
parent.content += '</li>'
|
|
@@ -450,6 +465,11 @@ const tokenized = computed(() => {
|
|
|
450
465
|
<table v-html="token.content"></table>
|
|
451
466
|
</div>
|
|
452
467
|
|
|
468
|
+
<div
|
|
469
|
+
v-else-if="token.tag === 'html'"
|
|
470
|
+
v-html="token.content"
|
|
471
|
+
></div>
|
|
472
|
+
|
|
453
473
|
<p
|
|
454
474
|
v-else-if="token.tag === 'p'"
|
|
455
475
|
v-html="token.content"
|
package/src/css/app.sass
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* --- Docsector Reader --- */
|
|
2
2
|
@font-face
|
|
3
3
|
font-family: "Fira Code Nerd Font"
|
|
4
|
-
src:
|
|
4
|
+
src: local('Fira Code Nerd Font'), local('FiraCode Nerd Font')
|
|
5
5
|
font-weight: normal
|
|
6
6
|
font-style: normal
|
|
7
7
|
// * General
|
package/src/i18n/helpers.js
CHANGED
|
@@ -117,9 +117,10 @@ export function filter (source) {
|
|
|
117
117
|
* @param {Object} options.pages - Page registry from pages/index.js
|
|
118
118
|
* @param {Object} options.boot - Boot meta from pages/boot.js
|
|
119
119
|
* @param {string[]} [options.langs] - Language codes to process (auto-detected from langModules if omitted)
|
|
120
|
+
* @param {Object<string,string>} [options.homePageOverride] - Optional per-language Home markdown override
|
|
120
121
|
* @returns {Object} Complete i18n messages object keyed by locale
|
|
121
122
|
*/
|
|
122
|
-
export function buildMessages ({ langModules, mdModules, pages, boot, langs }) {
|
|
123
|
+
export function buildMessages ({ langModules, mdModules, pages, boot, langs, homePageOverride = {} }) {
|
|
123
124
|
// Auto-detect languages from HJSON files if not provided
|
|
124
125
|
if (!langs) {
|
|
125
126
|
langs = Object.keys(langModules).map(key => {
|
|
@@ -145,6 +146,11 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs }) {
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
function loadHomepage (lang) {
|
|
149
|
+
const override = homePageOverride?.[lang] ?? homePageOverride?.['en-US']
|
|
150
|
+
if (typeof override === 'string' && override.length > 0) {
|
|
151
|
+
return filter(override)
|
|
152
|
+
}
|
|
153
|
+
|
|
148
154
|
const key = `../pages/Homepage.${lang}.md`
|
|
149
155
|
const fallbackKey = '../pages/Homepage.en-US.md'
|
|
150
156
|
|
|
@@ -159,6 +165,23 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs }) {
|
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
function extractHeadingFromHomepage (lang) {
|
|
168
|
+
const override = homePageOverride?.[lang] ?? homePageOverride?.['en-US']
|
|
169
|
+
if (typeof override === 'string' && override.length > 0) {
|
|
170
|
+
const htmlHeadingMatch = override.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i)
|
|
171
|
+
if (htmlHeadingMatch) {
|
|
172
|
+
const htmlHeading = htmlHeadingMatch[1]
|
|
173
|
+
.replace(/<[^>]+>/g, ' ')
|
|
174
|
+
.replace(/\s+/g, ' ')
|
|
175
|
+
.trim()
|
|
176
|
+
if (htmlHeading) {
|
|
177
|
+
return htmlHeading
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const overrideMatch = override.match(/^#\s+(.+)$/m)
|
|
182
|
+
return overrideMatch ? overrideMatch[1].trim() : ''
|
|
183
|
+
}
|
|
184
|
+
|
|
162
185
|
const key = `../pages/Homepage.${lang}.md`
|
|
163
186
|
const fallbackKey = '../pages/Homepage.en-US.md'
|
|
164
187
|
|
package/src/i18n/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ Import i18n message builder
|
|
2
2
|
import { buildMessages } from './helpers'
|
|
3
|
+
import homePageOverride from 'virtual:docsector-homepage-override'
|
|
3
4
|
|
|
4
5
|
// @ Import language HJSON files (Vite-compatible eager import)
|
|
5
6
|
const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
|
|
@@ -10,4 +11,4 @@ const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?r
|
|
|
10
11
|
import boot from 'pages/boot'
|
|
11
12
|
import pages from 'pages'
|
|
12
13
|
|
|
13
|
-
export default buildMessages({ langModules, mdModules, pages, boot })
|
|
14
|
+
export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
|
package/src/index.js
CHANGED
|
@@ -98,6 +98,11 @@
|
|
|
98
98
|
* @param {boolean} [config.webMcp.tools.getPage=true] - Enables tool get_page
|
|
99
99
|
* @param {boolean} [config.webMcp.tools.navigateTo=true] - Enables tool navigate_to
|
|
100
100
|
* @param {boolean} [config.webMcp.tools.copyCurrentPage=true] - Enables tool copy_current_page
|
|
101
|
+
* @param {Object} [config.homePage] - Home page content source settings
|
|
102
|
+
* @param {'local'|'remote-readme'} [config.homePage.source='local'] - Source strategy for home page markdown
|
|
103
|
+
* @param {string|null} [config.homePage.remoteReadmeUrl=null] - Absolute URL of remote README markdown when source is remote-readme
|
|
104
|
+
* @param {number} [config.homePage.timeoutMs=8000] - Timeout in milliseconds for remote README fetch during build
|
|
105
|
+
* @param {boolean} [config.homePage.fallbackToLocal=true] - Fallback to local Homepage.{lang}.md when remote fetch fails
|
|
101
106
|
* @returns {Object} Resolved Docsector configuration
|
|
102
107
|
*/
|
|
103
108
|
export function createDocsector (config = {}) {
|
|
@@ -217,6 +222,14 @@ export function createDocsector (config = {}) {
|
|
|
217
222
|
copyCurrentPage: true,
|
|
218
223
|
...(config.webMcp?.tools || {})
|
|
219
224
|
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
homePage: {
|
|
228
|
+
source: 'local',
|
|
229
|
+
remoteReadmeUrl: null,
|
|
230
|
+
timeoutMs: 8000,
|
|
231
|
+
fallbackToLocal: true,
|
|
232
|
+
...config.homePage
|
|
220
233
|
}
|
|
221
234
|
}
|
|
222
235
|
}
|
package/src/quasar.factory.js
CHANGED
|
@@ -141,7 +141,6 @@ function createPrerenderMetaPlugin (projectRoot) {
|
|
|
141
141
|
async closeBundle () {
|
|
142
142
|
const distDir = resolve(projectRoot, 'dist', 'spa')
|
|
143
143
|
const baseHtmlPath = resolve(distDir, 'index.html')
|
|
144
|
-
|
|
145
144
|
if (!existsSync(baseHtmlPath)) return
|
|
146
145
|
|
|
147
146
|
const baseHtml = readFileSync(baseHtmlPath, 'utf-8')
|
|
@@ -421,6 +420,148 @@ function createGitDatesPlugin (projectRoot) {
|
|
|
421
420
|
}
|
|
422
421
|
}
|
|
423
422
|
|
|
423
|
+
function getHomePageConfig (config = {}) {
|
|
424
|
+
const homePage = config.homePage || {}
|
|
425
|
+
return {
|
|
426
|
+
source: homePage.source || 'local',
|
|
427
|
+
remoteReadmeUrl: homePage.remoteReadmeUrl || null,
|
|
428
|
+
timeoutMs: Number.isFinite(homePage.timeoutMs)
|
|
429
|
+
? Math.max(1000, Number(homePage.timeoutMs))
|
|
430
|
+
: 8000,
|
|
431
|
+
fallbackToLocal: homePage.fallbackToLocal !== false
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function getConfiguredLanguages (config = {}) {
|
|
436
|
+
const defaultLang = config.defaultLanguage || config.languages?.[0]?.value || 'en-US'
|
|
437
|
+
const languageValues = config.languages?.map(language => language.value).filter(Boolean) || []
|
|
438
|
+
const langs = [...new Set([defaultLang, ...languageValues])]
|
|
439
|
+
return { defaultLang, langs }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function fetchRemoteMarkdown (url, timeoutMs = 8000) {
|
|
443
|
+
const controller = new AbortController()
|
|
444
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const response = await fetch(url, {
|
|
448
|
+
headers: {
|
|
449
|
+
Accept: 'text/markdown, text/plain;q=0.9, */*;q=0.8'
|
|
450
|
+
},
|
|
451
|
+
signal: controller.signal
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
if (!response.ok) {
|
|
455
|
+
throw new Error(`Remote README request failed with status ${response.status}`)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return await response.text()
|
|
459
|
+
} finally {
|
|
460
|
+
clearTimeout(timeout)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
|
|
465
|
+
const { logPrefix = '[docsector]' } = options
|
|
466
|
+
const pagesDir = resolve(projectRoot, 'src', 'pages')
|
|
467
|
+
const { defaultLang, langs } = getConfiguredLanguages(config)
|
|
468
|
+
const homePageConfig = getHomePageConfig(config)
|
|
469
|
+
|
|
470
|
+
const byLang = {}
|
|
471
|
+
let mode = 'local'
|
|
472
|
+
|
|
473
|
+
if (homePageConfig.source === 'remote-readme' && homePageConfig.remoteReadmeUrl) {
|
|
474
|
+
try {
|
|
475
|
+
const remote = await fetchRemoteMarkdown(homePageConfig.remoteReadmeUrl, homePageConfig.timeoutMs)
|
|
476
|
+
for (const lang of langs) {
|
|
477
|
+
byLang[lang] = remote
|
|
478
|
+
}
|
|
479
|
+
mode = 'remote-readme'
|
|
480
|
+
console.log(`\x1b[36m${logPrefix}\x1b[0m Loaded remote README for home page`)
|
|
481
|
+
return { mode, byLang, defaultLang, langs }
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const reason = error?.message || String(error)
|
|
484
|
+
console.warn(`${logPrefix} Failed to load remote README for home page: ${reason}`)
|
|
485
|
+
|
|
486
|
+
if (!homePageConfig.fallbackToLocal) {
|
|
487
|
+
throw error
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for (const lang of langs) {
|
|
493
|
+
const homepage = resolve(pagesDir, `Homepage.${lang}.md`)
|
|
494
|
+
if (existsSync(homepage)) {
|
|
495
|
+
byLang[lang] = readFileSync(homepage, 'utf-8')
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const fallback = resolve(pagesDir, `Homepage.${defaultLang}.md`)
|
|
500
|
+
if (existsSync(fallback)) {
|
|
501
|
+
byLang[lang] = readFileSync(fallback, 'utf-8')
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { mode, byLang, defaultLang, langs }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function createHomePageOverridePlugin (projectRoot) {
|
|
509
|
+
const virtualId = 'virtual:docsector-homepage-override'
|
|
510
|
+
const resolvedId = '\0' + virtualId
|
|
511
|
+
let byLang = null
|
|
512
|
+
let loadPromise = null
|
|
513
|
+
|
|
514
|
+
const ensureSources = async () => {
|
|
515
|
+
if (byLang) return byLang
|
|
516
|
+
if (!loadPromise) {
|
|
517
|
+
loadPromise = (async () => {
|
|
518
|
+
const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
|
|
519
|
+
const { default: config } = await import(configUrl)
|
|
520
|
+
const sources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
|
|
521
|
+
byLang = sources.byLang
|
|
522
|
+
return byLang
|
|
523
|
+
})().finally(() => {
|
|
524
|
+
loadPromise = null
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return loadPromise
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
name: 'docsector-homepage-override',
|
|
533
|
+
resolveId (id) {
|
|
534
|
+
if (id === virtualId) return resolvedId
|
|
535
|
+
},
|
|
536
|
+
async buildStart () {
|
|
537
|
+
await ensureSources()
|
|
538
|
+
},
|
|
539
|
+
configureServer () {
|
|
540
|
+
ensureSources().catch((error) => {
|
|
541
|
+
console.warn(`[docsector] Could not prepare home page override: ${error?.message || String(error)}`)
|
|
542
|
+
})
|
|
543
|
+
},
|
|
544
|
+
async load (id) {
|
|
545
|
+
if (id === resolvedId) {
|
|
546
|
+
await ensureSources()
|
|
547
|
+
return `export default ${JSON.stringify(byLang || {})}`
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
await ensureSources()
|
|
551
|
+
if (!byLang) return null
|
|
552
|
+
|
|
553
|
+
const match = id.match(/Homepage\.([A-Za-z0-9-]+)\.md\?raw(?:$|&)/)
|
|
554
|
+
if (!match) return null
|
|
555
|
+
|
|
556
|
+
const lang = match[1]
|
|
557
|
+
const content = byLang[lang]
|
|
558
|
+
if (typeof content !== 'string') return null
|
|
559
|
+
|
|
560
|
+
return `export default ${JSON.stringify(content)}`
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
424
565
|
/**
|
|
425
566
|
* Create a Vite plugin that serves raw Markdown content for `.md` suffixed URLs.
|
|
426
567
|
*
|
|
@@ -496,32 +637,63 @@ function createMarkdownEndpointPlugin (projectRoot) {
|
|
|
496
637
|
name: 'docsector-markdown-endpoint',
|
|
497
638
|
|
|
498
639
|
configureServer (server) {
|
|
499
|
-
// Read default language from config
|
|
500
640
|
let defaultLang = 'en-US'
|
|
501
641
|
let markdownNegotiationEnabled = true
|
|
502
642
|
let markdownAgentFallback = true
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
643
|
+
let homepageByLang = null
|
|
644
|
+
|
|
645
|
+
const configReady = (async () => {
|
|
646
|
+
try {
|
|
647
|
+
const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
|
|
648
|
+
const { default: config } = await import(configUrl)
|
|
649
|
+
|
|
650
|
+
defaultLang = config.defaultLanguage || config.languages?.[0]?.value || 'en-US'
|
|
651
|
+
const markdownNegotiationConfig = config.markdownNegotiation || {}
|
|
652
|
+
markdownNegotiationEnabled = markdownNegotiationConfig.enabled !== false
|
|
653
|
+
markdownAgentFallback = markdownNegotiationConfig.agentFallback !== false
|
|
654
|
+
|
|
655
|
+
const sources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
|
|
656
|
+
homepageByLang = sources.byLang
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.warn(`[docsector] Could not load config for markdown endpoint: ${error?.message || String(error)}`)
|
|
516
659
|
}
|
|
517
|
-
}
|
|
660
|
+
})()
|
|
661
|
+
|
|
662
|
+
server.middlewares.use(async (req, res, next) => {
|
|
663
|
+
await configReady
|
|
518
664
|
|
|
519
|
-
server.middlewares.use((req, res, next) => {
|
|
520
665
|
const url = new URL(req.url, 'http://localhost')
|
|
521
666
|
const accept = (req.headers.accept || '').toLowerCase()
|
|
522
667
|
const wantsMarkdown = accept.includes('text/markdown')
|
|
523
668
|
const lang = url.searchParams.get('lang') || defaultLang
|
|
524
669
|
|
|
670
|
+
const homepagePath = url.pathname === '/' || url.pathname === '/index.html'
|
|
671
|
+
const remoteHomepage = homepageByLang?.[lang] || homepageByLang?.[defaultLang] || null
|
|
672
|
+
|
|
673
|
+
const homepageMarkdownMatch = url.pathname.match(/^\/homepage(?:\.([A-Za-z0-9-]+))?\.md$/i)
|
|
674
|
+
if (homepageMarkdownMatch) {
|
|
675
|
+
const requestedLang = homepageMarkdownMatch[1] || lang
|
|
676
|
+
const homepageMarkdown = homepageByLang?.[requestedLang] || homepageByLang?.[defaultLang] || null
|
|
677
|
+
|
|
678
|
+
if (typeof homepageMarkdown === 'string' && homepageMarkdown.length > 0) {
|
|
679
|
+
res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
|
|
680
|
+
res.setHeader('Vary', 'Accept')
|
|
681
|
+
res.setHeader('x-markdown-tokens', String(estimateMarkdownTokens(homepageMarkdown)))
|
|
682
|
+
res.end(homepageMarkdown)
|
|
683
|
+
return
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (homepagePath && typeof remoteHomepage === 'string' && remoteHomepage.length > 0) {
|
|
688
|
+
if ((markdownNegotiationEnabled && wantsMarkdown) || (markdownAgentFallback && LLM_BOT_PATTERN.test(req.headers['user-agent'] || ''))) {
|
|
689
|
+
res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
|
|
690
|
+
res.setHeader('Vary', 'Accept')
|
|
691
|
+
res.setHeader('x-markdown-tokens', String(estimateMarkdownTokens(remoteHomepage)))
|
|
692
|
+
res.end(remoteHomepage)
|
|
693
|
+
return
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
525
697
|
// Explicit .md request
|
|
526
698
|
if (url.pathname.endsWith('.md')) {
|
|
527
699
|
const file = resolveMarkdownFile(url.pathname, lang)
|
|
@@ -620,16 +792,14 @@ function createMarkdownBuildPlugin (projectRoot) {
|
|
|
620
792
|
}
|
|
621
793
|
|
|
622
794
|
// Generate homepage markdown files so root content can be negotiated in production
|
|
623
|
-
const
|
|
624
|
-
const allLangs = [...new Set([defaultLang, ...languageValues])]
|
|
795
|
+
const homepageSources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
|
|
625
796
|
let homepageCount = 0
|
|
626
|
-
for (const lang of
|
|
627
|
-
const
|
|
628
|
-
if (
|
|
797
|
+
for (const lang of homepageSources.langs) {
|
|
798
|
+
const homepageContent = homepageSources.byLang?.[lang]
|
|
799
|
+
if (typeof homepageContent !== 'string' || homepageContent.length === 0) continue
|
|
629
800
|
|
|
630
|
-
const homepageContent = readFileSync(homepageSrc, 'utf-8')
|
|
631
801
|
writeFileSync(resolve(distDir, `homepage.${lang}.md`), homepageContent)
|
|
632
|
-
if (lang === defaultLang) {
|
|
802
|
+
if (lang === homepageSources.defaultLang) {
|
|
633
803
|
writeFileSync(resolve(distDir, 'homepage.md'), homepageContent)
|
|
634
804
|
}
|
|
635
805
|
homepageCount++
|
|
@@ -1705,6 +1875,7 @@ export function createQuasarConfig (options = {}) {
|
|
|
1705
1875
|
|
|
1706
1876
|
vitePlugins: [
|
|
1707
1877
|
createHjsonPlugin(),
|
|
1878
|
+
createHomePageOverridePlugin(projectRoot),
|
|
1708
1879
|
createGitDatesPlugin(projectRoot),
|
|
1709
1880
|
createMarkdownEndpointPlugin(projectRoot),
|
|
1710
1881
|
createMarkdownBuildPlugin(projectRoot),
|