sourcey 3.5.10 → 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 (100) hide show
  1. package/README.md +24 -97
  2. package/dist/adapters/index.d.ts +12 -0
  3. package/dist/adapters/index.d.ts.map +1 -0
  4. package/dist/adapters/index.js +130 -0
  5. package/dist/adapters/mkdocs.d.ts +5 -0
  6. package/dist/adapters/mkdocs.d.ts.map +1 -0
  7. package/dist/adapters/mkdocs.js +367 -0
  8. package/dist/adapters/shared.d.ts +12 -0
  9. package/dist/adapters/shared.d.ts.map +1 -0
  10. package/dist/adapters/shared.js +134 -0
  11. package/dist/adapters/types.d.ts +74 -0
  12. package/dist/adapters/types.d.ts.map +1 -0
  13. package/dist/adapters/types.js +1 -0
  14. package/dist/cli.js +28 -34
  15. package/dist/client/search.js +213 -82
  16. package/dist/client/tabs.js +23 -0
  17. package/dist/components/layout/Head.js +17 -4
  18. package/dist/components/layout/TableOfContents.d.ts.map +1 -1
  19. package/dist/components/layout/TableOfContents.js +4 -2
  20. package/dist/components/openapi/Introduction.d.ts.map +1 -1
  21. package/dist/components/openapi/Introduction.js +16 -1
  22. package/dist/components/openapi/Security.d.ts.map +1 -1
  23. package/dist/components/openapi/Security.js +18 -2
  24. package/dist/config.d.ts +101 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +95 -143
  27. package/dist/core/api-rendering.d.ts +47 -0
  28. package/dist/core/api-rendering.d.ts.map +1 -0
  29. package/dist/core/api-rendering.js +76 -0
  30. package/dist/core/doxygen-loader.d.ts.map +1 -1
  31. package/dist/core/doxygen-loader.js +36 -17
  32. package/dist/core/godoc-loader.d.ts.map +1 -1
  33. package/dist/core/godoc-loader.js +17 -6
  34. package/dist/core/markdown-loader.d.ts +14 -0
  35. package/dist/core/markdown-loader.d.ts.map +1 -1
  36. package/dist/core/markdown-loader.js +70 -26
  37. package/dist/core/mcp-normalizer.d.ts.map +1 -1
  38. package/dist/core/mcp-normalizer.js +48 -34
  39. package/dist/core/navigation.d.ts.map +1 -1
  40. package/dist/core/navigation.js +8 -7
  41. package/dist/core/parser.d.ts.map +1 -1
  42. package/dist/core/parser.js +75 -4
  43. package/dist/core/rustdoc-introspector.d.ts +29 -0
  44. package/dist/core/rustdoc-introspector.d.ts.map +1 -0
  45. package/dist/core/rustdoc-introspector.js +236 -0
  46. package/dist/core/rustdoc-loader.d.ts +25 -0
  47. package/dist/core/rustdoc-loader.d.ts.map +1 -0
  48. package/dist/core/rustdoc-loader.js +469 -0
  49. package/dist/core/rustdoc-render.d.ts +76 -0
  50. package/dist/core/rustdoc-render.d.ts.map +1 -0
  51. package/dist/core/rustdoc-render.js +785 -0
  52. package/dist/core/rustdoc-types.d.ts +314 -0
  53. package/dist/core/rustdoc-types.d.ts.map +1 -0
  54. package/dist/core/rustdoc-types.js +18 -0
  55. package/dist/core/search-indexer.d.ts +10 -0
  56. package/dist/core/search-indexer.d.ts.map +1 -1
  57. package/dist/core/search-indexer.js +6 -1
  58. package/dist/core/sourcey-rustdoc/Cargo.lock +705 -0
  59. package/dist/core/sourcey-rustdoc/Cargo.toml +38 -0
  60. package/dist/core/sourcey-rustdoc/README.md +59 -0
  61. package/dist/core/sourcey-rustdoc/src/diagnostics.rs +42 -0
  62. package/dist/core/sourcey-rustdoc/src/doctest.rs +268 -0
  63. package/dist/core/sourcey-rustdoc/src/extract.rs +492 -0
  64. package/dist/core/sourcey-rustdoc/src/lib.rs +58 -0
  65. package/dist/core/sourcey-rustdoc/src/links.rs +126 -0
  66. package/dist/core/sourcey-rustdoc/src/main.rs +256 -0
  67. package/dist/core/sourcey-rustdoc/src/signature.rs +415 -0
  68. package/dist/core/sourcey-rustdoc/src/spec.rs +390 -0
  69. package/dist/core/sourcey-rustdoc/tests/format_version.rs +45 -0
  70. package/dist/dev-server.d.ts.map +1 -1
  71. package/dist/dev-server.js +109 -33
  72. package/dist/index.d.ts +4 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +26 -14
  75. package/dist/init.d.ts.map +1 -1
  76. package/dist/init.js +21 -16
  77. package/dist/renderer/changelog-feed.d.ts.map +1 -1
  78. package/dist/renderer/changelog-feed.js +27 -19
  79. package/dist/renderer/html-builder.d.ts.map +1 -1
  80. package/dist/renderer/html-builder.js +4 -3
  81. package/dist/renderer/llms.js +57 -2
  82. package/dist/site-assembly.d.ts +5 -0
  83. package/dist/site-assembly.d.ts.map +1 -1
  84. package/dist/site-assembly.js +161 -25
  85. package/dist/themes/default/sourcey.css +300 -0
  86. package/dist/utils/code-samples.d.ts +1 -0
  87. package/dist/utils/code-samples.d.ts.map +1 -1
  88. package/dist/utils/code-samples.js +30 -22
  89. package/dist/utils/html.d.ts +10 -0
  90. package/dist/utils/html.d.ts.map +1 -0
  91. package/dist/utils/html.js +152 -0
  92. package/dist/utils/http.d.ts +0 -2
  93. package/dist/utils/http.d.ts.map +1 -1
  94. package/dist/utils/http.js +19 -34
  95. package/dist/utils/markdown.d.ts.map +1 -1
  96. package/dist/utils/markdown.js +115 -39
  97. package/dist/vite-plugin.d.ts +2 -0
  98. package/dist/vite-plugin.d.ts.map +1 -1
  99. package/dist/vite-plugin.js +71 -1
  100. package/package.json +11 -5
