sourcey 3.6.0 → 3.6.1

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.
Files changed (82) hide show
  1. package/README.md +5 -2
  2. package/dist/adapters/index.d.ts +3 -2
  3. package/dist/adapters/index.d.ts.map +1 -1
  4. package/dist/adapters/index.js +35 -3
  5. package/dist/adapters/types.d.ts +8 -2
  6. package/dist/adapters/types.d.ts.map +1 -1
  7. package/dist/cli.js +28 -34
  8. package/dist/client/search.js +181 -119
  9. package/dist/client/tabs.js +23 -0
  10. package/dist/components/layout/Head.js +2 -2
  11. package/dist/components/openapi/Introduction.d.ts.map +1 -1
  12. package/dist/components/openapi/Introduction.js +16 -1
  13. package/dist/components/openapi/Security.d.ts.map +1 -1
  14. package/dist/components/openapi/Security.js +18 -2
  15. package/dist/config.d.ts +63 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +21 -7
  18. package/dist/core/api-rendering.d.ts +47 -0
  19. package/dist/core/api-rendering.d.ts.map +1 -0
  20. package/dist/core/api-rendering.js +76 -0
  21. package/dist/core/doxygen-loader.d.ts.map +1 -1
  22. package/dist/core/doxygen-loader.js +35 -18
  23. package/dist/core/godoc-loader.d.ts.map +1 -1
  24. package/dist/core/godoc-loader.js +17 -6
  25. package/dist/core/markdown-loader.d.ts.map +1 -1
  26. package/dist/core/markdown-loader.js +8 -5
  27. package/dist/core/mcp-normalizer.d.ts.map +1 -1
  28. package/dist/core/mcp-normalizer.js +48 -34
  29. package/dist/core/parser.d.ts.map +1 -1
  30. package/dist/core/parser.js +75 -4
  31. package/dist/core/rustdoc-introspector.d.ts +29 -0
  32. package/dist/core/rustdoc-introspector.d.ts.map +1 -0
  33. package/dist/core/rustdoc-introspector.js +236 -0
  34. package/dist/core/rustdoc-loader.d.ts +25 -0
  35. package/dist/core/rustdoc-loader.d.ts.map +1 -0
  36. package/dist/core/rustdoc-loader.js +469 -0
  37. package/dist/core/rustdoc-render.d.ts +76 -0
  38. package/dist/core/rustdoc-render.d.ts.map +1 -0
  39. package/dist/core/rustdoc-render.js +785 -0
  40. package/dist/core/rustdoc-types.d.ts +314 -0
  41. package/dist/core/rustdoc-types.d.ts.map +1 -0
  42. package/dist/core/rustdoc-types.js +18 -0
  43. package/dist/core/search-indexer.js +1 -1
  44. package/dist/core/sourcey-rustdoc/Cargo.lock +705 -0
  45. package/dist/core/sourcey-rustdoc/Cargo.toml +38 -0
  46. package/dist/core/sourcey-rustdoc/README.md +59 -0
  47. package/dist/core/sourcey-rustdoc/src/diagnostics.rs +42 -0
  48. package/dist/core/sourcey-rustdoc/src/doctest.rs +268 -0
  49. package/dist/core/sourcey-rustdoc/src/extract.rs +492 -0
  50. package/dist/core/sourcey-rustdoc/src/lib.rs +58 -0
  51. package/dist/core/sourcey-rustdoc/src/links.rs +126 -0
  52. package/dist/core/sourcey-rustdoc/src/main.rs +256 -0
  53. package/dist/core/sourcey-rustdoc/src/signature.rs +415 -0
  54. package/dist/core/sourcey-rustdoc/src/spec.rs +390 -0
  55. package/dist/core/sourcey-rustdoc/tests/format_version.rs +45 -0
  56. package/dist/dev-server.d.ts.map +1 -1
  57. package/dist/dev-server.js +49 -0
  58. package/dist/index.d.ts +4 -2
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +22 -13
  61. package/dist/init.js +2 -2
  62. package/dist/renderer/changelog-feed.d.ts.map +1 -1
  63. package/dist/renderer/changelog-feed.js +27 -19
  64. package/dist/renderer/html-builder.d.ts.map +1 -1
  65. package/dist/renderer/html-builder.js +4 -3
  66. package/dist/renderer/llms.js +57 -2
  67. package/dist/site-assembly.d.ts +5 -0
  68. package/dist/site-assembly.d.ts.map +1 -1
  69. package/dist/site-assembly.js +55 -1
  70. package/dist/themes/default/sourcey.css +300 -0
  71. package/dist/utils/code-samples.d.ts +1 -0
  72. package/dist/utils/code-samples.d.ts.map +1 -1
  73. package/dist/utils/code-samples.js +30 -22
  74. package/dist/utils/html.d.ts +10 -0
  75. package/dist/utils/html.d.ts.map +1 -0
  76. package/dist/utils/html.js +152 -0
  77. package/dist/utils/http.d.ts +0 -2
  78. package/dist/utils/http.d.ts.map +1 -1
  79. package/dist/utils/http.js +19 -34
  80. package/dist/utils/markdown.d.ts.map +1 -1
  81. package/dist/utils/markdown.js +102 -37
  82. package/package.json +9 -4
