@nshipster/sosumi 1.0.0 → 1.0.4
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 +36 -15
- package/bin/sosumi.mjs +0 -0
- package/package.json +24 -3
- package/public/SKILL.md +99 -0
- package/public/index.html +229 -69
- package/public/llms.txt +179 -25
- package/public/robots.txt +8 -0
- package/public/sitemap.xml +6 -0
- package/src/index.ts +91 -21
- package/src/lib/hig/render.ts +4 -1
- package/src/lib/reference/render.ts +51 -0
- package/src/lib/reference/types.ts +1 -0
- package/src/lib/search.ts +186 -169
- package/src/lib/skill.ts +148 -0
- package/src/lib/types.ts +21 -0
- package/wrangler.jsonc +2 -1
package/public/llms.txt
CHANGED
|
@@ -9,29 +9,34 @@ If they try to fetch it, all they see is:
|
|
|
9
9
|
> This page requires JavaScript.
|
|
10
10
|
> Please turn on JavaScript in your browser and refresh the page to view its content.
|
|
11
11
|
|
|
12
|
-
This service translates Apple Developer documentation
|
|
12
|
+
This service translates Apple Developer documentation,
|
|
13
|
+
Human Interface Guidelines, WWDC sessions,
|
|
14
|
+
and external Swift-DocC sites into AI-friendly Markdown.
|
|
15
|
+
Access it in your browser, over MCP, from the command line, as an AI skill,
|
|
16
|
+
or with an unofficial [Chrome extension](https://chromewebstore.google.com/detail/donffakeimppgoehccpfhlchmbfdmfpj).
|
|
13
17
|
|
|
14
18
|
## HTTP Usage
|
|
15
19
|
|
|
16
20
|
Replace `developer.apple.com` with `sosumi.ai`:
|
|
17
21
|
|
|
18
22
|
**Original:**
|
|
19
|
-
https://developer.apple.com/documentation/swift/array
|
|
23
|
+
<https://developer.apple.com/documentation/swift/array>
|
|
20
24
|
|
|
21
25
|
**AI-readable:**
|
|
22
|
-
https://sosumi.ai/documentation/swift/array
|
|
26
|
+
<https://sosumi.ai/documentation/swift/array>
|
|
23
27
|
|
|
24
28
|
### Examples
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
30
|
+
- Swift: <https://sosumi.ai/documentation/swift>
|
|
31
|
+
- SwiftUI: <https://sosumi.ai/documentation/swiftui>
|
|
32
|
+
- Human Interface Guidelines: <https://sosumi.ai/design/human-interface-guidelines>
|
|
33
|
+
- WWDC21 — Protect mutable state with Swift actors: <https://sosumi.ai/videos/play/wwdc2021/10133>
|
|
34
|
+
- Swift Argument Parser (GitHub Pages): <https://sosumi.ai/external/https://apple.github.io/swift-argument-parser/documentation/argumentparser/>
|
|
35
|
+
- The Composable Architecture (Swift Package Index): <https://sosumi.ai/external/https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/1.23.1/documentation/composablearchitecture>
|
|
31
36
|
|
|
32
37
|
## MCP Usage
|
|
33
38
|
|
|
34
|
-
Connect your MCP client to `https://sosumi.ai/mcp
|
|
39
|
+
Connect your MCP client to `https://sosumi.ai/mcp`.
|
|
35
40
|
|
|
36
41
|
### GitHub Copilot for Xcode
|
|
37
42
|
|
|
@@ -89,7 +94,7 @@ Run the following command in your terminal:
|
|
|
89
94
|
claude mcp add --transport http sosumi https://sosumi.ai/mcp
|
|
90
95
|
```
|
|
91
96
|
|
|
92
|
-
### Other
|
|
97
|
+
### Other MCP Clients
|
|
93
98
|
|
|
94
99
|
Sosumi's MCP server supports Streamable HTTP and Server-Sent Events (SSE) transport.
|
|
95
100
|
**If your client supports either of these,
|
|
@@ -113,25 +118,21 @@ you can run this command to proxy over stdio:
|
|
|
113
118
|
|
|
114
119
|
- `searchAppleDocumentation` - Searches Apple Developer documentation
|
|
115
120
|
- Parameters: `query` (string)
|
|
116
|
-
- Returns structured results with titles,
|
|
117
|
-
URLs,
|
|
118
|
-
descriptions,
|
|
119
|
-
breadcrumbs,
|
|
120
|
-
and tags
|
|
121
|
+
- Returns structured results with titles, URLs, descriptions, breadcrumbs, and tags
|
|
121
122
|
|
|
122
123
|
- `fetchAppleDocumentation` - Fetches Apple Developer documentation and Human Interface Guidelines by path
|
|
123
124
|
- Parameters: `path` (string) - Documentation path (e.g., '/documentation/swift', 'design/human-interface-guidelines/foundations/color')
|
|
124
125
|
- Returns content as Markdown
|
|
125
126
|
|
|
126
|
-
- `fetchAppleVideoTranscript` - Fetches video transcripts, including WWDC sessions
|
|
127
|
-
- Parameters: `path` (string) - video path (e.g.,
|
|
127
|
+
- `fetchAppleVideoTranscript` - Fetches video transcripts, including WWDC sessions, by video path
|
|
128
|
+
- Parameters: `path` (string) - video path (e.g., '/videos/play/wwdc2021/10133', '/videos/play/meet-with-apple/208')
|
|
128
129
|
- Returns content as Markdown
|
|
129
130
|
|
|
130
131
|
- `fetchExternalDocumentation` - Fetches external Swift-DocC documentation by absolute HTTPS URL
|
|
131
|
-
- Parameters: `url` (string) - External URL (e.g.,
|
|
132
|
+
- Parameters: `url` (string) - External URL (e.g., '<https://apple.github.io/swift-argument-parser/documentation/argumentparser>')
|
|
132
133
|
- Returns content as Markdown
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
### Troubleshooting
|
|
135
136
|
|
|
136
137
|
If you're experiencing connection timeouts or network issues with the MCP server,
|
|
137
138
|
you may need to configure a proxy.
|
|
@@ -159,14 +160,163 @@ Replace `proxy.example.com:8080` with your actual proxy server details.
|
|
|
159
160
|
For authenticated proxies, use the format:
|
|
160
161
|
`http://username:password@proxy.example.com:8080`
|
|
161
162
|
|
|
162
|
-
##
|
|
163
|
+
## CLI Usage
|
|
164
|
+
|
|
165
|
+
Sosumi also provides a CLI that complements MCP.
|
|
166
|
+
|
|
167
|
+
### Node.js
|
|
168
|
+
|
|
169
|
+
Run directly with [`npx`](https://docs.npmjs.com/cli/commands/npx)
|
|
170
|
+
(included with [Node.js](https://nodejs.org/)):
|
|
171
|
+
|
|
172
|
+
```shell
|
|
173
|
+
npx @nshipster/sosumi fetch https://developer.apple.com/documentation/swift/array
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
If you use it regularly, install it once:
|
|
177
|
+
|
|
178
|
+
```shell
|
|
179
|
+
npm i -g @nshipster/sosumi
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Bun
|
|
183
|
+
|
|
184
|
+
Run directly with [`bunx`](https://bun.sh/docs/cli/bunx)
|
|
185
|
+
(included with [Bun](https://bun.sh/)):
|
|
186
|
+
|
|
187
|
+
```shell
|
|
188
|
+
bunx @nshipster/sosumi fetch https://developer.apple.com/documentation/swift/array
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If you use it regularly, install it once:
|
|
192
|
+
|
|
193
|
+
```shell
|
|
194
|
+
bun i -g @nshipster/sosumi
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Deno
|
|
198
|
+
|
|
199
|
+
Run directly with [`deno run`](https://docs.deno.com/runtime/reference/cli/run/):
|
|
200
|
+
|
|
201
|
+
```shell
|
|
202
|
+
deno run -A npm:@nshipster/sosumi fetch https://developer.apple.com/documentation/swift/array
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
If you use it regularly, install it once:
|
|
206
|
+
|
|
207
|
+
```shell
|
|
208
|
+
deno install -g -A npm:@nshipster/sosumi
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Fetching Documentation
|
|
212
|
+
|
|
213
|
+
Fetch any content type supported by the MCP tools —
|
|
214
|
+
API docs, Human Interface Guidelines, WWDC sessions, and external Swift-DocC pages:
|
|
215
|
+
|
|
216
|
+
```shell
|
|
217
|
+
sosumi fetch /documentation/swift/array
|
|
218
|
+
sosumi fetch /design/human-interface-guidelines/color
|
|
219
|
+
sosumi fetch /videos/play/wwdc2021/10133
|
|
220
|
+
sosumi fetch https://apple.github.io/swift-argument-parser/documentation/argumentparser
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Searching
|
|
224
|
+
|
|
225
|
+
Search Apple Developer documentation:
|
|
226
|
+
|
|
227
|
+
```shell
|
|
228
|
+
sosumi search "SwiftData"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### JSON Output
|
|
163
232
|
|
|
164
|
-
|
|
165
|
-
|
|
233
|
+
By default, output is plain text / Markdown.
|
|
234
|
+
Add `--json` for scripts:
|
|
235
|
+
|
|
236
|
+
```shell
|
|
237
|
+
sosumi fetch /documentation/swift/array --json
|
|
238
|
+
sosumi search "SwiftData" --json
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Local Server
|
|
242
|
+
|
|
243
|
+
Run a local instance of the server from the published package:
|
|
244
|
+
|
|
245
|
+
```shell
|
|
246
|
+
sosumi serve
|
|
247
|
+
sosumi serve --port 8787
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## AI Agent Skill
|
|
251
|
+
|
|
252
|
+
Teach your coding assistant to use Sosumi consistently with:
|
|
253
|
+
`https://sosumi.ai/SKILL.md`
|
|
254
|
+
|
|
255
|
+
Spec-compliant clients can also discover and install it from this domain:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
npx skills add https://sosumi.ai
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Claude Code
|
|
262
|
+
|
|
263
|
+
Install as a reusable skill (personal or project-level):
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
# Personal skill (all your projects)
|
|
267
|
+
mkdir -p ~/.claude/skills/sosumi
|
|
268
|
+
curl -o ~/.claude/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
|
|
269
|
+
|
|
270
|
+
# Project skill (just this project)
|
|
271
|
+
mkdir -p .claude/skills/sosumi
|
|
272
|
+
curl -o .claude/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Codex
|
|
276
|
+
|
|
277
|
+
Install globally:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# Global instructions (all your projects)
|
|
281
|
+
mkdir -p ~/.agents/skills/sosumi
|
|
282
|
+
curl -o ~/.agents/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Cursor
|
|
286
|
+
|
|
287
|
+
Install as a Cursor skill (global or project-level):
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# Personal skill (all your projects)
|
|
291
|
+
mkdir -p ~/.cursor/skills/sosumi
|
|
292
|
+
curl -o ~/.cursor/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
|
|
293
|
+
|
|
294
|
+
# Project skill (just this project)
|
|
295
|
+
mkdir -p .cursor/skills/sosumi
|
|
296
|
+
curl -o .cursor/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Other
|
|
300
|
+
|
|
301
|
+
For AGENTS-compatible tools, install as a reusable skill:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
mkdir -p ~/.agents/skills/sosumi
|
|
305
|
+
curl -o ~/.agents/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Use this together with Sosumi MCP at `https://sosumi.ai/mcp`.
|
|
309
|
+
|
|
310
|
+
## Legal
|
|
311
|
+
|
|
312
|
+
This is an unofficial, independent project
|
|
313
|
+
and is not affiliated with or endorsed by Apple Inc.
|
|
166
314
|
"Apple", "Xcode", and related marks are trademarks of Apple Inc.
|
|
167
315
|
|
|
168
|
-
This
|
|
169
|
-
|
|
316
|
+
This project is open source and available on
|
|
317
|
+
[GitHub](https://github.com/NSHipster/sosumi.ai).
|
|
318
|
+
|
|
319
|
+
This service is an accessibility-first, on-demand renderer.
|
|
170
320
|
It converts a single Apple Developer page to Markdown only when requested by a user.
|
|
171
321
|
It does not crawl, spider, or bulk download;
|
|
172
322
|
it does not attempt to bypass authentication or security;
|
|
@@ -181,4 +331,8 @@ Your use of this service must comply with Apple's Terms of Use and applicable la
|
|
|
181
331
|
You are solely responsible for how you access and use Apple's content through this tool.
|
|
182
332
|
Do not use this service to circumvent technical measures or for redistribution.
|
|
183
333
|
|
|
184
|
-
|
|
334
|
+
Fetches are made with the user agent `sosumi-ai/1.0 (+https://sosumi.ai/#bot)`.
|
|
335
|
+
For external Swift-DocC hosts, sosumi honors `robots.txt` rules
|
|
336
|
+
and opt-out response directives such as `X-Robots-Tag: noai`.
|
|
337
|
+
|
|
338
|
+
**Contact:** <info@sosumi.ai>
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StreamableHTTPTransport } from "@hono/mcp"
|
|
2
2
|
import { Hono } from "hono"
|
|
3
|
+
import { accepts } from "hono/accepts"
|
|
3
4
|
import { cache } from "hono/cache"
|
|
4
5
|
import { cors } from "hono/cors"
|
|
5
6
|
import { HTTPException } from "hono/http-exception"
|
|
@@ -21,6 +22,14 @@ import {
|
|
|
21
22
|
import { createMcpServer } from "./lib/mcp"
|
|
22
23
|
import { fetchJSONData, renderFromJSON } from "./lib/reference"
|
|
23
24
|
import { searchAppleDeveloperDocs } from "./lib/search"
|
|
25
|
+
import {
|
|
26
|
+
createSkillIndex,
|
|
27
|
+
loadSkill,
|
|
28
|
+
SKILL_NAME,
|
|
29
|
+
skillExists,
|
|
30
|
+
skillHeaders,
|
|
31
|
+
skillIndexHeaders,
|
|
32
|
+
} from "./lib/skill"
|
|
24
33
|
import { generateAppleDocUrl, isValidAppleDocUrl, normalizeDocumentationPath } from "./lib/url"
|
|
25
34
|
import { fetchVideoTranscriptMarkdown, TranscriptNotFoundError } from "./lib/video"
|
|
26
35
|
|
|
@@ -32,24 +41,6 @@ interface Env {
|
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
const app = new Hono<{ Bindings: Env }>()
|
|
35
|
-
const mcpServerCache = new Map<string, ReturnType<typeof createMcpServer>>()
|
|
36
|
-
|
|
37
|
-
function getMcpServer(env: Env) {
|
|
38
|
-
const allowlist = env.EXTERNAL_DOC_HOST_ALLOWLIST ?? ""
|
|
39
|
-
const blocklist = env.EXTERNAL_DOC_HOST_BLOCKLIST ?? ""
|
|
40
|
-
const cacheKey = `${allowlist}\n---\n${blocklist}`
|
|
41
|
-
const cached = mcpServerCache.get(cacheKey)
|
|
42
|
-
if (cached) {
|
|
43
|
-
return cached
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const server = createMcpServer({
|
|
47
|
-
EXTERNAL_DOC_HOST_ALLOWLIST: env.EXTERNAL_DOC_HOST_ALLOWLIST,
|
|
48
|
-
EXTERNAL_DOC_HOST_BLOCKLIST: env.EXTERNAL_DOC_HOST_BLOCKLIST,
|
|
49
|
-
})
|
|
50
|
-
mcpServerCache.set(cacheKey, server)
|
|
51
|
-
return server
|
|
52
|
-
}
|
|
53
44
|
|
|
54
45
|
app.use("*", async (c, next) => {
|
|
55
46
|
await next()
|
|
@@ -84,15 +75,89 @@ app.use("*", async (c, next) => {
|
|
|
84
75
|
await next()
|
|
85
76
|
})
|
|
86
77
|
|
|
78
|
+
app.get("/", async (c) => {
|
|
79
|
+
const accepted = accepts(c, {
|
|
80
|
+
header: "Accept",
|
|
81
|
+
supports: ["text/markdown", "text/html"],
|
|
82
|
+
default: "text/html",
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (accepted === "text/markdown") {
|
|
86
|
+
const llmsUrl = new URL("/llms.txt", c.req.url)
|
|
87
|
+
const llmsResponse = await c.env.ASSETS.fetch(new Request(llmsUrl.toString()))
|
|
88
|
+
|
|
89
|
+
if (!llmsResponse.ok) {
|
|
90
|
+
throw new HTTPException(500, {
|
|
91
|
+
message: "Failed to load llms.txt",
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const markdown = await llmsResponse.text()
|
|
96
|
+
return c.text(markdown, 200, {
|
|
97
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
98
|
+
"Cache-Control": "public, max-age=300, s-maxage=600",
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return c.env.ASSETS.fetch(c.req.raw)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
app.all("/.well-known/agent-skills/index.json", async (c) => {
|
|
106
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
107
|
+
return c.text("Method Not Allowed", 405, { Allow: "GET, HEAD" })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (c.req.method === "HEAD") {
|
|
111
|
+
if (!(await skillExists(c.env.ASSETS, c.req.url))) {
|
|
112
|
+
throw new HTTPException(500, { message: "Failed to load SKILL.md" })
|
|
113
|
+
}
|
|
114
|
+
return new Response(null, {
|
|
115
|
+
status: 200,
|
|
116
|
+
headers: skillIndexHeaders,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const skill = await loadSkill(c.env.ASSETS, c.req.url)
|
|
121
|
+
const index = await createSkillIndex(skill)
|
|
122
|
+
|
|
123
|
+
return c.json(index, 200, skillIndexHeaders)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
app.all(`/.well-known/agent-skills/${SKILL_NAME}/SKILL.md`, async (c) => {
|
|
127
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
128
|
+
return c.text("Method Not Allowed", 405, { Allow: "GET, HEAD" })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (c.req.method === "HEAD") {
|
|
132
|
+
if (!(await skillExists(c.env.ASSETS, c.req.url))) {
|
|
133
|
+
throw new HTTPException(500, { message: "Failed to load SKILL.md" })
|
|
134
|
+
}
|
|
135
|
+
return new Response(null, {
|
|
136
|
+
status: 200,
|
|
137
|
+
headers: skillHeaders,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const skill = await loadSkill(c.env.ASSETS, c.req.url)
|
|
142
|
+
|
|
143
|
+
return new Response(skill.bytes, {
|
|
144
|
+
status: 200,
|
|
145
|
+
headers: skillHeaders,
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
app.get("/bot", (c) => c.redirect("/#bot", 302))
|
|
150
|
+
|
|
87
151
|
app.all("/mcp", async (c) => {
|
|
88
|
-
const mcpServer =
|
|
152
|
+
const mcpServer = createMcpServer({
|
|
153
|
+
EXTERNAL_DOC_HOST_ALLOWLIST: c.env.EXTERNAL_DOC_HOST_ALLOWLIST,
|
|
154
|
+
EXTERNAL_DOC_HOST_BLOCKLIST: c.env.EXTERNAL_DOC_HOST_BLOCKLIST,
|
|
155
|
+
})
|
|
89
156
|
const transport = new StreamableHTTPTransport()
|
|
90
157
|
await mcpServer.connect(transport)
|
|
91
158
|
return transport.handleRequest(c)
|
|
92
159
|
})
|
|
93
160
|
|
|
94
|
-
app.get("/bot", (c) => c.redirect("/#bot", 302))
|
|
95
|
-
|
|
96
161
|
app.get("/search", async (c) => {
|
|
97
162
|
const query = c.req.query("q")?.trim() ?? ""
|
|
98
163
|
if (!query) {
|
|
@@ -183,6 +248,7 @@ This service only works with Apple Developer documentation URLs:
|
|
|
183
248
|
"Content-Location": appleUrl,
|
|
184
249
|
"Cache-Control": "public, max-age=3600, s-maxage=86400",
|
|
185
250
|
ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
|
|
251
|
+
"X-Robots-Tag": "noindex, nofollow, noarchive",
|
|
186
252
|
}
|
|
187
253
|
|
|
188
254
|
if (c.req.header("Accept")?.includes("application/json")) {
|
|
@@ -225,6 +291,7 @@ app.get("/external/*", async (c) => {
|
|
|
225
291
|
"Content-Location": targetUrl.toString(),
|
|
226
292
|
"Cache-Control": "public, max-age=3600, s-maxage=86400",
|
|
227
293
|
ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
|
|
294
|
+
"X-Robots-Tag": "noindex, nofollow, noarchive",
|
|
228
295
|
}
|
|
229
296
|
|
|
230
297
|
if (c.req.header("Accept")?.includes("application/json")) {
|
|
@@ -263,6 +330,7 @@ app.get("/design/human-interface-guidelines", async (c) => {
|
|
|
263
330
|
"Content-Location": sourceUrl,
|
|
264
331
|
"Cache-Control": "public, max-age=3600, s-maxage=86400",
|
|
265
332
|
ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
|
|
333
|
+
"X-Robots-Tag": "noindex, nofollow, noarchive",
|
|
266
334
|
}
|
|
267
335
|
|
|
268
336
|
if (c.req.header("Accept")?.includes("application/json")) {
|
|
@@ -305,6 +373,7 @@ app.get("/design/human-interface-guidelines/:path{.+}", async (c) => {
|
|
|
305
373
|
"Content-Location": sourceUrl,
|
|
306
374
|
"Cache-Control": "public, max-age=3600, s-maxage=86400",
|
|
307
375
|
ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
|
|
376
|
+
"X-Robots-Tag": "noindex, nofollow, noarchive",
|
|
308
377
|
}
|
|
309
378
|
|
|
310
379
|
if (c.req.header("Accept")?.includes("application/json")) {
|
|
@@ -358,6 +427,7 @@ app.get("/videos/play/:collection/:id", async (c) => {
|
|
|
358
427
|
"Content-Location": sourceUrl,
|
|
359
428
|
"Cache-Control": "public, max-age=3600, s-maxage=86400",
|
|
360
429
|
ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
|
|
430
|
+
"X-Robots-Tag": "noindex, nofollow, noarchive",
|
|
361
431
|
}
|
|
362
432
|
|
|
363
433
|
if (c.req.header("Accept")?.includes("application/json")) {
|
package/src/lib/hig/render.ts
CHANGED
|
@@ -496,7 +496,10 @@ function renderHIGTocItems(items: HIGTocItem[], headingLevel: number): string {
|
|
|
496
496
|
|
|
497
497
|
for (const item of items) {
|
|
498
498
|
if (item.type === "module" || item.type === "symbol") {
|
|
499
|
-
//
|
|
499
|
+
// Ensure blank line before heading when preceding content was a list
|
|
500
|
+
if (markdown && !markdown.endsWith("\n\n")) {
|
|
501
|
+
markdown += "\n"
|
|
502
|
+
}
|
|
500
503
|
const hashes = "#".repeat(Math.min(headingLevel, 6))
|
|
501
504
|
markdown += `${hashes} ${item.title}\n\n`
|
|
502
505
|
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
AppleDocJSON,
|
|
7
7
|
ContentItem,
|
|
8
8
|
IndexContentItem,
|
|
9
|
+
PossibleValueItem,
|
|
9
10
|
PropertyItem,
|
|
10
11
|
TopicSection,
|
|
11
12
|
Variant,
|
|
@@ -94,6 +95,18 @@ export async function renderFromJSON(
|
|
|
94
95
|
)
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
// Add possible values (used by enum/string type pages)
|
|
99
|
+
const possibleValuesSection = jsonData.primaryContentSections.find(
|
|
100
|
+
(s) => s.kind === "possibleValues",
|
|
101
|
+
)
|
|
102
|
+
if (possibleValuesSection?.values) {
|
|
103
|
+
markdown += renderPossibleValues(
|
|
104
|
+
possibleValuesSection.values,
|
|
105
|
+
jsonData.references,
|
|
106
|
+
options.externalOrigin,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
// Add content sections
|
|
98
111
|
const contentSections = jsonData.primaryContentSections.filter((s) => s.kind === "content")
|
|
99
112
|
for (const section of contentSections) {
|
|
@@ -294,6 +307,34 @@ function renderProperties(
|
|
|
294
307
|
return markdown
|
|
295
308
|
}
|
|
296
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Render possible values section for enum/string type pages.
|
|
312
|
+
*/
|
|
313
|
+
function renderPossibleValues(
|
|
314
|
+
values: PossibleValueItem[],
|
|
315
|
+
references?: Record<string, ContentItem>,
|
|
316
|
+
externalOrigin?: string,
|
|
317
|
+
): string {
|
|
318
|
+
if (values.length === 0) return ""
|
|
319
|
+
|
|
320
|
+
let markdown = "## Possible Values\n\n"
|
|
321
|
+
|
|
322
|
+
for (const value of values) {
|
|
323
|
+
if (!value.name) continue
|
|
324
|
+
|
|
325
|
+
markdown += `### \`${value.name}\`\n\n`
|
|
326
|
+
|
|
327
|
+
if (value.content && Array.isArray(value.content)) {
|
|
328
|
+
const text = renderContentArray(value.content, references, 0, externalOrigin).trim()
|
|
329
|
+
if (text) {
|
|
330
|
+
markdown += `${text}\n\n`
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return markdown
|
|
336
|
+
}
|
|
337
|
+
|
|
297
338
|
function renderPropertyType(
|
|
298
339
|
type: Array<{ text?: string; kind?: string; identifier?: string }> | undefined,
|
|
299
340
|
references?: Record<string, ContentItem>,
|
|
@@ -397,6 +438,16 @@ function renderContentArray(
|
|
|
397
438
|
markdown += `> [!${calloutType}]\n> ${cleanContent}\n\n`
|
|
398
439
|
} else if (item.type === "table") {
|
|
399
440
|
markdown += renderTable(item, references, depth, externalOrigin)
|
|
441
|
+
} else if (item.type === "tabNavigator" && item.tabs?.length) {
|
|
442
|
+
for (const tab of item.tabs) {
|
|
443
|
+
const label = tab.title?.trim()
|
|
444
|
+
if (label) {
|
|
445
|
+
markdown += `**${label}**\n\n`
|
|
446
|
+
}
|
|
447
|
+
if (tab.content?.length) {
|
|
448
|
+
markdown += renderContentArray(tab.content, references, depth + 1, externalOrigin)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
400
451
|
}
|
|
401
452
|
}
|
|
402
453
|
|