@nshipster/sosumi 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.
- package/LICENSE.md +19 -0
- package/README.md +304 -0
- package/bin/sosumi.mjs +76 -0
- package/package.json +53 -0
- package/public/_headers +2 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +7 -0
- package/public/icons/square.and.pencil.svg +15 -0
- package/public/index.html +898 -0
- package/public/llms.txt +184 -0
- package/public/sosumi.m4a +0 -0
- package/src/cli.ts +214 -0
- package/src/index.ts +507 -0
- package/src/lib/cli-endpoints.ts +106 -0
- package/src/lib/external/fetch.ts +133 -0
- package/src/lib/external/index.ts +8 -0
- package/src/lib/external/policy.ts +308 -0
- package/src/lib/external/types.ts +10 -0
- package/src/lib/fetch.ts +43 -0
- package/src/lib/hig/fetch.ts +186 -0
- package/src/lib/hig/index.ts +9 -0
- package/src/lib/hig/render.ts +514 -0
- package/src/lib/hig/types.ts +206 -0
- package/src/lib/hig/util.ts +30 -0
- package/src/lib/mcp.ts +315 -0
- package/src/lib/reference/fetch.ts +53 -0
- package/src/lib/reference/index.ts +8 -0
- package/src/lib/reference/render.ts +739 -0
- package/src/lib/reference/types.ts +31 -0
- package/src/lib/search.ts +221 -0
- package/src/lib/types.ts +334 -0
- package/src/lib/url.ts +55 -0
- package/src/lib/video/index.ts +179 -0
- package/wrangler.jsonc +27 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Developer Reference documentation rendering functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AppleDocJSON,
|
|
7
|
+
ContentItem,
|
|
8
|
+
IndexContentItem,
|
|
9
|
+
PropertyItem,
|
|
10
|
+
TopicSection,
|
|
11
|
+
Variant,
|
|
12
|
+
} from "./types"
|
|
13
|
+
|
|
14
|
+
interface RenderOptions {
|
|
15
|
+
externalOrigin?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render JSON-based extracted content to markdown
|
|
20
|
+
*/
|
|
21
|
+
export async function renderFromJSON(
|
|
22
|
+
jsonData: AppleDocJSON,
|
|
23
|
+
sourceUrl: string,
|
|
24
|
+
options: RenderOptions = {},
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
let markdown = ""
|
|
27
|
+
|
|
28
|
+
// Generate front matter
|
|
29
|
+
markdown += generateFrontMatterFromJSON(jsonData, sourceUrl)
|
|
30
|
+
|
|
31
|
+
// Add navigation breadcrumbs
|
|
32
|
+
const breadcrumbs = generateBreadcrumbs(sourceUrl, options.externalOrigin)
|
|
33
|
+
if (breadcrumbs) {
|
|
34
|
+
markdown += breadcrumbs
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Add symbol type and name
|
|
38
|
+
if (jsonData.metadata?.roleHeading) {
|
|
39
|
+
markdown += `**${jsonData.metadata.roleHeading}**\n\n`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add title
|
|
43
|
+
const title = jsonData.metadata?.title || ""
|
|
44
|
+
if (title) {
|
|
45
|
+
markdown += `# ${title}\n\n`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Add platform availability
|
|
49
|
+
if (jsonData.metadata?.platforms && jsonData.metadata.platforms.length > 0) {
|
|
50
|
+
const platforms = jsonData.metadata.platforms
|
|
51
|
+
.map((p) => `${p.name} ${p.introducedAt}+${p.beta ? " Beta" : ""}`)
|
|
52
|
+
.join(", ")
|
|
53
|
+
markdown += `**Available on:** ${platforms}\n\n`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Add abstract
|
|
57
|
+
if (jsonData.abstract && Array.isArray(jsonData.abstract)) {
|
|
58
|
+
const abstractText = jsonData.abstract
|
|
59
|
+
.filter((item) => item.type === "text")
|
|
60
|
+
.map((item) => item.text)
|
|
61
|
+
.join("")
|
|
62
|
+
|
|
63
|
+
if (abstractText.trim()) {
|
|
64
|
+
markdown += `> ${abstractText}\n\n`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add declaration
|
|
69
|
+
if (jsonData.primaryContentSections) {
|
|
70
|
+
const declarationSection = jsonData.primaryContentSections.find(
|
|
71
|
+
(s) => s.kind === "declarations",
|
|
72
|
+
)
|
|
73
|
+
if (declarationSection?.declarations) {
|
|
74
|
+
markdown += renderDeclarations(declarationSection.declarations)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add parameters
|
|
78
|
+
const parametersSection = jsonData.primaryContentSections.find((s) => s.kind === "parameters")
|
|
79
|
+
if (parametersSection?.parameters) {
|
|
80
|
+
markdown += renderParameters(
|
|
81
|
+
parametersSection.parameters,
|
|
82
|
+
jsonData.references,
|
|
83
|
+
options.externalOrigin,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add properties (used by object/dictionary pages in data docs)
|
|
88
|
+
const propertiesSection = jsonData.primaryContentSections.find((s) => s.kind === "properties")
|
|
89
|
+
if (propertiesSection?.items) {
|
|
90
|
+
markdown += renderProperties(
|
|
91
|
+
propertiesSection.items,
|
|
92
|
+
jsonData.references,
|
|
93
|
+
options.externalOrigin,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add content sections
|
|
98
|
+
const contentSections = jsonData.primaryContentSections.filter((s) => s.kind === "content")
|
|
99
|
+
for (const section of contentSections) {
|
|
100
|
+
if (section.content) {
|
|
101
|
+
markdown += renderContent(section.content, jsonData.references, options.externalOrigin)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add relationship sections (Inherited By, Conforming Types, etc.)
|
|
107
|
+
if (jsonData.relationshipsSections) {
|
|
108
|
+
markdown += renderRelationships(
|
|
109
|
+
jsonData.relationshipsSections,
|
|
110
|
+
jsonData.variants,
|
|
111
|
+
jsonData.references,
|
|
112
|
+
options.externalOrigin,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add topic sections
|
|
117
|
+
if (jsonData.topicSections) {
|
|
118
|
+
markdown += renderTopicSections(
|
|
119
|
+
jsonData.topicSections,
|
|
120
|
+
jsonData.variants,
|
|
121
|
+
jsonData.references,
|
|
122
|
+
options.externalOrigin,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add index content for framework pages
|
|
127
|
+
if (jsonData.interfaceLanguages?.swift) {
|
|
128
|
+
const swiftContent = jsonData.interfaceLanguages.swift[0]
|
|
129
|
+
if (swiftContent.children) {
|
|
130
|
+
markdown += renderIndexContent(swiftContent.children, options.externalOrigin)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add see also sections
|
|
135
|
+
if (jsonData.seeAlsoSections) {
|
|
136
|
+
markdown += renderSeeAlso(
|
|
137
|
+
jsonData.seeAlsoSections,
|
|
138
|
+
jsonData.variants,
|
|
139
|
+
jsonData.references,
|
|
140
|
+
options.externalOrigin,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Trim whitespace
|
|
145
|
+
markdown = markdown.trim()
|
|
146
|
+
|
|
147
|
+
// Add footer
|
|
148
|
+
markdown += `\n\n---\n\n`
|
|
149
|
+
markdown += `*Extracted by [sosumi.ai](https://sosumi.ai) - Making Apple docs AI-readable.*\n`
|
|
150
|
+
markdown += `*This is unofficial content. All documentation belongs to Apple Inc.*\n`
|
|
151
|
+
|
|
152
|
+
return markdown
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate YAML front-matter from JSON data
|
|
157
|
+
*/
|
|
158
|
+
function generateFrontMatterFromJSON(jsonData: AppleDocJSON, sourceUrl: string): string {
|
|
159
|
+
const frontMatter: Record<string, string> = {}
|
|
160
|
+
|
|
161
|
+
if (jsonData.metadata?.title) {
|
|
162
|
+
const cleanTitle = jsonData.metadata.title.replace("| Apple Developer Documentation", "").trim()
|
|
163
|
+
frontMatter.title = cleanTitle
|
|
164
|
+
} else if (jsonData.interfaceLanguages?.swift?.[0]?.title) {
|
|
165
|
+
frontMatter.title = jsonData.interfaceLanguages.swift[0].title
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (jsonData.abstract && Array.isArray(jsonData.abstract)) {
|
|
169
|
+
const description = jsonData.abstract
|
|
170
|
+
.filter((item) => item.type === "text")
|
|
171
|
+
.map((item) => item.text)
|
|
172
|
+
.join("")
|
|
173
|
+
.trim()
|
|
174
|
+
if (description) {
|
|
175
|
+
frontMatter.description = description
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
frontMatter.source = sourceUrl
|
|
180
|
+
frontMatter.timestamp = new Date().toISOString()
|
|
181
|
+
|
|
182
|
+
// Convert to YAML format
|
|
183
|
+
const yamlLines = Object.entries(frontMatter).map(([key, value]) => `${key}: ${value}`)
|
|
184
|
+
return `---\n${yamlLines.join("\n")}\n---\n\n`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate breadcrumb navigation
|
|
189
|
+
*/
|
|
190
|
+
function generateBreadcrumbs(sourceUrl: string, externalOrigin?: string): string {
|
|
191
|
+
const url = new URL(sourceUrl)
|
|
192
|
+
const pathParts = url.pathname.split("/").filter(Boolean)
|
|
193
|
+
const documentationIndex = pathParts.indexOf("documentation")
|
|
194
|
+
|
|
195
|
+
if (documentationIndex === -1 || pathParts.length <= documentationIndex + 2) {
|
|
196
|
+
return ""
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const framework = pathParts[documentationIndex + 1]
|
|
200
|
+
let breadcrumbs = `**Navigation:** [${framework.charAt(0).toUpperCase() + framework.slice(1)}](${rewriteDocumentationPath(
|
|
201
|
+
`/documentation/${framework}`,
|
|
202
|
+
externalOrigin,
|
|
203
|
+
)})`
|
|
204
|
+
|
|
205
|
+
if (pathParts.length > documentationIndex + 2) {
|
|
206
|
+
// Add intermediate breadcrumbs if needed
|
|
207
|
+
for (let i = documentationIndex + 2; i < pathParts.length - 1; i++) {
|
|
208
|
+
const part = pathParts[i]
|
|
209
|
+
const path = pathParts.slice(documentationIndex, i + 1).join("/")
|
|
210
|
+
breadcrumbs += ` › [${part}](${rewriteDocumentationPath(`/${path}`, externalOrigin)})`
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return `${breadcrumbs}\n\n`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Render declaration sections
|
|
219
|
+
*/
|
|
220
|
+
function renderDeclarations(declarations: Array<{ tokens?: Array<{ text?: string }> }>): string {
|
|
221
|
+
let markdown = ""
|
|
222
|
+
|
|
223
|
+
for (const decl of declarations) {
|
|
224
|
+
if (decl.tokens) {
|
|
225
|
+
// Simply concatenate the tokens as Apple has them formatted
|
|
226
|
+
const code = decl.tokens
|
|
227
|
+
.filter((token) => token != null)
|
|
228
|
+
.map((token) => token.text || "")
|
|
229
|
+
.join("")
|
|
230
|
+
.trim()
|
|
231
|
+
|
|
232
|
+
markdown += `\`\`\`swift\n${code}\n\`\`\`\n\n`
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return markdown
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Render parameters section
|
|
241
|
+
*/
|
|
242
|
+
function renderParameters(
|
|
243
|
+
parameters: Array<{ name: string; content?: ContentItem[] }>,
|
|
244
|
+
references?: Record<string, ContentItem>,
|
|
245
|
+
externalOrigin?: string,
|
|
246
|
+
): string {
|
|
247
|
+
if (parameters.length === 0) return ""
|
|
248
|
+
|
|
249
|
+
let markdown = "## Parameters\n\n"
|
|
250
|
+
|
|
251
|
+
for (const param of parameters) {
|
|
252
|
+
markdown += `**${param.name}**\n\n`
|
|
253
|
+
if (param.content && Array.isArray(param.content)) {
|
|
254
|
+
const paramText = renderContentArray(param.content, references, 0, externalOrigin)
|
|
255
|
+
markdown += `${paramText}\n\n`
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return markdown
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Render properties section for data dictionary pages.
|
|
264
|
+
*/
|
|
265
|
+
function renderProperties(
|
|
266
|
+
properties: PropertyItem[],
|
|
267
|
+
references?: Record<string, ContentItem>,
|
|
268
|
+
externalOrigin?: string,
|
|
269
|
+
): string {
|
|
270
|
+
if (properties.length === 0) return ""
|
|
271
|
+
|
|
272
|
+
let markdown = "## Properties\n\n"
|
|
273
|
+
|
|
274
|
+
for (const property of properties) {
|
|
275
|
+
if (!property.name) continue
|
|
276
|
+
|
|
277
|
+
const typeText = renderPropertyType(property.type, references, externalOrigin)
|
|
278
|
+
const requiredText = property.required === true ? "required" : "optional"
|
|
279
|
+
const metadata = [typeText, requiredText].filter(Boolean)
|
|
280
|
+
const headingSuffix = metadata.length > 0 ? ` *(${metadata.join(", ")})*` : ""
|
|
281
|
+
markdown += `### \`${property.name}\`${headingSuffix}\n\n`
|
|
282
|
+
|
|
283
|
+
if (property.content && Array.isArray(property.content)) {
|
|
284
|
+
markdown += `${renderContentArray(property.content, references, 0, externalOrigin)}`
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const allowedValues = property.attributes?.find((a) => a.kind === "allowedValues")?.values
|
|
288
|
+
if (allowedValues && allowedValues.length > 0) {
|
|
289
|
+
const possibleValues = allowedValues.map((value) => `\`${value}\``).join(", ")
|
|
290
|
+
markdown += `Possible Values: ${possibleValues}\n\n`
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return markdown
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderPropertyType(
|
|
298
|
+
type: Array<{ text?: string; kind?: string; identifier?: string }> | undefined,
|
|
299
|
+
references?: Record<string, ContentItem>,
|
|
300
|
+
externalOrigin?: string,
|
|
301
|
+
): string {
|
|
302
|
+
if (!type || type.length === 0) return ""
|
|
303
|
+
|
|
304
|
+
return type
|
|
305
|
+
.map((part) => {
|
|
306
|
+
if (part.kind === "typeIdentifier" && part.identifier && part.text) {
|
|
307
|
+
const url = convertIdentifierToURL(part.identifier, references, externalOrigin)
|
|
308
|
+
return url ? `[${part.text}](${url})` : part.text
|
|
309
|
+
}
|
|
310
|
+
return part.text || ""
|
|
311
|
+
})
|
|
312
|
+
.join("")
|
|
313
|
+
.trim()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Render main content sections
|
|
318
|
+
*/
|
|
319
|
+
function renderContent(
|
|
320
|
+
content: ContentItem[],
|
|
321
|
+
references?: Record<string, ContentItem>,
|
|
322
|
+
externalOrigin?: string,
|
|
323
|
+
): string {
|
|
324
|
+
return renderContentArray(content, references, 0, externalOrigin)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Render content array to markdown
|
|
329
|
+
*/
|
|
330
|
+
function renderContentArray(
|
|
331
|
+
content: ContentItem[],
|
|
332
|
+
references?: Record<string, ContentItem>,
|
|
333
|
+
depth: number = 0,
|
|
334
|
+
externalOrigin?: string,
|
|
335
|
+
): string {
|
|
336
|
+
// Prevent infinite recursion by limiting depth
|
|
337
|
+
if (depth > 50) {
|
|
338
|
+
console.warn("Maximum recursion depth reached in renderContentArray")
|
|
339
|
+
return "[Content too deeply nested]"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let markdown = ""
|
|
343
|
+
|
|
344
|
+
for (const item of content) {
|
|
345
|
+
if (item.type === "heading") {
|
|
346
|
+
const level = Math.min(item.level || 2, 6)
|
|
347
|
+
const hashes = "#".repeat(level)
|
|
348
|
+
markdown += `${hashes} ${item.text}\n\n`
|
|
349
|
+
} else if (item.type === "paragraph") {
|
|
350
|
+
if (item.inlineContent) {
|
|
351
|
+
const text = renderInlineContent(item.inlineContent, references, depth, externalOrigin)
|
|
352
|
+
markdown += `${text}\n\n`
|
|
353
|
+
}
|
|
354
|
+
} else if (item.type === "codeListing") {
|
|
355
|
+
let code = ""
|
|
356
|
+
if (Array.isArray(item.code)) {
|
|
357
|
+
code = item.code.join("\n")
|
|
358
|
+
} else {
|
|
359
|
+
code = String(item.code || "")
|
|
360
|
+
}
|
|
361
|
+
const syntax = item.syntax || "swift"
|
|
362
|
+
|
|
363
|
+
markdown += `\`\`\`${syntax}\n${code}\n\`\`\`\n\n`
|
|
364
|
+
} else if (item.type === "unorderedList") {
|
|
365
|
+
if (item.items) {
|
|
366
|
+
for (const listItem of item.items) {
|
|
367
|
+
const itemText = renderContentArray(
|
|
368
|
+
listItem.content || [],
|
|
369
|
+
references,
|
|
370
|
+
depth + 1,
|
|
371
|
+
externalOrigin,
|
|
372
|
+
)
|
|
373
|
+
markdown += `- ${itemText.replace(/\n\n$/, "")}\n`
|
|
374
|
+
}
|
|
375
|
+
markdown += "\n"
|
|
376
|
+
}
|
|
377
|
+
} else if (item.type === "orderedList") {
|
|
378
|
+
if (item.items) {
|
|
379
|
+
item.items.forEach((listItem: ContentItem, index: number) => {
|
|
380
|
+
const itemText = renderContentArray(
|
|
381
|
+
listItem.content || [],
|
|
382
|
+
references,
|
|
383
|
+
depth + 1,
|
|
384
|
+
externalOrigin,
|
|
385
|
+
)
|
|
386
|
+
markdown += `${index + 1}. ${itemText.replace(/\n\n$/, "")}\n`
|
|
387
|
+
})
|
|
388
|
+
markdown += "\n"
|
|
389
|
+
}
|
|
390
|
+
} else if (item.type === "aside") {
|
|
391
|
+
const style = item.style || "note"
|
|
392
|
+
const calloutType = mapAsideStyleToCallout(style)
|
|
393
|
+
const asideContent = item.content
|
|
394
|
+
? renderContentArray(item.content, references, depth + 1, externalOrigin)
|
|
395
|
+
: ""
|
|
396
|
+
const cleanContent = asideContent.trim().replace(/\n/g, "\n> ")
|
|
397
|
+
markdown += `> [!${calloutType}]\n> ${cleanContent}\n\n`
|
|
398
|
+
} else if (item.type === "table") {
|
|
399
|
+
markdown += renderTable(item, references, depth, externalOrigin)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return markdown
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Render a table content item to markdown.
|
|
408
|
+
* Apple's JSON uses header: "row" (first row is header) and rows where each cell is ContentItem[].
|
|
409
|
+
*/
|
|
410
|
+
function renderTable(
|
|
411
|
+
item: ContentItem,
|
|
412
|
+
references?: Record<string, ContentItem>,
|
|
413
|
+
depth: number = 0,
|
|
414
|
+
externalOrigin?: string,
|
|
415
|
+
): string {
|
|
416
|
+
const table = item as ContentItem & {
|
|
417
|
+
header?: string
|
|
418
|
+
rows?: ContentItem[][][] // rows[rowIndex][cellIndex] = ContentItem[]
|
|
419
|
+
}
|
|
420
|
+
const rows = table.rows ?? []
|
|
421
|
+
if (rows.length === 0) return ""
|
|
422
|
+
|
|
423
|
+
const escapeCell = (s: string) => s.replace(/\|/g, "\\|").replace(/\n/g, " ").trim()
|
|
424
|
+
const renderCell = (cell: ContentItem | ContentItem[]) => {
|
|
425
|
+
const items = Array.isArray(cell) ? cell : [cell]
|
|
426
|
+
const s = renderContentArray(items, references, depth + 1, externalOrigin)
|
|
427
|
+
return escapeCell(s)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const firstRowIsHeader = table.header === "row"
|
|
431
|
+
let markdown = ""
|
|
432
|
+
rows.forEach((row, rowIndex) => {
|
|
433
|
+
const cells = row.map((c) => renderCell(c))
|
|
434
|
+
if (cells.length === 0) return
|
|
435
|
+
markdown += `| ${cells.join(" | ")} |\n`
|
|
436
|
+
if (firstRowIsHeader && rowIndex === 0) {
|
|
437
|
+
markdown += `| ${cells.map(() => "---").join(" | ")} |\n`
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
return markdown ? `${markdown}\n` : ""
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Render inline content to markdown
|
|
445
|
+
*/
|
|
446
|
+
function renderInlineContent(
|
|
447
|
+
inlineContent: ContentItem[],
|
|
448
|
+
references?: Record<string, ContentItem>,
|
|
449
|
+
depth: number = 0,
|
|
450
|
+
externalOrigin?: string,
|
|
451
|
+
): string {
|
|
452
|
+
// Prevent infinite recursion by limiting depth
|
|
453
|
+
if (depth > 20) {
|
|
454
|
+
console.warn("Maximum recursion depth reached in renderInlineContent")
|
|
455
|
+
return "[Inline content too deeply nested]"
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return inlineContent
|
|
459
|
+
.map((item) => {
|
|
460
|
+
if (item.type === "text") {
|
|
461
|
+
return item.text
|
|
462
|
+
} else if (item.type === "codeVoice") {
|
|
463
|
+
return `\`${item.code}\``
|
|
464
|
+
} else if (item.type === "reference") {
|
|
465
|
+
const title =
|
|
466
|
+
item.title ||
|
|
467
|
+
item.text ||
|
|
468
|
+
(item.identifier ? extractTitleFromIdentifier(item.identifier) : "")
|
|
469
|
+
const url = item.identifier
|
|
470
|
+
? convertIdentifierToURL(item.identifier, references, externalOrigin)
|
|
471
|
+
: ""
|
|
472
|
+
return `[${title}](${url})`
|
|
473
|
+
} else if (item.type === "emphasis") {
|
|
474
|
+
return `*${item.inlineContent ? renderInlineContent(item.inlineContent, references, depth + 1, externalOrigin) : ""}*`
|
|
475
|
+
} else if (item.type === "strong") {
|
|
476
|
+
return `**${item.inlineContent ? renderInlineContent(item.inlineContent, references, depth + 1, externalOrigin) : ""}**`
|
|
477
|
+
} else if (item.type === "image" && item.identifier) {
|
|
478
|
+
const ref = references?.[item.identifier] as
|
|
479
|
+
| (ContentItem & { variants?: Array<{ url: string }>; alt?: string })
|
|
480
|
+
| undefined
|
|
481
|
+
const url = ref?.variants?.[0]?.url
|
|
482
|
+
const alt = ref?.alt ?? ""
|
|
483
|
+
return url ? `` : ""
|
|
484
|
+
}
|
|
485
|
+
return item.text || ""
|
|
486
|
+
})
|
|
487
|
+
.join("")
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Render relationship sections
|
|
492
|
+
*/
|
|
493
|
+
function renderRelationships(
|
|
494
|
+
relationships: ContentItem[],
|
|
495
|
+
variants?: Variant[],
|
|
496
|
+
references?: Record<string, ContentItem>,
|
|
497
|
+
externalOrigin?: string,
|
|
498
|
+
): string {
|
|
499
|
+
let markdown = ""
|
|
500
|
+
|
|
501
|
+
for (const rel of relationships) {
|
|
502
|
+
if (rel.title && rel.identifiers) {
|
|
503
|
+
markdown += `## ${rel.title}\n\n`
|
|
504
|
+
for (const id of rel.identifiers) {
|
|
505
|
+
const info = variants?.find((v: Variant) => v.identifier === id)
|
|
506
|
+
const reference = references?.[id]
|
|
507
|
+
const title = info?.title || reference?.title || extractTitleFromIdentifier(id)
|
|
508
|
+
const url = convertIdentifierToURL(id, references, externalOrigin)
|
|
509
|
+
markdown += `- [${title}](${url})\n`
|
|
510
|
+
}
|
|
511
|
+
markdown += "\n"
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return markdown
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Render topic sections
|
|
520
|
+
*/
|
|
521
|
+
function renderTopicSections(
|
|
522
|
+
topics: TopicSection[],
|
|
523
|
+
variants?: Variant[],
|
|
524
|
+
references?: Record<string, ContentItem>,
|
|
525
|
+
externalOrigin?: string,
|
|
526
|
+
): string {
|
|
527
|
+
let markdown = ""
|
|
528
|
+
|
|
529
|
+
for (const topic of topics) {
|
|
530
|
+
if (topic.title) {
|
|
531
|
+
markdown += `## ${topic.title}\n\n`
|
|
532
|
+
|
|
533
|
+
if (topic.identifiers) {
|
|
534
|
+
for (const id of topic.identifiers) {
|
|
535
|
+
const info = variants?.find((v: Variant) => v.identifier === id)
|
|
536
|
+
const reference = references?.[id]
|
|
537
|
+
if (info || reference) {
|
|
538
|
+
const title = info?.title || reference?.title || extractTitleFromIdentifier(id)
|
|
539
|
+
const url = convertIdentifierToURL(id, references, externalOrigin)
|
|
540
|
+
const abstract = info?.abstract
|
|
541
|
+
? info.abstract.map((a: { text: string }) => a.text).join("")
|
|
542
|
+
: reference?.abstract
|
|
543
|
+
? reference.abstract.map((a: { text: string }) => a.text).join("")
|
|
544
|
+
: ""
|
|
545
|
+
|
|
546
|
+
markdown += `- [${title}](${url})`
|
|
547
|
+
if (abstract) {
|
|
548
|
+
markdown += ` ${abstract}`
|
|
549
|
+
}
|
|
550
|
+
markdown += "\n"
|
|
551
|
+
} else {
|
|
552
|
+
const title = extractTitleFromIdentifier(id)
|
|
553
|
+
const url = convertIdentifierToURL(id, references, externalOrigin)
|
|
554
|
+
markdown += `- [${title}](${url})\n`
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
markdown += "\n"
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return markdown
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Render index content for framework pages
|
|
567
|
+
*/
|
|
568
|
+
function renderIndexContent(children: IndexContentItem[], externalOrigin?: string): string {
|
|
569
|
+
return renderIndexContentWithIndent(children, 2, externalOrigin)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Render index content with proper indentation and spacing
|
|
574
|
+
*/
|
|
575
|
+
function renderIndexContentWithIndent(
|
|
576
|
+
children: IndexContentItem[],
|
|
577
|
+
headingLevel: number,
|
|
578
|
+
externalOrigin?: string,
|
|
579
|
+
): string {
|
|
580
|
+
let markdown = ""
|
|
581
|
+
|
|
582
|
+
for (let i = 0; i < children.length; i++) {
|
|
583
|
+
const child = children[i]
|
|
584
|
+
|
|
585
|
+
if (child.type === "groupMarker") {
|
|
586
|
+
// Add spacing before group markers (except the first one)
|
|
587
|
+
if (i > 0) {
|
|
588
|
+
markdown += "\n"
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Group markers are headings at the current level
|
|
592
|
+
const hashes = "#".repeat(Math.min(headingLevel, 6))
|
|
593
|
+
markdown += `${hashes} ${child.title}\n\n`
|
|
594
|
+
} else if (child.path && child.title) {
|
|
595
|
+
const beta = child.beta ? " **Beta**" : ""
|
|
596
|
+
|
|
597
|
+
// List items are always unindented under their heading
|
|
598
|
+
const rewrittenPath = rewriteDocumentationPath(child.path, externalOrigin)
|
|
599
|
+
markdown += `- [${child.title}](${rewrittenPath})${beta}\n`
|
|
600
|
+
|
|
601
|
+
if (child.children) {
|
|
602
|
+
// Add spacing before nested content
|
|
603
|
+
markdown += "\n"
|
|
604
|
+
// Nested content gets a deeper heading level
|
|
605
|
+
const nestedContent = renderIndexContentWithIndent(
|
|
606
|
+
child.children,
|
|
607
|
+
headingLevel + 1,
|
|
608
|
+
externalOrigin,
|
|
609
|
+
)
|
|
610
|
+
markdown += nestedContent
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return markdown
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Render see also sections
|
|
620
|
+
*/
|
|
621
|
+
function renderSeeAlso(
|
|
622
|
+
seeAlso: Array<{ title: string; identifiers?: string[] }>,
|
|
623
|
+
variants?: Variant[],
|
|
624
|
+
references?: Record<string, ContentItem>,
|
|
625
|
+
externalOrigin?: string,
|
|
626
|
+
): string {
|
|
627
|
+
let markdown = ""
|
|
628
|
+
|
|
629
|
+
for (const section of seeAlso) {
|
|
630
|
+
if (section.title && section.identifiers) {
|
|
631
|
+
markdown += `## ${section.title}\n\n`
|
|
632
|
+
for (const id of section.identifiers) {
|
|
633
|
+
const info = variants?.find((v: Variant) => v.identifier === id)
|
|
634
|
+
const reference = references?.[id]
|
|
635
|
+
const title = info?.title || reference?.title || extractTitleFromIdentifier(id)
|
|
636
|
+
const url = convertIdentifierToURL(id, references, externalOrigin)
|
|
637
|
+
markdown += `- [${title}](${url})\n`
|
|
638
|
+
}
|
|
639
|
+
markdown += "\n"
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return markdown
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Map aside style to GitHub-style callout
|
|
648
|
+
*/
|
|
649
|
+
function mapAsideStyleToCallout(style: string): string {
|
|
650
|
+
switch (style.toLowerCase()) {
|
|
651
|
+
case "warning":
|
|
652
|
+
return "WARNING"
|
|
653
|
+
case "important":
|
|
654
|
+
return "IMPORTANT"
|
|
655
|
+
case "caution":
|
|
656
|
+
return "CAUTION"
|
|
657
|
+
case "tip":
|
|
658
|
+
return "TIP"
|
|
659
|
+
case "deprecated":
|
|
660
|
+
return "WARNING"
|
|
661
|
+
default:
|
|
662
|
+
return "NOTE"
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Convert doc:// identifier to sosumi.ai URL
|
|
668
|
+
*/
|
|
669
|
+
function convertIdentifierToURL(
|
|
670
|
+
identifier: string,
|
|
671
|
+
references?: Record<string, ContentItem>,
|
|
672
|
+
externalOrigin?: string,
|
|
673
|
+
): string {
|
|
674
|
+
// Check if we have a reference with a URL for this identifier
|
|
675
|
+
const reference = references?.[identifier]
|
|
676
|
+
if (reference?.url) {
|
|
677
|
+
return rewriteDocumentationPath(reference.url, externalOrigin)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (identifier.startsWith("doc://com.apple.SwiftUI/documentation/")) {
|
|
681
|
+
const path = identifier.replace("doc://com.apple.SwiftUI/documentation/", "/documentation/")
|
|
682
|
+
return rewriteDocumentationPath(path, externalOrigin)
|
|
683
|
+
} else if (identifier.startsWith("doc://com.apple.")) {
|
|
684
|
+
// Handle other Apple docs
|
|
685
|
+
const matches = identifier.match(/\/documentation\/(.+)/)
|
|
686
|
+
if (matches) {
|
|
687
|
+
return rewriteDocumentationPath(`/documentation/${matches[1]}`, externalOrigin)
|
|
688
|
+
}
|
|
689
|
+
} else if (identifier.startsWith("doc://")) {
|
|
690
|
+
const matches = identifier.match(/\/documentation\/(.+)/)
|
|
691
|
+
if (matches) {
|
|
692
|
+
return rewriteDocumentationPath(`/documentation/${matches[1]}`, externalOrigin)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return identifier
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function rewriteDocumentationPath(path: string | undefined, externalOrigin?: string): string {
|
|
699
|
+
if (!path) {
|
|
700
|
+
return ""
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (!externalOrigin || !path.startsWith("/documentation/")) {
|
|
704
|
+
return path
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return `/external/${externalOrigin}${path}`
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Extract title from identifier
|
|
712
|
+
*/
|
|
713
|
+
export function extractTitleFromIdentifier(identifier: string): string {
|
|
714
|
+
const parts = identifier.split("/")
|
|
715
|
+
const lastPart = parts[parts.length - 1]
|
|
716
|
+
|
|
717
|
+
// Handle disambiguation suffixes (e.g., "body-8kl5o" -> "body", "init(exactly:)-63925" -> "init(exactly:)")
|
|
718
|
+
const disambiguationMatch = lastPart.match(/^(.+?)(?:-\w+)?$/)
|
|
719
|
+
if (disambiguationMatch) {
|
|
720
|
+
const baseName = disambiguationMatch[1]
|
|
721
|
+
|
|
722
|
+
// If it looks like a method signature (contains parentheses), preserve it
|
|
723
|
+
if (baseName.includes("(") && baseName.includes(")")) {
|
|
724
|
+
return baseName
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// For simple identifiers, convert camelCase to readable format
|
|
728
|
+
return baseName
|
|
729
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
730
|
+
.replace(/\s+/g, " ")
|
|
731
|
+
.trim()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Fallback: convert camelCase to readable format
|
|
735
|
+
return lastPart
|
|
736
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
737
|
+
.replace(/\s+/g, " ")
|
|
738
|
+
.trim()
|
|
739
|
+
}
|