@mintlify/scraping 4.0.907 → 4.0.908
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/__test__/sdk-docfx.test.ts +204 -0
- package/__test__/sdk-javadoc.test.ts +150 -0
- package/__test__/sdk-phpdoc.test.ts +134 -0
- package/__test__/sdk-sphinx.test.ts +84 -0
- package/__test__/sdk-typedoc.test.ts +138 -0
- package/bin/index.d.ts +3 -0
- package/bin/index.js +2 -0
- package/bin/index.js.map +1 -1
- package/bin/sdk/convert.d.ts +6 -0
- package/bin/sdk/convert.js +90 -0
- package/bin/sdk/convert.js.map +1 -0
- package/bin/sdk/converters/docfx.d.ts +2 -0
- package/bin/sdk/converters/docfx.js +402 -0
- package/bin/sdk/converters/docfx.js.map +1 -0
- package/bin/sdk/converters/javadoc.d.ts +2 -0
- package/bin/sdk/converters/javadoc.js +318 -0
- package/bin/sdk/converters/javadoc.js.map +1 -0
- package/bin/sdk/converters/phpdoc.d.ts +2 -0
- package/bin/sdk/converters/phpdoc.js +351 -0
- package/bin/sdk/converters/phpdoc.js.map +1 -0
- package/bin/sdk/converters/sphinx.d.ts +2 -0
- package/bin/sdk/converters/sphinx.js +267 -0
- package/bin/sdk/converters/sphinx.js.map +1 -0
- package/bin/sdk/converters/typedoc.d.ts +2 -0
- package/bin/sdk/converters/typedoc.js +318 -0
- package/bin/sdk/converters/typedoc.js.map +1 -0
- package/bin/sdk/generateSdkReference.d.ts +2 -0
- package/bin/sdk/generateSdkReference.js +20 -0
- package/bin/sdk/generateSdkReference.js.map +1 -0
- package/bin/sdk/types.d.ts +19 -0
- package/bin/sdk/types.js +2 -0
- package/bin/sdk/types.js.map +1 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/index.ts +3 -0
- package/src/sdk/convert.ts +101 -0
- package/src/sdk/converters/docfx.ts +491 -0
- package/src/sdk/converters/javadoc.ts +330 -0
- package/src/sdk/converters/phpdoc.ts +446 -0
- package/src/sdk/converters/sphinx.ts +277 -0
- package/src/sdk/converters/typedoc.ts +423 -0
- package/src/sdk/generateSdkReference.ts +25 -0
- package/src/sdk/types.ts +24 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { convertDocfx } from '../src/sdk/converters/docfx.js';
|
|
7
|
+
import type { SdkReference } from '../src/sdk/types.js';
|
|
8
|
+
|
|
9
|
+
const clientYml = `### YamlMime:ManagedReference
|
|
10
|
+
items:
|
|
11
|
+
- uid: Acme.Widgets.Client
|
|
12
|
+
id: Client
|
|
13
|
+
parent: Acme.Widgets
|
|
14
|
+
children:
|
|
15
|
+
- Acme.Widgets.Client.Name
|
|
16
|
+
- Acme.Widgets.Client.Timeout
|
|
17
|
+
- Acme.Widgets.Client.PaintAsync(Acme.Widgets.Models.Color,System.Int32)
|
|
18
|
+
name: Client
|
|
19
|
+
fullName: Acme.Widgets.Client
|
|
20
|
+
type: Class
|
|
21
|
+
namespace: Acme.Widgets
|
|
22
|
+
summary: A widget client. Prefer <xref href="Acme.Widgets.Models.Color" data-throw-if-not-resolved="false"></xref> over raw values.
|
|
23
|
+
syntax:
|
|
24
|
+
content: public class Client
|
|
25
|
+
- uid: Acme.Widgets.Client.Name
|
|
26
|
+
id: Name
|
|
27
|
+
parent: Acme.Widgets.Client
|
|
28
|
+
name: Name
|
|
29
|
+
type: Property
|
|
30
|
+
namespace: Acme.Widgets
|
|
31
|
+
summary: The client name.
|
|
32
|
+
syntax:
|
|
33
|
+
content: public string Name { get; set; }
|
|
34
|
+
return:
|
|
35
|
+
type: System.String
|
|
36
|
+
- uid: Acme.Widgets.Client.Timeout
|
|
37
|
+
id: Timeout
|
|
38
|
+
parent: Acme.Widgets.Client
|
|
39
|
+
name: Timeout
|
|
40
|
+
type: Property
|
|
41
|
+
namespace: Acme.Widgets
|
|
42
|
+
summary: Timeout backed by <xref href="System.TimeSpan" data-throw-if-not-resolved="false"></xref>.
|
|
43
|
+
syntax:
|
|
44
|
+
content: public TimeSpan? Timeout { get; set; }
|
|
45
|
+
return:
|
|
46
|
+
type: System.TimeSpan
|
|
47
|
+
- uid: Acme.Widgets.Client.PaintAsync(Acme.Widgets.Models.Color,System.Int32)
|
|
48
|
+
id: PaintAsync(Acme.Widgets.Models.Color,System.Int32)
|
|
49
|
+
parent: Acme.Widgets.Client
|
|
50
|
+
name: PaintAsync(Color, int)
|
|
51
|
+
type: Method
|
|
52
|
+
namespace: Acme.Widgets
|
|
53
|
+
summary: Paints using a <code>Color</code>.
|
|
54
|
+
syntax:
|
|
55
|
+
content: public Task<bool> PaintAsync(Color color, int coats = 1)
|
|
56
|
+
parameters:
|
|
57
|
+
- id: color
|
|
58
|
+
type: Acme.Widgets.Models.Color
|
|
59
|
+
description: The <xref href="Acme.Widgets.Models.Color" data-throw-if-not-resolved="false"></xref> to apply.
|
|
60
|
+
- id: coats
|
|
61
|
+
type: System.Int32
|
|
62
|
+
description: Number of coats.
|
|
63
|
+
return:
|
|
64
|
+
type: System.Threading.Tasks.Task{System.Boolean}
|
|
65
|
+
references:
|
|
66
|
+
- uid: System.String
|
|
67
|
+
name: string
|
|
68
|
+
- uid: System.TimeSpan
|
|
69
|
+
name: TimeSpan
|
|
70
|
+
- uid: System.Int32
|
|
71
|
+
name: int
|
|
72
|
+
- uid: Acme.Widgets.Models.Color
|
|
73
|
+
name: Color
|
|
74
|
+
- uid: System.Threading.Tasks.Task{System.Boolean}
|
|
75
|
+
name: Task<bool>
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const colorYml = `### YamlMime:ManagedReference
|
|
79
|
+
items:
|
|
80
|
+
- uid: Acme.Widgets.Models.Color
|
|
81
|
+
id: Color
|
|
82
|
+
parent: Acme.Widgets.Models
|
|
83
|
+
children:
|
|
84
|
+
- Acme.Widgets.Models.Color.Red
|
|
85
|
+
name: Color
|
|
86
|
+
fullName: Acme.Widgets.Models.Color
|
|
87
|
+
type: Enum
|
|
88
|
+
namespace: Acme.Widgets.Models
|
|
89
|
+
summary: Available colors for <code>IDictionary<string, string></code> lookups.
|
|
90
|
+
syntax:
|
|
91
|
+
content: public enum Color
|
|
92
|
+
- uid: Acme.Widgets.Models.Color.Red
|
|
93
|
+
id: Red
|
|
94
|
+
parent: Acme.Widgets.Models.Color
|
|
95
|
+
name: Red
|
|
96
|
+
type: Field
|
|
97
|
+
namespace: Acme.Widgets.Models
|
|
98
|
+
summary: The color red.
|
|
99
|
+
syntax:
|
|
100
|
+
content: Red = 0
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const tocYml = `### YamlMime:TableOfContent
|
|
104
|
+
items:
|
|
105
|
+
- uid: Acme.Widgets
|
|
106
|
+
name: Acme.Widgets
|
|
107
|
+
type: Namespace
|
|
108
|
+
items:
|
|
109
|
+
- uid: Acme.Widgets.Client
|
|
110
|
+
name: Client
|
|
111
|
+
type: Class
|
|
112
|
+
- uid: Acme.Widgets.Models
|
|
113
|
+
name: Acme.Widgets.Models
|
|
114
|
+
type: Namespace
|
|
115
|
+
items:
|
|
116
|
+
- uid: Acme.Widgets.Models.Color
|
|
117
|
+
name: Color
|
|
118
|
+
type: Enum
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
describe('convertDocfx', () => {
|
|
122
|
+
let sourcePath: string;
|
|
123
|
+
let reference: SdkReference;
|
|
124
|
+
|
|
125
|
+
beforeAll(async () => {
|
|
126
|
+
sourcePath = await fse.mkdtemp(path.join(os.tmpdir(), 'docfx-test-'));
|
|
127
|
+
await fse.writeFile(path.join(sourcePath, 'Acme.Widgets.Client.yml'), clientYml);
|
|
128
|
+
await fse.writeFile(path.join(sourcePath, 'Acme.Widgets.Models.Color.yml'), colorYml);
|
|
129
|
+
await fse.writeFile(path.join(sourcePath, 'toc.yml'), tocYml);
|
|
130
|
+
reference = await convertDocfx(sourcePath);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterAll(async () => {
|
|
134
|
+
await fse.remove(sourcePath);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('creates one page per top-level type with namespace-scoped slugs', () => {
|
|
138
|
+
expect(reference.pages.map((page) => page.slug)).toEqual(['widgets/client', 'models/color']);
|
|
139
|
+
expect(reference.pages.map((page) => page.tag)).toEqual(['CLASS', 'ENUM']);
|
|
140
|
+
expect(reference.pages.map((page) => page.title)).toEqual(['Client', 'Color']);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('groups pages by namespace in toc order', () => {
|
|
144
|
+
expect(reference.groups).toEqual([
|
|
145
|
+
{ group: 'Acme.Widgets', pages: ['widgets/client'] },
|
|
146
|
+
{ group: 'Acme.Widgets.Models', pages: ['models/color'] },
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders properties as ResponseFields with nullability-driven required', () => {
|
|
151
|
+
const content = reference.pages[0]?.content ?? '';
|
|
152
|
+
expect(content).toContain('<ResponseField name={"Name"} type={"string"} required>');
|
|
153
|
+
expect(content).toContain('<ResponseField name={"Timeout"} type={"TimeSpan"}>');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('renders method signatures, parameters, and returns', () => {
|
|
157
|
+
const content = reference.pages[0]?.content ?? '';
|
|
158
|
+
expect(content).toContain('### PaintAsync()');
|
|
159
|
+
expect(content).toContain(
|
|
160
|
+
'```csharp\npublic Task<bool> PaintAsync(Color color, int coats = 1)\n```'
|
|
161
|
+
);
|
|
162
|
+
expect(content).toContain('<ResponseField name={"color"} type={"Color"} required>');
|
|
163
|
+
expect(content).toContain('<ResponseField name={"coats"} type={"int"}>');
|
|
164
|
+
expect(content).toContain('`Task<bool>`');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('resolves xrefs to page links or inline code', () => {
|
|
168
|
+
const content = reference.pages[0]?.content ?? '';
|
|
169
|
+
expect(content).toContain('Prefer [Color](/models/color) over raw values.');
|
|
170
|
+
expect(content).toContain('Timeout backed by `TimeSpan`.');
|
|
171
|
+
expect(content).toContain('The [Color](/models/color) to apply.');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('keeps namespace slugs unique when names differ only by separator', async () => {
|
|
175
|
+
const dir = await fse.mkdtemp(path.join(os.tmpdir(), 'docfx-collide-'));
|
|
176
|
+
const typeYml = (namespace: string) => `### YamlMime:ManagedReference
|
|
177
|
+
items:
|
|
178
|
+
- uid: ${namespace}.Thing
|
|
179
|
+
id: Thing
|
|
180
|
+
name: Thing
|
|
181
|
+
fullName: ${namespace}.Thing
|
|
182
|
+
type: Class
|
|
183
|
+
namespace: ${namespace}
|
|
184
|
+
syntax:
|
|
185
|
+
content: public class Thing
|
|
186
|
+
`;
|
|
187
|
+
await fse.writeFile(path.join(dir, 'a.yml'), typeYml('Acme.Sub.Zone'));
|
|
188
|
+
await fse.writeFile(path.join(dir, 'b.yml'), typeYml('Acme.Sub-Zone'));
|
|
189
|
+
const result = await convertDocfx(dir);
|
|
190
|
+
const slugs = result.pages.map((page) => page.slug);
|
|
191
|
+
expect(new Set(slugs).size).toBe(slugs.length);
|
|
192
|
+
expect(result.pages).toHaveLength(2);
|
|
193
|
+
await fse.rm(dir, { recursive: true, force: true });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('escapes generic angle brackets in prose', () => {
|
|
197
|
+
const content = reference.pages[1]?.content ?? '';
|
|
198
|
+
expect(content).toContain('`IDictionary<string, string>`');
|
|
199
|
+
expect(content).toContain('<ResponseField name={"Red"} type={"0"} required>');
|
|
200
|
+
expect(reference.pages[1]?.description).toBe(
|
|
201
|
+
'Available colors for IDictionary<string, string> lookups.'
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { convertJavadoc } from '../src/sdk/converters/javadoc.js';
|
|
7
|
+
|
|
8
|
+
const TRAVERSAL_INDEX = 'typeSearchIndex = [{"p":"com/../../../etc","l":"Passwd"}];';
|
|
9
|
+
import type { SdkReference } from '../src/sdk/types.js';
|
|
10
|
+
|
|
11
|
+
const page = (title: string, body: string) => `<!DOCTYPE HTML>
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head><title>${title} (fake API)</title></head>
|
|
14
|
+
<body class="class-declaration-page">
|
|
15
|
+
<main role="main">
|
|
16
|
+
${body}
|
|
17
|
+
</main>
|
|
18
|
+
</body>
|
|
19
|
+
</html>`;
|
|
20
|
+
|
|
21
|
+
const greeterHtml = page(
|
|
22
|
+
'Greeter',
|
|
23
|
+
`<div class="header">
|
|
24
|
+
<h1 title="Class Greeter" class="title">Class Greeter</h1>
|
|
25
|
+
</div>
|
|
26
|
+
<section class="class-description" id="class-description">
|
|
27
|
+
<hr>
|
|
28
|
+
<div class="type-signature"><span class="modifiers">public class </span><span class="element-name type-name-label">Greeter</span></div>
|
|
29
|
+
<div class="block">Greets people politely. Supports many languages.</div>
|
|
30
|
+
</section>
|
|
31
|
+
<section class="summary">
|
|
32
|
+
<ul class="summary-list">
|
|
33
|
+
<li>
|
|
34
|
+
<section class="method-summary" id="method-summary">
|
|
35
|
+
<h2>Method Summary</h2>
|
|
36
|
+
<div class="summary-table three-column-summary">
|
|
37
|
+
<div class="col-first even-row-color"><code>String</code></div>
|
|
38
|
+
<div class="col-second even-row-color"><code><a href="#greet(java.lang.String)" class="member-name-link">greet</a>(String name)</code></div>
|
|
39
|
+
<div class="col-last even-row-color"><div class="block">SUMMARY_ROW_MARKER greet a person.</div></div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="inherited-list">
|
|
42
|
+
<h3>Methods inherited from class java.lang.Object</h3>
|
|
43
|
+
<code>equals, hashCode, toString</code>
|
|
44
|
+
</div>
|
|
45
|
+
</section>
|
|
46
|
+
</li>
|
|
47
|
+
</ul>
|
|
48
|
+
</section>
|
|
49
|
+
<section class="details">
|
|
50
|
+
<ul class="details-list">
|
|
51
|
+
<li>
|
|
52
|
+
<section class="method-details" id="method-detail">
|
|
53
|
+
<h2>Method Details</h2>
|
|
54
|
+
<ul class="member-list">
|
|
55
|
+
<li>
|
|
56
|
+
<section class="detail" id="greet(java.lang.String)">
|
|
57
|
+
<h3>greet</h3>
|
|
58
|
+
<div class="member-signature"><span class="modifiers">public</span> <span class="return-type">String</span> <span class="element-name">greet</span><span class="parameters">(String name)</span></div>
|
|
59
|
+
<div class="block">Builds a greeting for the given <a href="Person.html" title="interface in com.example">Person</a> name.</div>
|
|
60
|
+
<dl class="notes">
|
|
61
|
+
<dt>Parameters:</dt>
|
|
62
|
+
<dd><code>name</code> - who to greet.</dd>
|
|
63
|
+
</dl>
|
|
64
|
+
</section>
|
|
65
|
+
</li>
|
|
66
|
+
</ul>
|
|
67
|
+
</section>
|
|
68
|
+
</li>
|
|
69
|
+
</ul>
|
|
70
|
+
</section>`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const personHtml = page(
|
|
74
|
+
'Person',
|
|
75
|
+
`<div class="header">
|
|
76
|
+
<h1 title="Interface Person" class="title">Interface Person</h1>
|
|
77
|
+
</div>
|
|
78
|
+
<section class="class-description" id="class-description">
|
|
79
|
+
<hr>
|
|
80
|
+
<div class="type-signature"><span class="modifiers">public interface </span><span class="element-name type-name-label">Person</span></div>
|
|
81
|
+
<div class="block">A person that can be greeted.</div>
|
|
82
|
+
</section>`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
let dir: string;
|
|
86
|
+
let ref: SdkReference;
|
|
87
|
+
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
dir = await fse.mkdtemp(path.join(os.tmpdir(), 'javadoc-test-'));
|
|
90
|
+
await fse.writeFile(path.join(dir, 'element-list'), 'com.example\n');
|
|
91
|
+
const pkgDir = path.join(dir, 'com', 'example');
|
|
92
|
+
await fse.mkdirp(pkgDir);
|
|
93
|
+
await fse.writeFile(path.join(pkgDir, 'Greeter.html'), greeterHtml);
|
|
94
|
+
await fse.writeFile(path.join(pkgDir, 'Person.html'), personHtml);
|
|
95
|
+
await fse.writeFile(
|
|
96
|
+
path.join(pkgDir, 'package-summary.html'),
|
|
97
|
+
page('com.example', '<h1>Package com.example</h1>')
|
|
98
|
+
);
|
|
99
|
+
ref = await convertJavadoc(dir);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterAll(async () => {
|
|
103
|
+
await fse.remove(dir);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('convertJavadoc', () => {
|
|
107
|
+
it('emits one page per type with package-based slugs', () => {
|
|
108
|
+
expect(ref.pages.map((p) => p.slug).sort()).toEqual([
|
|
109
|
+
'com-example/Greeter',
|
|
110
|
+
'com-example/Person',
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('groups types by package', () => {
|
|
115
|
+
expect(ref.groups).toEqual([
|
|
116
|
+
{ group: 'com.example', pages: ['com-example/Greeter', 'com-example/Person'] },
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('extracts title, tag, and first-sentence description', () => {
|
|
121
|
+
const greeter = ref.pages.find((p) => p.slug === 'com-example/Greeter');
|
|
122
|
+
expect(greeter?.title).toBe('Greeter');
|
|
123
|
+
expect(greeter?.tag).toBe('CLASS');
|
|
124
|
+
expect(greeter?.description).toBe('Greets people politely.');
|
|
125
|
+
const person = ref.pages.find((p) => p.slug === 'com-example/Person');
|
|
126
|
+
expect(person?.tag).toBe('INTERFACE');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('keeps method detail sections and rewrites type links', () => {
|
|
130
|
+
const content = ref.pages.find((p) => p.slug === 'com-example/Greeter')?.content ?? '';
|
|
131
|
+
expect(content).toContain('### greet');
|
|
132
|
+
expect(content).toContain('public String greet(String name)');
|
|
133
|
+
expect(content).toContain('`name` - who to greet.');
|
|
134
|
+
expect(content).toContain('[Person](/com-example/Person)');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('drops summary tables and inherited-method boilerplate', () => {
|
|
138
|
+
const content = ref.pages.find((p) => p.slug === 'com-example/Greeter')?.content ?? '';
|
|
139
|
+
expect(content).not.toContain('SUMMARY_ROW_MARKER');
|
|
140
|
+
expect(content).not.toContain('Method Summary');
|
|
141
|
+
expect(content).not.toContain('Methods inherited from');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('ignores search-index entries with invalid package names', async () => {
|
|
145
|
+
const evil = await fse.mkdtemp(path.join(os.tmpdir(), 'javadoc-evil-'));
|
|
146
|
+
await fse.writeFile(path.join(evil, 'type-search-index.js'), TRAVERSAL_INDEX);
|
|
147
|
+
await expect(convertJavadoc(evil)).rejects.toThrow(/No element-list or package-list found/);
|
|
148
|
+
await fse.rm(evil, { recursive: true, force: true });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { convertPhpdoc } from '../src/sdk/converters/phpdoc.js';
|
|
7
|
+
import type { SdkReference } from '../src/sdk/types.js';
|
|
8
|
+
|
|
9
|
+
const STRUCTURE_XML = `<?xml version="1.0"?>
|
|
10
|
+
<project name="Acme-PHP">
|
|
11
|
+
<file path="Widget/Greeter.php" hash="abc">
|
|
12
|
+
<namespace-alias name="\\Acme\\SDK\\Widget"/>
|
|
13
|
+
<class final="true" abstract="false" namespace="\\Acme\\SDK\\Widget" line="10">
|
|
14
|
+
<name>Greeter</name>
|
|
15
|
+
<full_name>\\Acme\\SDK\\Widget\\Greeter</full_name>
|
|
16
|
+
<docblock line="10">
|
|
17
|
+
<description>Greets people politely.</description>
|
|
18
|
+
<long-description>Supports optional shouting.</long-description>
|
|
19
|
+
</docblock>
|
|
20
|
+
<implements>\\Acme\\SDK\\Contract\\GreeterInterface</implements>
|
|
21
|
+
<property namespace="\\Acme\\SDK\\Widget\\Greeter" line="12" visibility="public">
|
|
22
|
+
<name>salutation</name>
|
|
23
|
+
<default>'Hello'</default>
|
|
24
|
+
<docblock line="12">
|
|
25
|
+
<description></description>
|
|
26
|
+
<long-description></long-description>
|
|
27
|
+
<tag name="var" description="The salutation to use." type="string"/>
|
|
28
|
+
</docblock>
|
|
29
|
+
</property>
|
|
30
|
+
<property namespace="\\Acme\\SDK\\Widget\\Greeter" line="13" visibility="private">
|
|
31
|
+
<name>secret</name>
|
|
32
|
+
<default></default>
|
|
33
|
+
</property>
|
|
34
|
+
<method final="false" abstract="false" static="false" namespace="\\Acme\\SDK\\Widget" line="20" visibility="public" returnByReference="false">
|
|
35
|
+
<name>greet</name>
|
|
36
|
+
<full_name>\\Acme\\SDK\\Widget\\Greeter::greet()</full_name>
|
|
37
|
+
<argument line="20" by_reference="false">
|
|
38
|
+
<name>name</name>
|
|
39
|
+
<default></default>
|
|
40
|
+
<type>string</type>
|
|
41
|
+
</argument>
|
|
42
|
+
<argument line="20" by_reference="false">
|
|
43
|
+
<name>shout</name>
|
|
44
|
+
<default>false</default>
|
|
45
|
+
<type>?bool</type>
|
|
46
|
+
</argument>
|
|
47
|
+
<docblock line="20">
|
|
48
|
+
<description>Greet a person by name.</description>
|
|
49
|
+
<long-description></long-description>
|
|
50
|
+
<tag name="param" description="Name of the person." variable="name" type="string"/>
|
|
51
|
+
<tag name="param" description="Whether to shout." variable="shout" type="?bool"/>
|
|
52
|
+
<tag name="return" description="The greeting." type="string"/>
|
|
53
|
+
<tag name="throws" description="when name is empty" type="\\Acme\\SDK\\Exception\\ArgumentException"/>
|
|
54
|
+
</docblock>
|
|
55
|
+
</method>
|
|
56
|
+
<method final="false" abstract="false" static="false" namespace="\\Acme\\SDK\\Widget" line="30" visibility="private" returnByReference="false">
|
|
57
|
+
<name>hidden</name>
|
|
58
|
+
<full_name>\\Acme\\SDK\\Widget\\Greeter::hidden()</full_name>
|
|
59
|
+
</method>
|
|
60
|
+
</class>
|
|
61
|
+
</file>
|
|
62
|
+
</project>
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
describe('convertPhpdoc', () => {
|
|
66
|
+
let dir: string;
|
|
67
|
+
let reference: SdkReference;
|
|
68
|
+
|
|
69
|
+
beforeAll(async () => {
|
|
70
|
+
dir = await fse.mkdtemp(path.join(os.tmpdir(), 'phpdoc-test-'));
|
|
71
|
+
await fse.writeFile(path.join(dir, 'structure.xml'), STRUCTURE_XML);
|
|
72
|
+
reference = await convertPhpdoc(dir);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
await fse.remove(dir);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('creates one page per class with a namespace-derived slug', () => {
|
|
80
|
+
expect(reference.pages).toHaveLength(1);
|
|
81
|
+
const page = reference.pages[0]!;
|
|
82
|
+
expect(page.slug).toBe('greeter');
|
|
83
|
+
expect(page.title).toBe('Greeter');
|
|
84
|
+
expect(page.tag).toBe('CLASS');
|
|
85
|
+
expect(page.description).toBe('Greets people politely.');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('groups pages by namespace', () => {
|
|
89
|
+
expect(reference.groups).toEqual([{ group: 'Acme\\SDK\\Widget', pages: ['greeter'] }]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('reconstructs the method signature', () => {
|
|
93
|
+
const content = reference.pages[0]!.content;
|
|
94
|
+
expect(content).toContain(
|
|
95
|
+
'```php\npublic function greet(string $name, ?bool $shout = false): string\n```'
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renders class heritage and sections', () => {
|
|
100
|
+
const content = reference.pages[0]!.content;
|
|
101
|
+
expect(content).toContain(
|
|
102
|
+
'```php\nfinal class Greeter implements \\Acme\\SDK\\Contract\\GreeterInterface\n```'
|
|
103
|
+
);
|
|
104
|
+
expect(content).toContain('## Properties');
|
|
105
|
+
expect(content).toContain('## Methods');
|
|
106
|
+
expect(content).toContain('### greet()');
|
|
107
|
+
expect(content).toContain('#### Returns');
|
|
108
|
+
expect(content).toContain('#### Throws');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('emits ResponseField entries for properties and parameters', () => {
|
|
112
|
+
const content = reference.pages[0]!.content;
|
|
113
|
+
expect(content).toContain(
|
|
114
|
+
'<ResponseField name={"salutation"} type={"string"} required>\n The salutation to use.\n\n Default: `\'Hello\'`\n</ResponseField>'
|
|
115
|
+
);
|
|
116
|
+
expect(content).toContain(
|
|
117
|
+
'<ResponseField name={"name"} type={"string"} required>\n Name of the person.\n</ResponseField>'
|
|
118
|
+
);
|
|
119
|
+
expect(content).toContain(
|
|
120
|
+
'<ResponseField name={"shout"} type={"?bool"}>\n Whether to shout.\n</ResponseField>'
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('excludes private members', () => {
|
|
125
|
+
const content = reference.pages[0]!.content;
|
|
126
|
+
expect(content).not.toContain('secret');
|
|
127
|
+
expect(content).not.toContain('hidden');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('accepts the structure.xml file path directly', async () => {
|
|
131
|
+
const direct = await convertPhpdoc(path.join(dir, 'structure.xml'));
|
|
132
|
+
expect(direct.pages[0]?.slug).toBe('greeter');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { convertSphinx } from '../src/sdk/converters/sphinx.js';
|
|
7
|
+
import type { SdkReference } from '../src/sdk/types.js';
|
|
8
|
+
|
|
9
|
+
const indexPage = {
|
|
10
|
+
current_page_name: 'index',
|
|
11
|
+
title: 'Example SDK documentation',
|
|
12
|
+
body: '<section><h1>Example SDK documentation<a class="headerlink" href="#example" title="Link to this heading"></a></h1><p>Welcome to the example SDK. It does things.</p><p>See the <a class="reference internal" href="widgets.html#widgets.Widget">Widget docs</a> and the <a class="reference external" href="https://example.com/docs">docs site</a>.</p><div class="admonition warning"><p class="admonition-title">Warning</p><p>Handle with care.</p></div></section>',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const widgetsPage = {
|
|
16
|
+
current_page_name: 'widgets',
|
|
17
|
+
title: 'widgets module',
|
|
18
|
+
body: '<section><h1>widgets module<a class="headerlink" href="#widgets" title="Link to this heading"></a></h1><dl class="py class"><dt class="sig sig-object py" id="widgets.Widget"><em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">widgets.</span></span><span class="sig-name descname"><span class="pre">Widget</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">name</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/widgets/#Widget"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#widgets.Widget" title="Link to this definition"></a></dt><dd><p>A widget.</p><dl class="py method"><dt class="sig sig-object py" id="widgets.Widget.spin"><span class="sig-name descname"><span class="pre">spin</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">speed</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">1</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#widgets.Widget.spin" title="Link to this definition"></a></dt><dd><p>Spins the widget.</p></dd></dl></dd></dl></section>',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const skippedPage = { current_page_name: 'genindex', title: 'Index', body: '<h1>Index</h1>' };
|
|
22
|
+
|
|
23
|
+
describe('convertSphinx', () => {
|
|
24
|
+
let dir: string;
|
|
25
|
+
let reference: SdkReference;
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
dir = await fse.mkdtemp(path.join(os.tmpdir(), 'sphinx-test-'));
|
|
29
|
+
await fse.writeJson(path.join(dir, 'index.fjson'), indexPage);
|
|
30
|
+
await fse.writeJson(path.join(dir, 'widgets.fjson'), widgetsPage);
|
|
31
|
+
await fse.writeJson(path.join(dir, 'genindex.fjson'), skippedPage);
|
|
32
|
+
reference = await convertSphinx(dir);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
await fse.remove(dir);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('emits one page per content file with slugs from current_page_name', () => {
|
|
40
|
+
expect(reference.pages.map((page) => page.slug).sort()).toEqual(['index', 'widgets']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('groups guide pages and autodoc module pages separately', () => {
|
|
44
|
+
expect(reference.groups).toEqual([
|
|
45
|
+
{ group: 'Getting Started', pages: ['index'] },
|
|
46
|
+
{ group: 'API Reference', pages: ['widgets'] },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('tags autodoc pages as MODULE', () => {
|
|
51
|
+
const widgets = reference.pages.find((page) => page.slug === 'widgets');
|
|
52
|
+
expect(widgets?.tag).toBe('MODULE');
|
|
53
|
+
expect(reference.pages.find((page) => page.slug === 'index')?.tag).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('converts signatures to python code blocks with headings', () => {
|
|
57
|
+
const content = reference.pages.find((page) => page.slug === 'widgets')?.content ?? '';
|
|
58
|
+
expect(content).toContain('### Widget');
|
|
59
|
+
expect(content).toContain('```python\nclass widgets.Widget(name)\n```');
|
|
60
|
+
expect(content).toContain('#### spin()');
|
|
61
|
+
expect(content).toContain('```python\nspin(speed=1)\n```');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('strips headerlinks and viewcode links', () => {
|
|
65
|
+
for (const page of reference.pages) {
|
|
66
|
+
expect(page.content).not.toContain('');
|
|
67
|
+
expect(page.content).not.toContain('[source]');
|
|
68
|
+
expect(page.content).not.toContain('_modules');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('rewrites internal links and keeps external ones', () => {
|
|
73
|
+
const index = reference.pages.find((page) => page.slug === 'index');
|
|
74
|
+
expect(index?.content).toContain('[Widget docs](/widgets)');
|
|
75
|
+
expect(index?.content).toContain('https://example.com/docs');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('converts admonitions to callouts and extracts descriptions', () => {
|
|
79
|
+
const index = reference.pages.find((page) => page.slug === 'index');
|
|
80
|
+
expect(index?.content).toContain('<Warning>');
|
|
81
|
+
expect(index?.content).toContain('Handle with care.');
|
|
82
|
+
expect(index?.description).toBe('Welcome to the example SDK.');
|
|
83
|
+
});
|
|
84
|
+
});
|