@diplodoc/cli-tests 5.35.3 → 5.36.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.
Files changed (38) hide show
  1. package/e2e/__snapshots__/bundles.spec.ts.snap +26 -26
  2. package/e2e/__snapshots__/files.spec.ts.snap +1 -2
  3. package/e2e/__snapshots__/merge-includes.spec.ts.snap +241 -30
  4. package/e2e/__snapshots__/pdf-page.spec.ts.snap +157 -6
  5. package/e2e/__snapshots__/preprocess.test.ts.snap +78 -78
  6. package/e2e/__snapshots__/regression.test.ts.snap +216 -55
  7. package/e2e/merge-includes.spec.ts +45 -1
  8. package/e2e/pdf-page.spec.ts +27 -0
  9. package/e2e/preprocess.test.ts +2 -2
  10. package/e2e/regression.test.ts +3 -3
  11. package/fixtures/utils/test.ts +112 -22
  12. package/mocks/merge-includes/hash-section-html/input/_includes/mixed.md +11 -0
  13. package/mocks/merge-includes/hash-section-html/input/index.md +3 -0
  14. package/mocks/merge-includes/hash-section-html/input/main.md +13 -0
  15. package/mocks/merge-includes/hash-section-html/input/toc.yaml +5 -0
  16. package/mocks/merge-includes/html-in-list/input/_includes/styles.md +7 -0
  17. package/mocks/merge-includes/html-in-list/input/index.md +3 -0
  18. package/mocks/merge-includes/html-in-list/input/main.md +11 -0
  19. package/mocks/merge-includes/html-in-list/input/toc.yaml +5 -0
  20. package/mocks/merge-includes/term-extract/input/_includes/chapter.md +7 -0
  21. package/mocks/merge-includes/term-extract/input/index.md +3 -0
  22. package/mocks/merge-includes/term-extract/input/main.md +7 -0
  23. package/mocks/merge-includes/term-extract/input/toc.yaml +5 -0
  24. package/mocks/merge-includes/yfm-table/input/_includes/cell-content.md +1 -0
  25. package/mocks/merge-includes/yfm-table/input/index.md +3 -0
  26. package/mocks/merge-includes/yfm-table/input/main.md +12 -0
  27. package/mocks/merge-includes/yfm-table/input/toc.yaml +5 -0
  28. package/mocks/pdf-page/custom-pdf-icon/input/.yfm +6 -0
  29. package/mocks/pdf-page/custom-pdf-icon/input/_assets/custom-pdf-icon.svg +1 -0
  30. package/mocks/pdf-page/custom-pdf-icon/input/index.md +1 -0
  31. package/mocks/pdf-page/custom-pdf-icon/input/pdf/output.pdf +0 -0
  32. package/mocks/pdf-page/custom-pdf-icon/input/toc.yaml +2 -0
  33. package/mocks/regression/input/autotitle.md +33 -1
  34. package/mocks/regression/input/includes/fragments.md +24 -0
  35. package/mocks/regression/input/includes/styles.md +8 -0
  36. package/mocks/regression/input/includes.md +6 -0
  37. package/mocks/regression/input/toc.yaml +2 -0
  38. package/package.json +1 -1
@@ -53,7 +53,7 @@ describe('Merge includes (md2md)', () => {
53
53
  await TestAdapter.testBuildPass(inputPath, outputPath, {
54
54
  md2md: true,
55
55
  md2html: false,
56
- args: '--merge-includes',
56
+ args: '--merge-includes --multiline-term-definitions',
57
57
  });
58
58
  await compareDirectories(outputPath);
59
59
  });
@@ -69,6 +69,50 @@ describe('Merge includes (md2md)', () => {
69
69
  await compareDirectories(outputPath);
70
70
  });
71
71
 