@@ -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;AAKzD;;;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;AAgJD;;;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,6 +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
+ import { escapeAttr, safeUrl, sanitizeRenderedHtml } from "./html.js";
5
6
  /**
6
7
  * Shared code block renderer used by all markdown rendering.
7
8
  * Outputs the prose-code-block wrapper with copy button and Shiki highlighting.
@@ -23,33 +24,46 @@ function parseDirectiveAttrs(raw) {
23
24
  attrs[k] = v;
24
25
  return attrs;
25
26
  }
26
- function escAttr(s) {
27
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
28
- }
29
- function requireHttps(url) {
30
- try {
31
- const u = new URL(url);
32
- if (u.protocol !== "https:" && u.protocol !== "http:")
33
- return null;
34
- 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
+ }
35
56
  }
36
- catch {
57
+ const rawVideoUrl = safeUrl(url, {
58
+ allowedProtocols: ["http:", "https:"],
59
+ allowRelative: true,
60
+ allowAnchor: false,
61
+ });
62
+ if (!rawVideoUrl)
37
63
  return null;
38
- }
39
- }
40
- function parseVideoUrl(url) {
41
- // YouTube
42
- let m = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
43
- if (m)
44
- return { src: `https://www.youtube-nocookie.com/embed/${m[1]}`, type: "iframe" };
45
- // Vimeo
46
- m = url.match(/vimeo\.com\/(\d+)/);
47
- if (m)
48
- return { src: `https://player.vimeo.com/video/${m[1]}`, type: "iframe" };
49
- // Raw video
50
- const ext = url.split(".").pop()?.toLowerCase();
64
+ const ext = rawVideoUrl.split(/[?#]/)[0]?.split(".").pop()?.toLowerCase();
51
65
  const mime = ext === "webm" ? "video/webm" : "video/mp4";
52
- return { src: url, type: "video", mime };
66
+ return { src: rawVideoUrl, type: "video", mime };
53
67
  }
54
68
  const videoExtension = {
55
69
  extensions: [
@@ -69,15 +83,17 @@ const videoExtension = {
69
83
  renderer(token) {
70
84
  const { url, title } = token;
71
85
  const parsed = parseVideoUrl(url);
72
- const safeTitle = escAttr(title);
86
+ if (!parsed)
87
+ return `<p>[video: invalid URL]</p>\n`;
88
+ const safeTitle = escapeAttr(title);
73
89
  if (parsed.type === "iframe") {
74
90
  return `<div class="prose-video not-prose">
75
- <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>
76
92
  </div>\n`;
77
93
  }
78
94
  return `<div class="prose-video not-prose">
79
95
  <video controls preload="metadata" title="${safeTitle}">
80
- <source src="${parsed.src}" type="${parsed.mime}" />
96
+ <source src="${escapeAttr(parsed.src)}" type="${escapeAttr(parsed.mime ?? "video/mp4")}" />
81
97
  </video>
82
98
  </div>\n`;
83
99
  },
@@ -98,15 +114,25 @@ const iframeExtension = {
98
114
  return undefined;
99
115
  const attrs = parseDirectiveAttrs(match[2] ?? "");
100
116
  const height = parseInt(attrs.height ?? "", 10);
101
- return { type: "iframe", raw: match[0], url: match[1], title: attrs.title ?? "", height: Number.isFinite(height) ? height : 400 };
117
+ return {
118
+ type: "iframe",
119
+ raw: match[0],
120
+ url: match[1],
121
+ title: attrs.title ?? "",
122
+ height: Number.isFinite(height) ? height : 400,
123
+ };
102
124
  },
103
125
  renderer(token) {
104
126
  const { url, title, height } = token;
105
- const safeUrl = requireHttps(url);
106
- if (!safeUrl)
127
+ const iframeUrl = safeUrl(url, {
128
+ allowedProtocols: ["http:", "https:"],
129
+ allowRelative: false,
130
+ allowAnchor: false,
131
+ });
132
+ if (!iframeUrl)
107
133
  return `<p>[iframe: invalid URL]</p>\n`;
108
134
  return `<div class="prose-iframe not-prose" style="height:${height}px">
109
- <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>
110
136
  </div>\n`;
111
137
  },
112
138
  },
@@ -124,16 +150,23 @@ const marked = new Marked(videoExtension, iframeExtension, {
124
150
  return `<h${depth} id="${id}">${this.parser.parseInline(tokens)}</h${depth}>\n`;
125
151
  },
126
152
  table(token) {
127
- const header = token.header.map((cell) => `<th>${this.parser.parseInline(cell.tokens)}</th>`).join("");
128
- const body = token.rows.map((row) => `<tr>${row.map((cell) => `<td>${this.parser.parseInline(cell.tokens)}</td>`).join("")}</tr>`).join("\n");
153
+ const header = token.header
154
+ .map((cell) => `<th>${this.parser.parseInline(cell.tokens)}</th>`)
155
+ .join("");
156
+ const body = token.rows
157
+ .map((row) => `<tr>${row.map((cell) => `<td>${this.parser.parseInline(cell.tokens)}</td>`).join("")}</tr>`)
158
+ .join("\n");
129
159
  return `<div class="table-wrap"><table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`;
130
160
  },
131
161
  link({ href, title, tokens }) {
132
162
  const text = this.parser.parseInline(tokens);
133
- const titleAttr = title ? ` title="${title}"` : "";
134
- 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://");
135
168
  const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
136
- return `<a href="${href}"${titleAttr}${target}>${text}</a>`;
169
+ return `<a href="${escapeAttr(linkUrl)}"${titleAttr}${target}>${text}</a>`;
137
170
  },
138
171
  },
139
172
  });
@@ -193,7 +226,50 @@ export function extractFirstParagraph(input) {
193
226
  export function stripMarkdownLinks(input) {
194
227
  if (!input)
195
228
  return "";
196
- return input.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$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;
197
273
  }
198
274
  /**
199
275
  * Render Markdown to HTML.
@@ -201,7 +277,7 @@ export function stripMarkdownLinks(input) {
201
277
  export function renderMarkdown(input) {
202
278
  if (!input)
203
279
  return "";
204
- return marked.parse(input, { async: false });
280
+ return sanitizeRenderedHtml(marked.parse(input, { async: false }));
205
281
  }
206
282
  /**
207
283
  * Render Markdown to inline HTML (strip wrapping <p> tags).
@@ -209,5 +285,5 @@ export function renderMarkdown(input) {
209
285
  export function renderMarkdownInline(input) {
210
286
  if (!input)
211
287
  return "";
212
- return marked.parseInline(input, { async: false });
288
+ return sanitizeRenderedHtml(marked.parseInline(input, { async: false }));
213
289
  }
@@ -8,6 +8,8 @@ export interface SourceyPluginOptions {
8
8
  render: (url: string) => Promise<string | null>;
9
9
  /** Optional: generate search index JSON on demand */
10
10
  searchIndex?: () => Promise<string>;
11
+ /** Optional: serve generated/copied files that do not go through Vite's static handler. */
12
+ extraFile?: (outputPath: string) => Promise<string | Buffer | undefined>;
11
13
  }
12
14
  /**
13
15
  * Vite plugin for Sourcey.
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAKlD,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,iEAAiE;IACjE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChD,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACrC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CA4GnE"}
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAKlD,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,iEAAiE;IACjE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChD,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,2FAA2F;IAC3F,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;CAC1E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CA6HnE"}