@docpensieve/shared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/package.json +44 -0
- package/src/constants.js +74 -0
- package/src/errors.js +54 -0
- package/src/index.js +12 -0
- package/src/slug.js +184 -0
- package/types/constants.d.ts +65 -0
- package/types/errors.d.ts +48 -0
- package/types/index.d.ts +11 -0
- package/types/slug.d.ts +119 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valentin Chevoleau
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @docpensieve/shared
|
|
2
|
+
|
|
3
|
+
> Shared constants, errors and slugs
|
|
4
|
+
|
|
5
|
+
Part of [DocPensieve](https://github.com/Juniors017/docpensieve), a static documentation site generator:
|
|
6
|
+
Markdown and MDX in, static HTML out, one version per orphan branch, JSON-LD
|
|
7
|
+
structured data from the frontmatter.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @docpensieve/shared
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
This package depends on nothing: it is the leaf of DocPensieve's dependency
|
|
18
|
+
graph. Anything two or more packages need lives here.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { slugify, filePathToSlug, DocPensieveError } from '@docpensieve/shared';
|
|
22
|
+
|
|
23
|
+
slugify('Crème Brûlée Recipes'); // 'creme-brulee-recipes'
|
|
24
|
+
filePathToSlug('guide/01-install.md'); // 'guide/install'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Accents are stripped from slugs, which keeps URLs readable instead of
|
|
28
|
+
percent-encoded.
|
|
29
|
+
|
|
30
|
+
## Documentation
|
|
31
|
+
|
|
32
|
+
See the [repository](https://github.com/Juniors017/docpensieve#readme).
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@docpensieve/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Utilities, constants and errors shared by the DocPensieve packages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.0.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./types/index.d.ts",
|
|
14
|
+
"default": "./src/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"types"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"docpensieve",
|
|
23
|
+
"documentation",
|
|
24
|
+
"slug",
|
|
25
|
+
"utils"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Juniors017/docpensieve.git",
|
|
30
|
+
"directory": "packages/shared"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/Juniors017/docpensieve#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/Juniors017/docpensieve/issues"
|
|
35
|
+
},
|
|
36
|
+
"author": "Valentin Chevoleau (Juniors017)",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"types": "./types/index.d.ts",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"prepack": "tsc -b tsconfig.build.json"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants shared by every DocPensieve package.
|
|
3
|
+
* @module @docpensieve/shared/constants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** File extensions recognised as documentation pages. */
|
|
7
|
+
export const DOC_EXTENSIONS = ['.md', '.mdx'];
|
|
8
|
+
|
|
9
|
+
/** Name of the configuration file at the project root. */
|
|
10
|
+
export const CONFIG_FILENAME = 'docpensieve.config.js';
|
|
11
|
+
|
|
12
|
+
/** Default output directory of a build. */
|
|
13
|
+
export const DEFAULT_OUT_DIR = 'dist';
|
|
14
|
+
|
|
15
|
+
/** Version manifest written by every build. */
|
|
16
|
+
export const VERSIONS_MANIFEST = 'versions.json';
|
|
17
|
+
|
|
18
|
+
/** Page slugs treated as the root of their folder (they take the folder's own URL). */
|
|
19
|
+
export const INDEX_SLUGS = ['index', 'readme'];
|
|
20
|
+
|
|
21
|
+
/** JSON-LD types supported by the `jsonld.type` frontmatter field. */
|
|
22
|
+
export const JSONLD_TYPES = ['Article', 'TechArticle', 'BlogPosting'];
|
|
23
|
+
|
|
24
|
+
/** CSS frameworks known to the ThemeEngine. */
|
|
25
|
+
export const THEME_FRAMEWORKS = ['tailwind', 'custom'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Layouts accepted in a page's frontmatter.
|
|
29
|
+
*
|
|
30
|
+
* `doc` is the documentation layout: menu on the left, table of contents on
|
|
31
|
+
* the right, content held to reading width. `home` removes all three, which
|
|
32
|
+
* is what a landing page expects.
|
|
33
|
+
*
|
|
34
|
+
* @type {readonly string[]}
|
|
35
|
+
*/
|
|
36
|
+
export const PAGE_LAYOUTS = Object.freeze(['doc', 'home']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Class slots of the page shell.
|
|
40
|
+
*
|
|
41
|
+
* Templates hard-code no class: they ask the theme for the class of each
|
|
42
|
+
* slot. A provider only redefines what it wants to change; everything else
|
|
43
|
+
* falls back to these values. That is what lets a single template render
|
|
44
|
+
* either `dp-nav` or a string of Tailwind utilities.
|
|
45
|
+
*
|
|
46
|
+
* This table is shared: `core` reads it in its templates, `theme` extends it
|
|
47
|
+
* in its providers.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_THEME_CLASSES = Object.freeze({
|
|
50
|
+
skip: 'dp-skip',
|
|
51
|
+
header: 'dp-header',
|
|
52
|
+
brand: 'dp-brand',
|
|
53
|
+
versions: 'dp-versions',
|
|
54
|
+
versionsList: 'dp-versions-list',
|
|
55
|
+
shell: 'dp-shell',
|
|
56
|
+
shellWide: 'dp-shell dp-shell--wide',
|
|
57
|
+
sidebar: 'dp-sidebar',
|
|
58
|
+
nav: 'dp-nav',
|
|
59
|
+
navItem: 'dp-nav-item',
|
|
60
|
+
navItemParent: 'dp-nav-item--parent',
|
|
61
|
+
navLink: 'dp-nav-link',
|
|
62
|
+
navLabel: 'dp-nav-label',
|
|
63
|
+
notice: 'dp-notice',
|
|
64
|
+
skillIcon: 'dp-skill-icon',
|
|
65
|
+
main: 'dp-main',
|
|
66
|
+
article: 'dp-article',
|
|
67
|
+
toc: 'dp-toc',
|
|
68
|
+
tocTitle: 'dp-toc-title',
|
|
69
|
+
tocList: 'dp-toc-list',
|
|
70
|
+
tocItem: 'dp-toc-item',
|
|
71
|
+
footer: 'dp-footer',
|
|
72
|
+
scrollTop: 'dp-scroll-top',
|
|
73
|
+
scrollTopIcon: 'dp-scroll-top-icon',
|
|
74
|
+
});
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocPensieve error hierarchy.
|
|
3
|
+
*
|
|
4
|
+
* Every domain error extends `DocPensieveError`: that is how the CLI tells an
|
|
5
|
+
* expected error (a clear message for the user) from a bug (full stack trace).
|
|
6
|
+
*
|
|
7
|
+
* @module @docpensieve/shared/errors
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Expected domain error — the CLI prints it without a stack trace. */
|
|
11
|
+
export class DocPensieveError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} message
|
|
14
|
+
* @param {{ cause?: unknown, hint?: string }} [options] `hint` is printed
|
|
15
|
+
* by the CLI as a suggested fix.
|
|
16
|
+
*/
|
|
17
|
+
constructor(message, options = {}) {
|
|
18
|
+
super(message, { cause: options.cause });
|
|
19
|
+
this.name = this.constructor.name;
|
|
20
|
+
this.hint = options.hint;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Configuration missing, unreadable or invalid. */
|
|
25
|
+
export class ConfigError extends DocPensieveError {}
|
|
26
|
+
|
|
27
|
+
/** A source file could not be read or parsed. */
|
|
28
|
+
export class LoaderError extends DocPensieveError {}
|
|
29
|
+
|
|
30
|
+
/** MDX/Markdown compilation failed. */
|
|
31
|
+
export class CompileError extends DocPensieveError {}
|
|
32
|
+
|
|
33
|
+
/** Invalid `jsonld` frontmatter or inconsistent structured data. */
|
|
34
|
+
export class StructuredDataError extends DocPensieveError {}
|
|
35
|
+
|
|
36
|
+
/** Theme provider missing, invalid, or whose compilation failed. */
|
|
37
|
+
export class ThemeError extends DocPensieveError {}
|
|
38
|
+
|
|
39
|
+
/** The site could not be generated or written. */
|
|
40
|
+
export class GeneratorError extends DocPensieveError {}
|
|
41
|
+
|
|
42
|
+
/** Milestone not implemented yet — points to the roadmap section. */
|
|
43
|
+
export class NotImplementedError extends DocPensieveError {
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} what What is not implemented.
|
|
46
|
+
* @param {string} roadmapSection E.g. `'1.1 DocLoader'`.
|
|
47
|
+
*/
|
|
48
|
+
constructor(what, roadmapSection) {
|
|
49
|
+
super(`${what} is not available yet.`, {
|
|
50
|
+
hint: `Planned in section ${roadmapSection} of the roadmap.`,
|
|
51
|
+
});
|
|
52
|
+
this.roadmapSection = roadmapSection;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docpensieve/shared — shared utilities, constants and errors.
|
|
3
|
+
*
|
|
4
|
+
* This package depends on no other DocPensieve package: it is the leaf of the
|
|
5
|
+
* dependency graph. Anything useful to two packages or more lives here.
|
|
6
|
+
*
|
|
7
|
+
* @module @docpensieve/shared
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from './constants.js';
|
|
11
|
+
export * from './errors.js';
|
|
12
|
+
export * from './slug.js';
|
package/src/slug.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug and URL generation from file paths.
|
|
3
|
+
* @module @docpensieve/shared/slug
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { INDEX_SLUGS } from './constants.js';
|
|
7
|
+
|
|
8
|
+
/** Numeric ordering prefix: `01-`, `02_`, `10.` */
|
|
9
|
+
const ORDER_PREFIX = /^\d+[-_.]/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Turns free text into a URL-safe slug.
|
|
13
|
+
*
|
|
14
|
+
* Accents are decomposed then dropped (`Crème` → `creme`), which keeps URLs
|
|
15
|
+
* readable instead of percent-encoded.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} input
|
|
18
|
+
* @returns {string} Lowercase slug, dash-separated.
|
|
19
|
+
*/
|
|
20
|
+
export function slugify(input) {
|
|
21
|
+
return String(input)
|
|
22
|
+
.normalize('NFD')
|
|
23
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
24
|
+
.replace(/['’]/g, '')
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Converts a file path, relative to the version folder, into a page slug.
|
|
32
|
+
*
|
|
33
|
+
* Ordering prefixes are removed from every segment: they sort the sidebar,
|
|
34
|
+
* they do not build the URL.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* filePathToSlug('guide/01-install.md') // 'guide/install'
|
|
38
|
+
* filePathToSlug('intro.mdx') // 'intro'
|
|
39
|
+
* filePathToSlug('guide/index.md') // 'guide'
|
|
40
|
+
* filePathToSlug('index.md') // ''
|
|
41
|
+
*
|
|
42
|
+
* @param {string} relativePath Path relative to the version folder.
|
|
43
|
+
* @returns {string} Slug with no leading or trailing slash.
|
|
44
|
+
*/
|
|
45
|
+
export function filePathToSlug(relativePath) {
|
|
46
|
+
const withoutExt = String(relativePath).replace(/\.mdx?$/i, '');
|
|
47
|
+
const segments = withoutExt
|
|
48
|
+
.split(/[/\\]/)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map((segment) => slugify(segment.replace(ORDER_PREFIX, '')));
|
|
51
|
+
|
|
52
|
+
const last = segments.at(-1);
|
|
53
|
+
if (last !== undefined && INDEX_SLUGS.includes(last)) {
|
|
54
|
+
segments.pop();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return segments.filter(Boolean).join('/');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Maps an asset path into URL space.
|
|
62
|
+
*
|
|
63
|
+
* Folders follow the page rule — ordering prefix removed, segment slugified —
|
|
64
|
+
* so that `02-guide/diagram.png` lands under `/guide/`, where the pages of the
|
|
65
|
+
* same folder expect it. Otherwise the sorting prefix, which never shows in a
|
|
66
|
+
* page URL, would show in the URL of its images.
|
|
67
|
+
*
|
|
68
|
+
* The file name itself stays untouched: it is the one the author writes in
|
|
69
|
+
* their Markdown, and rewriting it would break the reference.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* assetPathToSlug('02-guide/diagram.png') // 'guide/diagram.png'
|
|
73
|
+
* assetPathToSlug('My Notes/a.png') // 'my-notes/a.png'
|
|
74
|
+
*
|
|
75
|
+
* @param {string} relativePath Path relative to the version folder.
|
|
76
|
+
* @returns {string} Output path, slash-separated.
|
|
77
|
+
*/
|
|
78
|
+
export function assetPathToSlug(relativePath) {
|
|
79
|
+
const segments = String(relativePath).split(/[/\\]/).filter(Boolean);
|
|
80
|
+
|
|
81
|
+
const file = segments.pop();
|
|
82
|
+
const folders = segments.map((segment) => slugify(segment.replace(ORDER_PREFIX, '')));
|
|
83
|
+
|
|
84
|
+
return [...folders, file].filter(Boolean).join('/');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Segments of a path that leave nothing behind once slugified.
|
|
89
|
+
*
|
|
90
|
+
* `slugify` only keeps Latin letters and digits: a name made entirely of
|
|
91
|
+
* ideograms, or of punctuation, vanishes. For a page, that meant taking the
|
|
92
|
+
* home page's URL; for a folder, disappearing from the address.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* blankSegments('guide/日本語.md') // ['日本語']
|
|
96
|
+
* blankSegments('02-guide/page.md') // []
|
|
97
|
+
*
|
|
98
|
+
* @param {string} relativePath Path relative to the version folder.
|
|
99
|
+
* @returns {string[]} The offending segments, extension removed.
|
|
100
|
+
*/
|
|
101
|
+
export function blankSegments(relativePath) {
|
|
102
|
+
return String(relativePath)
|
|
103
|
+
.replace(/\.mdx?$/i, '')
|
|
104
|
+
.split(/[/\\]/)
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.filter((segment) => slugify(segment.replace(ORDER_PREFIX, '')) === '');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Maps a **folder** path into URL space.
|
|
111
|
+
*
|
|
112
|
+
* Sibling of `assetPathToSlug`, but with no file name to spare: every segment
|
|
113
|
+
* goes through the page rule. Applying `assetPathToSlug` to a folder left its
|
|
114
|
+
* last segment untouched — `02-guide` stayed `02-guide` — and every relative
|
|
115
|
+
* target of a page in that folder missed the file actually copied.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* dirPathToSlug('02-guide/03-sub') // 'guide/sub'
|
|
119
|
+
* dirPathToSlug('My Notes') // 'my-notes'
|
|
120
|
+
* dirPathToSlug('') // ''
|
|
121
|
+
*
|
|
122
|
+
* @param {string} relativePath Path relative to the version folder.
|
|
123
|
+
* @returns {string} Output path, slash-separated.
|
|
124
|
+
*/
|
|
125
|
+
export function dirPathToSlug(relativePath) {
|
|
126
|
+
return String(relativePath)
|
|
127
|
+
.split(/[/\\]/)
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.map((segment) => slugify(segment.replace(ORDER_PREFIX, '')))
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.join('/');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Converts a page slug into an absolute site URL (trailing slash included).
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* slugToUrl('guide/install') // '/guide/install/'
|
|
139
|
+
* slugToUrl('') // '/'
|
|
140
|
+
* slugToUrl('intro', 'v1.0') // '/versions/v1.0/intro/'
|
|
141
|
+
*
|
|
142
|
+
* @param {string} slug Slug produced by {@link filePathToSlug}.
|
|
143
|
+
* @param {string} [versionSlug] When given, prefixes `/versions/<version>`.
|
|
144
|
+
* @returns {string} URL starting and ending with `/`.
|
|
145
|
+
*/
|
|
146
|
+
export function slugToUrl(slug, versionSlug) {
|
|
147
|
+
const parts = [];
|
|
148
|
+
if (versionSlug) parts.push('versions', versionSlug);
|
|
149
|
+
if (slug) parts.push(...slug.split('/').filter(Boolean));
|
|
150
|
+
return parts.length ? `/${parts.join('/')}/` : '/';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Turns a slug segment back into a readable label.
|
|
155
|
+
*
|
|
156
|
+
* A deliberate, imperfect fallback: since `slugify` dropped the accents,
|
|
157
|
+
* `creme-brulee` comes back as “Creme brulee”. Only use it when there is no
|
|
158
|
+
* real title — the sidebar and the breadcrumb prefer the frontmatter title.
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* humanizeSlug('getting-started') // 'Getting started'
|
|
162
|
+
*
|
|
163
|
+
* @param {string} segment
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
export function humanizeSlug(segment) {
|
|
167
|
+
const words = String(segment).replace(/[-_]+/g, ' ').trim();
|
|
168
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reads the ordering weight of a prefixed file name.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* orderOf('01-install.md') // 1
|
|
176
|
+
* orderOf('install.md') // Infinity
|
|
177
|
+
*
|
|
178
|
+
* @param {string} filename
|
|
179
|
+
* @returns {number} The prefix number, or `Infinity` when absent (sorted last).
|
|
180
|
+
*/
|
|
181
|
+
export function orderOf(filename) {
|
|
182
|
+
const match = String(filename).match(/^(\d+)[-_.]/);
|
|
183
|
+
return match ? Number(match[1]) : Infinity;
|
|
184
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants shared by every DocPensieve package.
|
|
3
|
+
* @module @docpensieve/shared/constants
|
|
4
|
+
*/
|
|
5
|
+
/** File extensions recognised as documentation pages. */
|
|
6
|
+
export declare const DOC_EXTENSIONS: string[];
|
|
7
|
+
/** Name of the configuration file at the project root. */
|
|
8
|
+
export declare const CONFIG_FILENAME = "docpensieve.config.js";
|
|
9
|
+
/** Default output directory of a build. */
|
|
10
|
+
export declare const DEFAULT_OUT_DIR = "dist";
|
|
11
|
+
/** Version manifest written by every build. */
|
|
12
|
+
export declare const VERSIONS_MANIFEST = "versions.json";
|
|
13
|
+
/** Page slugs treated as the root of their folder (they take the folder's own URL). */
|
|
14
|
+
export declare const INDEX_SLUGS: string[];
|
|
15
|
+
/** JSON-LD types supported by the `jsonld.type` frontmatter field. */
|
|
16
|
+
export declare const JSONLD_TYPES: string[];
|
|
17
|
+
/** CSS frameworks known to the ThemeEngine. */
|
|
18
|
+
export declare const THEME_FRAMEWORKS: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Layouts accepted in a page's frontmatter.
|
|
21
|
+
*
|
|
22
|
+
* `doc` is the documentation layout: menu on the left, table of contents on
|
|
23
|
+
* the right, content held to reading width. `home` removes all three, which
|
|
24
|
+
* is what a landing page expects.
|
|
25
|
+
*
|
|
26
|
+
* @type {readonly string[]}
|
|
27
|
+
*/
|
|
28
|
+
export declare const PAGE_LAYOUTS: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* Class slots of the page shell.
|
|
31
|
+
*
|
|
32
|
+
* Templates hard-code no class: they ask the theme for the class of each
|
|
33
|
+
* slot. A provider only redefines what it wants to change; everything else
|
|
34
|
+
* falls back to these values. That is what lets a single template render
|
|
35
|
+
* either `dp-nav` or a string of Tailwind utilities.
|
|
36
|
+
*
|
|
37
|
+
* This table is shared: `core` reads it in its templates, `theme` extends it
|
|
38
|
+
* in its providers.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DEFAULT_THEME_CLASSES: Readonly<{
|
|
41
|
+
skip: "dp-skip";
|
|
42
|
+
header: "dp-header";
|
|
43
|
+
brand: "dp-brand";
|
|
44
|
+
versions: "dp-versions";
|
|
45
|
+
versionsList: "dp-versions-list";
|
|
46
|
+
shell: "dp-shell";
|
|
47
|
+
shellWide: "dp-shell dp-shell--wide";
|
|
48
|
+
sidebar: "dp-sidebar";
|
|
49
|
+
nav: "dp-nav";
|
|
50
|
+
navItem: "dp-nav-item";
|
|
51
|
+
navItemParent: "dp-nav-item--parent";
|
|
52
|
+
navLink: "dp-nav-link";
|
|
53
|
+
navLabel: "dp-nav-label";
|
|
54
|
+
notice: "dp-notice";
|
|
55
|
+
skillIcon: "dp-skill-icon";
|
|
56
|
+
main: "dp-main";
|
|
57
|
+
article: "dp-article";
|
|
58
|
+
toc: "dp-toc";
|
|
59
|
+
tocTitle: "dp-toc-title";
|
|
60
|
+
tocList: "dp-toc-list";
|
|
61
|
+
tocItem: "dp-toc-item";
|
|
62
|
+
footer: "dp-footer";
|
|
63
|
+
scrollTop: "dp-scroll-top";
|
|
64
|
+
scrollTopIcon: "dp-scroll-top-icon";
|
|
65
|
+
}>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocPensieve error hierarchy.
|
|
3
|
+
*
|
|
4
|
+
* Every domain error extends `DocPensieveError`: that is how the CLI tells an
|
|
5
|
+
* expected error (a clear message for the user) from a bug (full stack trace).
|
|
6
|
+
*
|
|
7
|
+
* @module @docpensieve/shared/errors
|
|
8
|
+
*/
|
|
9
|
+
/** Expected domain error — the CLI prints it without a stack trace. */
|
|
10
|
+
export declare class DocPensieveError extends Error {
|
|
11
|
+
hint: string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} message
|
|
14
|
+
* @param {{ cause?: unknown, hint?: string }} [options] `hint` is printed
|
|
15
|
+
* by the CLI as a suggested fix.
|
|
16
|
+
*/
|
|
17
|
+
constructor(message: string, options?: {
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
hint?: string;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Configuration missing, unreadable or invalid. */
|
|
23
|
+
export declare class ConfigError extends DocPensieveError {
|
|
24
|
+
}
|
|
25
|
+
/** A source file could not be read or parsed. */
|
|
26
|
+
export declare class LoaderError extends DocPensieveError {
|
|
27
|
+
}
|
|
28
|
+
/** MDX/Markdown compilation failed. */
|
|
29
|
+
export declare class CompileError extends DocPensieveError {
|
|
30
|
+
}
|
|
31
|
+
/** Invalid `jsonld` frontmatter or inconsistent structured data. */
|
|
32
|
+
export declare class StructuredDataError extends DocPensieveError {
|
|
33
|
+
}
|
|
34
|
+
/** Theme provider missing, invalid, or whose compilation failed. */
|
|
35
|
+
export declare class ThemeError extends DocPensieveError {
|
|
36
|
+
}
|
|
37
|
+
/** The site could not be generated or written. */
|
|
38
|
+
export declare class GeneratorError extends DocPensieveError {
|
|
39
|
+
}
|
|
40
|
+
/** Milestone not implemented yet — points to the roadmap section. */
|
|
41
|
+
export declare class NotImplementedError extends DocPensieveError {
|
|
42
|
+
roadmapSection: string;
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} what What is not implemented.
|
|
45
|
+
* @param {string} roadmapSection E.g. `'1.1 DocLoader'`.
|
|
46
|
+
*/
|
|
47
|
+
constructor(what: string, roadmapSection: string);
|
|
48
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docpensieve/shared — shared utilities, constants and errors.
|
|
3
|
+
*
|
|
4
|
+
* This package depends on no other DocPensieve package: it is the leaf of the
|
|
5
|
+
* dependency graph. Anything useful to two packages or more lives here.
|
|
6
|
+
*
|
|
7
|
+
* @module @docpensieve/shared
|
|
8
|
+
*/
|
|
9
|
+
export * from './constants.js';
|
|
10
|
+
export * from './errors.js';
|
|
11
|
+
export * from './slug.js';
|
package/types/slug.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug and URL generation from file paths.
|
|
3
|
+
* @module @docpensieve/shared/slug
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Turns free text into a URL-safe slug.
|
|
7
|
+
*
|
|
8
|
+
* Accents are decomposed then dropped (`Crème` → `creme`), which keeps URLs
|
|
9
|
+
* readable instead of percent-encoded.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} input
|
|
12
|
+
* @returns {string} Lowercase slug, dash-separated.
|
|
13
|
+
*/
|
|
14
|
+
export declare function slugify(input: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Converts a file path, relative to the version folder, into a page slug.
|
|
17
|
+
*
|
|
18
|
+
* Ordering prefixes are removed from every segment: they sort the sidebar,
|
|
19
|
+
* they do not build the URL.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* filePathToSlug('guide/01-install.md') // 'guide/install'
|
|
23
|
+
* filePathToSlug('intro.mdx') // 'intro'
|
|
24
|
+
* filePathToSlug('guide/index.md') // 'guide'
|
|
25
|
+
* filePathToSlug('index.md') // ''
|
|
26
|
+
*
|
|
27
|
+
* @param {string} relativePath Path relative to the version folder.
|
|
28
|
+
* @returns {string} Slug with no leading or trailing slash.
|
|
29
|
+
*/
|
|
30
|
+
export declare function filePathToSlug(relativePath: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Maps an asset path into URL space.
|
|
33
|
+
*
|
|
34
|
+
* Folders follow the page rule — ordering prefix removed, segment slugified —
|
|
35
|
+
* so that `02-guide/diagram.png` lands under `/guide/`, where the pages of the
|
|
36
|
+
* same folder expect it. Otherwise the sorting prefix, which never shows in a
|
|
37
|
+
* page URL, would show in the URL of its images.
|
|
38
|
+
*
|
|
39
|
+
* The file name itself stays untouched: it is the one the author writes in
|
|
40
|
+
* their Markdown, and rewriting it would break the reference.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* assetPathToSlug('02-guide/diagram.png') // 'guide/diagram.png'
|
|
44
|
+
* assetPathToSlug('My Notes/a.png') // 'my-notes/a.png'
|
|
45
|
+
*
|
|
46
|
+
* @param {string} relativePath Path relative to the version folder.
|
|
47
|
+
* @returns {string} Output path, slash-separated.
|
|
48
|
+
*/
|
|
49
|
+
export declare function assetPathToSlug(relativePath: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Segments of a path that leave nothing behind once slugified.
|
|
52
|
+
*
|
|
53
|
+
* `slugify` only keeps Latin letters and digits: a name made entirely of
|
|
54
|
+
* ideograms, or of punctuation, vanishes. For a page, that meant taking the
|
|
55
|
+
* home page's URL; for a folder, disappearing from the address.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* blankSegments('guide/日本語.md') // ['日本語']
|
|
59
|
+
* blankSegments('02-guide/page.md') // []
|
|
60
|
+
*
|
|
61
|
+
* @param {string} relativePath Path relative to the version folder.
|
|
62
|
+
* @returns {string[]} The offending segments, extension removed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function blankSegments(relativePath: string): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Maps a **folder** path into URL space.
|
|
67
|
+
*
|
|
68
|
+
* Sibling of `assetPathToSlug`, but with no file name to spare: every segment
|
|
69
|
+
* goes through the page rule. Applying `assetPathToSlug` to a folder left its
|
|
70
|
+
* last segment untouched — `02-guide` stayed `02-guide` — and every relative
|
|
71
|
+
* target of a page in that folder missed the file actually copied.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* dirPathToSlug('02-guide/03-sub') // 'guide/sub'
|
|
75
|
+
* dirPathToSlug('My Notes') // 'my-notes'
|
|
76
|
+
* dirPathToSlug('') // ''
|
|
77
|
+
*
|
|
78
|
+
* @param {string} relativePath Path relative to the version folder.
|
|
79
|
+
* @returns {string} Output path, slash-separated.
|
|
80
|
+
*/
|
|
81
|
+
export declare function dirPathToSlug(relativePath: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Converts a page slug into an absolute site URL (trailing slash included).
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* slugToUrl('guide/install') // '/guide/install/'
|
|
87
|
+
* slugToUrl('') // '/'
|
|
88
|
+
* slugToUrl('intro', 'v1.0') // '/versions/v1.0/intro/'
|
|
89
|
+
*
|
|
90
|
+
* @param {string} slug Slug produced by {@link filePathToSlug}.
|
|
91
|
+
* @param {string} [versionSlug] When given, prefixes `/versions/<version>`.
|
|
92
|
+
* @returns {string} URL starting and ending with `/`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function slugToUrl(slug: string, versionSlug?: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Turns a slug segment back into a readable label.
|
|
97
|
+
*
|
|
98
|
+
* A deliberate, imperfect fallback: since `slugify` dropped the accents,
|
|
99
|
+
* `creme-brulee` comes back as “Creme brulee”. Only use it when there is no
|
|
100
|
+
* real title — the sidebar and the breadcrumb prefer the frontmatter title.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* humanizeSlug('getting-started') // 'Getting started'
|
|
104
|
+
*
|
|
105
|
+
* @param {string} segment
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
export declare function humanizeSlug(segment: string): string;
|
|
109
|
+
/**
|
|
110
|
+
* Reads the ordering weight of a prefixed file name.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* orderOf('01-install.md') // 1
|
|
114
|
+
* orderOf('install.md') // Infinity
|
|
115
|
+
*
|
|
116
|
+
* @param {string} filename
|
|
117
|
+
* @returns {number} The prefix number, or `Infinity` when absent (sorted last).
|
|
118
|
+
*/
|
|
119
|
+
export declare function orderOf(filename: string): number;
|