72
+ test('term-extract: dep with terms → terms extracted, content inlined (Step 4)', async () => {
73
+ const {inputPath, outputPath} = getTestPaths('mocks/merge-includes/term-extract');
74
+
75
+ await TestAdapter.testBuildPass(inputPath, outputPath, {
76
+ md2md: true,
77
+ md2html: false,
78
+ args: '--merge-includes --multiline-term-definitions',
79
+ });
80
+ await compareDirectories(outputPath);
81
+ });
82
+
83
+ test('yfm-table: include followed by || separator is inlined', async () => {
84
+ const {inputPath, outputPath} = getTestPaths('mocks/merge-includes/yfm-table');
85
+
86
+ await TestAdapter.testBuildPass(inputPath, outputPath, {
87
+ md2md: true,
88
+ md2html: false,
89
+ args: '--merge-includes',
90
+ });
91
+ await compareDirectories(outputPath);
92
+ });
93
+
94
+ test('html-in-list: include with <style> in list (any indent) uses fallback', async () => {
95
+ const {inputPath, outputPath} = getTestPaths('mocks/merge-includes/html-in-list');
96
+
97
+ await TestAdapter.testBuildPass(inputPath, outputPath, {
98
+ md2md: true,
99
+ md2html: false,
100
+ args: '--merge-includes',
101
+ });
102
+ await compareDirectories(outputPath);
103
+ });
104
+
105
+ test('hash-section-html: hash-section include in list is inlined when only other section has <style>', async () => {
106
+ const {inputPath, outputPath} = getTestPaths('mocks/merge-includes/hash-section-html');
107
+
108
+ await TestAdapter.testBuildPass(inputPath, outputPath, {
109
+ md2md: true,
110
+ md2html: false,
111
+ args: '--merge-includes',
112
+ });
113
+ await compareDirectories(outputPath);
114
+ });
115
+
72
116
  test('without flag: includes are NOT merged', async () => {
73
117
  const {inputPath, outputPath} = getTestPaths('mocks/merge-includes/basic');
74
118
 
@@ -98,3 +98,30 @@ describe('Pdf generation with md2md phase, only files structure', () => {
98
98
  'mocks/pdf-page/title-pages',
99
99
  );
100
100
  });
101
+
102
+ describe('Pdf page with custom icon', () => {
103
+ test('md2md copies custom pdf icon asset to output', async () => {
104
+ const {inputPath, outputPath} = getTestPaths('mocks/pdf-page/custom-pdf-icon');
105
+
106
+ await TestAdapter.testBuildPass(inputPath, outputPath, {
107
+ md2md: true,
108
+ md2html: false,
109
+ args: '--allow-custom-resources',
110
+ });
111
+
112
+ await compareDirectories(outputPath, true);
113
+ await cleanupDirectory(outputPath);
114
+ });
115
+
116
+ test('md2html copies all files and produces correct pdfIconConfig in __DATA__', async () => {
117
+ const {inputPath, outputPath} = getTestPaths('mocks/pdf-page/custom-pdf-icon');
118
+
119
+ await TestAdapter.testBuildPass(inputPath, outputPath, {
120
+ md2md: false,
121
+ md2html: true,
122
+ });
123
+
124
+ await compareDirectories(outputPath);
125
+ await cleanupDirectory(outputPath);
126
+ });
127
+ });
@@ -12,12 +12,12 @@ const generateFilesYamlTestTemplate = (
12
12
  await TestAdapter.testBuildPass(inputPath, outputPath, {
13
13
  md2md: true,
14
14
  md2html: false,
15
- args: args.concat(['--keep-not-var']).join(' '),
15
+ args: args.concat(['--keep-not-var', '--id-generator', 'deterministic']).join(' '),
16
16
  });
17
17
  await TestAdapter.testBuildPass(outputPath, outputPath + '-html', {
18
18
  md2md: false,
19
19
  md2html: true,
20
- args: args.join(' '),
20
+ args: args.concat(['--id-generator', 'deterministic']).join(' '),
21
21
  });
22
22
  await compareDirectories(outputPath);
23
23
  });
