@nshipster/sosumi 1.0.6 → 1.0.7
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/README.md +44 -1
- package/package.json +4 -2
- package/public/index.html +3 -1
- package/public/llms.txt +1 -1
- package/public/robots.txt +1 -0
- package/public/webmcp.js +71 -0
- package/src/index.ts +112 -1
- package/src/lib/a2a.ts +90 -0
- package/src/lib/external/fetch.ts +3 -0
- package/src/lib/hig/render.ts +94 -73
- package/src/lib/markdown.ts +115 -0
- package/src/lib/mcp.ts +150 -115
- package/src/lib/reference/render.ts +112 -156
- package/src/lib/search.ts +115 -55
- package/src/lib/types.ts +12 -0
- package/src/lib/url.ts +35 -0
- package/src/lib/webbotauth.ts +261 -0
- package/src/lib/webmcp.ts +38 -0
- package/wrangler.jsonc +1 -1
package/src/lib/hig/render.ts
CHANGED
|
@@ -2,8 +2,19 @@
|
|
|
2
2
|
* Human Interface Guidelines (HIG) rendering functionality
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
CONTENT_TOO_DEEP,
|
|
7
|
+
formatCallout,
|
|
8
|
+
formatCodeBlock,
|
|
9
|
+
formatList,
|
|
10
|
+
formatTable,
|
|
11
|
+
INLINE_CONTENT_TOO_DEEP,
|
|
12
|
+
MAX_CONTENT_DEPTH,
|
|
13
|
+
MAX_INLINE_DEPTH,
|
|
14
|
+
mapAsideStyleToCallout,
|
|
15
|
+
} from "../markdown"
|
|
6
16
|
import type { ContentItem, TextFragment } from "../types"
|
|
17
|
+
import { extractTitleFromIdentifier } from "../url"
|
|
7
18
|
import type {
|
|
8
19
|
HIGExternalReference,
|
|
9
20
|
HIGImageReference,
|
|
@@ -184,7 +195,14 @@ function generateHIGBreadcrumbs(sourceUrl: string): string {
|
|
|
184
195
|
function renderHIGContent(
|
|
185
196
|
content: ContentItem[],
|
|
186
197
|
references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
|
|
198
|
+
depth: number = 0,
|
|
187
199
|
): string {
|
|
200
|
+
// Prevent infinite recursion by limiting depth
|
|
201
|
+
if (depth > MAX_CONTENT_DEPTH) {
|
|
202
|
+
console.warn("Maximum recursion depth reached in renderHIGContent")
|
|
203
|
+
return CONTENT_TOO_DEEP
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
let markdown = ""
|
|
189
207
|
|
|
190
208
|
for (const item of content) {
|
|
@@ -212,7 +230,7 @@ function renderHIGContent(
|
|
|
212
230
|
markdown += "\n"
|
|
213
231
|
} else {
|
|
214
232
|
// Handle other content types using the existing content renderer
|
|
215
|
-
markdown += renderContentItem(item, references)
|
|
233
|
+
markdown += renderContentItem(item, references, depth)
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
|
|
@@ -225,6 +243,7 @@ function renderHIGContent(
|
|
|
225
243
|
function renderContentItem(
|
|
226
244
|
item: ContentItem,
|
|
227
245
|
references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
|
|
246
|
+
depth: number = 0,
|
|
228
247
|
): string {
|
|
229
248
|
let markdown = ""
|
|
230
249
|
|
|
@@ -234,36 +253,26 @@ function renderContentItem(
|
|
|
234
253
|
markdown += `${hashes} ${item.text}\n\n`
|
|
235
254
|
} else if (item.type === "paragraph") {
|
|
236
255
|
if (item.inlineContent) {
|
|
256
|
+
// Inline nesting is counted separately from block nesting, so a
|
|
257
|
+
// paragraph's inline content starts a fresh depth budget.
|
|
237
258
|
const text = renderHIGInlineContent(item.inlineContent, references)
|
|
238
259
|
markdown += `${text}\n\n`
|
|
239
260
|
}
|
|
240
261
|
} else if (item.type === "codeListing") {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const syntax = item.syntax || "swift"
|
|
248
|
-
markdown += `\`\`\`${syntax}\n${code}\n\`\`\`\n\n`
|
|
249
|
-
} else if (item.type === "unorderedList" && item.items) {
|
|
250
|
-
for (const listItem of item.items) {
|
|
251
|
-
const itemText = renderHIGContent(listItem.content || [], references)
|
|
252
|
-
markdown += `- ${itemText.replace(/\n\n$/, "")}\n`
|
|
253
|
-
}
|
|
254
|
-
markdown += "\n"
|
|
255
|
-
} else if (item.type === "orderedList" && item.items) {
|
|
256
|
-
item.items.forEach((listItem: ContentItem, index: number) => {
|
|
257
|
-
const itemText = renderHIGContent(listItem.content || [], references)
|
|
258
|
-
markdown += `${index + 1}. ${itemText.replace(/\n\n$/, "")}\n`
|
|
259
|
-
})
|
|
260
|
-
markdown += "\n"
|
|
262
|
+
markdown += formatCodeBlock(item.code, item.syntax)
|
|
263
|
+
} else if ((item.type === "unorderedList" || item.type === "orderedList") && item.items) {
|
|
264
|
+
markdown += formatList(
|
|
265
|
+
item.items.map((listItem) => renderHIGContent(listItem.content || [], references, depth + 1)),
|
|
266
|
+
item.type === "orderedList",
|
|
267
|
+
)
|
|
261
268
|
} else if (item.type === "table") {
|
|
262
|
-
markdown += renderHIGTable(item, references)
|
|
269
|
+
markdown += renderHIGTable(item, references, depth)
|
|
263
270
|
} else if (item.type === "aside") {
|
|
264
|
-
markdown += renderHIGAside(item, references)
|
|
271
|
+
markdown += renderHIGAside(item, references, depth)
|
|
272
|
+
} else if (item.type === "tabNavigator") {
|
|
273
|
+
markdown += renderHIGTabNavigator(item, references, depth)
|
|
265
274
|
} else if (item.type === "row") {
|
|
266
|
-
markdown += renderHIGRow(item, references)
|
|
275
|
+
markdown += renderHIGRow(item, references, depth)
|
|
267
276
|
} else if (item.type === "video") {
|
|
268
277
|
markdown += renderHIGVideo(item, references)
|
|
269
278
|
}
|
|
@@ -278,34 +287,16 @@ function renderContentItem(
|
|
|
278
287
|
function renderHIGTable(
|
|
279
288
|
item: ContentItem,
|
|
280
289
|
references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
|
|
290
|
+
depth: number = 0,
|
|
281
291
|
): string {
|
|
282
292
|
const table = item as ContentItem & {
|
|
283
293
|
header?: string
|
|
284
294
|
rows?: ContentItem[][][]
|
|
285
295
|
}
|
|
286
|
-
const rows = table.rows ?? []
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const renderCell = (cell: ContentItem | ContentItem[]) => {
|
|
291
|
-
const items = Array.isArray(cell) ? cell : [cell]
|
|
292
|
-
const text = renderHIGContent(items, references)
|
|
293
|
-
return escapeCell(text)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const firstRowIsHeader = table.header === "row"
|
|
297
|
-
let markdown = ""
|
|
298
|
-
|
|
299
|
-
rows.forEach((row, rowIndex) => {
|
|
300
|
-
const cells = row.map((cell) => renderCell(cell))
|
|
301
|
-
if (cells.length === 0) return
|
|
302
|
-
markdown += `| ${cells.join(" | ")} |\n`
|
|
303
|
-
if (firstRowIsHeader && rowIndex === 0) {
|
|
304
|
-
markdown += `| ${cells.map(() => "---").join(" | ")} |\n`
|
|
305
|
-
}
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
return markdown ? `${markdown}\n` : ""
|
|
296
|
+
const rows = (table.rows ?? []).map((row) =>
|
|
297
|
+
row.map((cell) => renderHIGContent(Array.isArray(cell) ? cell : [cell], references, depth + 1)),
|
|
298
|
+
)
|
|
299
|
+
return formatTable(rows, table.header === "row")
|
|
309
300
|
}
|
|
310
301
|
|
|
311
302
|
/**
|
|
@@ -314,14 +305,49 @@ function renderHIGTable(
|
|
|
314
305
|
function renderHIGAside(
|
|
315
306
|
item: ContentItem,
|
|
316
307
|
references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
|
|
308
|
+
depth: number = 0,
|
|
317
309
|
): string {
|
|
318
310
|
const aside = item as ContentItem & { style?: string; name?: string }
|
|
319
311
|
const rawType = (aside.style || aside.name || "note").toLowerCase()
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
312
|
+
const asideContent = item.content ? renderHIGContent(item.content, references, depth + 1) : ""
|
|
313
|
+
return formatCallout(mapAsideStyleToCallout(rawType), asideContent)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Render a HIG tabNavigator by flattening its tabs into a sequence.
|
|
318
|
+
* Only one tab is visible at a time on the web,
|
|
319
|
+
* so rendering them all keeps the content of every tab
|
|
320
|
+
* (such as the per-Dynamic Type size tables on the Typography page).
|
|
321
|
+
*/
|
|
322
|
+
function renderHIGTabNavigator(
|
|
323
|
+
item: ContentItem,
|
|
324
|
+
references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
|
|
325
|
+
depth: number = 0,
|
|
326
|
+
): string {
|
|
327
|
+
const tabs = item.tabs ?? []
|
|
328
|
+
if (tabs.length === 0) return ""
|
|
329
|
+
|
|
330
|
+
let markdown = ""
|
|
331
|
+
for (const tab of tabs) {
|
|
332
|
+
// Tabs usually repeat their title as a heading in their own content,
|
|
333
|
+
// so only add a label when the title would otherwise be lost.
|
|
334
|
+
// A leading heading only stands in for the label when it repeats the title,
|
|
335
|
+
// possibly with a qualifier, as in "Small" and "Small (default 38mm)".
|
|
336
|
+
const label = tab.title?.trim()
|
|
337
|
+
const first = tab.content?.[0]
|
|
338
|
+
const leadingHeading = first?.type === "heading" ? (first.text ?? "").trim() : undefined
|
|
339
|
+
const headingRepeatsTitle =
|
|
340
|
+
label !== undefined &&
|
|
341
|
+
leadingHeading !== undefined &&
|
|
342
|
+
leadingHeading.toLowerCase().startsWith(label.toLowerCase())
|
|
343
|
+
if (label && !headingRepeatsTitle) {
|
|
344
|
+
markdown += `**${label}**\n\n`
|
|
345
|
+
}
|
|
346
|
+
if (tab.content?.length) {
|
|
347
|
+
markdown += renderHIGContent(tab.content, references, depth + 1)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return markdown
|
|
325
351
|
}
|
|
326
352
|
|
|
327
353
|
/**
|
|
@@ -330,6 +356,7 @@ function renderHIGAside(
|
|
|
330
356
|
function renderHIGRow(
|
|
331
357
|
item: ContentItem,
|
|
332
358
|
references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
|
|
359
|
+
depth: number = 0,
|
|
333
360
|
): string {
|
|
334
361
|
const row = item as ContentItem & {
|
|
335
362
|
columns?: Array<{
|
|
@@ -341,7 +368,7 @@ function renderHIGRow(
|
|
|
341
368
|
let markdown = ""
|
|
342
369
|
for (const column of row.columns) {
|
|
343
370
|
if (column.content && column.content.length > 0) {
|
|
344
|
-
markdown += renderHIGContent(column.content, references)
|
|
371
|
+
markdown += renderHIGContent(column.content, references, depth + 1)
|
|
345
372
|
}
|
|
346
373
|
}
|
|
347
374
|
return markdown
|
|
@@ -389,7 +416,14 @@ function renderHIGVideo(
|
|
|
389
416
|
function renderHIGInlineContent(
|
|
390
417
|
inlineContent: ContentItem[],
|
|
391
418
|
references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
|
|
419
|
+
depth: number = 0,
|
|
392
420
|
): string {
|
|
421
|
+
// Prevent infinite recursion by limiting depth
|
|
422
|
+
if (depth > MAX_INLINE_DEPTH) {
|
|
423
|
+
console.warn("Maximum recursion depth reached in renderHIGInlineContent")
|
|
424
|
+
return INLINE_CONTENT_TOO_DEEP
|
|
425
|
+
}
|
|
426
|
+
|
|
393
427
|
return inlineContent
|
|
394
428
|
.map((item) => {
|
|
395
429
|
if (item.type === "text") {
|
|
@@ -411,11 +445,15 @@ function renderHIGInlineContent(
|
|
|
411
445
|
return `[${title}](${url})`
|
|
412
446
|
} else if (item.type === "emphasis") {
|
|
413
447
|
return `*${
|
|
414
|
-
item.inlineContent
|
|
448
|
+
item.inlineContent
|
|
449
|
+
? renderHIGInlineContent(item.inlineContent, references, depth + 1)
|
|
450
|
+
: ""
|
|
415
451
|
}*`
|
|
416
452
|
} else if (item.type === "strong") {
|
|
417
453
|
return `**${
|
|
418
|
-
item.inlineContent
|
|
454
|
+
item.inlineContent
|
|
455
|
+
? renderHIGInlineContent(item.inlineContent, references, depth + 1)
|
|
456
|
+
: ""
|
|
419
457
|
}**`
|
|
420
458
|
} else if (item.type === "image" && item.identifier) {
|
|
421
459
|
const reference = references[item.identifier]
|
|
@@ -471,23 +509,6 @@ function renderHIGTopicSections(
|
|
|
471
509
|
return markdown
|
|
472
510
|
}
|
|
473
511
|
|
|
474
|
-
function mapHIGAsideStyleToCallout(style: string): string {
|
|
475
|
-
switch (style.toLowerCase()) {
|
|
476
|
-
case "warning":
|
|
477
|
-
return "WARNING"
|
|
478
|
-
case "important":
|
|
479
|
-
return "IMPORTANT"
|
|
480
|
-
case "caution":
|
|
481
|
-
return "CAUTION"
|
|
482
|
-
case "tip":
|
|
483
|
-
return "TIP"
|
|
484
|
-
case "deprecated":
|
|
485
|
-
return "WARNING"
|
|
486
|
-
default:
|
|
487
|
-
return "NOTE"
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
512
|
/**
|
|
492
513
|
* Render HIG table of contents items
|
|
493
514
|
*/
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown formatting and nesting limits shared by the reference and HIG
|
|
3
|
+
* renderers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Depth limit guarding against cyclic or pathologically nested block content. */
|
|
7
|
+
export const MAX_CONTENT_DEPTH = 50
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Depth limit guarding against cyclic or pathologically nested inline content.
|
|
11
|
+
* Counted independently of block nesting, since inline content recurses on its
|
|
12
|
+
* own stack.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_INLINE_DEPTH = 20
|
|
15
|
+
|
|
16
|
+
/** Emitted in place of block content that exceeds `MAX_CONTENT_DEPTH`. */
|
|
17
|
+
export const CONTENT_TOO_DEEP = "[Content too deeply nested]"
|
|
18
|
+
|
|
19
|
+
/** Emitted in place of inline content that exceeds `MAX_INLINE_DEPTH`. */
|
|
20
|
+
export const INLINE_CONTENT_TOO_DEEP = "[Inline content too deeply nested]"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format rendered content as a Markdown list item, indenting continuation
|
|
24
|
+
* lines (including nested lists) under the marker.
|
|
25
|
+
*/
|
|
26
|
+
function formatListItem(marker: string, content: string): string {
|
|
27
|
+
const trimmed = content.replace(/\n+$/, "")
|
|
28
|
+
if (!trimmed) {
|
|
29
|
+
return `${marker.trimEnd()}\n`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Collapse blank lines immediately before a nested list so it stays tight
|
|
33
|
+
// under its parent item; keep other paragraph breaks.
|
|
34
|
+
const tightened = trimmed.replace(/\n{2,}(?=(?:[-*]|\d+\.) )/g, "\n")
|
|
35
|
+
const indent = " ".repeat(marker.length)
|
|
36
|
+
const [first, ...rest] = tightened.split("\n")
|
|
37
|
+
const continuation = rest.map((line) => (line ? indent + line : ""))
|
|
38
|
+
return `${marker}${[first, ...continuation].join("\n")}\n`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format pre-rendered item contents as a Markdown list, terminated by a blank
|
|
43
|
+
* line so following blocks aren't absorbed into the last item.
|
|
44
|
+
*/
|
|
45
|
+
export function formatList(itemContents: string[], ordered: boolean): string {
|
|
46
|
+
const items = itemContents.map((content, index) =>
|
|
47
|
+
formatListItem(ordered ? `${index + 1}. ` : "- ", content),
|
|
48
|
+
)
|
|
49
|
+
return `${items.join("")}\n`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Format a DocC code listing as a fenced code block, defaulting to Swift.
|
|
54
|
+
*/
|
|
55
|
+
export function formatCodeBlock(code?: string | string[], syntax?: string): string {
|
|
56
|
+
const body = Array.isArray(code) ? code.join("\n") : String(code || "")
|
|
57
|
+
// DocC uses `occ` for Objective-C internally, but highlighters expect `objc`.
|
|
58
|
+
const language = syntax === "occ" ? "objc" : syntax || "swift"
|
|
59
|
+
return `\`\`\`${language}\n${body}\n\`\`\`\n\n`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format rendered cell contents as a Markdown table, escaping pipes and
|
|
64
|
+
* flattening newlines that would otherwise break a row.
|
|
65
|
+
*/
|
|
66
|
+
export function formatTable(rows: string[][], hasHeaderRow: boolean): string {
|
|
67
|
+
let markdown = ""
|
|
68
|
+
rows.forEach((row, index) => {
|
|
69
|
+
if (row.length === 0) return
|
|
70
|
+
const cells = row.map((cell) => cell.replace(/\|/g, "\\|").replace(/\n/g, " ").trim())
|
|
71
|
+
markdown += `| ${cells.join(" | ")} |\n`
|
|
72
|
+
if (hasHeaderRow && index === 0) {
|
|
73
|
+
markdown += `| ${cells.map(() => "---").join(" | ")} |\n`
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
return markdown ? `${markdown}\n` : ""
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format rendered content as a GitHub-style callout. An optional lead becomes
|
|
81
|
+
* the callout's first paragraph, ahead of the content.
|
|
82
|
+
*/
|
|
83
|
+
export function formatCallout(type: string, content: string, lead?: string): string {
|
|
84
|
+
const paragraphs = [lead, content.trim()].filter(Boolean)
|
|
85
|
+
if (paragraphs.length === 0) {
|
|
86
|
+
return ""
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const quoted = paragraphs
|
|
90
|
+
.join("\n\n")
|
|
91
|
+
.split("\n")
|
|
92
|
+
.map((line) => (line ? `> ${line}` : ">"))
|
|
93
|
+
.join("\n")
|
|
94
|
+
return `> [!${type}]\n${quoted}\n\n`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Map a DocC aside style to a GitHub-style callout type.
|
|
99
|
+
*/
|
|
100
|
+
export function mapAsideStyleToCallout(style: string): string {
|
|
101
|
+
switch (style.toLowerCase()) {
|
|
102
|
+
case "warning":
|
|
103
|
+
return "WARNING"
|
|
104
|
+
case "important":
|
|
105
|
+
return "IMPORTANT"
|
|
106
|
+
case "caution":
|
|
107
|
+
return "CAUTION"
|
|
108
|
+
case "tip":
|
|
109
|
+
return "TIP"
|
|
110
|
+
case "deprecated":
|
|
111
|
+
return "WARNING"
|
|
112
|
+
default:
|
|
113
|
+
return "NOTE"
|
|
114
|
+
}
|
|
115
|
+
}
|