@@ -4,11 +4,11 @@ import { generateExample } from "./example-generator.js";
4
4
  // ---------------------------------------------------------------------------
5
5
  const generators = {
6
6
  curl({ url, method, contentType, body }) {
7
- const parts = [`curl -X ${method} '${url}'`];
7
+ const parts = [`curl -X ${method} ${shellSingleQuoted(url)}`];
8
8
  if (contentType)
9
- parts.push(` -H 'Content-Type: ${contentType}'`);
9
+ parts.push(` -H ${shellSingleQuoted(`Content-Type: ${contentType}`)}`);
10
10
  if (body)
11
- parts.push(` -d '${indent(body, 2)}'`);
11
+ parts.push(` -d ${shellSingleQuoted(indent(body, 2))}`);
12
12
  return { lang: "bash", label: "cURL", source: parts.join(" \\\n") };
13
13
  },
14
14
  javascript({ url, method, contentType, body }) {
@@ -51,7 +51,7 @@ const generators = {
51
51
  return { lang: "python", label: "Python", source: lines.join("\n") };
52
52
  },
53
53
  go({ url, method, body }) {
54
- const lines = ["package main", "", "import (", ` "fmt"`, ` "io"`, ` "net/http"`,];
54
+ const lines = ["package main", "", "import (", ` "fmt"`, ` "io"`, ` "net/http"`];
55
55
  if (body)
56
56
  lines.push(` "strings"`);
57
57
  lines.push(")", "");
@@ -63,7 +63,8 @@ const generators = {
63
63
  else {
64
64
  lines.push(` req, _ := http.NewRequest("${method}", "${url}", nil)`);
65
65
  }
66
- lines.push(` req.Header.Set("Content-Type", "application/json")`);
66
+ if (body)
67
+ lines.push(` req.Header.Set("Content-Type", "application/json")`);
67
68
  lines.push(` resp, _ := http.DefaultClient.Do(req)`);
68
69
  lines.push(` defer resp.Body.Close()`);
69
70
  lines.push(` data, _ := io.ReadAll(resp.Body)`);
@@ -76,21 +77,23 @@ const generators = {
76
77
  `require 'net/http'`,
77
78
  `require 'json'`,
78
79
  ``,
79
- `uri = URI('${url}')`,
80
+ `uri = URI('${rubySingleQuoted(url)}')`,
80
81
  ];
81
82
  const methodClass = rubyShortcutMethod(method);
82
83
  if (methodClass) {
83
84
  lines.push(`request = Net::HTTP::${methodClass}.new(uri)`);
84
85
  if (contentType)
85
- lines.push(`request['Content-Type'] = '${contentType}'`);
86
+ lines.push(`request['Content-Type'] = '${rubySingleQuoted(contentType)}'`);
86
87
  if (body)
87
- lines.push(`request.body = '${indent(body, 2)}'`);
88
+ lines.push(`request.body = '${rubySingleQuoted(indent(body, 2))}'`);
88
89
  }
89
90
  else {
90
- const headers = contentType ? `{ 'Content-Type' => '${contentType}' }` : "{}";
91
+ const headers = contentType
92
+ ? `{ 'Content-Type' => '${rubySingleQuoted(contentType)}' }`
93
+ : "{}";
91
94
  lines.push(`request = Net::HTTPGenericRequest.new('${method}', ${body ? "true" : "false"}, true, uri.request_uri, ${headers})`);
92
95
  if (body)
93
- lines.push(`request.body = '${indent(body, 2)}'`);
96
+ lines.push(`request.body = '${rubySingleQuoted(indent(body, 2))}'`);
94
97
  }
95
98
  lines.push(``, `response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }`);
96
99
  lines.push(`data = JSON.parse(response.body)`);
@@ -121,16 +124,16 @@ const generators = {
121
124
  return { lang: "java", label: "Java", source: lines.join("\n") };
122
125
  },
123
126
  php({ url, method, contentType, body }) {
124
- const lines = [`<?php`, `$ch = curl_init('${url}');`];
127
+ const lines = [`<?php`, `$ch = curl_init('${phpSingleQuoted(url)}');`];
125
128
  lines.push(`curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);`);
126
129
  if (method !== "GET") {
127
- lines.push(`curl_setopt($ch, CURLOPT_CUSTOMREQUEST, '${method}');`);
130
+ lines.push(`curl_setopt($ch, CURLOPT_CUSTOMREQUEST, '${phpSingleQuoted(method)}');`);
128
131
  }
129
132
  if (contentType) {
130
- lines.push(`curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: ${contentType}']);`);
133
+ lines.push(`curl_setopt($ch, CURLOPT_HTTPHEADER, ['${phpSingleQuoted(`Content-Type: ${contentType}`)}']);`);
131
134
  }
132
135
  if (body) {
133
- lines.push(`curl_setopt($ch, CURLOPT_POSTFIELDS, '${indent(body, 2)}');`);
136
+ lines.push(`curl_setopt($ch, CURLOPT_POSTFIELDS, '${phpSingleQuoted(indent(body, 2))}');`);
134
137
  }
135
138
  lines.push(``, `$response = curl_exec($ch);`, `curl_close($ch);`);
136
139
  lines.push(`$data = json_decode($response, true);`);
@@ -165,10 +168,7 @@ const generators = {
165
168
  return { lang: "rust", label: "Rust", source: lines.join("\n") };
166
169
  },
167
170
  csharp({ url, method, body }) {
168
- const lines = [
169
- `using var client = new HttpClient();`,
170
- ``,
171
- ];
171
+ const lines = [`using var client = new HttpClient();`, ``];
172
172
  const httpMethod = csharpShortcutMethod(method)
173
173
  ? `HttpMethod.${method[0] + method.slice(1).toLowerCase()}`
174
174
  : `new HttpMethod("${method}")`;
@@ -185,6 +185,7 @@ const generators = {
185
185
  };
186
186
  /** Default languages when none configured. */
187
187
  export const DEFAULT_CODE_SAMPLE_LANGS = ["curl", "javascript", "python"];
188
+ export const SUPPORTED_CODE_SAMPLE_LANGS = Object.freeze(Object.keys(generators));
188
189
  // ---------------------------------------------------------------------------
189
190
  // Public API
190
191
  // ---------------------------------------------------------------------------
@@ -203,14 +204,12 @@ export function generateCodeSamples(op, serverUrl, langs = DEFAULT_CODE_SAMPLE_L
203
204
  const method = op.method.toUpperCase();
204
205
  const hasBody = !!op.requestBody;
205
206
  const contentType = hasBody
206
- ? Object.keys(op.requestBody.content)[0] ?? "application/json"
207
+ ? (Object.keys(op.requestBody.content)[0] ?? "application/json")
207
208
  : undefined;
208
209
  const bodyExample = hasBody ? getRequestBodyExample(op) : undefined;
209
210
  const body = bodyExample ? JSON.stringify(bodyExample, null, 2) : undefined;
210
211
  const req = { url, method, contentType, body };
211
- return langs
212
- .map((lang) => generators[lang]?.(req))
213
- .filter((s) => !!s);
212
+ return langs.map((lang) => generators[lang]?.(req)).filter((s) => !!s);
214
213
  }
215
214
  // ---------------------------------------------------------------------------
216
215
  // Helpers
@@ -259,6 +258,15 @@ function rustShortcutMethod(method) {
259
258
  function csharpShortcutMethod(method) {
260
259
  return ["GET", "POST", "PUT", "DELETE", "PATCH"].includes(method);
261
260
  }
261
+ function shellSingleQuoted(value) {
262
+ return `'${value.replace(/'/g, "'\\''")}'`;
263
+ }
264
+ function rubySingleQuoted(value) {
265
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
266
+ }
267
+ function phpSingleQuoted(value) {
268
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
269
+ }
262
270
  /** Indent all lines of a multi-line string except the first. */
263
271
  function indent(str, spaces) {
264
272
  const pad = " ".repeat(spaces);
@@ -0,0 +1,10 @@
1
+ export interface SafeUrlOptions {
2
+ allowedProtocols?: readonly string[];
3
+ allowRelative?: boolean;
4
+ allowAnchor?: boolean;
5
+ }
6
+ export declare function escapeHtml(value: string): string;
7
+ export declare function escapeAttr(value: string): string;
8
+ export declare function safeUrl(input: string, options?: SafeUrlOptions): string | null;
9
+ export declare function sanitizeRenderedHtml(input: string): string;
10
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/utils/html.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,GAAG,IAAI,CAkBlF;AAUD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA2G1D"}
@@ -0,0 +1,152 @@
1
+ import sanitizeHtml from "sanitize-html";
2
+ const BASE_URL = "https://sourcey.invalid/";
3
+ const EXPLICIT_SCHEME_RE = /^[a-z][a-z\d+.-]*:/i;
4
+ export function escapeHtml(value) {
5
+ return value
6
+ .replace(/&/g, "&amp;")
7
+ .replace(/</g, "&lt;")
8
+ .replace(/>/g, "&gt;")
9
+ .replace(/"/g, "&quot;")
10
+ .replace(/'/g, "&#39;");
11
+ }
12
+ export function escapeAttr(value) {
13
+ return escapeHtml(value);
14
+ }
15
+ export function safeUrl(input, options = {}) {
16
+ const allowedProtocols = options.allowedProtocols ?? ["http:", "https:", "mailto:"];
17
+ const allowRelative = options.allowRelative ?? true;
18
+ const allowAnchor = options.allowAnchor ?? true;
19
+ const value = input.trim();
20
+ if (!value || hasRawControlOrSpace(value))
21
+ return null;
22
+ if (value.startsWith("#"))
23
+ return allowAnchor ? value : null;
24
+ if (value.startsWith("//"))
25
+ return null;
26
+ try {
27
+ const parsed = new URL(value, BASE_URL);
28
+ const hasExplicitScheme = EXPLICIT_SCHEME_RE.test(value);
29
+ if (!hasExplicitScheme)
30
+ return allowRelative ? value : null;
31
+ return allowedProtocols.includes(parsed.protocol) ? parsed.href : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ function hasRawControlOrSpace(value) {
38
+ for (const char of value) {
39
+ const code = char.charCodeAt(0);
40
+ if (code <= 0x20 || code === 0x7f)
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ export function sanitizeRenderedHtml(input) {
46
+ return sanitizeHtml(input, {
47
+ allowedTags: [
48
+ ...sanitizeHtml.defaults.allowedTags,
49
+ "button",
50
+ "details",
51
+ "iframe",
52
+ "img",
53
+ "path",
54
+ "source",
55
+ "summary",
56
+ "svg",
57
+ "video",
58
+ ],
59
+ allowedAttributes: {
60
+ "*": [
61
+ "class",
62
+ "id",
63
+ "role",
64
+ "tabindex",
65
+ "title",
66
+ "aria-expanded",
67
+ "aria-hidden",
68
+ "aria-label",
69
+ "aria-selected",
70
+ "data-clipboard-target",
71
+ "data-cols",
72
+ "data-copy-source",
73
+ "data-display",
74
+ "data-tab-group",
75
+ "data-tab-index",
76
+ "data-target",
77
+ "data-traverse-target",
78
+ ],
79
+ a: ["href", "name", "target", "rel", "title", "class", "id"],
80
+ button: [
81
+ "type",
82
+ "class",
83
+ "aria-expanded",
84
+ "aria-label",
85
+ "aria-selected",
86
+ "data-copy-source",
87
+ "data-tab-group",
88
+ "data-tab-index",
89
+ ],
90
+ div: [
91
+ "class",
92
+ "id",
93
+ "role",
94
+ "style",
95
+ "data-cols",
96
+ "data-tab-group",
97
+ "data-tab-index",
98
+ "data-traverse-target",
99
+ ],
100
+ details: ["class", "id", "open"],
101
+ h1: ["id", "class"],
102
+ h2: ["id", "class"],
103
+ h3: ["id", "class"],
104
+ h4: ["id", "class"],
105
+ h5: ["id", "class"],
106
+ h6: ["id", "class"],
107
+ iframe: ["src", "title", "frameborder", "allow", "allowfullscreen", "loading", "class", "id"],
108
+ img: ["src", "alt", "title", "width", "height", "loading", "class", "id"],
109
+ path: ["d", "fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin"],
110
+ pre: ["class", "tabindex", "style"],
111
+ source: ["src", "type"],
112
+ span: ["class", "style", "title"],
113
+ svg: [
114
+ "xmlns",
115
+ "width",
116
+ "height",
117
+ "viewBox",
118
+ "fill",
119
+ "stroke",
120
+ "stroke-width",
121
+ "stroke-linecap",
122
+ "stroke-linejoin",
123
+ "class",
124
+ "aria-hidden",
125
+ "focusable",
126
+ ],
127
+ table: ["class"],
128
+ td: ["class", "colspan", "rowspan"],
129
+ th: ["class", "colspan", "rowspan", "scope"],
130
+ video: ["controls", "preload", "title", "class", "id"],
131
+ },
132
+ allowedSchemes: ["http", "https", "mailto"],
133
+ allowedSchemesByTag: {
134
+ iframe: ["http", "https"],
135
+ img: ["http", "https"],
136
+ source: ["http", "https"],
137
+ },
138
+ allowProtocolRelative: false,
139
+ allowedStyles: {
140
+ "*": {
141
+ background: [/^transparent$/, /^#[0-9a-fA-F]{3,8}$/],
142
+ "background-color": [/^#[0-9a-fA-F]{3,8}$/],
143
+ color: [/^#[0-9a-fA-F]{3,8}$/],
144
+ height: [/^\d{1,4}px$/],
145
+ "--shiki-dark": [/^#[0-9a-fA-F]{3,8}$/],
146
+ "--shiki-dark-bg": [/^#[0-9a-fA-F]{3,8}$/],
147
+ "--shiki-light": [/^#[0-9a-fA-F]{3,8}$/],
148
+ "--shiki-light-bg": [/^#[0-9a-fA-F]{3,8}$/],
149
+ },
150
+ },
151
+ });
152
+ }
@@ -9,6 +9,4 @@ export declare function schemaRange(schema: {
9
9
  exclusiveMinimum?: number | boolean;
10
10
  exclusiveMaximum?: number | boolean;
11
11
  }): string | undefined;
12
- /** HTTP method color classes for badges */
13
- export declare function methodColor(method: string): string;
14
12
  //# sourceMappingURL=http.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/utils/http.ts"],"names":[],"mappings":"AAiDA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACrC,GAAG,MAAM,GAAG,SAAS,CA0BrB;AAED,2CAA2C;AAC3C,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAmBlD"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/utils/http.ts"],"names":[],"mappings":"AAiDA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACrC,GAAG,MAAM,GAAG,SAAS,CA4BrB"}
@@ -54,43 +54,28 @@ export function httpStatusText(code) {
54
54
  * Fixes legacy bug where maximumExclusive was checked with minimumExclusive.
55
55
  */
56
56
  export function schemaRange(schema) {
57
- const parts = [];
57
+ let min;
58
+ let max;
58
59
  if (schema.minimum !== undefined) {
59
- const exclusive = schema.exclusiveMinimum === true ||
60
- typeof schema.exclusiveMinimum === "number";
61
- const min = typeof schema.exclusiveMinimum === "number"
62
- ? schema.exclusiveMinimum
63
- : schema.minimum;
64
- parts.push(`${exclusive ? "(" : "["}${min}`);
60
+ const exclusive = schema.exclusiveMinimum === true || typeof schema.exclusiveMinimum === "number";
61
+ min = {
62
+ value: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.minimum,
63
+ exclusive,
64
+ };
65
65
  }
66
66
  if (schema.maximum !== undefined) {
67
- const exclusive = schema.exclusiveMaximum === true ||
68
- typeof schema.exclusiveMaximum === "number";
69
- const max = typeof schema.exclusiveMaximum === "number"
70
- ? schema.exclusiveMaximum
71
- : schema.maximum;
72
- parts.push(`${max}${exclusive ? ")" : "]"}`);
67
+ const exclusive = schema.exclusiveMaximum === true || typeof schema.exclusiveMaximum === "number";
68
+ max = {
69
+ value: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.maximum,
70
+ exclusive,
71
+ };
73
72
  }
74
- return parts.length === 2 ? parts.join(", ") : undefined;
75
- }
76
- /** HTTP method color classes for badges */
77
- export function methodColor(method) {
78
- switch (method.toLowerCase()) {
79
- case "get":
80
- return "bg-emerald-600";
81
- case "post":
82
- return "bg-blue-600";
83
- case "put":
84
- return "bg-amber-600";
85
- case "delete":
86
- return "bg-red-600";
87
- case "patch":
88
- return "bg-purple-600";
89
- case "options":
90
- return "bg-gray-500";
91
- case "head":
92
- return "bg-gray-600";
93
- default:
94
- return "bg-gray-500";
73
+ if (min && max) {
74
+ return `${min.exclusive ? "(" : "["}${min.value}, ${max.value}${max.exclusive ? ")" : "]"}`;
95
75
  }
76
+ if (min)
77
+ return `${min.exclusive ? ">" : ">="} ${min.value}`;
78
+ if (max)
79
+ return `${max.exclusive ? "<" : "<="} ${max.value}`;
80
+ return undefined;
96
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/utils/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,KAAK,EAAe,MAAM,QAAQ,CAAC;AAOzD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CASnE;AAED,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AA2JD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,EAAE,CAGnD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAe5D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAY5D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3D"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/utils/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,KAAK,EAAe,MAAM,QAAQ,CAAC;AAMzD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CASnE;AAED,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAkLD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,EAAE,CAGnD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAe5D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAY5D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CA2BzD;AAsBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3D"}
@@ -2,7 +2,7 @@ import { Marked } from "marked";
2
2
  import { highlightCode } from "./highlighter.js";
3
3
  import { COPY_ICON_SVG } from "./copy-svg.js";
4
4
  import { htmlId } from "./html-id.js";
5
- const MARKDOWN_LINK_RE = /\[((?:`[^`]*`|[^\]])+)\]\(([^)]+)\)/g;
5
+ import { escapeAttr, safeUrl, sanitizeRenderedHtml } from "./html.js";
6
6
  /**
7
7
  * Shared code block renderer used by all markdown rendering.
8
8
  * Outputs the prose-code-block wrapper with copy button and Shiki highlighting.
@@ -24,33 +24,46 @@ function parseDirectiveAttrs(raw) {
24
24
  attrs[k] = v;
25
25
  return attrs;
26
26
  }
27
- function escAttr(s) {
28
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
29
- }
30
- function requireHttps(url) {
31
- try {
32
- const u = new URL(url);
33
- if (u.protocol !== "https:" && u.protocol !== "http:")
34
- return null;
35
- return u.href;
27
+ function parseVideoUrl(url) {
28
+ const webUrl = safeUrl(url, {
29
+ allowedProtocols: ["http:", "https:"],
30
+ allowRelative: false,
31
+ allowAnchor: false,
32
+ });
33
+ if (webUrl) {
34
+ const parsed = new URL(webUrl);
35
+ const hostname = parsed.hostname.replace(/^www\./, "");
36
+ if (hostname === "youtube.com" || hostname === "m.youtube.com") {
37
+ const id = parsed.searchParams.get("v");
38
+ if (id && /^[\w-]+$/.test(id)) {
39
+ return { src: `https://www.youtube-nocookie.com/embed/${id}`, type: "iframe" };
40
+ }
41
+ }
42
+ if (hostname === "youtu.be") {
43
+ const id = parsed.pathname.split("/").filter(Boolean)[0];
44
+ if (id && /^[\w-]+$/.test(id)) {
45
+ return { src: `https://www.youtube-nocookie.com/embed/${id}`, type: "iframe" };
46
+ }
47
+ }
48
+ if (hostname === "vimeo.com" || hostname.endsWith(".vimeo.com")) {
49
+ const id = parsed.pathname
50
+ .split("/")
51
+ .filter(Boolean)
52
+ .find((segment) => /^\d+$/.test(segment));
53
+ if (id)
54
+ return { src: `https://player.vimeo.com/video/${id}`, type: "iframe" };
55
+ }
36
56
  }
37
- catch {
57
+ const rawVideoUrl = safeUrl(url, {
58
+ allowedProtocols: ["http:", "https:"],
59
+ allowRelative: true,
60
+ allowAnchor: false,
61
+ });
62
+ if (!rawVideoUrl)
38
63
  return null;
39
- }
40
- }
41
- function parseVideoUrl(url) {
42
- // YouTube
43
- let m = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
44
- if (m)
45
- return { src: `https://www.youtube-nocookie.com/embed/${m[1]}`, type: "iframe" };
46
- // Vimeo
47
- m = url.match(/vimeo\.com\/(\d+)/);
48
- if (m)
49
- return { src: `https://player.vimeo.com/video/${m[1]}`, type: "iframe" };
50
- // Raw video
51
- const ext = url.split(".").pop()?.toLowerCase();
64
+ const ext = rawVideoUrl.split(/[?#]/)[0]?.split(".").pop()?.toLowerCase();
52
65
  const mime = ext === "webm" ? "video/webm" : "video/mp4";
53
- return { src: url, type: "video", mime };
66
+ return { src: rawVideoUrl, type: "video", mime };
54
67
  }
55
68
  const videoExtension = {
56
69
  extensions: [
@@ -70,15 +83,17 @@ const videoExtension = {
70
83
  renderer(token) {
71
84
  const { url, title } = token;
72
85
  const parsed = parseVideoUrl(url);
73
- const safeTitle = escAttr(title);
86
+ if (!parsed)
87
+ return `<p>[video: invalid URL]</p>\n`;
88
+ const safeTitle = escapeAttr(title);
74
89
  if (parsed.type === "iframe") {
75
90
  return `<div class="prose-video not-prose">
76
- <iframe src="${parsed.src}" title="${safeTitle}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe>
91
+ <iframe src="${escapeAttr(parsed.src)}" title="${safeTitle}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe>
77
92
  </div>\n`;
78
93
  }
79
94
  return `<div class="prose-video not-prose">
80
95
  <video controls preload="metadata" title="${safeTitle}">
81
- <source src="${parsed.src}" type="${parsed.mime}" />
96
+ <source src="${escapeAttr(parsed.src)}" type="${escapeAttr(parsed.mime ?? "video/mp4")}" />
82
97
  </video>
83
98
  </div>\n`;
84
99
  },
@@ -109,11 +124,15 @@ const iframeExtension = {
109
124
  },
110
125
  renderer(token) {
111
126
  const { url, title, height } = token;
112
- const safeUrl = requireHttps(url);
113
- if (!safeUrl)
127
+ const iframeUrl = safeUrl(url, {
128
+ allowedProtocols: ["http:", "https:"],
129
+ allowRelative: false,
130
+ allowAnchor: false,
131
+ });
132
+ if (!iframeUrl)
114
133
  return `<p>[iframe: invalid URL]</p>\n`;
115
134
  return `<div class="prose-iframe not-prose" style="height:${height}px">
116
- <iframe src="${escAttr(safeUrl)}" title="${escAttr(title)}" frameborder="0" loading="lazy" allowfullscreen></iframe>
135
+ <iframe src="${escapeAttr(iframeUrl)}" title="${escapeAttr(title)}" frameborder="0" loading="lazy" allowfullscreen></iframe>
117
136
  </div>\n`;
118
137
  },
119
138
  },
@@ -141,10 +160,13 @@ const marked = new Marked(videoExtension, iframeExtension, {
141
160
  },
142
161
  link({ href, title, tokens }) {
143
162
  const text = this.parser.parseInline(tokens);
144
- const titleAttr = title ? ` title="${title}"` : "";
145
- const isExternal = href.startsWith("http://") || href.startsWith("https://");
163
+ const linkUrl = safeUrl(href);
164
+ if (!linkUrl)
165
+ return text;
166
+ const titleAttr = title ? ` title="${escapeAttr(title)}"` : "";
167
+ const isExternal = linkUrl.startsWith("http://") || linkUrl.startsWith("https://");
146
168
  const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
147
- return `<a href="${href}"${titleAttr}${target}>${text}</a>`;
169
+ return `<a href="${escapeAttr(linkUrl)}"${titleAttr}${target}>${text}</a>`;
148
170
  },
149
171
  },
150
172
  });
@@ -204,7 +226,50 @@ export function extractFirstParagraph(input) {
204
226
  export function stripMarkdownLinks(input) {
205
227
  if (!input)
206
228
  return "";
207
- return input.replace(MARKDOWN_LINK_RE, "$1");
229
+ let output = "";
230
+ let index = 0;
231
+ while (index < input.length) {
232
+ const isImage = input[index] === "!" && input[index + 1] === "[";
233
+ const isLink = input[index] === "[";
234
+ const labelStart = isImage ? index + 2 : isLink ? index + 1 : -1;
235
+ if (labelStart !== -1) {
236
+ const labelEnd = findMarkdownLabelEnd(input, labelStart);
237
+ if (labelEnd !== -1 && input[labelEnd + 1] === "(") {
238
+ const hrefEnd = findMarkdownHrefEnd(input, labelEnd + 2);
239
+ if (hrefEnd !== -1) {
240
+ output += input.slice(labelStart, labelEnd);
241
+ index = hrefEnd + 1;
242
+ continue;
243
+ }
244
+ }
245
+ }
246
+ output += input[index];
247
+ index += 1;
248
+ }
249
+ return output;
250
+ }
251
+ function findMarkdownLabelEnd(input, start) {
252
+ let inCode = false;
253
+ for (let i = start; i < input.length; i += 1) {
254
+ const char = input[i];
255
+ if (char === "`")
256
+ inCode = !inCode;
257
+ if (char === "\n")
258
+ return -1;
259
+ if (char === "]" && !inCode)
260
+ return i;
261
+ }
262
+ return -1;
263
+ }
264
+ function findMarkdownHrefEnd(input, start) {
265
+ for (let i = start; i < input.length; i += 1) {
266
+ const char = input[i];
267
+ if (char === "\n")
268
+ return -1;
269
+ if (char === ")")
270
+ return i;
271
+ }
272
+ return -1;
208
273
  }
209
274
  /**
210
275
  * Render Markdown to HTML.
@@ -212,7 +277,7 @@ export function stripMarkdownLinks(input) {
212
277
  export function renderMarkdown(input) {
213
278
  if (!input)
214
279
  return "";
215
- return marked.parse(input, { async: false });
280
+ return sanitizeRenderedHtml(marked.parse(input, { async: false }));
216
281
  }
217
282
  /**
218
283
  * Render Markdown to inline HTML (strip wrapping <p> tags).
@@ -220,5 +285,5 @@ export function renderMarkdown(input) {
220
285
  export function renderMarkdownInline(input) {
221
286
  if (!input)
222
287
  return "";
223
- return marked.parseInline(input, { async: false });
288
+ return sanitizeRenderedHtml(marked.parseInline(input, { async: false }));
224
289
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sourcey",
3
- "version": "3.6.0",
4
- "description": "Precision documentation from OpenAPI, MCP, Doxygen, godoc, and Markdown. Static HTML you own.",
3
+ "version": "3.6.1",
4
+ "description": "Precision documentation from OpenAPI, MCP, Doxygen, godoc, rustdoc, and Markdown. Static HTML you own.",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=20"
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "scripts": {
23
- "build": "tsc && cp -r src/themes dist/ && cp src/client/*.js dist/client/ && cp -r src/og/fonts dist/og/ && rm -rf dist/core/sourcey-godoc dist/core/godoc-introspect && mkdir -p dist/core/sourcey-godoc && cp -R go/sourcey-godoc/go.mod go/sourcey-godoc/doc.go go/sourcey-godoc/cmd dist/core/sourcey-godoc/",
23
+ "build": "tsc && cp -r src/themes dist/ && cp src/client/*.js dist/client/ && cp -r src/og/fonts dist/og/ && rm -rf dist/core/sourcey-godoc dist/core/godoc-introspect && mkdir -p dist/core/sourcey-godoc && cp -R go/sourcey-godoc/go.mod go/sourcey-godoc/doc.go go/sourcey-godoc/cmd dist/core/sourcey-godoc/ && rm -rf dist/core/sourcey-rustdoc && mkdir -p dist/core/sourcey-rustdoc && cp -R rust/sourcey-rustdoc/Cargo.toml rust/sourcey-rustdoc/Cargo.lock rust/sourcey-rustdoc/README.md rust/sourcey-rustdoc/src rust/sourcey-rustdoc/tests dist/core/sourcey-rustdoc/",
24
24
  "watch": "npm run build && tsc --watch --preserveWatchOutput",
25
25
  "test": "vitest run",
26
26
  "test:watch": "vitest",
@@ -37,7 +37,6 @@
37
37
  "dependencies": {
38
38
  "@apidevtools/json-schema-ref-parser": "^13.0.5",
39
39
  "@preact/preset-vite": "^2.10.4",
40
- "@readme/openapi-parser": "^4.0.0",
41
40
  "@resvg/resvg-js": "^2.6.2",
42
41
  "@tailwindcss/typography": "^0.5.19",
43
42
  "@tailwindcss/vite": "^4.2.2",
@@ -50,6 +49,7 @@
50
49
  "moxygen": "^2.1.8",
51
50
  "preact": "^10.28.4",
52
51
  "preact-render-to-string": "^6.6.6",
52
+ "sanitize-html": "^2.17.4",
53
53
  "satori": "^0.26.0",
54
54
  "shiki": "^4.0.2",
55
55
  "swagger2openapi": "^7.0.8",
@@ -57,8 +57,10 @@
57
57
  "vite": "^8.0.9"
58
58
  },
59
59
  "devDependencies": {
60
+ "@eslint/js": "^9.39.4",
60
61
  "@types/js-yaml": "^4.0.9",
61
62
  "@types/node": "^22.19.15",
63
+ "@types/sanitize-html": "^2.16.1",
62
64
  "eslint": "^9.0.0",
63
65
  "eslint-config-prettier": "^10.0.0",
64
66
  "prettier": "^3.4.0",
@@ -78,9 +80,12 @@
78
80
  "developer-documentation",
79
81
  "doxygen",
80
82
  "godoc",
83
+ "rustdoc",
81
84
  "golang",
85
+ "rust",
82
86
  "go-docs",
83
87
  "go-documentation",
88
+ "rust-docs",
84
89
  "mcp",
85
90
  "llms-txt",
86
91
  "static-docs"