@mui/internal-markdown 1.0.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.
@@ -0,0 +1,417 @@
1
+ import { expect } from 'chai';
2
+ import prepareMarkdown from './prepareMarkdown';
3
+
4
+ describe('prepareMarkdown', () => {
5
+ const defaultParams = {
6
+ fileRelativeContext: 'test/bar',
7
+ options: {
8
+ env: {},
9
+ },
10
+ };
11
+
12
+ it('returns the table of contents with html and emojis preserved and <a> tags stripped', () => {
13
+ const markdown = `
14
+ # Support
15
+
16
+ <p class="description">Foo</p>
17
+
18
+ ## Community help (free)
19
+ ### GitHub <img src="/static/images/logos/github.svg" width="24" height="24" alt="GitHub logo" loading="lazy" />
20
+ ### Unofficial 👍
21
+ ### Warning ⚠️
22
+ ### Header with Pro plan <a title="Pro plan" href="/x/introduction/licensing/#plan-pro"><span class="plan-pro"></span></a>
23
+ ### Header with \`code\`
24
+ `;
25
+
26
+ const {
27
+ docs: {
28
+ en: { toc },
29
+ },
30
+ } = prepareMarkdown({
31
+ ...defaultParams,
32
+ translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }],
33
+ });
34
+
35
+ expect(toc).to.have.deep.ordered.members([
36
+ {
37
+ children: [
38
+ {
39
+ hash: 'github',
40
+ level: 3,
41
+ text: 'GitHub <img src="/static/images/logos/github.svg" width="24" height="24" alt="GitHub logo" loading="lazy" />',
42
+ },
43
+ { hash: 'unofficial', level: 3, text: 'Unofficial 👍' },
44
+ { hash: 'warning', level: 3, text: 'Warning ⚠️' },
45
+ {
46
+ hash: 'header-with-pro-plan',
47
+ level: 3,
48
+ text: 'Header with Pro plan <span class="plan-pro"></span>',
49
+ },
50
+ {
51
+ hash: 'header-with-code',
52
+ level: 3,
53
+ text: 'Header with code',
54
+ },
55
+ ],
56
+ hash: 'community-help-free',
57
+ level: 2,
58
+ text: 'Community help (free)',
59
+ },
60
+ ]);
61
+ });
62
+
63
+ it('enables word-break for function signatures', () => {
64
+ const markdown = `
65
+ # Theming
66
+
67
+ <p class="description">Foo</p>
68
+
69
+ ## API
70
+ ### responsiveFontSizes(theme, options) => theme
71
+ ### createTheme(options, ...args) => theme
72
+ `;
73
+
74
+ const {
75
+ docs: {
76
+ en: { toc },
77
+ },
78
+ } = prepareMarkdown({
79
+ ...defaultParams,
80
+ translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }],
81
+ });
82
+
83
+ expect(toc).to.have.deep.ordered.members([
84
+ {
85
+ children: [
86
+ {
87
+ hash: 'responsivefontsizes-theme-options-theme',
88
+ level: 3,
89
+ text: 'responsiveFontSizes(&#8203;theme, options) =&gt; theme',
90
+ },
91
+ {
92
+ hash: 'createtheme-options-args-theme',
93
+ level: 3,
94
+ text: 'createTheme(&#8203;options, ...args) =&gt; theme',
95
+ },
96
+ ],
97
+ hash: 'api',
98
+ level: 2,
99
+ text: 'API',
100
+ },
101
+ ]);
102
+ });
103
+
104
+ it('use english hash for different locales', () => {
105
+ const markdownEn = `
106
+ # Localization
107
+
108
+ <p class="description">Foo</p>
109
+
110
+ ## Locales
111
+ ### Example
112
+ ### Use same hash
113
+ `;
114
+
115
+ const markdownPt = `
116
+ # Localização
117
+
118
+ <p class="description">Foo</p>
119
+
120
+ ## Idiomas
121
+ ### Exemplo
122
+ ### Usar o mesmo hash
123
+ `;
124
+
125
+ const markdownZh = `
126
+ # 所在位置
127
+
128
+ <p class="description">Foo</p>
129
+
130
+ ## 语言环境
131
+ ### 例
132
+ ### 使用相同的哈希
133
+ `;
134
+ const {
135
+ docs: {
136
+ en: { toc: tocEn },
137
+ pt: { toc: tocPt },
138
+ zh: { toc: tocZh },
139
+ },
140
+ } = prepareMarkdown({
141
+ pageFilename: '/same-hash-test',
142
+ translations: [
143
+ { filename: 'localization.md', markdown: markdownEn, userLanguage: 'en' },
144
+ { filename: 'localization-pt.md', markdown: markdownPt, userLanguage: 'pt' },
145
+ { filename: 'localization-zh.md', markdown: markdownZh, userLanguage: 'zh' },
146
+ ],
147
+ });
148
+
149
+ expect(tocZh).to.have.deep.ordered.members([
150
+ {
151
+ children: [
152
+ {
153
+ hash: 'example',
154
+ level: 3,
155
+ text: '例',
156
+ },
157
+ {
158
+ hash: 'use-same-hash',
159
+ level: 3,
160
+ text: '使用相同的哈希',
161
+ },
162
+ ],
163
+ hash: 'locales',
164
+ level: 2,
165
+ text: '语言环境',
166
+ },
167
+ ]);
168
+
169
+ expect(tocPt).to.have.deep.ordered.members([
170
+ {
171
+ children: [
172
+ {
173
+ hash: 'example',
174
+ level: 3,
175
+ text: 'Exemplo',
176
+ },
177
+ {
178
+ hash: 'use-same-hash',
179
+ level: 3,
180
+ text: 'Usar o mesmo hash',
181
+ },
182
+ ],
183
+ hash: 'locales',
184
+ level: 2,
185
+ text: 'Idiomas',
186
+ },
187
+ ]);
188
+
189
+ expect(tocEn).to.have.deep.ordered.members([
190
+ {
191
+ children: [
192
+ {
193
+ hash: 'example',
194
+ level: 3,
195
+ text: 'Example',
196
+ },
197
+ {
198
+ hash: 'use-same-hash',
199
+ level: 3,
200
+ text: 'Use same hash',
201
+ },
202
+ ],
203
+ hash: 'locales',
204
+ level: 2,
205
+ text: 'Locales',
206
+ },
207
+ ]);
208
+ });
209
+
210
+ it('use translated hash for translations are not synced', () => {
211
+ const markdownEn = `
212
+ # Localization
213
+
214
+ <p class="description">Foo</p>
215
+
216
+ ## Locales
217
+ ### Example
218
+ ### Use same hash
219
+ `;
220
+
221
+ const markdownPt = `
222
+ # Localização
223
+
224
+ <p class="description">Foo</p>
225
+
226
+ ## Idiomas
227
+ ### Exemplo
228
+ ### Usar o mesmo hash
229
+ ### Usar traduzido
230
+ `;
231
+
232
+ const {
233
+ docs: {
234
+ en: { toc: tocEn },
235
+ pt: { toc: tocPt },
236
+ },
237
+ } = prepareMarkdown({
238
+ pageFilename: '/same-hash-test',
239
+ translations: [
240
+ { filename: 'localization.md', markdown: markdownEn, userLanguage: 'en' },
241
+ { filename: 'localization-pt.md', markdown: markdownPt, userLanguage: 'pt' },
242
+ ],
243
+ });
244
+
245
+ expect(tocPt).to.have.deep.ordered.members([
246
+ {
247
+ children: [
248
+ {
249
+ hash: 'example',
250
+ level: 3,
251
+ text: 'Exemplo',
252
+ },
253
+ {
254
+ hash: 'use-same-hash',
255
+ level: 3,
256
+ text: 'Usar o mesmo hash',
257
+ },
258
+ {
259
+ hash: 'usar-traduzido',
260
+ level: 3,
261
+ text: 'Usar traduzido',
262
+ },
263
+ ],
264
+ hash: 'locales',
265
+ level: 2,
266
+ text: 'Idiomas',
267
+ },
268
+ ]);
269
+
270
+ expect(tocEn).to.have.deep.ordered.members([
271
+ {
272
+ children: [
273
+ {
274
+ hash: 'example',
275
+ level: 3,
276
+ text: 'Example',
277
+ },
278
+ {
279
+ hash: 'use-same-hash',
280
+ level: 3,
281
+ text: 'Use same hash',
282
+ },
283
+ ],
284
+ hash: 'locales',
285
+ level: 2,
286
+ text: 'Locales',
287
+ },
288
+ ]);
289
+ });
290
+
291
+ it('should report missing trailing splashes', () => {
292
+ const markdown = `
293
+ # Localization
294
+
295
+ <p class="description">Foo</p>
296
+
297
+ [bar](/bar/)
298
+ [foo](/foo)
299
+ `;
300
+
301
+ expect(() => {
302
+ prepareMarkdown({
303
+ ...defaultParams,
304
+ translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }],
305
+ });
306
+ }).to.throw(`docs-infra: Missing trailing slash. The following link:
307
+ [foo](/foo) in /test/bar/index.md is missing a trailing slash, please add it.
308
+
309
+ See https://ahrefs.com/blog/trailing-slash/ for more details.
310
+ `);
311
+ });
312
+
313
+ it('should report missing leading splashes', () => {
314
+ const markdown = `
315
+ # Localization
316
+
317
+ <p class="description">Foo</p>
318
+
319
+ [bar](/bar/)
320
+ [foo](foo/)
321
+ `;
322
+
323
+ expect(() => {
324
+ prepareMarkdown({
325
+ ...defaultParams,
326
+ translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }],
327
+ });
328
+ }).to.throw(`docs-infra: Missing leading slash. The following link:
329
+ [foo](foo/) in /test/bar/index.md is missing a leading slash, please add it.
330
+ `);
331
+ });
332
+
333
+ it('should report title too long', () => {
334
+ const markdown = `
335
+ # Foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
336
+
337
+ <p class="description">Foo</p>
338
+
339
+ `;
340
+
341
+ expect(() => {
342
+ prepareMarkdown({
343
+ ...defaultParams,
344
+ translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }],
345
+ });
346
+ }).to
347
+ .throw(`docs-infra: The title "Foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" is too long (117 characters).
348
+ It needs to have fewer than 70 characters—ideally less than 60. For more details, see:
349
+ https://developers.google.com/search/docs/advanced/appearance/title-link
350
+ `);
351
+ });
352
+
353
+ it('should report description too long', () => {
354
+ const markdown = `
355
+ # Foo
356
+
357
+ <p class="description">Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo</p>
358
+
359
+ `;
360
+
361
+ expect(() => {
362
+ prepareMarkdown({
363
+ ...defaultParams,
364
+ translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }],
365
+ });
366
+ }).to
367
+ .throw(`docs-infra: The description "Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" is too long (188 characters).
368
+ It needs to have fewer than 170 characters—ideally less than 160. For more details, see:
369
+ https://ahrefs.com/blog/meta-description/#4-be-concise
370
+ `);
371
+ });
372
+
373
+ it('should not accept sh', () => {
374
+ const markdown = `
375
+ # Foo
376
+
377
+ <p class="description">Fo</p>
378
+
379
+ \`\`\`sh
380
+ npm install @mui/material
381
+ \`\`\`
382
+
383
+ `;
384
+
385
+ expect(() => {
386
+ prepareMarkdown({
387
+ ...defaultParams,
388
+ translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }],
389
+ });
390
+ }).to.throw(`docs-infra: Unsupported language: "sh" in:
391
+
392
+ \`\`\`sh
393
+ npm install @mui/material
394
+ \`\`\`
395
+
396
+ Use "bash" instead.
397
+ `);
398
+ });
399
+
400
+ it('should report duplicated trailing splashes', () => {
401
+ const markdown = `
402
+ # Localization
403
+
404
+ <p class="description">Foo</p>
405
+
406
+ [foo](/foo/)
407
+ [bar](/bar//#foo)
408
+ `;
409
+
410
+ expect(() => {
411
+ prepareMarkdown({
412
+ ...defaultParams,
413
+ translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }],
414
+ });
415
+ }).to.throw(`docs-infra: Duplicated trailing slashes.`);
416
+ });
417
+ });
package/prism.d.ts ADDED
@@ -0,0 +1 @@
1
+ export default function highlight(code: string, language: string): string;
package/prism.js ADDED
@@ -0,0 +1,61 @@
1
+ const prism = require('prismjs');
2
+ require('prismjs/components/prism-css');
3
+ require('prismjs/components/prism-bash');
4
+ require('prismjs/components/prism-diff');
5
+ require('prismjs/components/prism-javascript');
6
+ require('prismjs/components/prism-json');
7
+ require('prismjs/components/prism-jsx');
8
+ require('prismjs/components/prism-markup');
9
+ require('prismjs/components/prism-yaml');
10
+ require('prismjs/components/prism-tsx');
11
+
12
+ function highlight(code, language) {
13
+ let prismLanguage;
14
+ switch (language) {
15
+ case 'ts':
16
+ prismLanguage = prism.languages.tsx;
17
+ break;
18
+
19
+ case 'js':
20
+ prismLanguage = prism.languages.jsx;
21
+ break;
22
+
23
+ case 'sh':
24
+ throw new Error(
25
+ [
26
+ `docs-infra: Unsupported language: "sh" in:`,
27
+ '',
28
+ '```sh',
29
+ code,
30
+ '```',
31
+ '',
32
+ 'Use "bash" instead.',
33
+ '',
34
+ ].join('\n'),
35
+ );
36
+
37
+ case 'diff':
38
+ prismLanguage = { ...prism.languages.diff };
39
+ // original `/^[-<].*$/m` matches lines starting with `<` which matches
40
+ // <SomeComponent />
41
+ // we will only use `-` as the deleted marker
42
+ prismLanguage.deleted = /^[-].*$/m;
43
+ break;
44
+
45
+ default:
46
+ prismLanguage = prism.languages[language];
47
+ break;
48
+ }
49
+
50
+ if (!prismLanguage) {
51
+ if (language) {
52
+ throw new Error(`unsupported language: "${language}", "${code}"`);
53
+ } else {
54
+ prismLanguage = prism.languages.jsx;
55
+ }
56
+ }
57
+
58
+ return prism.highlight(code, prismLanguage);
59
+ }
60
+
61
+ module.exports = highlight;
package/textToHash.js ADDED
@@ -0,0 +1,36 @@
1
+ function makeUnique(hash, unique, i = 1) {
2
+ const uniqueHash = i === 1 ? hash : `${hash}-${i}`;
3
+
4
+ if (!unique[uniqueHash]) {
5
+ unique[uniqueHash] = true;
6
+ return uniqueHash;
7
+ }
8
+
9
+ return makeUnique(hash, unique, i + 1);
10
+ }
11
+
12
+ /**
13
+ * @param {string} text - HTML from e.g. parseMarkdown#render
14
+ * @param {Record<string, boolean>} [unique] - Ensures that each output is unique in `unique`
15
+ * @returns {string} that is safe to use in fragment links
16
+ */
17
+ function textToHash(text, unique = {}) {
18
+ return makeUnique(
19
+ encodeURI(
20
+ text
21
+ .toLowerCase()
22
+ .replace(/<\/?[^>]+(>|$)/g, '') // remove HTML
23
+ .replace(/=&gt;|&lt;| \/&gt;|<code>|<\/code>|&#39;/g, '')
24
+ .replace(/[!@#$%^&*()=_+[\]{}`~;:'"|,.<>/?\s]+/g, '-')
25
+ .replace(
26
+ /([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])\uFE0F?/g,
27
+ '',
28
+ ) // remove emojis
29
+ .replace(/-+/g, '-')
30
+ .replace(/^-|-$/g, ''),
31
+ ),
32
+ unique,
33
+ );
34
+ }
35
+
36
+ module.exports = textToHash;
@@ -0,0 +1,32 @@
1
+ import { expect } from 'chai';
2
+ import { parseInline as renderInlineMarkdown } from 'marked';
3
+ import textToHash from './textToHash';
4
+
5
+ describe('textToHash', () => {
6
+ it('should hash as expected', () => {
7
+ const table = [
8
+ ['createTheme(options) => theme', 'createtheme-options-theme'],
9
+ ['Typography - Font family', 'typography-font-family'],
10
+ ["barre d'application", 'barre-dapplication'],
11
+ [
12
+ 'createGenerateClassName([options]) => class name generator',
13
+ 'creategenerateclassname-options-class-name-generator',
14
+ ],
15
+ ['@mui/material/styles vs @mui/styles', 'mui-material-styles-vs-mui-styles'],
16
+ ['Blog 📝', 'blog'],
17
+ ];
18
+ table.forEach((entry, index) => {
19
+ const [markdown, expected] = entry;
20
+ const text = renderInlineMarkdown(markdown, { mangle: false, headerIds: false });
21
+ const actual = textToHash(text);
22
+
23
+ expect(actual).to.equal(expected, `snapshot #${index} matches`);
24
+ });
25
+ });
26
+
27
+ it('should generate a unique hash', () => {
28
+ const unique = {};
29
+ expect(textToHash('Styling solution', unique)).to.equal('styling-solution');
30
+ expect(textToHash('Styling solution', unique)).to.equal('styling-solution-2');
31
+ });
32
+ });