@docusaurus/utils 3.9.2 → 3.10.1-canary-6591
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/index.d.ts +5 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +13 -11
- package/lib/index.js.map +1 -1
- package/lib/lastUpdateUtils.d.ts +2 -6
- package/lib/lastUpdateUtils.d.ts.map +1 -1
- package/lib/lastUpdateUtils.js +13 -60
- package/lib/lastUpdateUtils.js.map +1 -1
- package/lib/markdownHeadingIdUtils.d.ts +49 -0
- package/lib/markdownHeadingIdUtils.d.ts.map +1 -0
- package/lib/markdownHeadingIdUtils.js +148 -0
- package/lib/markdownHeadingIdUtils.js.map +1 -0
- package/lib/markdownUtils.d.ts +0 -31
- package/lib/markdownUtils.d.ts.map +1 -1
- package/lib/markdownUtils.js +0 -89
- package/lib/markdownUtils.js.map +1 -1
- package/lib/moduleUtils.d.ts.map +1 -1
- package/lib/moduleUtils.js +4 -4
- package/lib/moduleUtils.js.map +1 -1
- package/lib/{gitUtils.d.ts → vcs/gitUtils.d.ts} +18 -0
- package/lib/vcs/gitUtils.d.ts.map +1 -0
- package/lib/vcs/gitUtils.js +343 -0
- package/lib/vcs/gitUtils.js.map +1 -0
- package/lib/vcs/vcs.d.ts +19 -0
- package/lib/vcs/vcs.d.ts.map +1 -0
- package/lib/vcs/vcs.js +46 -0
- package/lib/vcs/vcs.js.map +1 -0
- package/lib/vcs/vcsDefaultV1.d.ts +13 -0
- package/lib/vcs/vcsDefaultV1.d.ts.map +1 -0
- package/lib/vcs/vcsDefaultV1.js +33 -0
- package/lib/vcs/vcsDefaultV1.js.map +1 -0
- package/lib/vcs/vcsDefaultV2.d.ts +13 -0
- package/lib/vcs/vcsDefaultV2.d.ts.map +1 -0
- package/lib/vcs/vcsDefaultV2.js +33 -0
- package/lib/vcs/vcsDefaultV2.js.map +1 -0
- package/lib/vcs/vcsDisabled.d.ts +12 -0
- package/lib/vcs/vcsDisabled.d.ts.map +1 -0
- package/lib/vcs/vcsDisabled.js +24 -0
- package/lib/vcs/vcsDisabled.js.map +1 -0
- package/lib/vcs/vcsGitAdHoc.d.ts +16 -0
- package/lib/vcs/vcsGitAdHoc.d.ts.map +1 -0
- package/lib/vcs/vcsGitAdHoc.js +29 -0
- package/lib/vcs/vcsGitAdHoc.js.map +1 -0
- package/lib/vcs/vcsGitEager.d.ts +10 -0
- package/lib/vcs/vcsGitEager.d.ts.map +1 -0
- package/lib/vcs/vcsGitEager.js +89 -0
- package/lib/vcs/vcsGitEager.js.map +1 -0
- package/lib/vcs/vcsHardcoded.d.ts +17 -0
- package/lib/vcs/vcsHardcoded.d.ts.map +1 -0
- package/lib/vcs/vcsHardcoded.js +41 -0
- package/lib/vcs/vcsHardcoded.js.map +1 -0
- package/package.json +11 -11
- package/src/index.ts +11 -8
- package/src/lastUpdateUtils.ts +18 -76
- package/src/markdownHeadingIdUtils.ts +209 -0
- package/src/markdownUtils.ts +0 -119
- package/src/moduleUtils.ts +6 -8
- package/src/vcs/gitUtils.ts +541 -0
- package/src/vcs/vcs.ts +54 -0
- package/src/vcs/vcsDefaultV1.ts +33 -0
- package/src/vcs/vcsDefaultV2.ts +33 -0
- package/src/vcs/vcsDisabled.ts +25 -0
- package/src/vcs/vcsGitAdHoc.ts +30 -0
- package/src/vcs/vcsGitEager.ts +135 -0
- package/src/vcs/vcsHardcoded.ts +45 -0
- package/lib/cliUtils.d.ts +0 -14
- package/lib/cliUtils.d.ts.map +0 -1
- package/lib/cliUtils.js +0 -49
- package/lib/cliUtils.js.map +0 -1
- package/lib/gitUtils.d.ts.map +0 -1
- package/lib/gitUtils.js +0 -103
- package/lib/gitUtils.js.map +0 -1
- package/src/cliUtils.ts +0 -65
- package/src/gitUtils.ts +0 -200
|
@@ -0,0 +1,209 @@
|
|
|
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 {createSlugger, type Slugger, type SluggerOptions} from './slugger';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The syntax to use for heading IDs.
|
|
12
|
+
* - `classic` => `{#id}` (invalid MDX, but commonly supported)
|
|
13
|
+
* - `mdx-comment` => `{/* #id * /}` (valid MDX)
|
|
14
|
+
*/
|
|
15
|
+
export type HeadingIdSyntax = 'classic' | 'mdx-comment';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parses custom ID from a heading. The ID can contain any characters except
|
|
19
|
+
* `{#` and `}`.
|
|
20
|
+
*
|
|
21
|
+
* @param heading e.g. `## Some heading {#some-heading}` where the last
|
|
22
|
+
* character must be `}` for the ID to be recognized
|
|
23
|
+
* @param syntax which heading ID syntax to recognize
|
|
24
|
+
*/
|
|
25
|
+
export function parseMarkdownHeadingId(
|
|
26
|
+
heading: string,
|
|
27
|
+
syntax: HeadingIdSyntax = 'classic',
|
|
28
|
+
): {
|
|
29
|
+
/**
|
|
30
|
+
* The heading content sans the ID part, right-trimmed. e.g. `## Some heading`
|
|
31
|
+
*/
|
|
32
|
+
text: string;
|
|
33
|
+
/** The heading ID. e.g. `some-heading` */
|
|
34
|
+
id: string | undefined;
|
|
35
|
+
} {
|
|
36
|
+
// Classic syntax: {#my-id}
|
|
37
|
+
if (syntax === 'classic') {
|
|
38
|
+
const customHeadingIdRegex = /\s*\{#(?<id>(?:.(?!\{#|\}))*.)\}$/;
|
|
39
|
+
const matches = customHeadingIdRegex.exec(heading);
|
|
40
|
+
if (matches) {
|
|
41
|
+
return {
|
|
42
|
+
text: heading.replace(matches[0]!, ''),
|
|
43
|
+
id: matches.groups!.id!.trim(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// MDX comment syntax: {/* #my-id */}
|
|
48
|
+
// Note: this is only used for the "write-heading-ids" CLI
|
|
49
|
+
// The mdx loader is using a real MDX parser to find these comments
|
|
50
|
+
else if (syntax === 'mdx-comment') {
|
|
51
|
+
const mdxCommentHeadingIdRegex = /\s*\{\/\*\s*#(?<id>\S+)\s*\*\/\}$/;
|
|
52
|
+
const mdxMatches = mdxCommentHeadingIdRegex.exec(heading);
|
|
53
|
+
if (mdxMatches) {
|
|
54
|
+
return {
|
|
55
|
+
text: heading.replace(mdxMatches[0]!, ''),
|
|
56
|
+
id: mdxMatches.groups!.id!.trim(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Unhandled cases, shouldn't happen
|
|
61
|
+
else {
|
|
62
|
+
throw new Error(`unknown heading id syntax '${syntax}'`);
|
|
63
|
+
}
|
|
64
|
+
return {text: heading, id: undefined};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* For our classic syntax, MDX v2+ now requires escaping { to compile: \{#id}.
|
|
69
|
+
* See https://mdxjs.com/docs/troubleshooting-mdx/#could-not-parse-expression-with-acorn-error
|
|
70
|
+
*/
|
|
71
|
+
export function escapeMarkdownHeadingIds(content: string): string {
|
|
72
|
+
const markdownHeadingRegexp = /(?:^|\n)#{1,6}(?!#).*/g;
|
|
73
|
+
return content.replaceAll(markdownHeadingRegexp, (substring) =>
|
|
74
|
+
// TODO probably not the most efficient impl...
|
|
75
|
+
substring
|
|
76
|
+
.replace('{#', '\\{#')
|
|
77
|
+
// prevent duplicate escaping
|
|
78
|
+
.replace('\\\\{#', '\\{#'),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function addHeadingId(
|
|
83
|
+
line: string,
|
|
84
|
+
slugger: Slugger,
|
|
85
|
+
maintainCase: boolean,
|
|
86
|
+
syntax: HeadingIdSyntax,
|
|
87
|
+
headingId: string | undefined,
|
|
88
|
+
): string {
|
|
89
|
+
let headingLevel = 0;
|
|
90
|
+
while (line.charAt(headingLevel) === '#') {
|
|
91
|
+
headingLevel += 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const headingHashes = line.slice(0, headingLevel);
|
|
95
|
+
|
|
96
|
+
const headingContent = line.slice(headingLevel).trimEnd();
|
|
97
|
+
|
|
98
|
+
function getHeadingId() {
|
|
99
|
+
if (headingId) {
|
|
100
|
+
return headingId;
|
|
101
|
+
}
|
|
102
|
+
// Unwrap links
|
|
103
|
+
// "[ Hello](https://example.com) World " => "Hello world"
|
|
104
|
+
const headingText = headingContent
|
|
105
|
+
.replace(/\[(?<alt>[^\]]+)\]\([^)]+\)/g, (_match, p1: string) => p1)
|
|
106
|
+
.trim();
|
|
107
|
+
|
|
108
|
+
return slugger.slug(headingText, {
|
|
109
|
+
maintainCase,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const headingIdSuffix =
|
|
114
|
+
syntax === 'mdx-comment'
|
|
115
|
+
? `{/* #${getHeadingId()} */}`
|
|
116
|
+
: `{#${getHeadingId()}}`;
|
|
117
|
+
|
|
118
|
+
return `${headingHashes}${headingContent} ${headingIdSuffix}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type WriteHeadingIDOptions = SluggerOptions & {
|
|
122
|
+
/** The target syntax to use for heading IDs. */
|
|
123
|
+
syntax?: HeadingIdSyntax;
|
|
124
|
+
/** Migrate the existing heading IDs to the target syntax */
|
|
125
|
+
migrate?: boolean;
|
|
126
|
+
/** Overwrite existing heading IDs by re-generating them from the text. */
|
|
127
|
+
overwrite?: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Takes Markdown content, returns new content with heading IDs written.
|
|
132
|
+
* Respects existing IDs (unless `overwrite=true`) and never generates colliding
|
|
133
|
+
* IDs (through the slugger).
|
|
134
|
+
*/
|
|
135
|
+
export function writeMarkdownHeadingId(
|
|
136
|
+
content: string,
|
|
137
|
+
options: WriteHeadingIDOptions = {},
|
|
138
|
+
): string {
|
|
139
|
+
const {
|
|
140
|
+
syntax = 'classic', // Maybe we'll want to change this default later?
|
|
141
|
+
overwrite = false,
|
|
142
|
+
migrate = false,
|
|
143
|
+
maintainCase = false,
|
|
144
|
+
} = options;
|
|
145
|
+
|
|
146
|
+
// For now, we have 2 booleans (retro compatible)
|
|
147
|
+
// but it could be useful to have a "mode" enum instead?
|
|
148
|
+
if (overwrite && migrate) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
'Heading ids can either be overwritten or migrated, not both at the same time',
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const lines = content.split('\n');
|
|
155
|
+
const slugger = createSlugger();
|
|
156
|
+
|
|
157
|
+
// Parse heading ID trying both syntaxes (classic first, then mdx-comment)
|
|
158
|
+
function parseHeadingIdAnySyntax(heading: string) {
|
|
159
|
+
const classic = parseMarkdownHeadingId(heading, 'classic');
|
|
160
|
+
if (classic.id) {
|
|
161
|
+
return classic;
|
|
162
|
+
}
|
|
163
|
+
return parseMarkdownHeadingId(heading, 'mdx-comment');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If we can't overwrite existing slugs, make sure other headings don't
|
|
167
|
+
// generate colliding slugs by first marking these slugs as occupied
|
|
168
|
+
if (!overwrite) {
|
|
169
|
+
lines.forEach((line) => {
|
|
170
|
+
const parsedHeading = parseHeadingIdAnySyntax(line);
|
|
171
|
+
if (parsedHeading.id) {
|
|
172
|
+
slugger.slug(parsedHeading.id);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let inCode = false;
|
|
178
|
+
return lines
|
|
179
|
+
.map((line) => {
|
|
180
|
+
if (line.startsWith('```')) {
|
|
181
|
+
inCode = !inCode;
|
|
182
|
+
return line;
|
|
183
|
+
}
|
|
184
|
+
// Ignore h1 headings, as we don't create anchor links for those
|
|
185
|
+
if (inCode || !line.startsWith('##')) {
|
|
186
|
+
return line;
|
|
187
|
+
}
|
|
188
|
+
const parsedHeading = parseHeadingIdAnySyntax(line);
|
|
189
|
+
|
|
190
|
+
// Preserve the line if id is already there, unless we migrate/overwrite
|
|
191
|
+
if (parsedHeading.id && !overwrite && !migrate) {
|
|
192
|
+
return line;
|
|
193
|
+
}
|
|
194
|
+
const headingId = overwrite
|
|
195
|
+
? undefined
|
|
196
|
+
: migrate
|
|
197
|
+
? parsedHeading.id
|
|
198
|
+
: undefined;
|
|
199
|
+
|
|
200
|
+
return addHeadingId(
|
|
201
|
+
parsedHeading.text,
|
|
202
|
+
slugger,
|
|
203
|
+
maintainCase,
|
|
204
|
+
syntax,
|
|
205
|
+
headingId,
|
|
206
|
+
);
|
|
207
|
+
})
|
|
208
|
+
.join('\n');
|
|
209
|
+
}
|
package/src/markdownUtils.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import logger from '@docusaurus/logger';
|
|
9
9
|
import matter from 'gray-matter';
|
|
10
|
-
import {createSlugger, type Slugger, type SluggerOptions} from './slugger';
|
|
11
10
|
import type {
|
|
12
11
|
ParseFrontMatter,
|
|
13
12
|
DefaultParseFrontMatter,
|
|
@@ -17,47 +16,6 @@ import type {
|
|
|
17
16
|
// server-side when we infer metadata like `title` and `description` from the
|
|
18
17
|
// content. Most parsing is still done in MDX through the mdx-loader.
|
|
19
18
|
|
|
20
|
-
/**
|
|
21
|
-
* Parses custom ID from a heading. The ID can contain any characters except
|
|
22
|
-
* `{#` and `}`.
|
|
23
|
-
*
|
|
24
|
-
* @param heading e.g. `## Some heading {#some-heading}` where the last
|
|
25
|
-
* character must be `}` for the ID to be recognized
|
|
26
|
-
*/
|
|
27
|
-
export function parseMarkdownHeadingId(heading: string): {
|
|
28
|
-
/**
|
|
29
|
-
* The heading content sans the ID part, right-trimmed. e.g. `## Some heading`
|
|
30
|
-
*/
|
|
31
|
-
text: string;
|
|
32
|
-
/** The heading ID. e.g. `some-heading` */
|
|
33
|
-
id: string | undefined;
|
|
34
|
-
} {
|
|
35
|
-
const customHeadingIdRegex = /\s*\{#(?<id>(?:.(?!\{#|\}))*.)\}$/;
|
|
36
|
-
const matches = customHeadingIdRegex.exec(heading);
|
|
37
|
-
if (matches) {
|
|
38
|
-
return {
|
|
39
|
-
text: heading.replace(matches[0]!, ''),
|
|
40
|
-
id: matches.groups!.id!,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return {text: heading, id: undefined};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* MDX 2 requires escaping { with a \ so our anchor syntax need that now.
|
|
48
|
-
* See https://mdxjs.com/docs/troubleshooting-mdx/#could-not-parse-expression-with-acorn-error
|
|
49
|
-
*/
|
|
50
|
-
export function escapeMarkdownHeadingIds(content: string): string {
|
|
51
|
-
const markdownHeadingRegexp = /(?:^|\n)#{1,6}(?!#).*/g;
|
|
52
|
-
return content.replaceAll(markdownHeadingRegexp, (substring) =>
|
|
53
|
-
// TODO probably not the most efficient impl...
|
|
54
|
-
substring
|
|
55
|
-
.replace('{#', '\\{#')
|
|
56
|
-
// prevent duplicate escaping
|
|
57
|
-
.replace('\\\\{#', '\\{#'),
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
19
|
/**
|
|
62
20
|
* Hacky temporary escape hatch for Crowdin bad MDX support
|
|
63
21
|
* See https://docusaurus.io/docs/i18n/crowdin#mdx
|
|
@@ -383,80 +341,3 @@ This can happen if you use special characters in front matter values (try using
|
|
|
383
341
|
throw err;
|
|
384
342
|
}
|
|
385
343
|
}
|
|
386
|
-
|
|
387
|
-
function unwrapMarkdownLinks(line: string): string {
|
|
388
|
-
return line.replace(
|
|
389
|
-
/\[(?<alt>[^\]]+)\]\([^)]+\)/g,
|
|
390
|
-
(match, p1: string) => p1,
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function addHeadingId(
|
|
395
|
-
line: string,
|
|
396
|
-
slugger: Slugger,
|
|
397
|
-
maintainCase: boolean,
|
|
398
|
-
): string {
|
|
399
|
-
let headingLevel = 0;
|
|
400
|
-
while (line.charAt(headingLevel) === '#') {
|
|
401
|
-
headingLevel += 1;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const headingText = line.slice(headingLevel).trimEnd();
|
|
405
|
-
const headingHashes = line.slice(0, headingLevel);
|
|
406
|
-
const slug = slugger.slug(unwrapMarkdownLinks(headingText).trim(), {
|
|
407
|
-
maintainCase,
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
return `${headingHashes}${headingText} {#${slug}}`;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export type WriteHeadingIDOptions = SluggerOptions & {
|
|
414
|
-
/** Overwrite existing heading IDs. */
|
|
415
|
-
overwrite?: boolean;
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Takes Markdown content, returns new content with heading IDs written.
|
|
420
|
-
* Respects existing IDs (unless `overwrite=true`) and never generates colliding
|
|
421
|
-
* IDs (through the slugger).
|
|
422
|
-
*/
|
|
423
|
-
export function writeMarkdownHeadingId(
|
|
424
|
-
content: string,
|
|
425
|
-
options: WriteHeadingIDOptions = {maintainCase: false, overwrite: false},
|
|
426
|
-
): string {
|
|
427
|
-
const {maintainCase = false, overwrite = false} = options;
|
|
428
|
-
const lines = content.split('\n');
|
|
429
|
-
const slugger = createSlugger();
|
|
430
|
-
|
|
431
|
-
// If we can't overwrite existing slugs, make sure other headings don't
|
|
432
|
-
// generate colliding slugs by first marking these slugs as occupied
|
|
433
|
-
if (!overwrite) {
|
|
434
|
-
lines.forEach((line) => {
|
|
435
|
-
const parsedHeading = parseMarkdownHeadingId(line);
|
|
436
|
-
if (parsedHeading.id) {
|
|
437
|
-
slugger.slug(parsedHeading.id);
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
let inCode = false;
|
|
443
|
-
return lines
|
|
444
|
-
.map((line) => {
|
|
445
|
-
if (line.startsWith('```')) {
|
|
446
|
-
inCode = !inCode;
|
|
447
|
-
return line;
|
|
448
|
-
}
|
|
449
|
-
// Ignore h1 headings, as we don't create anchor links for those
|
|
450
|
-
if (inCode || !line.startsWith('##')) {
|
|
451
|
-
return line;
|
|
452
|
-
}
|
|
453
|
-
const parsedHeading = parseMarkdownHeadingId(line);
|
|
454
|
-
|
|
455
|
-
// Do not process if id is already there
|
|
456
|
-
if (parsedHeading.id && !overwrite) {
|
|
457
|
-
return line;
|
|
458
|
-
}
|
|
459
|
-
return addHeadingId(parsedHeading.text, slugger, maintainCase);
|
|
460
|
-
})
|
|
461
|
-
.join('\n');
|
|
462
|
-
}
|
package/src/moduleUtils.ts
CHANGED
|
@@ -12,12 +12,12 @@ import logger from '@docusaurus/logger';
|
|
|
12
12
|
jiti is able to load ESM, CJS, JSON, TS modules
|
|
13
13
|
*/
|
|
14
14
|
export async function loadFreshModule(modulePath: string): Promise<unknown> {
|
|
15
|
+
if (typeof modulePath !== 'string') {
|
|
16
|
+
throw new Error(
|
|
17
|
+
logger.interpolate`Invalid module path of type "name=${typeof modulePath}" with value "name=${modulePath}"`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
15
20
|
try {
|
|
16
|
-
if (typeof modulePath !== 'string') {
|
|
17
|
-
throw new Error(
|
|
18
|
-
logger.interpolate`Invalid module path of type name=${modulePath}`,
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
21
|
const load = jiti(__filename, {
|
|
22
22
|
// Transpilation cache, can be safely enabled
|
|
23
23
|
cache: true,
|
|
@@ -34,9 +34,7 @@ export async function loadFreshModule(modulePath: string): Promise<unknown> {
|
|
|
34
34
|
return load(modulePath);
|
|
35
35
|
} catch (error) {
|
|
36
36
|
throw new Error(
|
|
37
|
-
logger.interpolate`Docusaurus could not load module at path path=${modulePath}
|
|
38
|
-
(error as Error).message
|
|
39
|
-
}`,
|
|
37
|
+
logger.interpolate`Docusaurus could not load module at path path=${modulePath}`,
|
|
40
38
|
{cause: error},
|
|
41
39
|
);
|
|
42
40
|
}
|