@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.
@@ -0,0 +1,514 @@
1
+ /**
2
+ * Human Interface Guidelines (HIG) rendering functionality
3
+ */
4
+
5
+ import { extractTitleFromIdentifier } from "../reference/render"
6
+ import type { ContentItem, TextFragment } from "../types"
7
+ import type {
8
+ HIGExternalReference,
9
+ HIGImageReference,
10
+ HIGPageJSON,
11
+ HIGReference,
12
+ HIGTableOfContents,
13
+ HIGTocItem,
14
+ HIGTopicSection,
15
+ } from "./types"
16
+ import { isHIGImageReference } from "./util"
17
+
18
+ // ============================================================================
19
+ // RENDERING FUNCTIONS
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Render HIG page JSON to markdown
24
+ */
25
+ export async function renderHIGFromJSON(jsonData: HIGPageJSON, sourceUrl: string): Promise<string> {
26
+ let markdown = ""
27
+
28
+ // Generate front matter
29
+ markdown += generateHIGFrontMatter(jsonData, sourceUrl)
30
+
31
+ // Add navigation breadcrumbs for HIG
32
+ const breadcrumbs = generateHIGBreadcrumbs(sourceUrl)
33
+ if (breadcrumbs) {
34
+ markdown += breadcrumbs
35
+ }
36
+
37
+ // Add role heading if available
38
+ if (jsonData.metadata?.role) {
39
+ const roleDisplay =
40
+ jsonData.metadata.role === "collectionGroup" ? "Guide Collection" : jsonData.metadata.role
41
+ markdown += `**${roleDisplay}**\n\n`
42
+ }
43
+
44
+ // Add title
45
+ const title = jsonData.metadata?.title || ""
46
+ if (title) {
47
+ markdown += `# ${title}\n\n`
48
+ }
49
+
50
+ // Add abstract
51
+ if (jsonData.abstract && Array.isArray(jsonData.abstract)) {
52
+ const abstractText = jsonData.abstract
53
+ .filter((item: TextFragment) => item.type === "text")
54
+ .map((item: TextFragment) => item.text)
55
+ .join("")
56
+
57
+ if (abstractText.trim()) {
58
+ markdown += `> ${abstractText}\n\n`
59
+ }
60
+ }
61
+
62
+ // Add primary content sections
63
+ if (jsonData.primaryContentSections) {
64
+ for (const section of jsonData.primaryContentSections) {
65
+ if (section.kind === "content" && section.content) {
66
+ markdown += renderHIGContent(section.content, jsonData.references)
67
+ }
68
+ }
69
+ }
70
+
71
+ // Add regular content sections
72
+ if (jsonData.sections && jsonData.sections.length > 0) {
73
+ markdown += renderHIGContent(jsonData.sections, jsonData.references)
74
+ }
75
+
76
+ // Add topic sections (unless hidden)
77
+ if (jsonData.topicSections && jsonData.topicSectionsStyle !== "hidden") {
78
+ markdown += renderHIGTopicSections(jsonData.topicSections, jsonData.references)
79
+ }
80
+
81
+ // Trim whitespace
82
+ markdown = markdown.trim()
83
+
84
+ // Add footer
85
+ markdown += `\n\n---\n\n`
86
+ markdown += `*Extracted by [sosumi.ai](https://sosumi.ai) - Making Apple docs AI-readable.*\n`
87
+ markdown += `*This is unofficial content. All Human Interface Guidelines belong to Apple Inc.*\n`
88
+
89
+ return markdown
90
+ }
91
+
92
+ /**
93
+ * Render HIG table of contents to markdown
94
+ */
95
+ export async function renderHIGTableOfContents(tocData: HIGTableOfContents): Promise<string> {
96
+ let markdown = ""
97
+
98
+ // Generate front matter
99
+ markdown += `---\n`
100
+ markdown += `title: Human Interface Guidelines\n`
101
+ markdown += `description: Apple's Human Interface Guidelines - Complete table of contents\n`
102
+ markdown += `source: https://developer.apple.com/design/human-interface-guidelines/\n`
103
+ markdown += `timestamp: ${new Date().toISOString()}\n`
104
+ markdown += `---\n\n`
105
+
106
+ // Add title and introduction
107
+ markdown += `# Human Interface Guidelines\n\n`
108
+ markdown += `> Apple's comprehensive guide to designing interfaces for all Apple platforms.\n\n`
109
+
110
+ // Render the table of contents
111
+ if (tocData.interfaceLanguages?.swift) {
112
+ markdown += renderHIGTocItems(tocData.interfaceLanguages.swift, 2)
113
+ }
114
+
115
+ // Add footer
116
+ markdown += `\n\n---\n\n`
117
+ markdown += `*Extracted by [sosumi.ai](https://sosumi.ai) - Making Apple docs AI-readable.*\n`
118
+ markdown += `*This is unofficial content. All Human Interface Guidelines belong to Apple Inc.*\n`
119
+
120
+ return markdown
121
+ }
122
+
123
+ // ============================================================================
124
+ // PRIVATE HELPER FUNCTIONS
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Generate front matter for HIG pages
129
+ */
130
+ function generateHIGFrontMatter(jsonData: HIGPageJSON, sourceUrl: string): string {
131
+ const frontMatter: Record<string, string> = {}
132
+
133
+ if (jsonData.metadata?.title) {
134
+ frontMatter.title = jsonData.metadata.title
135
+ }
136
+
137
+ if (jsonData.abstract && Array.isArray(jsonData.abstract)) {
138
+ const description = jsonData.abstract
139
+ .filter((item: TextFragment) => item.type === "text")
140
+ .map((item: TextFragment) => item.text)
141
+ .join("")
142
+ .trim()
143
+ if (description) {
144
+ frontMatter.description = description
145
+ }
146
+ }
147
+
148
+ frontMatter.source = sourceUrl
149
+ frontMatter.timestamp = new Date().toISOString()
150
+
151
+ // Convert to YAML format
152
+ const yamlLines = Object.entries(frontMatter).map(([key, value]) => `${key}: ${value}`)
153
+ return `---\n${yamlLines.join("\n")}\n---\n\n`
154
+ }
155
+
156
+ /**
157
+ * Generate breadcrumb navigation for HIG
158
+ */
159
+ function generateHIGBreadcrumbs(sourceUrl: string): string {
160
+ const url = new URL(sourceUrl)
161
+ const pathParts = url.pathname.split("/").filter(Boolean)
162
+ // pathParts will be: ["design", "human-interface-guidelines", "foundations", "color"] for foundations/color
163
+
164
+ if (pathParts.length < 3) return "" // Need at least /design/human-interface-guidelines
165
+
166
+ let breadcrumbs = `**Navigation:** [Human Interface Guidelines](/design/human-interface-guidelines)`
167
+
168
+ // Add breadcrumbs for all parts after /design/human-interface-guidelines
169
+ // This includes both intermediate and final parts
170
+ for (let i = 3; i < pathParts.length; i++) {
171
+ const part = pathParts[i]
172
+ // Build path up to this point
173
+ const path = `/${pathParts.slice(0, i + 1).join("/")}`
174
+ const formattedPart = part.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())
175
+ breadcrumbs += ` › [${formattedPart}](${path})`
176
+ }
177
+
178
+ return `${breadcrumbs}\n\n`
179
+ }
180
+
181
+ /**
182
+ * Render HIG content items
183
+ */
184
+ function renderHIGContent(
185
+ content: ContentItem[],
186
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
187
+ ): string {
188
+ let markdown = ""
189
+
190
+ for (const item of content) {
191
+ if (item.type === "links" && item.items && item.style === "compactGrid") {
192
+ // Handle the special case of link grids (like on the getting started page)
193
+ for (const linkId of item.items) {
194
+ if (typeof linkId === "string") {
195
+ const reference = references[linkId]
196
+ if (reference && !isHIGImageReference(reference)) {
197
+ const title = reference.title || "Untitled"
198
+ const url = reference.url || "#"
199
+ const refAbstract = (reference as HIGReference).abstract
200
+ const abstract = Array.isArray(refAbstract)
201
+ ? refAbstract.map((a: TextFragment) => a.text).join("")
202
+ : ""
203
+
204
+ markdown += `- [${title}](${url})`
205
+ if (abstract) {
206
+ markdown += ` - ${abstract}`
207
+ }
208
+ markdown += "\n"
209
+ }
210
+ }
211
+ }
212
+ markdown += "\n"
213
+ } else {
214
+ // Handle other content types using the existing content renderer
215
+ markdown += renderContentItem(item, references)
216
+ }
217
+ }
218
+
219
+ return markdown
220
+ }
221
+
222
+ /**
223
+ * Render individual content item
224
+ */
225
+ function renderContentItem(
226
+ item: ContentItem,
227
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
228
+ ): string {
229
+ let markdown = ""
230
+
231
+ if (item.type === "heading") {
232
+ const level = Math.min(item.level || 2, 6)
233
+ const hashes = "#".repeat(level)
234
+ markdown += `${hashes} ${item.text}\n\n`
235
+ } else if (item.type === "paragraph") {
236
+ if (item.inlineContent) {
237
+ const text = renderHIGInlineContent(item.inlineContent, references)
238
+ markdown += `${text}\n\n`
239
+ }
240
+ } else if (item.type === "codeListing") {
241
+ let code = ""
242
+ if (Array.isArray(item.code)) {
243
+ code = item.code.join("\n")
244
+ } else {
245
+ code = String(item.code || "")
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"
261
+ } else if (item.type === "table") {
262
+ markdown += renderHIGTable(item, references)
263
+ } else if (item.type === "aside") {
264
+ markdown += renderHIGAside(item, references)
265
+ } else if (item.type === "row") {
266
+ markdown += renderHIGRow(item, references)
267
+ } else if (item.type === "video") {
268
+ markdown += renderHIGVideo(item, references)
269
+ }
270
+
271
+ return markdown
272
+ }
273
+
274
+ /**
275
+ * Render a HIG table to markdown.
276
+ * HIG tables use header: "row" and rows[rowIndex][cellIndex] = ContentItem[].
277
+ */
278
+ function renderHIGTable(
279
+ item: ContentItem,
280
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
281
+ ): string {
282
+ const table = item as ContentItem & {
283
+ header?: string
284
+ rows?: ContentItem[][][]
285
+ }
286
+ const rows = table.rows ?? []
287
+ if (rows.length === 0) return ""
288
+
289
+ const escapeCell = (s: string) => s.replace(/\|/g, "\\|").replace(/\n/g, " ").trim()
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` : ""
309
+ }
310
+
311
+ /**
312
+ * Render a HIG aside/callout block to markdown.
313
+ */
314
+ function renderHIGAside(
315
+ item: ContentItem,
316
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
317
+ ): string {
318
+ const aside = item as ContentItem & { style?: string; name?: string }
319
+ const rawType = (aside.style || aside.name || "note").toLowerCase()
320
+ const calloutType = mapHIGAsideStyleToCallout(rawType)
321
+ const asideContent = item.content ? renderHIGContent(item.content, references) : ""
322
+ const cleanContent = asideContent.trim().replace(/\n/g, "\n> ")
323
+ if (!cleanContent) return ""
324
+ return `> [!${calloutType}]\n> ${cleanContent}\n\n`
325
+ }
326
+
327
+ /**
328
+ * Render a HIG row block by rendering each column content in order.
329
+ */
330
+ function renderHIGRow(
331
+ item: ContentItem,
332
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
333
+ ): string {
334
+ const row = item as ContentItem & {
335
+ columns?: Array<{
336
+ content?: ContentItem[]
337
+ }>
338
+ }
339
+ if (!row.columns || row.columns.length === 0) return ""
340
+
341
+ let markdown = ""
342
+ for (const column of row.columns) {
343
+ if (column.content && column.content.length > 0) {
344
+ markdown += renderHIGContent(column.content, references)
345
+ }
346
+ }
347
+ return markdown
348
+ }
349
+
350
+ /**
351
+ * Render a HIG video block as a markdown link.
352
+ */
353
+ function renderHIGVideo(
354
+ item: ContentItem,
355
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
356
+ ): string {
357
+ const video = item as ContentItem & {
358
+ identifier?: string
359
+ metadata?: {
360
+ abstract?: TextFragment[]
361
+ }
362
+ }
363
+ if (!video.identifier) return ""
364
+
365
+ const reference = references[video.identifier] as
366
+ | {
367
+ type?: string
368
+ alt?: string
369
+ variants?: Array<{ url?: string }>
370
+ }
371
+ | undefined
372
+
373
+ const videoUrl = reference?.variants?.[0]?.url
374
+ if (!videoUrl) return ""
375
+
376
+ const abstractText = (video.metadata?.abstract ?? [])
377
+ .filter((fragment) => fragment.type === "text")
378
+ .map((fragment) => fragment.text)
379
+ .join("")
380
+ .trim()
381
+ const label = abstractText || reference?.alt || "Video"
382
+
383
+ return `[${label}](${videoUrl})\n\n`
384
+ }
385
+
386
+ /**
387
+ * Render HIG inline content
388
+ */
389
+ function renderHIGInlineContent(
390
+ inlineContent: ContentItem[],
391
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
392
+ ): string {
393
+ return inlineContent
394
+ .map((item) => {
395
+ if (item.type === "text") {
396
+ return item.text
397
+ } else if (item.type === "codeVoice") {
398
+ return `\`${item.code}\``
399
+ } else if (item.type === "reference") {
400
+ const reference = item.identifier ? references[item.identifier] : undefined
401
+ const refTitle =
402
+ reference && !isHIGImageReference(reference)
403
+ ? (reference as HIGReference | HIGExternalReference).title
404
+ : undefined
405
+ const title =
406
+ item.title ||
407
+ item.text ||
408
+ refTitle ||
409
+ (item.identifier ? extractTitleFromIdentifier(item.identifier) : "")
410
+ const url = reference ? (isHIGImageReference(reference) ? "#" : reference.url) : "#"
411
+ return `[${title}](${url})`
412
+ } else if (item.type === "emphasis") {
413
+ return `*${
414
+ item.inlineContent ? renderHIGInlineContent(item.inlineContent, references) : ""
415
+ }*`
416
+ } else if (item.type === "strong") {
417
+ return `**${
418
+ item.inlineContent ? renderHIGInlineContent(item.inlineContent, references) : ""
419
+ }**`
420
+ } else if (item.type === "image" && item.identifier) {
421
+ const reference = references[item.identifier]
422
+ if (reference && isHIGImageReference(reference)) {
423
+ const imageUrl = reference.variants?.[0]?.url
424
+ if (imageUrl) {
425
+ return `![${reference.alt ?? ""}](${imageUrl})`
426
+ }
427
+ }
428
+ return ""
429
+ }
430
+ return item.text || ""
431
+ })
432
+ .join("")
433
+ }
434
+
435
+ /**
436
+ * Render HIG topic sections
437
+ */
438
+ function renderHIGTopicSections(
439
+ topicSections: HIGTopicSection[],
440
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>,
441
+ ): string {
442
+ let markdown = ""
443
+
444
+ for (const section of topicSections) {
445
+ if (section.title) {
446
+ markdown += `## ${section.title}\n\n`
447
+ }
448
+
449
+ if (section.identifiers) {
450
+ for (const id of section.identifiers) {
451
+ const reference = references[id]
452
+ if (reference && !isHIGImageReference(reference)) {
453
+ const title = reference.title || "Untitled"
454
+ const url = reference.url || "#"
455
+ const refAbstract = (reference as HIGReference).abstract
456
+ const abstract = Array.isArray(refAbstract)
457
+ ? refAbstract.map((a: TextFragment) => a.text).join("")
458
+ : ""
459
+
460
+ markdown += `- [${title}](${url})`
461
+ if (abstract) {
462
+ markdown += ` - ${abstract}`
463
+ }
464
+ markdown += "\n"
465
+ }
466
+ }
467
+ markdown += "\n"
468
+ }
469
+ }
470
+
471
+ return markdown
472
+ }
473
+
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
+ /**
492
+ * Render HIG table of contents items
493
+ */
494
+ function renderHIGTocItems(items: HIGTocItem[], headingLevel: number): string {
495
+ let markdown = ""
496
+
497
+ for (const item of items) {
498
+ if (item.type === "module" || item.type === "symbol") {
499
+ // Main sections get headings
500
+ const hashes = "#".repeat(Math.min(headingLevel, 6))
501
+ markdown += `${hashes} ${item.title}\n\n`
502
+
503
+ if (item.children) {
504
+ markdown += renderHIGTocItems(item.children, headingLevel + 1)
505
+ }
506
+ } else if (item.type === "article") {
507
+ // Articles get listed as links
508
+ const url = item.path
509
+ markdown += `- [${item.title}](${url})\n`
510
+ }
511
+ }
512
+
513
+ return markdown
514
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Human Interface Guidelines (HIG) specific types
3
+ */
4
+
5
+ import type { ContentItem, PrimaryContentSection, TextFragment } from "../types"
6
+
7
+ // ============================================================================
8
+ // HIG TYPES
9
+ // ============================================================================
10
+
11
+ /**
12
+ * Represents an icon reference in the HIG ToC
13
+ */
14
+ export interface HIGIconReference {
15
+ alt: string
16
+ identifier: string
17
+ type: "image"
18
+ variants: Array<{
19
+ traits: string[]
20
+ url: string
21
+ }>
22
+ }
23
+
24
+ /**
25
+ * Represents an item in the HIG table of contents
26
+ */
27
+ export interface HIGTocItem {
28
+ children?: HIGTocItem[]
29
+ icon?: string
30
+ path: string
31
+ title: string
32
+ type: "module" | "symbol" | "article"
33
+ }
34
+
35
+ /**
36
+ * Represents the complete HIG table of contents structure
37
+ */
38
+ export interface HIGTableOfContents {
39
+ includedArchiveIdentifiers: string[]
40
+ interfaceLanguages: {
41
+ swift: HIGTocItem[]
42
+ }
43
+ references: Record<string, HIGIconReference>
44
+ schemaVersion: {
45
+ major: number
46
+ minor: number
47
+ patch: number
48
+ }
49
+ }
50
+
51
+ /**
52
+ * HIG-specific image metadata with card/icon types
53
+ */
54
+ export interface HIGImage {
55
+ identifier: string
56
+ type: "icon" | "card" | "image"
57
+ }
58
+
59
+ /**
60
+ * HIG page metadata with locale and image support
61
+ */
62
+ export interface HIGMetadata {
63
+ role: string
64
+ title: string
65
+ images?: HIGImage[]
66
+ availableLocales?: string[]
67
+ }
68
+
69
+ /**
70
+ * HIG-specific identifier with interface language
71
+ */
72
+ export interface HIGIdentifier {
73
+ interfaceLanguage: string
74
+ url: string
75
+ }
76
+
77
+ /**
78
+ * HIG hierarchy information
79
+ */
80
+ export interface HIGHierarchy {
81
+ paths: string[][]
82
+ }
83
+
84
+ /**
85
+ * HIG topic section structure
86
+ */
87
+ export interface HIGTopicSection {
88
+ title?: string
89
+ identifiers: string[]
90
+ anchor?: string
91
+ }
92
+
93
+ /**
94
+ * HIG image variant with resolution and color mode support
95
+ */
96
+ export interface HIGImageVariant {
97
+ traits: string[]
98
+ url: string
99
+ }
100
+
101
+ /**
102
+ * HIG image reference with variants
103
+ */
104
+ export interface HIGImageReference {
105
+ alt: string | null
106
+ identifier: string
107
+ type: "icon" | "card" | "image"
108
+ variants: HIGImageVariant[]
109
+ }
110
+
111
+ /**
112
+ * HIG reference item (for linked articles and topics)
113
+ */
114
+ export interface HIGReference {
115
+ kind: string
116
+ role?: string
117
+ title: string
118
+ url: string
119
+ abstract?: TextFragment[]
120
+ identifier: string
121
+ images?: HIGImage[]
122
+ type: "topic"
123
+ }
124
+
125
+ /**
126
+ * External/non-topic reference appearing in HIG references map
127
+ */
128
+ export interface HIGExternalReference {
129
+ title: string
130
+ identifier: string
131
+ titleInlineContent?: TextFragment[]
132
+ url: string
133
+ type: string // e.g. "link"
134
+ }
135
+
136
+ /**
137
+ * HIG legal notices
138
+ */
139
+ export interface HIGLegalNotices {
140
+ copyright: string
141
+ termsOfUse: string
142
+ privacy?: string
143
+ privacyPolicy?: string
144
+ }
145
+
146
+ /**
147
+ * The main HIG page JSON structure
148
+ */
149
+ export interface HIGPageJSON {
150
+ // Metadata and identification
151
+ metadata: HIGMetadata
152
+ kind: "article"
153
+ identifier: HIGIdentifier
154
+ hierarchy: HIGHierarchy
155
+
156
+ // Content sections
157
+ sections: ContentItem[]
158
+ primaryContentSections: PrimaryContentSection[]
159
+ abstract: TextFragment[]
160
+
161
+ // Topic organization
162
+ topicSections?: HIGTopicSection[]
163
+ topicSectionsStyle?: "hidden" | "list" | "compactGrid"
164
+
165
+ // References to other content and images
166
+ references: Record<string, HIGReference | HIGImageReference | HIGExternalReference>
167
+
168
+ // Schema version
169
+ schemaVersion: {
170
+ major: number
171
+ minor: number
172
+ patch: number
173
+ }
174
+
175
+ // Legal information
176
+ legalNotices?: HIGLegalNotices
177
+ }
178
+
179
+ // ============================================================================
180
+ // TYPE GUARDS
181
+ // ============================================================================
182
+
183
+ /**
184
+ * Type guard to check if a reference is an image reference
185
+ */
186
+ export function isHIGImageReference(
187
+ ref: HIGReference | HIGImageReference | HIGExternalReference,
188
+ ): ref is HIGImageReference {
189
+ return "alt" in ref && "variants" in ref
190
+ }
191
+
192
+ /**
193
+ * Type guard to check if a reference is a topic reference
194
+ */
195
+ export function isHIGTopicReference(
196
+ ref: HIGReference | HIGImageReference | HIGExternalReference,
197
+ ): ref is HIGReference {
198
+ return ref.type === "topic"
199
+ }
200
+
201
+ /**
202
+ * Type guard to check if a ToC item has children
203
+ */
204
+ export function hasChildren(item: HIGTocItem): boolean {
205
+ return item.children !== undefined && item.children.length > 0
206
+ }