@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,31 @@
1
+ /**
2
+ * Apple Developer Reference documentation specific types
3
+ */
4
+
5
+ // Most types used by reference docs are already in the shared types.ts
6
+ // This file is for reference-specific types only, if any are needed in the future
7
+
8
+ // Re-export commonly used types from shared types for convenience
9
+ export type {
10
+ AppleDocJSON,
11
+ ContentItem,
12
+ Declaration,
13
+ DocumentationIdentifier,
14
+ DocumentationMetadata,
15
+ ImageVariant,
16
+ IndexContentItem,
17
+ isImageVariant,
18
+ isLanguageVariant,
19
+ isSymbolVariant,
20
+ LanguageVariant,
21
+ Parameter,
22
+ Platform,
23
+ PrimaryContentSection,
24
+ PropertyItem,
25
+ SeeAlsoSection,
26
+ SwiftInterfaceItem,
27
+ SymbolVariant,
28
+ TextFragment,
29
+ TopicSection,
30
+ Variant,
31
+ } from "../types"
@@ -0,0 +1,221 @@
1
+ import { getRandomUserAgent } from "./fetch"
2
+
3
+ export interface SearchResult {
4
+ title: string
5
+ url: string
6
+ description: string
7
+ breadcrumbs: string[]
8
+ tags: string[]
9
+ type: string // 'documentation' | 'general' etc.
10
+ }
11
+
12
+ export interface SearchResponse {
13
+ query: string
14
+ results: SearchResult[]
15
+ }
16
+
17
+ class SearchResultParser {
18
+ private results: SearchResult[] = []
19
+ private currentResult: Partial<SearchResult> = {}
20
+ private currentBreadcrumbs: string[] = []
21
+ private currentTags: string[] = []
22
+ private isInResultTitle = false
23
+ private isInResultDescription = false
24
+ private isInBreadcrumb = false
25
+ private isInTag = false
26
+
27
+ getResults(): SearchResult[] {
28
+ return this.results
29
+ }
30
+
31
+ private resetCurrentResult() {
32
+ this.currentResult = {}
33
+ this.currentBreadcrumbs = []
34
+ this.currentTags = []
35
+ this.isInResultTitle = false
36
+ this.isInResultDescription = false
37
+ this.isInBreadcrumb = false
38
+ this.isInTag = false
39
+ }
40
+
41
+ private finalizeCurrentResult() {
42
+ if (this.currentResult.title && this.currentResult.url) {
43
+ this.results.push({
44
+ title: this.currentResult.title,
45
+ url: this.currentResult.url,
46
+ description: this.currentResult.description || "",
47
+ breadcrumbs: [...this.currentBreadcrumbs],
48
+ tags: [...this.currentTags],
49
+ type: this.currentResult.type || "unknown",
50
+ })
51
+ }
52
+ this.resetCurrentResult()
53
+ }
54
+
55
+ element(element: Element) {
56
+ // Start of a search result
57
+ if (element.tagName === "li" && element.getAttribute("class")?.includes("search-result")) {
58
+ this.finalizeCurrentResult() // Finalize previous result if any
59
+
60
+ // Extract result type from class
61
+ const className = element.getAttribute("class") || ""
62
+ if (className.includes("documentation")) {
63
+ this.currentResult.type = "documentation"
64
+ } else if (className.includes("general")) {
65
+ this.currentResult.type = "general"
66
+ } else {
67
+ this.currentResult.type = "other"
68
+ }
69
+ }
70
+
71
+ // Result title link
72
+ if (
73
+ element.tagName === "a" &&
74
+ element.getAttribute("class")?.includes("click-analytics-result")
75
+ ) {
76
+ const href = element.getAttribute("href")
77
+ if (href) {
78
+ this.currentResult.url = href.startsWith("/") ? `https://developer.apple.com${href}` : href
79
+ }
80
+ this.isInResultTitle = true
81
+ }
82
+
83
+ // Result description
84
+ if (element.tagName === "p" && element.getAttribute("class")?.includes("result-description")) {
85
+ this.isInResultDescription = true
86
+ }
87
+
88
+ // Breadcrumb items
89
+ if (
90
+ element.tagName === "li" &&
91
+ element.getAttribute("class")?.includes("breadcrumb-list-item")
92
+ ) {
93
+ this.isInBreadcrumb = true
94
+ }
95
+
96
+ // Tag spans
97
+ if (
98
+ element.tagName === "span" &&
99
+ element.parentElement?.getAttribute("class")?.includes("result-tag")
100
+ ) {
101
+ this.isInTag = true
102
+ }
103
+
104
+ // Tag list items (for languages like "Swift", "Objective-C")
105
+ if (
106
+ element.tagName === "li" &&
107
+ element.getAttribute("class")?.includes("result-tag language")
108
+ ) {
109
+ this.isInTag = true
110
+ }
111
+ }
112
+
113
+ text(text: Text) {
114
+ const content = text.text.trim()
115
+ if (!content) return
116
+
117
+ if (this.isInResultTitle && this.currentResult.url) {
118
+ this.currentResult.title = content
119
+ this.isInResultTitle = false
120
+ } else if (this.isInResultDescription) {
121
+ this.currentResult.description = content
122
+ this.isInResultDescription = false
123
+ } else if (this.isInBreadcrumb) {
124
+ this.currentBreadcrumbs.push(content)
125
+ this.isInBreadcrumb = false
126
+ } else if (this.isInTag) {
127
+ this.currentTags.push(content)
128
+ this.isInTag = false
129
+ }
130
+ }
131
+
132
+ end() {
133
+ this.finalizeCurrentResult() // Finalize the last result
134
+ }
135
+ }
136
+
137
+ export async function searchAppleDeveloperDocs(query: string): Promise<SearchResponse> {
138
+ const searchUrl = `https://developer.apple.com/search/?q=${encodeURIComponent(query)}`
139
+ const response = await fetch(searchUrl, {
140
+ headers: {
141
+ "User-Agent": getRandomUserAgent(),
142
+ },
143
+ })
144
+
145
+ if (!response.ok) {
146
+ throw new Error(`Search request failed: ${response.status}`)
147
+ }
148
+
149
+ const html = await response.text()
150
+ let results: SearchResult[] = []
151
+ if (typeof HTMLRewriter !== "undefined") {
152
+ const parser = new SearchResultParser()
153
+ const rewriter = new HTMLRewriter()
154
+ .on("li.search-result", parser)
155
+ .on("li.search-result a.click-analytics-result", parser)
156
+ .on("li.search-result p.result-description", parser)
157
+ .on("li.search-result li.breadcrumb-list-item", parser)
158
+ .on("li.search-result li.result-tag", parser)
159
+ .on("li.search-result li.result-tag span", parser)
160
+
161
+ // We need to consume the transformed response to trigger parsing callbacks.
162
+ await rewriter.transform(new Response(html)).text()
163
+ parser.end()
164
+ results = parser.getResults()
165
+ } else {
166
+ results = await parseSearchResultsWithCheerio(html)
167
+ }
168
+
169
+ return {
170
+ query,
171
+ results,
172
+ }
173
+ }
174
+
175
+ async function parseSearchResultsWithCheerio(html: string): Promise<SearchResult[]> {
176
+ const { load } = await import("cheerio")
177
+ const $ = load(html)
178
+ const results: SearchResult[] = []
179
+
180
+ $("li.search-result").each((_, element) => {
181
+ const item = $(element)
182
+ const link = item.find("a.click-analytics-result").first()
183
+ const rawHref = link.attr("href")
184
+ const title = link.text().trim()
185
+
186
+ if (!rawHref || !title) {
187
+ return
188
+ }
189
+
190
+ const description = item.find("p.result-description").first().text().trim()
191
+ const breadcrumbs = item
192
+ .find("li.breadcrumb-list-item")
193
+ .toArray()
194
+ .map((breadcrumb) => $(breadcrumb).text().trim())
195
+ .filter(Boolean)
196
+
197
+ const tags = item
198
+ .find("li.result-tag span, li.result-tag.language")
199
+ .toArray()
200
+ .map((tag) => $(tag).text().trim())
201
+ .filter(Boolean)
202
+
203
+ const className = item.attr("class") ?? ""
204
+ const type = className.includes("documentation")
205
+ ? "documentation"
206
+ : className.includes("general")
207
+ ? "general"
208
+ : "other"
209
+
210
+ results.push({
211
+ title,
212
+ url: rawHref.startsWith("/") ? `https://developer.apple.com${rawHref}` : rawHref,
213
+ description,
214
+ breadcrumbs,
215
+ tags,
216
+ type,
217
+ })
218
+ })
219
+
220
+ return results
221
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Type definitions for Apple Developer documentation JSON API
3
+ *
4
+ * This module consolidates all types used across the codebase for processing
5
+ * Apple documentation. Types are organized by category and complexity.
6
+ */
7
+
8
+ // ============================================================================
9
+ // CORE CONTENT TYPES
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Represents a text fragment with optional styling information
14
+ */
15
+ export interface TextFragment {
16
+ text: string
17
+ type?: string
18
+ }
19
+
20
+ /**
21
+ * Represents a code fragment with syntax highlighting
22
+ */
23
+ export interface CodeFragment {
24
+ code: string | string[]
25
+ syntax?: string
26
+ }
27
+
28
+ /**
29
+ * Represents a tokenized piece of content
30
+ */
31
+ export interface Token {
32
+ text?: string
33
+ }
34
+
35
+ /**
36
+ * The main content item type used throughout the documentation structure.
37
+ * Can represent text, code, lists, headings, and other content elements.
38
+ */
39
+ export interface ContentItem {
40
+ // Basic content properties
41
+ text?: string
42
+ type?: string
43
+ title?: string
44
+ name?: string
45
+
46
+ // Tokenized content
47
+ tokens?: Token[]
48
+
49
+ // Nested content
50
+ content?: ContentItem[]
51
+ inlineContent?: ContentItem[]
52
+ items?: ContentItem[]
53
+
54
+ // Code content
55
+ code?: string | string[]
56
+ syntax?: string
57
+
58
+ // Structural properties
59
+ level?: number
60
+ style?: string
61
+ identifier?: string
62
+ identifiers?: string[]
63
+ url?: string
64
+
65
+ // Abstract content
66
+ abstract?: TextFragment[]
67
+ }
68
+
69
+ // ============================================================================
70
+ // DECLARATION & PARAMETER TYPES
71
+ // ============================================================================
72
+
73
+ /**
74
+ * Represents a code declaration with tokenized content
75
+ */
76
+ export interface Declaration {
77
+ tokens?: Token[]
78
+ }
79
+
80
+ /**
81
+ * Represents a function or method parameter
82
+ */
83
+ export interface Parameter {
84
+ name: string
85
+ content?: ContentItem[]
86
+ }
87
+
88
+ // ============================================================================
89
+ // SECTION TYPES
90
+ // ============================================================================
91
+
92
+ /**
93
+ * Represents a topic section in the documentation
94
+ */
95
+ export interface TopicSection {
96
+ title: string
97
+ identifiers?: string[]
98
+ children?: TopicSection[]
99
+ abstract?: ContentItem[]
100
+ anchor?: string
101
+ }
102
+
103
+ /**
104
+ * Represents a "see also" section with related identifiers
105
+ */
106
+ export interface SeeAlsoSection {
107
+ title: string
108
+ identifiers?: string[]
109
+ }
110
+
111
+ /**
112
+ * Represents a primary content section with specific kind and content
113
+ */
114
+ export interface PrimaryContentSection {
115
+ kind: string
116
+ content?: ContentItem[]
117
+ declarations?: Declaration[]
118
+ parameters?: Parameter[]
119
+ items?: PropertyItem[]
120
+ }
121
+
122
+ /**
123
+ * Represents a property item used in data dictionary pages.
124
+ */
125
+ export interface PropertyItem {
126
+ name: string
127
+ required?: boolean
128
+ content?: ContentItem[]
129
+ type?: Array<{
130
+ text?: string
131
+ kind?: string
132
+ identifier?: string
133
+ }>
134
+ attributes?: Array<{
135
+ kind?: string
136
+ values?: string[]
137
+ }>
138
+ }
139
+
140
+ // ============================================================================
141
+ // VARIANT TYPES
142
+ // ============================================================================
143
+
144
+ /**
145
+ * Common properties shared across all variant types
146
+ */
147
+ interface BaseVariant {
148
+ title?: string
149
+ abstract?: TextFragment[]
150
+ identifier?: string
151
+ type?: string
152
+ role?: string
153
+ kind?: string
154
+ }
155
+
156
+ /**
157
+ * Represents a language-specific variant of documentation
158
+ */
159
+ export interface LanguageVariant extends BaseVariant {
160
+ traits: Array<{ interfaceLanguage: string }>
161
+ paths: string[]
162
+ }
163
+
164
+ /**
165
+ * Represents an image variant with URL and traits
166
+ */
167
+ export interface ImageVariant extends BaseVariant {
168
+ url: string
169
+ traits: string[]
170
+ }
171
+
172
+ /**
173
+ * Represents a symbol variant with detailed metadata
174
+ */
175
+ export interface SymbolVariant extends BaseVariant {
176
+ url?: string
177
+ fragments?: Array<{
178
+ kind: string
179
+ text?: string
180
+ preciseIdentifier?: string
181
+ }>
182
+ conformance?: {
183
+ constraints?: Array<{
184
+ code?: string
185
+ type: string
186
+ text?: string
187
+ }>
188
+ conformancePrefix?: Array<{
189
+ text: string
190
+ type: string
191
+ }>
192
+ availabilityPrefix?: Array<{
193
+ type: string
194
+ text: string
195
+ }>
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Union type representing any variant type
201
+ */
202
+ export type Variant = LanguageVariant | ImageVariant | SymbolVariant
203
+
204
+ // ============================================================================
205
+ // INTERFACE & INDEX TYPES
206
+ // ============================================================================
207
+
208
+ /**
209
+ * Represents a Swift interface item in the documentation index
210
+ */
211
+ export interface SwiftInterfaceItem {
212
+ path: string
213
+ title: string
214
+ type: string
215
+ children?: SwiftInterfaceItem[]
216
+ external?: boolean
217
+ beta?: boolean
218
+ }
219
+
220
+ /**
221
+ * Represents an item in the documentation index
222
+ */
223
+ export interface IndexContentItem {
224
+ type?: string
225
+ title?: string
226
+ path?: string
227
+ beta?: boolean
228
+ children?: IndexContentItem[]
229
+ }
230
+
231
+ // ============================================================================
232
+ // METADATA TYPES
233
+ // ============================================================================
234
+
235
+ /**
236
+ * Platform information for documentation
237
+ */
238
+ export interface Platform {
239
+ name: string
240
+ introducedAt: string
241
+ beta?: boolean
242
+ }
243
+
244
+ /**
245
+ * Documentation metadata
246
+ */
247
+ export interface DocumentationMetadata {
248
+ title?: string
249
+ platforms?: Platform[]
250
+ roleHeading?: string
251
+ symbolKind?: string
252
+ }
253
+
254
+ /**
255
+ * Documentation identifier with URL and language
256
+ */
257
+ export interface DocumentationIdentifier {
258
+ url: string
259
+ interfaceLanguage?: string
260
+ }
261
+
262
+ // ============================================================================
263
+ // MAIN DOCUMENTATION STRUCTURE
264
+ // ============================================================================
265
+
266
+ /**
267
+ * The main Apple documentation JSON structure.
268
+ * This is the root type for all Apple documentation responses.
269
+ */
270
+ export interface AppleDocJSON {
271
+ // Metadata
272
+ metadata?: DocumentationMetadata
273
+ kind?: string
274
+ identifier?: DocumentationIdentifier
275
+
276
+ // Content
277
+ abstract?: Array<{ text: string; type: string }>
278
+ sections?: ContentItem[]
279
+
280
+ // Primary content sections
281
+ primaryContentSections?: PrimaryContentSection[]
282
+
283
+ // Topic sections
284
+ topicSections?: TopicSection[]
285
+ seeAlsoSections?: SeeAlsoSection[]
286
+
287
+ // Variants and relationships
288
+ variants?: Variant[]
289
+ relationshipsSections?: ContentItem[]
290
+
291
+ // References
292
+ references?: Record<string, ContentItem>
293
+
294
+ // Index-specific fields
295
+ interfaceLanguages?: {
296
+ swift?: SwiftInterfaceItem[]
297
+ }
298
+ }
299
+
300
+ // ============================================================================
301
+ // UTILITY TYPES
302
+ // ============================================================================
303
+
304
+ /**
305
+ * Type guard to check if a variant is a language variant
306
+ */
307
+ export function isLanguageVariant(variant: Variant): variant is LanguageVariant {
308
+ return (
309
+ "traits" in variant &&
310
+ Array.isArray(variant.traits) &&
311
+ variant.traits.length > 0 &&
312
+ typeof variant.traits[0] === "object" &&
313
+ "interfaceLanguage" in variant.traits[0]
314
+ )
315
+ }
316
+
317
+ /**
318
+ * Type guard to check if a variant is an image variant
319
+ */
320
+ export function isImageVariant(variant: Variant): variant is ImageVariant {
321
+ return (
322
+ "url" in variant &&
323
+ "traits" in variant &&
324
+ Array.isArray(variant.traits) &&
325
+ typeof variant.traits[0] === "string"
326
+ )
327
+ }
328
+
329
+ /**
330
+ * Type guard to check if a variant is a symbol variant
331
+ */
332
+ export function isSymbolVariant(variant: Variant): variant is SymbolVariant {
333
+ return "fragments" in variant || "conformance" in variant
334
+ }
package/src/lib/url.ts ADDED
@@ -0,0 +1,55 @@
1
+ // Constants for Apple Developer documentation URLs
2
+ const APPLE_DOC_BASE_URL = "https://developer.apple.com/documentation/"
3
+
4
+ /**
5
+ * Normalizes documentation paths by removing leading slashes, whitespace, and documentation prefixes.
6
+ *
7
+ * This function handles various input formats:
8
+ * - `/swift/array` → `swift/array`
9
+ * - `documentation/swift/array` → `swift/array`
10
+ * - ` swift/array ` → `swift/array`
11
+ * - `/documentation/swift/array` → `swift/array`
12
+ *
13
+ * @param path - The documentation path to normalize
14
+ * @returns The normalized path without leading slashes or documentation prefixes
15
+ */
16
+ export function normalizeDocumentationPath(path: string): string {
17
+ if (!path || typeof path !== "string") {
18
+ return ""
19
+ }
20
+
21
+ return (
22
+ path
23
+ .trim()
24
+ // Remove leading slashes and documentation prefixes
25
+ .replace(/^\/?(?:documentation\/?)?/, "")
26
+ )
27
+ }
28
+
29
+ /**
30
+ * Generates a complete Apple Developer documentation URL from a normalized path.
31
+ *
32
+ * @param normalizedPath - The normalized documentation path (e.g., "swift/array")
33
+ * @returns The complete Apple Developer documentation URL
34
+ */
35
+ export function generateAppleDocUrl(normalizedPath: string): string {
36
+ if (!normalizedPath || typeof normalizedPath !== "string") {
37
+ return APPLE_DOC_BASE_URL
38
+ }
39
+
40
+ return `${APPLE_DOC_BASE_URL}${normalizedPath}`
41
+ }
42
+
43
+ /**
44
+ * Validates if a URL is a proper Apple Developer documentation URL.
45
+ *
46
+ * @param url - The URL to validate
47
+ * @returns True if the URL is a valid Apple Developer documentation URL
48
+ */
49
+ export function isValidAppleDocUrl(url: string): boolean {
50
+ if (!url || typeof url !== "string") {
51
+ return false
52
+ }
53
+
54
+ return url.startsWith(APPLE_DOC_BASE_URL)
55
+ }