@docusaurus/utils 2.0.0-beta.16 → 2.0.0-beta.19
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/lib/constants.d.ts +51 -1
- package/lib/constants.d.ts.map +1 -1
- package/lib/constants.js +57 -10
- package/lib/constants.js.map +1 -1
- package/lib/dataFileUtils.d.ts +38 -2
- package/lib/dataFileUtils.d.ts.map +1 -1
- package/lib/dataFileUtils.js +39 -13
- package/lib/dataFileUtils.js.map +1 -1
- package/lib/emitUtils.d.ts +32 -0
- package/lib/emitUtils.d.ts.map +1 -0
- package/lib/emitUtils.js +80 -0
- package/lib/emitUtils.js.map +1 -0
- package/lib/gitUtils.d.ts +54 -5
- package/lib/gitUtils.d.ts.map +1 -1
- package/lib/gitUtils.js +17 -14
- package/lib/gitUtils.js.map +1 -1
- package/lib/globUtils.d.ts +28 -0
- package/lib/globUtils.d.ts.map +1 -1
- package/lib/globUtils.js +36 -13
- package/lib/globUtils.js.map +1 -1
- package/lib/hashUtils.d.ts +5 -4
- package/lib/hashUtils.d.ts.map +1 -1
- package/lib/hashUtils.js +7 -6
- package/lib/hashUtils.js.map +1 -1
- package/lib/i18nUtils.d.ts +51 -0
- package/lib/i18nUtils.d.ts.map +1 -0
- package/lib/i18nUtils.js +69 -0
- package/lib/i18nUtils.js.map +1 -0
- package/lib/index.d.ts +10 -54
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +56 -256
- package/lib/index.js.map +1 -1
- package/lib/jsUtils.d.ts +45 -0
- package/lib/jsUtils.d.ts.map +1 -0
- package/lib/jsUtils.js +94 -0
- package/lib/jsUtils.js.map +1 -0
- package/lib/markdownLinks.d.ts +48 -5
- package/lib/markdownLinks.d.ts.map +1 -1
- package/lib/markdownLinks.js +29 -13
- package/lib/markdownLinks.js.map +1 -1
- package/lib/markdownUtils.d.ts +112 -0
- package/lib/markdownUtils.d.ts.map +1 -0
- package/lib/markdownUtils.js +271 -0
- package/lib/markdownUtils.js.map +1 -0
- package/lib/pathUtils.d.ts +2 -1
- package/lib/pathUtils.d.ts.map +1 -1
- package/lib/pathUtils.js +16 -7
- package/lib/pathUtils.js.map +1 -1
- package/lib/slugger.d.ts +10 -0
- package/lib/slugger.d.ts.map +1 -1
- package/lib/slugger.js +6 -2
- package/lib/slugger.js.map +1 -1
- package/lib/tags.d.ts +42 -10
- package/lib/tags.d.ts.map +1 -1
- package/lib/tags.js +40 -26
- package/lib/tags.js.map +1 -1
- package/lib/urlUtils.d.ts +57 -0
- package/lib/urlUtils.d.ts.map +1 -1
- package/lib/urlUtils.js +132 -6
- package/lib/urlUtils.js.map +1 -1
- package/lib/webpackUtils.d.ts +5 -0
- package/lib/webpackUtils.d.ts.map +1 -1
- package/lib/webpackUtils.js +8 -5
- package/lib/webpackUtils.js.map +1 -1
- package/package.json +11 -11
- package/src/constants.ts +65 -9
- package/src/dataFileUtils.ts +44 -12
- package/src/emitUtils.ts +99 -0
- package/src/gitUtils.ts +77 -17
- package/src/globUtils.ts +34 -13
- package/src/hashUtils.ts +6 -5
- package/src/i18nUtils.ts +115 -0
- package/src/index.ts +43 -307
- package/src/jsUtils.ts +102 -0
- package/src/markdownLinks.ts +71 -28
- package/src/markdownUtils.ts +354 -0
- package/src/pathUtils.ts +15 -7
- package/src/slugger.ts +13 -1
- package/src/tags.ts +53 -28
- package/src/urlUtils.ts +145 -7
- package/src/webpackUtils.ts +11 -4
- package/lib/markdownParser.d.ts +0 -32
- package/lib/markdownParser.d.ts.map +0 -1
- package/lib/markdownParser.js +0 -161
- package/lib/markdownParser.js.map +0 -1
- package/src/markdownParser.ts +0 -201
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import logger from '@docusaurus/logger';
|
|
9
|
+
import matter from 'gray-matter';
|
|
10
|
+
import {createSlugger, type Slugger, type SluggerOptions} from './slugger';
|
|
11
|
+
|
|
12
|
+
// Some utilities for parsing Markdown content. These things are only used on
|
|
13
|
+
// server-side when we infer metadata like `title` and `description` from the
|
|
14
|
+
// content. Most parsing is still done in MDX through the mdx-loader.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parses custom ID from a heading. The ID must be composed of letters,
|
|
18
|
+
* underscores, and dashes only.
|
|
19
|
+
*
|
|
20
|
+
* @param heading e.g. `## Some heading {#some-heading}` where the last
|
|
21
|
+
* character must be `}` for the ID to be recognized
|
|
22
|
+
*/
|
|
23
|
+
export function parseMarkdownHeadingId(heading: string): {
|
|
24
|
+
/**
|
|
25
|
+
* The heading content sans the ID part, right-trimmed. e.g. `## Some heading`
|
|
26
|
+
*/
|
|
27
|
+
text: string;
|
|
28
|
+
/** The heading ID. e.g. `some-heading` */
|
|
29
|
+
id?: string;
|
|
30
|
+
} {
|
|
31
|
+
const customHeadingIdRegex = /\s*\{#(?<id>[\w-]+)\}$/;
|
|
32
|
+
const matches = customHeadingIdRegex.exec(heading);
|
|
33
|
+
if (matches) {
|
|
34
|
+
return {
|
|
35
|
+
text: heading.replace(matches[0]!, ''),
|
|
36
|
+
id: matches.groups!.id!,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {text: heading, id: undefined};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// TODO: Find a better way to do so, possibly by compiling the Markdown content,
|
|
43
|
+
// stripping out HTML tags and obtaining the first line.
|
|
44
|
+
/**
|
|
45
|
+
* Creates an excerpt of a Markdown file. This function will:
|
|
46
|
+
*
|
|
47
|
+
* - Ignore h1 headings (setext or atx)
|
|
48
|
+
* - Ignore import/export
|
|
49
|
+
* - Ignore code blocks
|
|
50
|
+
*
|
|
51
|
+
* And for the first contentful line, it will strip away most Markdown
|
|
52
|
+
* syntax, including HTML tags, emphasis, links (keeping the text), etc.
|
|
53
|
+
*/
|
|
54
|
+
export function createExcerpt(fileString: string): string | undefined {
|
|
55
|
+
const fileLines = fileString
|
|
56
|
+
.trimStart()
|
|
57
|
+
// Remove Markdown alternate title
|
|
58
|
+
.replace(/^[^\n]*\n[=]+/g, '')
|
|
59
|
+
.split('\n');
|
|
60
|
+
let inCode = false;
|
|
61
|
+
let inImport = false;
|
|
62
|
+
let lastCodeFence = '';
|
|
63
|
+
|
|
64
|
+
for (const fileLine of fileLines) {
|
|
65
|
+
if (fileLine === '' && inImport) {
|
|
66
|
+
inImport = false;
|
|
67
|
+
}
|
|
68
|
+
// Skip empty line.
|
|
69
|
+
if (!fileLine.trim()) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Skip import/export declaration.
|
|
74
|
+
if ((/^(?:import|export)\s.*/.test(fileLine) || inImport) && !inCode) {
|
|
75
|
+
inImport = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Skip code block line.
|
|
80
|
+
if (fileLine.trim().startsWith('```')) {
|
|
81
|
+
const codeFence = fileLine.trim().match(/^`+/)![0]!;
|
|
82
|
+
if (!inCode) {
|
|
83
|
+
inCode = true;
|
|
84
|
+
lastCodeFence = codeFence;
|
|
85
|
+
// If we are in a ````-fenced block, all ``` would be plain text instead
|
|
86
|
+
// of fences
|
|
87
|
+
} else if (codeFence.length >= lastCodeFence.length) {
|
|
88
|
+
inCode = false;
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
} else if (inCode) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cleanedLine = fileLine
|
|
96
|
+
// Remove HTML tags.
|
|
97
|
+
.replace(/<[^>]*>/g, '')
|
|
98
|
+
// Remove Title headers
|
|
99
|
+
.replace(/^#[^#]+#?/gm, '')
|
|
100
|
+
// Remove Markdown + ATX-style headers
|
|
101
|
+
.replace(/^#{1,6}\s*(?<text>[^#]*)\s*#{0,6}/gm, '$1')
|
|
102
|
+
// Remove emphasis.
|
|
103
|
+
.replace(/(?<opening>[*_]{1,3})(?<text>.*?)\1/g, '$2')
|
|
104
|
+
// Remove strikethroughs.
|
|
105
|
+
.replace(/~~(?<text>\S.*\S)~~/g, '$1')
|
|
106
|
+
// Remove images.
|
|
107
|
+
.replace(/!\[(?<alt>.*?)\][[(].*?[\])]/g, '$1')
|
|
108
|
+
// Remove footnotes.
|
|
109
|
+
.replace(/\[\^.+?\](?:: .*$)?/g, '')
|
|
110
|
+
// Remove inline links.
|
|
111
|
+
.replace(/\[(?<alt>.*?)\][[(].*?[\])]/g, '$1')
|
|
112
|
+
// Remove inline code.
|
|
113
|
+
.replace(/`(?<text>.+?)`/g, '$1')
|
|
114
|
+
// Remove blockquotes.
|
|
115
|
+
.replace(/^\s{0,3}>\s?/g, '')
|
|
116
|
+
// Remove admonition definition.
|
|
117
|
+
.replace(/:::.*/, '')
|
|
118
|
+
// Remove Emoji names within colons include preceding whitespace.
|
|
119
|
+
.replace(/\s?:(?:::|[^:\n])+:/g, '')
|
|
120
|
+
// Remove custom Markdown heading id.
|
|
121
|
+
.replace(/\{#*[\w-]+\}/, '')
|
|
122
|
+
.trim();
|
|
123
|
+
|
|
124
|
+
if (cleanedLine) {
|
|
125
|
+
return cleanedLine;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Takes a raw Markdown file content, and parses the front matter using
|
|
134
|
+
* gray-matter. Worth noting that gray-matter accepts TOML and other markup
|
|
135
|
+
* languages as well.
|
|
136
|
+
*
|
|
137
|
+
* @throws Throws when gray-matter throws. e.g.:
|
|
138
|
+
* ```md
|
|
139
|
+
* ---
|
|
140
|
+
* foo: : bar
|
|
141
|
+
* ---
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function parseFrontMatter(markdownFileContent: string): {
|
|
145
|
+
/** Front matter as parsed by gray-matter. */
|
|
146
|
+
frontMatter: {[key: string]: unknown};
|
|
147
|
+
/** The remaining content, trimmed. */
|
|
148
|
+
content: string;
|
|
149
|
+
} {
|
|
150
|
+
const {data, content} = matter(markdownFileContent);
|
|
151
|
+
return {
|
|
152
|
+
frontMatter: data,
|
|
153
|
+
content: content.trim(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toTextContentTitle(contentTitle: string): string {
|
|
158
|
+
if (contentTitle.startsWith('`') && contentTitle.endsWith('`')) {
|
|
159
|
+
return contentTitle.substring(1, contentTitle.length - 1);
|
|
160
|
+
}
|
|
161
|
+
return contentTitle;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type ParseMarkdownContentTitleOptions = {
|
|
165
|
+
/**
|
|
166
|
+
* If `true`, the matching title will be removed from the returned content.
|
|
167
|
+
* We can promise that at least one empty line will be left between the
|
|
168
|
+
* content before and after, but you shouldn't make too much assumption
|
|
169
|
+
* about what's left.
|
|
170
|
+
*/
|
|
171
|
+
removeContentTitle?: boolean;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Takes the raw Markdown content, without front matter, and tries to find an h1
|
|
176
|
+
* title (setext or atx) to be used as metadata.
|
|
177
|
+
*
|
|
178
|
+
* It only searches until the first contentful paragraph, ignoring import/export
|
|
179
|
+
* declarations.
|
|
180
|
+
*
|
|
181
|
+
* It will try to convert markdown to reasonable text, but won't be best effort,
|
|
182
|
+
* since it's only used as a fallback when `frontMatter.title` is not provided.
|
|
183
|
+
* For now, we just unwrap inline code (``# `config.js` `` => `config.js`).
|
|
184
|
+
*/
|
|
185
|
+
export function parseMarkdownContentTitle(
|
|
186
|
+
contentUntrimmed: string,
|
|
187
|
+
options?: ParseMarkdownContentTitleOptions,
|
|
188
|
+
): {
|
|
189
|
+
/** The content, optionally without the content title. */
|
|
190
|
+
content: string;
|
|
191
|
+
/** The title, trimmed and without the `#`. */
|
|
192
|
+
contentTitle: string | undefined;
|
|
193
|
+
} {
|
|
194
|
+
const removeContentTitleOption = options?.removeContentTitle ?? false;
|
|
195
|
+
|
|
196
|
+
const content = contentUntrimmed.trim();
|
|
197
|
+
// We only need to detect import statements that will be parsed by MDX as
|
|
198
|
+
// `import` nodes, as broken syntax can't render anyways. That means any block
|
|
199
|
+
// that has `import` at the very beginning and surrounded by empty lines.
|
|
200
|
+
const contentWithoutImport = content
|
|
201
|
+
.replace(/^(?:import\s(?:.|\r?\n(?!\r?\n))*(?:\r?\n){2,})*/, '')
|
|
202
|
+
.trim();
|
|
203
|
+
|
|
204
|
+
const regularTitleMatch = /^#[ \t]+(?<title>[^ \t].*)(?:\r?\n|$)/.exec(
|
|
205
|
+
contentWithoutImport,
|
|
206
|
+
);
|
|
207
|
+
const alternateTitleMatch = /^(?<title>.*)\r?\n=+(?:\r?\n|$)/.exec(
|
|
208
|
+
contentWithoutImport,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const titleMatch = regularTitleMatch ?? alternateTitleMatch;
|
|
212
|
+
if (!titleMatch) {
|
|
213
|
+
return {content, contentTitle: undefined};
|
|
214
|
+
}
|
|
215
|
+
const newContent = removeContentTitleOption
|
|
216
|
+
? content.replace(titleMatch[0]!, '')
|
|
217
|
+
: content;
|
|
218
|
+
if (regularTitleMatch) {
|
|
219
|
+
return {
|
|
220
|
+
content: newContent.trim(),
|
|
221
|
+
contentTitle: toTextContentTitle(
|
|
222
|
+
regularTitleMatch
|
|
223
|
+
.groups!.title!.trim()
|
|
224
|
+
.replace(/\s*(?:\{#*[\w-]+\}|#+)$/, ''),
|
|
225
|
+
).trim(),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
content: newContent.trim(),
|
|
230
|
+
contentTitle: toTextContentTitle(
|
|
231
|
+
alternateTitleMatch!.groups!.title!.trim().replace(/\s*=+$/, ''),
|
|
232
|
+
).trim(),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Makes a full-round parse.
|
|
238
|
+
*
|
|
239
|
+
* @throws Throws when `parseFrontMatter` throws, usually because of invalid
|
|
240
|
+
* syntax.
|
|
241
|
+
*/
|
|
242
|
+
export function parseMarkdownString(
|
|
243
|
+
markdownFileContent: string,
|
|
244
|
+
options?: ParseMarkdownContentTitleOptions,
|
|
245
|
+
): {
|
|
246
|
+
/** @see {@link parseFrontMatter} */
|
|
247
|
+
frontMatter: {[key: string]: unknown};
|
|
248
|
+
/** @see {@link parseMarkdownContentTitle} */
|
|
249
|
+
contentTitle: string | undefined;
|
|
250
|
+
/** @see {@link createExcerpt} */
|
|
251
|
+
excerpt: string | undefined;
|
|
252
|
+
/**
|
|
253
|
+
* Content without front matter and (optionally) without title, depending on
|
|
254
|
+
* the `removeContentTitle` option.
|
|
255
|
+
*/
|
|
256
|
+
content: string;
|
|
257
|
+
} {
|
|
258
|
+
try {
|
|
259
|
+
const {frontMatter, content: contentWithoutFrontMatter} =
|
|
260
|
+
parseFrontMatter(markdownFileContent);
|
|
261
|
+
|
|
262
|
+
const {content, contentTitle} = parseMarkdownContentTitle(
|
|
263
|
+
contentWithoutFrontMatter,
|
|
264
|
+
options,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const excerpt = createExcerpt(content);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
frontMatter,
|
|
271
|
+
content,
|
|
272
|
+
contentTitle,
|
|
273
|
+
excerpt,
|
|
274
|
+
};
|
|
275
|
+
} catch (err) {
|
|
276
|
+
logger.error(`Error while parsing Markdown front matter.
|
|
277
|
+
This can happen if you use special characters in front matter values (try using double quotes around that value).`);
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function unwrapMarkdownLinks(line: string): string {
|
|
283
|
+
return line.replace(/\[(?<alt>[^\]]+)\]\([^)]+\)/g, (match, p1) => p1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function addHeadingId(
|
|
287
|
+
line: string,
|
|
288
|
+
slugger: Slugger,
|
|
289
|
+
maintainCase: boolean,
|
|
290
|
+
): string {
|
|
291
|
+
let headingLevel = 0;
|
|
292
|
+
while (line.charAt(headingLevel) === '#') {
|
|
293
|
+
headingLevel += 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const headingText = line.slice(headingLevel).trimEnd();
|
|
297
|
+
const headingHashes = line.slice(0, headingLevel);
|
|
298
|
+
const slug = slugger.slug(unwrapMarkdownLinks(headingText).trim(), {
|
|
299
|
+
maintainCase,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return `${headingHashes}${headingText} {#${slug}}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export type WriteHeadingIDOptions = SluggerOptions & {
|
|
306
|
+
/** Overwrite existing heading IDs. */
|
|
307
|
+
overwrite?: boolean;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Takes Markdown content, returns new content with heading IDs written.
|
|
312
|
+
* Respects existing IDs (unless `overwrite=true`) and never generates colliding
|
|
313
|
+
* IDs (through the slugger).
|
|
314
|
+
*/
|
|
315
|
+
export function writeMarkdownHeadingId(
|
|
316
|
+
content: string,
|
|
317
|
+
options: WriteHeadingIDOptions = {maintainCase: false, overwrite: false},
|
|
318
|
+
): string {
|
|
319
|
+
const {maintainCase = false, overwrite = false} = options;
|
|
320
|
+
const lines = content.split('\n');
|
|
321
|
+
const slugger = createSlugger();
|
|
322
|
+
|
|
323
|
+
// If we can't overwrite existing slugs, make sure other headings don't
|
|
324
|
+
// generate colliding slugs by first marking these slugs as occupied
|
|
325
|
+
if (!overwrite) {
|
|
326
|
+
lines.forEach((line) => {
|
|
327
|
+
const parsedHeading = parseMarkdownHeadingId(line);
|
|
328
|
+
if (parsedHeading.id) {
|
|
329
|
+
slugger.slug(parsedHeading.id);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let inCode = false;
|
|
335
|
+
return lines
|
|
336
|
+
.map((line) => {
|
|
337
|
+
if (line.startsWith('```')) {
|
|
338
|
+
inCode = !inCode;
|
|
339
|
+
return line;
|
|
340
|
+
}
|
|
341
|
+
// Ignore h1 headings, as we don't create anchor links for those
|
|
342
|
+
if (inCode || !line.startsWith('##')) {
|
|
343
|
+
return line;
|
|
344
|
+
}
|
|
345
|
+
const parsedHeading = parseMarkdownHeadingId(line);
|
|
346
|
+
|
|
347
|
+
// Do not process if id is already there
|
|
348
|
+
if (parsedHeading.id && !overwrite) {
|
|
349
|
+
return line;
|
|
350
|
+
}
|
|
351
|
+
return addHeadingId(parsedHeading.text, slugger, maintainCase);
|
|
352
|
+
})
|
|
353
|
+
.join('\n');
|
|
354
|
+
}
|
package/src/pathUtils.ts
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
// Based on https://github.com/gatsbyjs/gatsby/pull/21518/files
|
|
9
|
-
|
|
10
8
|
import path from 'path';
|
|
11
9
|
|
|
12
|
-
//
|
|
10
|
+
// Based on https://github.com/gatsbyjs/gatsby/pull/21518/files
|
|
11
|
+
// macOS (APFS) and Windows (NTFS) filename length limit = 255 chars,
|
|
13
12
|
// Others = 255 bytes
|
|
14
13
|
const MAX_PATH_SEGMENT_CHARS = 255;
|
|
15
14
|
const MAX_PATH_SEGMENT_BYTES = 255;
|
|
@@ -22,10 +21,12 @@ const isWindows = () => process.platform === 'win32';
|
|
|
22
21
|
export const isNameTooLong = (str: string): boolean =>
|
|
23
22
|
// Not entirely correct: we can't assume FS from OS. But good enough?
|
|
24
23
|
isMacOs() || isWindows()
|
|
25
|
-
?
|
|
26
|
-
|
|
24
|
+
? // Windows (NTFS) and macOS (APFS) filename length limit (255 chars)
|
|
25
|
+
str.length + SPACE_FOR_APPENDING > MAX_PATH_SEGMENT_CHARS
|
|
26
|
+
: // Other (255 bytes)
|
|
27
|
+
Buffer.from(str).length + SPACE_FOR_APPENDING > MAX_PATH_SEGMENT_BYTES;
|
|
27
28
|
|
|
28
|
-
export
|
|
29
|
+
export function shortName(str: string): string {
|
|
29
30
|
if (isMacOs() || isWindows()) {
|
|
30
31
|
const overflowingChars = str.length - MAX_PATH_SEGMENT_CHARS;
|
|
31
32
|
return str.slice(
|
|
@@ -42,7 +43,7 @@ export const shortName = (str: string): string => {
|
|
|
42
43
|
Buffer.byteLength(strBuffer) - overflowingBytes - SPACE_FOR_APPENDING - 1,
|
|
43
44
|
)
|
|
44
45
|
.toString();
|
|
45
|
-
}
|
|
46
|
+
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Convert Windows backslash paths to posix style paths.
|
|
@@ -113,3 +114,10 @@ export function escapePath(str: string): string {
|
|
|
113
114
|
// Remove the " around the json string;
|
|
114
115
|
return escaped.substring(1, escaped.length - 1);
|
|
115
116
|
}
|
|
117
|
+
|
|
118
|
+
export function addTrailingPathSeparator(str: string): string {
|
|
119
|
+
return str.endsWith(path.sep)
|
|
120
|
+
? str
|
|
121
|
+
: // If this is Windows, we need to change the forward slash to backward
|
|
122
|
+
`${str.replace(/\/$/, '')}${path.sep}`;
|
|
123
|
+
}
|
package/src/slugger.ts
CHANGED
|
@@ -10,12 +10,24 @@ import GithubSlugger from 'github-slugger';
|
|
|
10
10
|
// We create our own abstraction on top of the lib:
|
|
11
11
|
// - unify usage everywhere in the codebase
|
|
12
12
|
// - ability to add extra options
|
|
13
|
-
export type SluggerOptions = {
|
|
13
|
+
export type SluggerOptions = {
|
|
14
|
+
/** Keep the headings' casing, otherwise make all lowercase. */
|
|
15
|
+
maintainCase?: boolean;
|
|
16
|
+
};
|
|
14
17
|
|
|
15
18
|
export type Slugger = {
|
|
19
|
+
/**
|
|
20
|
+
* Takes a Markdown heading like "Josh Cena" and sluggifies it according to
|
|
21
|
+
* GitHub semantics (in this case `josh-cena`). Stateful, because if you try
|
|
22
|
+
* to sluggify "Josh Cena" again it would return `josh-cena-1`.
|
|
23
|
+
*/
|
|
16
24
|
slug: (value: string, options?: SluggerOptions) => string;
|
|
17
25
|
};
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A thin wrapper around github-slugger. This is a factory function that returns
|
|
29
|
+
* a stateful Slugger object.
|
|
30
|
+
*/
|
|
19
31
|
export function createSlugger(): Slugger {
|
|
20
32
|
const githubSlugger = new GithubSlugger();
|
|
21
33
|
return {
|
package/src/tags.ts
CHANGED
|
@@ -8,14 +8,28 @@
|
|
|
8
8
|
import _ from 'lodash';
|
|
9
9
|
import {normalizeUrl} from './urlUtils';
|
|
10
10
|
|
|
11
|
+
/** What the user configures. */
|
|
11
12
|
export type Tag = {
|
|
12
13
|
label: string;
|
|
14
|
+
/** Permalink to this tag's page, without the `/tags/` base path. */
|
|
13
15
|
permalink: string;
|
|
14
16
|
};
|
|
15
17
|
|
|
18
|
+
/** What the tags list page should know about each tag. */
|
|
19
|
+
export type TagsListItem = Tag & {
|
|
20
|
+
/** Number of posts/docs with this tag. */
|
|
21
|
+
count: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** What the tag's own page should know about the tag. */
|
|
25
|
+
export type TagModule = TagsListItem & {
|
|
26
|
+
/** The tags list page's permalink. */
|
|
27
|
+
allTagsPath: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
16
30
|
export type FrontMatterTag = string | Tag;
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
function normalizeFrontMatterTag(
|
|
19
33
|
tagsPath: string,
|
|
20
34
|
frontMatterTag: FrontMatterTag,
|
|
21
35
|
): Tag {
|
|
@@ -28,7 +42,7 @@ export function normalizeFrontMatterTag(
|
|
|
28
42
|
|
|
29
43
|
// TODO maybe make ensure the permalink is valid url path?
|
|
30
44
|
function normalizeTagPermalink(permalink: string): string {
|
|
31
|
-
//
|
|
45
|
+
// Note: we always apply tagsPath on purpose. For versioned docs, v1/doc.md
|
|
32
46
|
// and v2/doc.md tags with custom permalinks don't lead to the same created
|
|
33
47
|
// page. tagsPath is different for each doc version
|
|
34
48
|
return normalizeUrl([tagsPath, permalink]);
|
|
@@ -45,8 +59,19 @@ export function normalizeFrontMatterTag(
|
|
|
45
59
|
};
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Takes tag objects as they are defined in front matter, and normalizes each
|
|
64
|
+
* into a standard tag object. The permalink is created by appending the
|
|
65
|
+
* sluggified label to `tagsPath`. Front matter tags already containing
|
|
66
|
+
* permalinks would still have `tagsPath` prepended.
|
|
67
|
+
*
|
|
68
|
+
* The result will always be unique by permalinks. The behavior with colliding
|
|
69
|
+
* permalinks is undetermined.
|
|
70
|
+
*/
|
|
48
71
|
export function normalizeFrontMatterTags(
|
|
72
|
+
/** Base path to append the tag permalinks to. */
|
|
49
73
|
tagsPath: string,
|
|
74
|
+
/** Can be `undefined`, so that we can directly pipe in `frontMatter.tags`. */
|
|
50
75
|
frontMatterTags: FrontMatterTag[] | undefined = [],
|
|
51
76
|
): Tag[] {
|
|
52
77
|
const tags = frontMatterTags.map((tag) =>
|
|
@@ -56,42 +81,42 @@ export function normalizeFrontMatterTags(
|
|
|
56
81
|
return _.uniqBy(tags, (tag) => tag.permalink);
|
|
57
82
|
}
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
type TaggedItemGroup<Item> = {
|
|
60
85
|
tag: Tag;
|
|
61
86
|
items: Item[];
|
|
62
87
|
};
|
|
63
88
|
|
|
64
89
|
/**
|
|
65
|
-
* Permits to group docs/
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
90
|
+
* Permits to group docs/blog posts by tag (provided by front matter).
|
|
91
|
+
*
|
|
92
|
+
* @returns a map from tag permalink to the items and other relevant tag data.
|
|
93
|
+
* The record is indexed by permalink, because routes must be unique in the end.
|
|
94
|
+
* Labels may vary on 2 MD files but they are normalized. Docs with
|
|
95
|
+
* label='some label' and label='some-label' should end up in the same page.
|
|
71
96
|
*/
|
|
72
97
|
export function groupTaggedItems<Item>(
|
|
73
|
-
items: Item[],
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// the same tag but the permalink is different for each
|
|
82
|
-
// For now, the first tag found wins
|
|
83
|
-
result[tag.permalink] = result[tag.permalink] ?? {
|
|
84
|
-
tag,
|
|
85
|
-
items: [],
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// Add item to group
|
|
89
|
-
result[tag.permalink].items.push(item);
|
|
90
|
-
}
|
|
98
|
+
items: readonly Item[],
|
|
99
|
+
/**
|
|
100
|
+
* A callback telling me how to get the tags list of the current item. Usually
|
|
101
|
+
* simply getting it from some metadata of the current item.
|
|
102
|
+
*/
|
|
103
|
+
getItemTags: (item: Item) => readonly Tag[],
|
|
104
|
+
): {[permalink: string]: TaggedItemGroup<Item>} {
|
|
105
|
+
const result: {[permalink: string]: TaggedItemGroup<Item>} = {};
|
|
91
106
|
|
|
92
107
|
items.forEach((item) => {
|
|
93
108
|
getItemTags(item).forEach((tag) => {
|
|
94
|
-
|
|
109
|
+
// Init missing tag groups
|
|
110
|
+
// TODO: it's not really clear what should be the behavior if 2 tags have
|
|
111
|
+
// the same permalink but the label is different for each
|
|
112
|
+
// For now, the first tag found wins
|
|
113
|
+
result[tag.permalink] ??= {
|
|
114
|
+
tag,
|
|
115
|
+
items: [],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Add item to group
|
|
119
|
+
result[tag.permalink]!.items.push(item);
|
|
95
120
|
});
|
|
96
121
|
});
|
|
97
122
|
|