svelte-docsmith 0.11.0 → 0.12.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/dist/buildtime/archive.js +5 -3
- package/dist/buildtime/cli/archive-version.js +16 -5
- package/dist/buildtime/git.d.ts +30 -2
- package/dist/buildtime/git.js +58 -8
- package/dist/buildtime/paths.d.ts +8 -3
- package/dist/buildtime/paths.js +12 -4
- package/dist/buildtime/vite/collect.d.ts +25 -16
- package/dist/buildtime/vite/collect.js +100 -106
- package/dist/buildtime/vite/extract.d.ts +0 -2
- package/dist/buildtime/vite/extract.js +1 -1
- package/dist/buildtime/vite/index.d.ts +0 -2
- package/dist/buildtime/vite/index.js +53 -22
- package/dist/buildtime/vite/pages.d.ts +51 -0
- package/dist/buildtime/vite/pages.js +57 -0
- package/dist/buildtime/vite/releases.d.ts +0 -2
- package/dist/buildtime/vite/releases.js +0 -5
- package/dist/components/layouts/copy-page-menu.svelte +4 -2
- package/dist/components/layouts/docs-header.svelte +9 -5
- package/dist/components/layouts/docs-sidebar.svelte +1 -1
- package/dist/components/layouts/seo-head.svelte +7 -5
- package/dist/core/changelog.d.ts +3 -0
- package/dist/core/docs-page.js +2 -4
- package/dist/core/version.js +5 -9
- package/dist/generate/feed.d.ts +4 -1
- package/dist/generate/feed.js +7 -19
- package/dist/generate/llms.d.ts +4 -1
- package/dist/generate/llms.js +3 -2
- package/dist/generate/sitemap.d.ts +4 -1
- package/dist/generate/sitemap.js +3 -16
- package/dist/generate/xml.d.ts +12 -0
- package/dist/generate/xml.js +27 -0
- package/dist/utils/url.d.ts +54 -0
- package/dist/utils/url.js +84 -0
- package/package.json +1 -1
- package/dist/utils/normalize-path.d.ts +0 -6
- package/dist/utils/normalize-path.js +0 -8
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Kept here rather than in `bin/` so they are typechecked and unit-tested; the
|
|
8
8
|
* CLI is a thin wrapper over them. See `docs/adr/0002-archives-are-rewritten-source-copies.md`.
|
|
9
9
|
*/
|
|
10
|
+
import { atBoundary, firstSegmentUnder } from '../utils/url.js';
|
|
10
11
|
import { outsideCodeFences, withFrontmatter } from './markdown-source.js';
|
|
11
12
|
/** Route files at the docs root that an archive nested inside it already inherits. */
|
|
12
13
|
export function isInheritedRouteFile(name) {
|
|
@@ -28,10 +29,11 @@ export function rewriteDocsLinks(text, options) {
|
|
|
28
29
|
const skip = new Set([...(options.archivedIds ?? []), versionId]);
|
|
29
30
|
const pattern = new RegExp(`(\\]\\(|href="|href='|\\]:[ \\t]+)(${escapeRe(docsBase)})([^)"'\\s]*)`, 'g');
|
|
30
31
|
return outsideCodeFences(text, (chunk) => chunk.replace(pattern, (match, prefix, base, rest) => {
|
|
31
|
-
//
|
|
32
|
-
|
|
32
|
+
// The regex has already matched the base, so guard the boundary on what
|
|
33
|
+
// follows it: `/docsmith` must not become `/docs/v1mith`.
|
|
34
|
+
if (!atBoundary(rest))
|
|
33
35
|
return match;
|
|
34
|
-
const firstSegment =
|
|
36
|
+
const firstSegment = firstSegmentUnder(rest, '');
|
|
35
37
|
if (firstSegment && skip.has(firstSegment))
|
|
36
38
|
return match;
|
|
37
39
|
return `${prefix}${base}/${versionId}${rest}`;
|
|
@@ -13,7 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import { assertValidVersionId } from '../../core/version.js';
|
|
14
14
|
import { freezeLastUpdated, isInheritedRouteFile, rewriteDocsLinks } from '../archive.js';
|
|
15
15
|
import { ARCHIVE_MARKER, discoverArchives, markerContents } from '../archives.js';
|
|
16
|
-
import {
|
|
16
|
+
import { commitDates } from '../git.js';
|
|
17
17
|
import { docsBaseFrom } from '../paths.js';
|
|
18
18
|
import { isPageFile } from '../vite/pages.js';
|
|
19
19
|
import { CliError } from './error.js';
|
|
@@ -79,6 +79,14 @@ export function archiveVersion(options, log) {
|
|
|
79
79
|
yield { file, source: path.join(contentDir, path.relative(toDir, file)) };
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
// Read the dates before copying, so the walk sees the docs root as it was
|
|
83
|
+
// rather than a tree half full of untracked copies. Say so if they could not
|
|
84
|
+
// be read: an archive is written once and never edited again, so a page that
|
|
85
|
+
// misses its date here keeps the wrong one for good.
|
|
86
|
+
const dates = commitDates(contentDir, (reason) => {
|
|
87
|
+
log(`\n! Could not read commit dates: ${reason}`);
|
|
88
|
+
log(' Archived pages will keep no last-updated date.\n');
|
|
89
|
+
});
|
|
82
90
|
copyCurrent(contentDir, toDir);
|
|
83
91
|
fs.writeFileSync(path.join(toDir, ARCHIVE_MARKER), markerContents(id));
|
|
84
92
|
let pages = 0;
|
|
@@ -87,12 +95,15 @@ export function archiveVersion(options, log) {
|
|
|
87
95
|
continue;
|
|
88
96
|
pages++;
|
|
89
97
|
const text = fs.readFileSync(file, 'utf-8');
|
|
90
|
-
fs.writeFileSync(file, freezeLastUpdated(rewriteDocsLinks(text, { docsBase, versionId: id, archivedIds }),
|
|
98
|
+
fs.writeFileSync(file, freezeLastUpdated(rewriteDocsLinks(text, { docsBase, versionId: id, archivedIds }), dates.get(source)));
|
|
91
99
|
}
|
|
92
100
|
log(`\n✓ Archived the current docs into ${rel(toDir)} (${pages} pages)\n`);
|
|
93
|
-
log('Links were rewritten to stay inside the archive
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
log('Links were rewritten to stay inside the archive.');
|
|
102
|
+
// Only claim the dates were kept when the walk actually produced some.
|
|
103
|
+
if (dates.size)
|
|
104
|
+
log('Each page kept its real last-updated date.');
|
|
105
|
+
log('Review the diff, then update docsmith({ versions }) so the archive');
|
|
106
|
+
log('is served:\n');
|
|
96
107
|
log(' versions: {');
|
|
97
108
|
log(" current: { id: '<new release>', label: '<new release>' },");
|
|
98
109
|
log(` archived: [{ id: '${id}', label: '${label}' }${archivedIds.size ? ', …' : ''}]`);
|
package/dist/buildtime/git.d.ts
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Commit day (`YYYY-MM-DD`) every tracked file under `dir` last changed, keyed by
|
|
3
|
+
* absolute path. One history walk rather than a `git log` per file: the per-file
|
|
4
|
+
* form costs a process each, which is the build's largest single expense once a
|
|
5
|
+
* site has more than a handful of pages.
|
|
6
|
+
*
|
|
7
|
+
* `--relative` makes git print paths relative to `dir`, so they join straight
|
|
8
|
+
* back onto it with no second call to find the repository root. `--no-renames`
|
|
9
|
+
* makes a rename list both of its paths, which keeps the answer identical to
|
|
10
|
+
* asking about one file at a time.
|
|
11
|
+
*
|
|
12
|
+
* An empty map for a directory outside a repository, or one git could not read.
|
|
13
|
+
* That is the same "no date" every caller already handles, and it is deliberately
|
|
14
|
+
* all-or-nothing: a truncated read would date some pages and not others, which
|
|
15
|
+
* looks like real data and is not.
|
|
16
|
+
*
|
|
17
|
+
* Only a truncated read calls `onFail`, because it is the one failure that would
|
|
18
|
+
* otherwise date some pages and not others. A directory outside a repository, a
|
|
19
|
+
* missing directory, or a machine without git all yield no dates, which every
|
|
20
|
+
* caller already handles and which the plugin already reports where it matters.
|
|
21
|
+
* Reporting is the caller's to do, which is why this takes a callback rather
|
|
22
|
+
* than writing to the console itself.
|
|
23
|
+
*/
|
|
24
|
+
export declare function commitDates(dir: string, onFail?: (reason: string) => void,
|
|
25
|
+
/**
|
|
26
|
+
* Cap on the walk's output. The default is far above any real docs root; it is
|
|
27
|
+
* a parameter so the truncation path can be exercised without a repository
|
|
28
|
+
* large enough to produce 256 MB of history.
|
|
29
|
+
*/
|
|
30
|
+
maxBuffer?: number): Map<string, string>;
|
|
3
31
|
/**
|
|
4
32
|
* Release dates keyed by version, read from the commit that introduced each
|
|
5
33
|
* `## <version>` heading in a changelog. Changesets does not write dates, and a
|
package/dist/buildtime/git.js
CHANGED
|
@@ -12,14 +12,64 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Commit day (`YYYY-MM-DD`) every tracked file under `dir` last changed, keyed by
|
|
17
|
+
* absolute path. One history walk rather than a `git log` per file: the per-file
|
|
18
|
+
* form costs a process each, which is the build's largest single expense once a
|
|
19
|
+
* site has more than a handful of pages.
|
|
20
|
+
*
|
|
21
|
+
* `--relative` makes git print paths relative to `dir`, so they join straight
|
|
22
|
+
* back onto it with no second call to find the repository root. `--no-renames`
|
|
23
|
+
* makes a rename list both of its paths, which keeps the answer identical to
|
|
24
|
+
* asking about one file at a time.
|
|
25
|
+
*
|
|
26
|
+
* An empty map for a directory outside a repository, or one git could not read.
|
|
27
|
+
* That is the same "no date" every caller already handles, and it is deliberately
|
|
28
|
+
* all-or-nothing: a truncated read would date some pages and not others, which
|
|
29
|
+
* looks like real data and is not.
|
|
30
|
+
*
|
|
31
|
+
* Only a truncated read calls `onFail`, because it is the one failure that would
|
|
32
|
+
* otherwise date some pages and not others. A directory outside a repository, a
|
|
33
|
+
* missing directory, or a machine without git all yield no dates, which every
|
|
34
|
+
* caller already handles and which the plugin already reports where it matters.
|
|
35
|
+
* Reporting is the caller's to do, which is why this takes a callback rather
|
|
36
|
+
* than writing to the console itself.
|
|
37
|
+
*/
|
|
38
|
+
export function commitDates(dir, onFail,
|
|
39
|
+
/**
|
|
40
|
+
* Cap on the walk's output. The default is far above any real docs root; it is
|
|
41
|
+
* a parameter so the truncation path can be exercised without a repository
|
|
42
|
+
* large enough to produce 256 MB of history.
|
|
43
|
+
*/
|
|
44
|
+
maxBuffer = 256 * 1024 * 1024) {
|
|
45
|
+
const dates = new Map();
|
|
46
|
+
const res = spawnSync('git', ['log', '--format=%cs', '--name-only', '--relative', '--no-renames', '--', '.'],
|
|
47
|
+
// The 1 MB default would truncate rather than throw on a large history,
|
|
48
|
+
// which is why the cap is raised and the result checked below.
|
|
49
|
+
{ cwd: dir, encoding: 'utf-8', maxBuffer });
|
|
50
|
+
// `ENOBUFS` is the output outgrowing maxBuffer, which truncates rather than
|
|
51
|
+
// throws and is the only failure that could leave a half-filled map. `ENOENT`
|
|
52
|
+
// (no such directory, or no git installed) and a non-zero status ("not a
|
|
53
|
+
// repository") both mean no dates at all, which is a state callers expect.
|
|
54
|
+
if (res.error?.code === 'ENOBUFS') {
|
|
55
|
+
onFail?.(res.error.message);
|
|
56
|
+
}
|
|
57
|
+
if (res.error || res.status !== 0)
|
|
58
|
+
return dates;
|
|
59
|
+
let date;
|
|
60
|
+
for (const line of res.stdout.split('\n')) {
|
|
61
|
+
if (!line)
|
|
62
|
+
continue;
|
|
63
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(line)) {
|
|
64
|
+
date = line;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Newest commit first, so the first date a file appears under is its latest.
|
|
68
|
+
const file = path.resolve(dir, line);
|
|
69
|
+
if (date && !dates.has(file))
|
|
70
|
+
dates.set(file, date);
|
|
71
|
+
}
|
|
72
|
+
return dates;
|
|
23
73
|
}
|
|
24
74
|
/**
|
|
25
75
|
* Release dates keyed by version, read from the commit that introduced each
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The URL a directory under the routes root is served at: `<routes>/docs/intro`
|
|
3
|
+
* becomes `/docs/intro`. The routes root itself becomes `/`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function urlFor(routesDir: string, dir: string): string;
|
|
1
6
|
/**
|
|
2
7
|
* The docs URL base, e.g. `/docs`, from the content directory's location under
|
|
3
|
-
* the routes directory.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
8
|
+
* the routes directory. The same mapping a page's own directory goes through, so
|
|
9
|
+
* a page at `<routes>/docs/intro/+page.md` is served at `/docs/intro` under a
|
|
10
|
+
* base of `/docs`.
|
|
6
11
|
*/
|
|
7
12
|
export declare function docsBaseFrom(routesDir: string, contentDir: string): string;
|
package/dist/buildtime/paths.js
CHANGED
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
* separately is how they drift.
|
|
5
5
|
*/
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
/**
|
|
8
|
+
* The URL a directory under the routes root is served at: `<routes>/docs/intro`
|
|
9
|
+
* becomes `/docs/intro`. The routes root itself becomes `/`.
|
|
10
|
+
*/
|
|
11
|
+
export function urlFor(routesDir, dir) {
|
|
12
|
+
const rel = path.relative(routesDir, dir).split(path.sep).join('/');
|
|
13
|
+
return '/' + rel;
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
16
|
* The docs URL base, e.g. `/docs`, from the content directory's location under
|
|
9
|
-
* the routes directory.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
17
|
+
* the routes directory. The same mapping a page's own directory goes through, so
|
|
18
|
+
* a page at `<routes>/docs/intro/+page.md` is served at `/docs/intro` under a
|
|
19
|
+
* base of `/docs`.
|
|
12
20
|
*/
|
|
13
21
|
export function docsBaseFrom(routesDir, contentDir) {
|
|
14
|
-
return
|
|
22
|
+
return urlFor(routesDir, contentDir);
|
|
15
23
|
}
|
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import type { DocsContentItem, LlmsDoc, SearchDoc } from '../../core/content.js';
|
|
2
2
|
import type { DocsVersions } from '../../core/version.js';
|
|
3
|
+
import { type SourcePage } from './pages.js';
|
|
4
|
+
/** Where the pages sit, which is what turns a page's file into a URL and a version. */
|
|
5
|
+
export type IndexOptions = {
|
|
6
|
+
/** The docs root the pages were read from. */
|
|
7
|
+
contentDir: string;
|
|
8
|
+
/** Routes root, so `<routes>/docs/intro/+page.md` becomes `/docs/intro`. */
|
|
9
|
+
routesDir: string;
|
|
10
|
+
/** Declared versions, when the site has them. */
|
|
11
|
+
versions?: DocsVersions;
|
|
12
|
+
};
|
|
3
13
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
14
|
+
* The sidebar's index: the frontmatter fields navigation needs, plus the heading
|
|
15
|
+
* list for a server-rendered TOC and an estimated reading time. Served as the
|
|
16
|
+
* eagerly-imported `svelte-docsmith/content` virtual module.
|
|
17
|
+
*
|
|
18
|
+
* `lastUpdated` is read off the page rather than looked up here, so pass pages
|
|
19
|
+
* that have been through `withCommitDates` or the column comes out blank.
|
|
8
20
|
*/
|
|
9
|
-
export declare function
|
|
21
|
+
export declare function contentIndex(pages: SourcePage[], options: IndexOptions): DocsContentItem[];
|
|
10
22
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* bloating the eagerly-imported nav index. The missing-directory case is
|
|
15
|
-
* already reported by {@link collectDocs}, so this stays quiet.
|
|
23
|
+
* The search index: title, section, description, heading list, and plain-text
|
|
24
|
+
* body. Served as the lazy-loaded `svelte-docsmith/search` virtual module so
|
|
25
|
+
* search can index bodies without bloating the eagerly-imported content index.
|
|
16
26
|
*/
|
|
17
|
-
export declare function
|
|
27
|
+
export declare function searchIndex(pages: SourcePage[], options: IndexOptions): SearchDoc[];
|
|
18
28
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* to the client.
|
|
29
|
+
* The LLM index: title, section, description, and the full markdown content.
|
|
30
|
+
* Served as the `svelte-docsmith/llms` virtual module and consumed server-side
|
|
31
|
+
* by `llms.txt` / `llms-full.txt` routes, so it never ships to the client.
|
|
23
32
|
*/
|
|
24
|
-
export declare function
|
|
33
|
+
export declare function llmsIndex(pages: SourcePage[], options: IndexOptions): LlmsDoc[];
|
|
@@ -1,9 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The three generated indexes, each a projection of the same page list. Nothing
|
|
3
|
+
* here reads the filesystem or spawns a process: a build hands these functions
|
|
4
|
+
* pages read by `./pages.js`, and a test hands them literals. Both go down the
|
|
5
|
+
* same path, so what the sidebar, search and llms.txt show is decided in one
|
|
6
|
+
* place and can be checked without a docs root on disk.
|
|
7
|
+
*/
|
|
2
8
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
9
|
+
import { firstSegmentUnder } from '../../utils/url.js';
|
|
10
|
+
import { docsBaseFrom, urlFor } from '../paths.js';
|
|
11
|
+
import { titleOf } from './pages.js';
|
|
5
12
|
import { extractLlmsContent, extractSearchText, extractToc, readingMinutes } from './extract.js';
|
|
6
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Resolve every titled page against the site's layout. Shared by all three
|
|
15
|
+
* indexes so they can never disagree about which pages exist or where they live.
|
|
16
|
+
*/
|
|
17
|
+
function indexedPages(pages, options) {
|
|
18
|
+
const resolved = [];
|
|
19
|
+
const docsBase = docsBaseFrom(options.routesDir, options.contentDir);
|
|
20
|
+
for (const page of pages) {
|
|
21
|
+
const title = titleOf(page);
|
|
22
|
+
if (title === undefined)
|
|
23
|
+
continue;
|
|
24
|
+
const url = urlFor(options.routesDir, path.dirname(page.file));
|
|
25
|
+
resolved.push({ ...page, url, title, version: versionOf(url) });
|
|
26
|
+
}
|
|
27
|
+
// Stable output keeps the generated modules diff-friendly across rebuilds.
|
|
28
|
+
return resolved.sort((a, b) => a.url.localeCompare(b.url));
|
|
29
|
+
/**
|
|
30
|
+
* A page belongs to an archived version when the first segment of its URL below
|
|
31
|
+
* the docs base is that archive's id; everything else is the current version,
|
|
32
|
+
* which lives unprefixed at the docs base.
|
|
33
|
+
*
|
|
34
|
+
* Decided in URL space, by the same rule `activeVersion` applies to the reader's
|
|
35
|
+
* pathname at runtime. Deciding it from the directory instead would be a second
|
|
36
|
+
* rule for the same question, in a second vocabulary, free to drift from the one
|
|
37
|
+
* the reader is actually served by. No versions ⇒ undefined, which keeps an
|
|
38
|
+
* unversioned site's index byte-for-byte what it was.
|
|
39
|
+
*/
|
|
40
|
+
function versionOf(url) {
|
|
41
|
+
const { versions } = options;
|
|
42
|
+
if (!versions)
|
|
43
|
+
return undefined;
|
|
44
|
+
const segment = firstSegmentUnder(url, docsBase);
|
|
45
|
+
const archived = versions.archived?.find((v) => v.id === segment);
|
|
46
|
+
return archived ? archived.id : versions.current.id;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
7
49
|
/**
|
|
8
50
|
* Read a page's `section` frontmatter: a string is one level, an array is a
|
|
9
51
|
* nested group path. Non-string array members are dropped; anything else is
|
|
@@ -33,116 +75,68 @@ function sectionKey(value) {
|
|
|
33
75
|
return Array.isArray(section) ? section.join(' / ') : section;
|
|
34
76
|
}
|
|
35
77
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* built from a single read of each file.
|
|
78
|
+
* A frontmatter field, when it holds the type the index expects. Frontmatter is
|
|
79
|
+
* whatever the author typed, so a field of the wrong type is dropped rather than
|
|
80
|
+
* carried into the index as one.
|
|
40
81
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const dir = path.dirname(file);
|
|
48
|
-
const url = '/' + path.relative(routesDir, dir).split(path.sep).join('/');
|
|
49
|
-
yield { source, front, url, title: front.title, file, version: versionOf(dir) };
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* A page belongs to an archived version when its first directory segment under
|
|
53
|
-
* the content dir is that archive's id; everything else is the current
|
|
54
|
-
* version, which lives unprefixed at the docs root. No versions ⇒ undefined,
|
|
55
|
-
* which keeps an unversioned site's index byte-for-byte what it was.
|
|
56
|
-
*/
|
|
57
|
-
function versionOf(dir) {
|
|
58
|
-
if (!versions)
|
|
59
|
-
return undefined;
|
|
60
|
-
const firstSegment = path.relative(contentDir, dir).split(path.sep)[0];
|
|
61
|
-
const archived = versions.archived?.find((v) => v.id === firstSegment);
|
|
62
|
-
return archived ? archived.id : versions.current.id;
|
|
63
|
-
}
|
|
82
|
+
function stringField(front, key) {
|
|
83
|
+
return typeof front[key] === 'string' ? front[key] : undefined;
|
|
84
|
+
}
|
|
85
|
+
/** As {@link stringField}, for the numeric fields. */
|
|
86
|
+
function numberField(front, key) {
|
|
87
|
+
return typeof front[key] === 'number' ? front[key] : undefined;
|
|
64
88
|
}
|
|
65
89
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
90
|
+
* The sidebar's index: the frontmatter fields navigation needs, plus the heading
|
|
91
|
+
* list for a server-rendered TOC and an estimated reading time. Served as the
|
|
92
|
+
* eagerly-imported `svelte-docsmith/content` virtual module.
|
|
93
|
+
*
|
|
94
|
+
* `lastUpdated` is read off the page rather than looked up here, so pass pages
|
|
95
|
+
* that have been through `withCommitDates` or the column comes out blank.
|
|
70
96
|
*/
|
|
71
|
-
export function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
order: typeof front.order === 'number' ? front.order : undefined,
|
|
85
|
-
sourcePath: path.relative(process.cwd(), file).split(path.sep).join('/'),
|
|
86
|
-
// Frontmatter wins over git so an archived page keeps the date it last
|
|
87
|
-
// really changed: archiving copies every page in one commit, which would
|
|
88
|
-
// otherwise stamp the whole archive with the day it was created.
|
|
89
|
-
lastUpdated: typeof front.lastUpdated === 'string' ? front.lastUpdated : lastCommitDate(file),
|
|
90
|
-
readingTime: readingMinutes(extractSearchText(source)),
|
|
91
|
-
toc: extractToc(source),
|
|
92
|
-
version
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
if (items.length === 0) {
|
|
96
|
-
console.warn(`[svelte-docsmith] no doc pages found under ${contentDir}\n` +
|
|
97
|
-
` Add \`+page.md\` files with at least a \`title:\` in their frontmatter to populate the sidebar.`);
|
|
98
|
-
}
|
|
99
|
-
// Stable output keeps the generated module diff-friendly across rebuilds.
|
|
100
|
-
return items.sort((a, b) => a.path.localeCompare(b.path));
|
|
97
|
+
export function contentIndex(pages, options) {
|
|
98
|
+
return indexedPages(pages, options).map((page) => ({
|
|
99
|
+
title: page.title,
|
|
100
|
+
path: page.url,
|
|
101
|
+
description: stringField(page.frontmatter, 'description'),
|
|
102
|
+
section: readSection(page.frontmatter.section),
|
|
103
|
+
order: numberField(page.frontmatter, 'order'),
|
|
104
|
+
sourcePath: path.relative(process.cwd(), page.file).split(path.sep).join('/'),
|
|
105
|
+
lastUpdated: page.lastUpdated,
|
|
106
|
+
readingTime: readingMinutes(extractSearchText(page.source)),
|
|
107
|
+
toc: extractToc(page.source),
|
|
108
|
+
version: page.version
|
|
109
|
+
}));
|
|
101
110
|
}
|
|
102
111
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* bloating the eagerly-imported nav index. The missing-directory case is
|
|
107
|
-
* already reported by {@link collectDocs}, so this stays quiet.
|
|
112
|
+
* The search index: title, section, description, heading list, and plain-text
|
|
113
|
+
* body. Served as the lazy-loaded `svelte-docsmith/search` virtual module so
|
|
114
|
+
* search can index bodies without bloating the eagerly-imported content index.
|
|
108
115
|
*/
|
|
109
|
-
export function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
headings: extractToc(source).map((entry) => entry.title),
|
|
120
|
-
text: extractSearchText(source),
|
|
121
|
-
version
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
return docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
116
|
+
export function searchIndex(pages, options) {
|
|
117
|
+
return indexedPages(pages, options).map((page) => ({
|
|
118
|
+
path: page.url,
|
|
119
|
+
title: page.title,
|
|
120
|
+
section: sectionLabel(page.frontmatter.section),
|
|
121
|
+
description: stringField(page.frontmatter, 'description'),
|
|
122
|
+
headings: extractToc(page.source).map((entry) => entry.title),
|
|
123
|
+
text: extractSearchText(page.source),
|
|
124
|
+
version: page.version
|
|
125
|
+
}));
|
|
125
126
|
}
|
|
126
127
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* to the client.
|
|
128
|
+
* The LLM index: title, section, description, and the full markdown content.
|
|
129
|
+
* Served as the `svelte-docsmith/llms` virtual module and consumed server-side
|
|
130
|
+
* by `llms.txt` / `llms-full.txt` routes, so it never ships to the client.
|
|
131
131
|
*/
|
|
132
|
-
export function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
description: typeof front.description === 'string' ? front.description : undefined,
|
|
143
|
-
content: extractLlmsContent(source, title),
|
|
144
|
-
version
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
return docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
132
|
+
export function llmsIndex(pages, options) {
|
|
133
|
+
return indexedPages(pages, options).map((page) => ({
|
|
134
|
+
path: page.url,
|
|
135
|
+
title: page.title,
|
|
136
|
+
section: sectionKey(page.frontmatter.section),
|
|
137
|
+
order: numberField(page.frontmatter, 'order'),
|
|
138
|
+
description: stringField(page.frontmatter, 'description'),
|
|
139
|
+
content: extractLlmsContent(page.source, page.title),
|
|
140
|
+
version: page.version
|
|
141
|
+
}));
|
|
148
142
|
}
|
|
@@ -3,8 +3,6 @@ export type TocEntry = {
|
|
|
3
3
|
title: string;
|
|
4
4
|
depth: 2 | 3;
|
|
5
5
|
};
|
|
6
|
-
/** Strip inline markdown so a heading's TOC label is plain text. */
|
|
7
|
-
export declare function stripInlineMarkdown(text: string): string;
|
|
8
6
|
/**
|
|
9
7
|
* Reduce a markdown page to plain, searchable body text: prose and heading text
|
|
10
8
|
* with frontmatter, `<script>`/`<style>` blocks, fenced code, HTML/Svelte tags,
|
|
@@ -5,7 +5,7 @@ function withoutSetupBlocks(body) {
|
|
|
5
5
|
return body.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
6
6
|
}
|
|
7
7
|
/** Strip inline markdown so a heading's TOC label is plain text. */
|
|
8
|
-
|
|
8
|
+
function stripInlineMarkdown(text) {
|
|
9
9
|
return text
|
|
10
10
|
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
11
11
|
.replace(/`([^`]+)`/g, '$1')
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import { type DocsVersions } from '../../core/version.js';
|
|
3
|
-
export { collectDocs, collectLlmsDocs, collectSearchDocs } from './collect.js';
|
|
4
|
-
export { collectReleases } from './releases.js';
|
|
5
3
|
export interface DocsmithViteOptions {
|
|
6
4
|
/** Directory scanned for doc pages. Default: `'src/routes/docs'`. */
|
|
7
5
|
content?: string;
|