@@ -10,17 +10,17 @@ function test(_description: string) {
10
10
  await TestAdapter.testBuildPass(inputPath, outputPath, {
11
11
  md2md: true,
12
12
  md2html: false,
13
- args: '-j2 --keep-not-var --add-system-meta',
13
+ args: '-j2 --keep-not-var --add-system-meta --id-generator deterministic',
14
14
  });
15
15
  await TestAdapter.testBuildPass(outputPath, outputPath + '-html', {
16
16
  md2md: false,
17
17
  md2html: true,
18
- args: '-j2',
18
+ args: '-j2 --id-generator deterministic',
19
19
  });
20
20
  await TestAdapter.testBuildPass(outputPath, outputPath + '-static-html', {
21
21
  md2md: false,
22
22
  md2html: true,
23
- args: '-j2 --static-content',
23
+ args: '-j2 --static-content --id-generator deterministic',
24
24
  });
25
25
  await compareDirectories(outputPath);
26
26
  await compareDirectories(outputPath + '-html');
@@ -31,35 +31,125 @@ export function platformless(text: string): string {
31
31
  export function hashless(text: string): string {
32
32
  return text
33
33
  .replace(/-[a-z0-9]{12,16}\./g, '-hash.')
34
- .replace(/rnd-[a-z0-9]{1,8}__/g, 'rnd-hash__')
34
+ .replace(/(rnd|svg)-[a-z0-9]{3,8}__/g, 'rnd-hash__')
35
35
  .replace(/(\/|\\)[a-z0-9]{12,16}-(index|registry|resources)\./g, '/hash-$2.');
36
36
  }
37
37
 
38
+ /**
39
+ * Replaces hashed bundle filenames from the client manifest with stable labels
40
+ * and sorts bundle references to normalize platform-dependent ordering.
41
+ *
42
+ * Examples: "app-3ff8bc0b40bc2914.js" → "app-js",
43
+ * "vendor-00121562c7b7d3b5.rtl.css" → "vendor-rtl-css",
44
+ * "976-40cbc1d2518eb8ea.js" → "chunk-js" (numeric webpack chunk ID).
45
+ */
38
46
  export function bundleless(text: string): string {
39
- for (const [entryKey, entry] of Object.entries(assets)) {
40
- for (const [typeKey, type] of Object.entries(entry)) {
41
- for (let index = 0; index < type.length; index++) {
42
- // Extract base name from manifest filename (e.g. "app-3ff8bc0b40bc2914.js" -> "app")
43
- // Also handles suffixes like "vendor-00121562c7b7d3b5.rtl.css" -> base="vendor", suffix=".rtl", ext="css"
44
- const filename = type[index];
45
- const match = filename.match(/^(.+?)-[a-z0-9]{12,16}(\.[a-z]+)*\.([a-z]+)$/);
46
- if (!match) {
47
- // Fallback: exact string replacement for filenames without hash pattern
48
- let prev = '';
49
- while (prev !== text) {
50
- prev = text;
51
- text = text.replace(filename, `${entryKey}-${typeKey}-${index}`);
52
- }
53
- continue;
54
- }
55
-
56
- const [, base, suffixes, ext] = match;
57
- const suffixPattern = suffixes ? suffixes.replace(/\./g, '\\.') : '';
58
- const pattern = new RegExp(`${base}-[a-z0-9]{12,16}${suffixPattern}\\.${ext}`, 'g');
59
- text = text.replace(pattern, `${entryKey}-${typeKey}-${index}`);
47
+ // On Windows the CLI may emit `_bundle\file` or JSON-escaped `_bundle\\/file`.
48
+ // Collapse any mix of backslashes and forward slashes after `_bundle` into `/`.
49
+ text = text.replace(/_bundle[/\\]+/g, '_bundle/');
50
+
51
+ for (const entry of Object.values(assets)) {
52
+ for (const files of Object.values(entry)) {
53
+ for (const filename of files) {
54
+ const match = filename.match(
55
+ /^([a-z0-9]+)-[a-f0-9]{12,16}((?:\.[a-z]+)*)\.([a-z]+)$/,
56
+ );
57
+ if (!match) continue;
58
+
59
+ const [, rawBase, suffixes, ext] = match;
60
+ const base = /^\d+$/.test(rawBase) ? 'chunk' : rawBase;
61
+ const suffixPart = suffixes.replace(/\./g, '-').slice(1); // ".rtl" → "rtl"
62
+ const label = suffixPart ? `${base}-${suffixPart}-${ext}` : `${base}-${ext}`;
63
+ const escapedSuffixes = suffixes.replace(/\./g, '\\.');
64
+ const pattern = new RegExp(
65
+ `${rawBase}-[a-f0-9]{12,16}${escapedSuffixes}\\.${ext}`,
66
+ 'g',
67
+ );
68
+ text = text.replace(pattern, label);
60
69
  }
61
70
  }
62
71
  }
63
72
 
73
+ return sortBundleTokens(text);
74
+ }
75
+
76
+ /**
77
+ * Sorts contiguous runs of `_bundle/` references so snapshots stay stable
78
+ * regardless of manifest ordering or platform.
79
+ *
80
+ * Pass 1 (line-level): sorts groups of consecutive lines containing `_bundle/`
81
+ * — handles HTML `<link>` / `<script>` tags on separate lines.
82
+ *
83
+ * Pass 2 (inline): sorts groups of `_bundle/` tokens on the same line separated
84
+ * only by punctuation — handles JSON arrays like `"cssLink":["_bundle/b","_bundle/a"]`.
85
+ */
86
+ function sortBundleTokens(text: string): string {
87
+ const bundleRe = /_bundle\/[a-z0-9][-a-z0-9]*/;
88
+
89
+ // --- Pass 1: line-level sort ---
90
+ const lines = text.split('\n');
91
+ let i = 0;
92
+ while (i < lines.length) {
93
+ if (!bundleRe.test(lines[i])) {
94
+ i++;
95
+ continue;
96
+ }
97
+ const start = i;
98
+ while (i < lines.length && bundleRe.test(lines[i])) {
99
+ i++;
100
+ }
101
+ if (i - start < 2) continue;
102
+
103
+ const slice = lines.slice(start, i);
104
+ const sorted = [...slice].sort((a, b) => {
105
+ const ka = a.match(bundleRe)?.[0] ?? '';
106
+ const kb = b.match(bundleRe)?.[0] ?? '';
107
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
108
+ });
109
+ for (let j = 0; j < sorted.length; j++) {
110
+ lines[start + j] = sorted[j];
111
+ }
112
+ }
113
+ text = lines.join('\n');
114
+
115
+ // --- Pass 2: inline sort (JSON arrays, etc.) ---
116
+ const tokenRe = /_bundle\/[a-z0-9][-a-z0-9]*/g;
117
+ const hits: {start: number; end: number; value: string}[] = [];
118
+ let m;
119
+ while ((m = tokenRe.exec(text)) !== null) {
120
+ hits.push({start: m.index, end: m.index + m[0].length, value: m[0]});
121
+ }
122
+ if (hits.length < 2) return text;
123
+
124
+ // Gap between two tokens is "glue" when it contains no alphanumeric chars
125
+ // (ignoring `_bundle` substrings that may appear in the gap).
126
+ const isGlue = (gap: string) => !/[a-zA-Z0-9]/.test(gap.replace(/_bundle/g, ''));
127
+
128
+ // Group consecutive same-line tokens into runs.
129
+ const runs: number[][] = [[0]];
130
+ for (let idx = 1; idx < hits.length; idx++) {
131
+ const gap = text.slice(hits[idx - 1].end, hits[idx].start);
132
+ if (!gap.includes('\n') && isGlue(gap)) {
133
+ runs[runs.length - 1].push(idx);
134
+ } else {
135
+ runs.push([idx]);
136
+ }
137
+ }
138
+
139
+ // Sort each run with 2+ tokens, rewriting from the end to preserve offsets.
140
+ for (let r = runs.length - 1; r >= 0; r--) {
141
+ const run = runs[r];
142
+ if (run.length < 2) continue;
143
+
144
+ const values = run.map((idx) => hits[idx].value);
145
+ const sorted = [...values].sort();
146
+ if (values.every((v, idx) => v === sorted[idx])) continue;
147
+
148
+ for (let k = run.length - 1; k >= 0; k--) {
149
+ const {start: s, end: e} = hits[run[k]];
150
+ text = text.slice(0, s) + sorted[k] + text.slice(e);
151
+ }
152
+ }
153
+
64
154
  return text;
65
155
  }
@@ -0,0 +1,11 @@
1
+ ## Section with style {#with-style}
2
+
3
+ <style>
4
+ .highlight { background: yellow; }
5
+ </style>
6
+
7
+ Some styled content.
8
+
9
+ ## Section without style {#no-style}
10
+
11
+ Just plain text content without any HTML blocks.
@@ -0,0 +1,3 @@
1
+ # Index
2
+
3
+ Welcome page.
@@ -0,0 +1,13 @@
1
+ # Main Document
2
+
3
+ Content before list.
4
+
5
+ 1. First item
6
+
7
+ 2. Second item with hash-section include (section has no HTML, but other section in same file does):
8
+
9
+ {% include notitle [no-style](_includes/mixed.md#no-style) %}
10
+
11
+ 3. Third item
12
+
13
+ Content after list.
@@ -0,0 +1,5 @@
1
+ title: Merge Includes Hash Section HTML
2
+ href: index.md
3
+ items:
4
+ - name: Main
5
+ href: main.md
@@ -0,0 +1,7 @@
1
+ <style>
2
+
3
+ .yfm .highlight {
4
+ background: yellow;
5
+ }
6
+
7
+ </style>
@@ -0,0 +1,3 @@
1
+ # Index
2
+
3
+ Welcome page.
@@ -0,0 +1,11 @@
1
+ # Main Document
2
+
3
+ Content before list.
4
+
5
+ - List item with inline include (any non-zero indent → HTML block must use fallback)
6
+
7
+ {% include [styles](_includes/styles.md) %}
8
+
9
+ - Another list item
10
+
11
+ Content after list.
@@ -0,0 +1,5 @@
1
+ title: Merge Includes HTML in List
2
+ href: index.md
3
+ items:
4
+ - name: Main
5
+ href: main.md
@@ -0,0 +1,7 @@
1
+ ## Chapter
2
+
3
+ Chapter content with [link](./local.md).
4
+
5
+ [*api]: Application Programming Interface
6
+
7
+ [*sdk]: Software Development Kit
@@ -0,0 +1,3 @@
1
+ # Index
2
+
3
+ Welcome page.
@@ -0,0 +1,7 @@
1
+ # API Docs
2
+
3
+ Use the [API](*api) and [SDK](*sdk) to build apps.
4
+
5
+ {% include [chapter](_includes/chapter.md) %}
6
+
7
+ [*api]: Application Programming Interface
@@ -0,0 +1,5 @@
1
+ title: Merge Includes Term Extract
2
+ href: index.md
3
+ items:
4
+ - name: Main
5
+ href: main.md
@@ -0,0 +1 @@
1
+ Buy now at [our store](https://example.com/store).
@@ -0,0 +1,3 @@
1
+ # Index
2
+
3
+ Welcome page.
@@ -0,0 +1,12 @@
1
+ # Main Document
2
+
3
+ #|
4
+ || **Option** | **Details** ||
5
+ || Option A |
6
+ Some description of option A.
7
+
8
+ {% include [cell](_includes/cell-content.md) %}||
9
+ || Option B | Plain text cell. ||
10
+ |#
11
+
12
+ Content after table.
@@ -0,0 +1,5 @@
1
+ title: Merge Includes YFM Table
2
+ href: index.md
3
+ items:
4
+ - name: Main
5
+ href: main.md
@@ -0,0 +1,6 @@
1
+ pdf:
2
+ enabled: true
3
+ size: M
4
+ position: header
5
+ icon: _assets/custom-pdf-icon.svg
6
+ pdfFileUrl: pdf/output.pdf
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" class="g-icon Item-module__gn-composite-bar-item__collapse-item-icon___CD-ao"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path fill="#657B8F" d="M7.181 13.318h3.887v2.045H5.136V5.341h2.045zm7.774-6.955h-3.683v4.705H9.227v-6.75h3.682V2.682h-9.41V5.34H1.456V.637h13.5z"/></svg></svg>
@@ -0,0 +1 @@
1
+ # PDF mock index
@@ -0,0 +1,2 @@
1
+ title: Pdf custom link
2
+ href: index.md
@@ -21,6 +21,38 @@ Special local title
21
21
  Circular title
22
22
  [{#T}](./autotitle.md#header)
23
23
 
24
+ Autotitle from include
25
+ <!-- [{#T}](includes/fragments.md#f3) -->
26
+
27
+ Include with autotitle
28
+ <!-- {% include [test](includes/fragments.md#f4) %} -->
29
+
30
+ link with [some local term1](*term1-1)
31
+
32
+ {% list tabs %}
33
+
34
+ - Название таба 1
35
+
36
+ Текст таба 1.
37
+
38
+ * Можно использовать списки.
39
+ * И **другую** разметку.
40
+
41
+ - Название таба 2
42
+
43
+ Текст таба 2.
44
+
45
+ {% endlist %}
46
+
47
+ All fragments
48
+
49
+ {% include [test](includes/fragments.md) %}
50
+
51
+
24
52
  ## Header {#header}
25
53
 
26
- Content
54
+ Content2
55
+
56
+ {% include [test](includes/styles.md) %}
57
+
58
+ [*term1-1]: {% include [test](includes/fragments.md#f3) %}
@@ -15,3 +15,27 @@ Some paragraph with anchor {#p1}
15
15
  Some paragraph without anchor
16
16
 
17
17
  Some paragraph with anchor {#p2}
18
+
19
+ ## F4 {#f4}
20
+ Content F4
21
+ [{#T}](../autotitle.md#header)
22
+
23
+ ### F4.1 {#f4.1}
24
+ link with [some term1](*term1)
25
+
26
+ {% list tabs %}
27
+
28
+ - Название таба 1
29
+
30
+ Текст таба 1.
31
+
32
+ * Можно использовать списки.
33
+ * И **другую** разметку.
34
+
35
+ - Название таба 2
36
+
37
+ Текст таба 2.
38
+
39
+ {% endlist %}
40
+
41
+ [*term1]: Some description
@@ -0,0 +1,8 @@
1
+ <style>
2
+
3
+ .yfm .border-yes {
4
+ border: 1px solid #ccc;
5
+ border-radius: 10px;
6
+ }
7
+
8
+ </style>
@@ -15,10 +15,16 @@ Text
15
15
 
16
16
  [[?](*term)](http://ya.ru)
17
17
 
18
+ [Term 1](*term1) [Term 2](*term2)
18
19
 
19
20
  Link after include
20
21
  [{#T}](./1.md#subtitle)
21
22
 
23
+ Autotitle include
24
+ <!-- [{#T}](includes/fragments.md#f3) -->
25
+
22
26
  Link after include
23
27
 
24
28
  [*term]: Test terms
29
+ [*term1]: {% include [test](includes/fragments.md#f3) %}
30
+ [*term2]: {% include [test](includes/fragments.md#f3) %}
@@ -11,6 +11,8 @@ items:
11
11
  href:
12
12
  - name: Multitoc item
13
13
  href: ./merge/merged.md
14
+ - name: Fragments
15
+ href: ./includes/fragments.md
14
16
  - include: {path: toc-i.yaml}
15
17
  - include: {path: sub/toc.yaml, mode: link}
16
18
  - href: ./mermaid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diplodoc/cli-tests",
3
- "version": "5.35.3",
3
+ "version": "5.36.1",
4
4
  "bin": {
5
5
  "diplodoc-cli-test": "bin.mjs"
6
6
  },