@dogsbay/format-openapi 0.2.0-beta.13 → 0.2.0-beta.130

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,29 @@
1
+ /**
2
+ * The spec references an embed names, in order. Each is the string the
3
+ * viewer would request, and whether it is resolved against the page URL
4
+ * (`window.location.pathname + "../api.yaml"`, or any relative URL) or the
5
+ * site root (`/api.yaml`). URLs (`https:`) are not returned.
6
+ */
7
+ export declare function embeddedSpecRefs(html: string): string[];
8
+ export interface EmbedSpecOptions {
9
+ /** The source root; the spec must be inside it once symlinks resolve. */
10
+ srcDir: string;
11
+ /** The page's docname: `reference/api/ams-api` (no suffix). */
12
+ docname: string;
13
+ /** Directories copied to the site root (Sphinx `html_extra_path`). */
14
+ extraDirs?: readonly string[];
15
+ /** Directories served at `/_static/` (Sphinx `html_static_path`). */
16
+ staticDirs?: readonly string[];
17
+ }
18
+ /**
19
+ * The source file a spec reference loads, as a `/`-separated path relative
20
+ * to `srcDir`, or null.
21
+ *
22
+ * A relative reference is resolved against the page URL. Sphinx serves a
23
+ * page at `/reference/api/ams-api/` (dirhtml) or `/reference/api/ams-api.html`
24
+ * (html), and the two resolve `../` differently, so both are tried. The site
25
+ * path is then looked up where Sphinx would have put that file: the source
26
+ * tree, `html_extra_path` (copied to the root) and `html_static_path`
27
+ * (served at `/_static/`). Last, a file of the same name next to the page.
28
+ */
29
+ export declare function resolveEmbeddedSpec(ref: string, opts: EmbedSpecOptions): string | null;
package/dist/embed.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * A spec embedded in a page by a JavaScript API viewer: Swagger UI, Redoc or
3
+ * RapiDoc. Importers drop raw `<script>`, which leaves such a page EMPTY:
4
+ * the reference was never in the page, the viewer drew it in the browser
5
+ * from a spec file. This finds that file so the importer can emit
6
+ * `:::openapi{spec="…"}` instead, and the site build renders the spec.
7
+ *
8
+ * No dependencies (it is imported by format-rst and format-myst, which do not
9
+ * need the spec parser), and nothing is fetched: only a file in the source
10
+ * tree is ever returned.
11
+ */
12
+ import { existsSync, realpathSync, statSync } from "node:fs";
13
+ import { join, posix, relative, sep } from "node:path";
14
+ const SPEC_FILE = /\.(ya?ml|json)$/i;
15
+ /** An embed is a few hundred bytes; a larger block is not scanned. Every
16
+ * pattern below is also linear: `[^<>]` cannot run on into the next tag. */
17
+ const MAX_HTML = 64 * 1024;
18
+ /**
19
+ * The spec references an embed names, in order. Each is the string the
20
+ * viewer would request, and whether it is resolved against the page URL
21
+ * (`window.location.pathname + "../api.yaml"`, or any relative URL) or the
22
+ * site root (`/api.yaml`). URLs (`https:`) are not returned.
23
+ */
24
+ export function embeddedSpecRefs(html) {
25
+ const refs = [];
26
+ if (html.length > MAX_HTML)
27
+ return refs;
28
+ const add = (ref) => {
29
+ if (!ref)
30
+ return;
31
+ const clean = ref.trim();
32
+ if (!clean || /^[a-z][a-z0-9+.-]*:|^\/\//i.test(clean))
33
+ return;
34
+ if (!SPEC_FILE.test(clean.split(/[?#]/)[0]))
35
+ return;
36
+ if (!refs.includes(clean))
37
+ refs.push(clean);
38
+ };
39
+ // Swagger UI: SwaggerUIBundle({ url: … }) / SwaggerUI({ url: … }), and
40
+ // `urls: [{ url: … }]`. The value may concatenate location.pathname with a
41
+ // relative literal; the literal is what matters.
42
+ if (/\bSwaggerUI(?:Bundle)?\s*\(/.test(html)) {
43
+ for (const m of html.matchAll(/\burl\s*:\s*([^,\n}]+)/g)) {
44
+ const literals = [...m[1].matchAll(/(["'`])((?:(?!\1)[^\\\n]|\\.)*)\1/g)].map((l) => l[2]);
45
+ add(literals.join(""));
46
+ }
47
+ }
48
+ // Redoc: Redoc.init("spec.yaml", …) and <redoc spec-url="…">.
49
+ for (const m of html.matchAll(/\bRedoc\.init\s*\(\s*(["'`])([^"'`\n]+)\1/g))
50
+ add(m[2]);
51
+ // Redoc and RapiDoc elements.
52
+ for (const m of html.matchAll(/<(?:redoc|rapi-doc)\b[^<>]*?\bspec-url\s*=\s*(["'])([^"'<>]+)\1/gi))
53
+ add(m[2]);
54
+ return refs;
55
+ }
56
+ /**
57
+ * The source file a spec reference loads, as a `/`-separated path relative
58
+ * to `srcDir`, or null.
59
+ *
60
+ * A relative reference is resolved against the page URL. Sphinx serves a
61
+ * page at `/reference/api/ams-api/` (dirhtml) or `/reference/api/ams-api.html`
62
+ * (html), and the two resolve `../` differently, so both are tried. The site
63
+ * path is then looked up where Sphinx would have put that file: the source
64
+ * tree, `html_extra_path` (copied to the root) and `html_static_path`
65
+ * (served at `/_static/`). Last, a file of the same name next to the page.
66
+ */
67
+ export function resolveEmbeddedSpec(ref, opts) {
68
+ const path = ref.split(/[?#]/)[0].replace(/\\/g, "/");
69
+ const docname = opts.docname.replace(/\\/g, "/");
70
+ const docDir = posix.dirname(docname) === "." ? "" : posix.dirname(docname);
71
+ const pageDirs = docname === "index" || docname.endsWith("/index") ? [docDir] : [docname, docDir];
72
+ const sitePaths = [];
73
+ if (path.startsWith("/")) {
74
+ sitePaths.push(path.replace(/^\/+/, ""));
75
+ }
76
+ else {
77
+ for (const base of pageDirs) {
78
+ const joined = posix.normalize(posix.join("/", base, path));
79
+ // posix.join("/", …) clamps at the root; a reference that climbs above
80
+ // it under one URL shape is simply not that shape.
81
+ if (climbsAbove(base, path))
82
+ continue;
83
+ sitePaths.push(joined.replace(/^\/+/, ""));
84
+ }
85
+ }
86
+ const candidates = [];
87
+ for (const p of sitePaths) {
88
+ candidates.push(join(opts.srcDir, p));
89
+ for (const d of opts.extraDirs ?? [])
90
+ candidates.push(join(d, p));
91
+ if (p.startsWith("_static/")) {
92
+ for (const d of opts.staticDirs ?? [])
93
+ candidates.push(join(d, p.slice("_static/".length)));
94
+ }
95
+ }
96
+ candidates.push(join(opts.srcDir, docDir, posix.basename(path)));
97
+ let realRoot;
98
+ try {
99
+ realRoot = realpathSync(opts.srcDir);
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ for (const c of candidates) {
105
+ if (!existsSync(c))
106
+ continue;
107
+ let real;
108
+ try {
109
+ real = realpathSync(c);
110
+ if (!statSync(real).isFile())
111
+ continue;
112
+ }
113
+ catch {
114
+ continue;
115
+ }
116
+ if (!real.startsWith(realRoot + sep))
117
+ continue;
118
+ return relative(realRoot, real).split(sep).join("/");
119
+ }
120
+ return null;
121
+ }
122
+ /** Does `rel`, resolved from the directory `base`, go above the root? */
123
+ function climbsAbove(base, rel) {
124
+ let depth = base ? base.split("/").filter(Boolean).length : 0;
125
+ for (const seg of rel.split("/")) {
126
+ if (seg === "..") {
127
+ if (--depth < 0)
128
+ return true;
129
+ }
130
+ else if (seg && seg !== ".")
131
+ depth++;
132
+ }
133
+ return false;
134
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { SchemaProperty, Parameter, ResponseDef, CodeSample, EndpointProps, } from "./types.js";
2
- export { parseOpenApiSpec, detectOpenApiSource, detectSpecPath } from "./parse.js";
3
- export type { ParseOpenApiOptions } from "./parse.js";
2
+ export { parseOpenApiSpec, detectOpenApiSource, detectSpecPath, openApiEmbed, loadSpec } from "./parse.js";
3
+ export type { ParseOpenApiOptions, OpenApiEmbed, OpenApiEmbedOptions } from "./parse.js";
4
+ export { createSpecCache, directiveOperation, embedSpec, findSpec, parseSpecText, prepareSpec, specCandidates, specToPages, VALUE_TOO_LARGE, type SpecLookup, } from "./core.js";
4
5
  export { operationToSlug, slugifyTag } from "./slug.js";
5
6
  export { resolveCodeSamples } from "./code-samples.js";
6
7
  export { plugin } from "./cli.js";
7
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- export { parseOpenApiSpec, detectOpenApiSource, detectSpecPath } from "./parse.js";
1
+ export { parseOpenApiSpec, detectOpenApiSource, detectSpecPath, openApiEmbed, loadSpec } from "./parse.js";
2
+ // The Worker-safe core is also its own entry, `@dogsbay/format-openapi/core`.
3
+ export { createSpecCache, directiveOperation, embedSpec, findSpec, parseSpecText, prepareSpec, specCandidates, specToPages, VALUE_TOO_LARGE, } from "./core.js";
2
4
  export { operationToSlug, slugifyTag } from "./slug.js";
3
5
  export { resolveCodeSamples } from "./code-samples.js";
4
6
  export { plugin } from "./cli.js";
5
- //# sourceMappingURL=index.js.map
package/dist/parse.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { ExportPage, NavItem } from "@dogsbay/types";
2
+ import { type OpenApiEmbed, type OpenApiEmbedOptions, type ParseOpenApiOptions } from "./core.js";
3
+ export type { OpenApiEmbed, OpenApiEmbedOptions, ParseOpenApiOptions };
2
4
  /** Auto-detect spec location given the configured source path. */
3
5
  export declare function detectSpecPath(sourcePath: string): string | null;
4
6
  /**
@@ -7,14 +9,15 @@ export declare function detectSpecPath(sourcePath: string): string | null;
7
9
  * extension whose contents declare `openapi:`).
8
10
  */
9
11
  export declare function detectOpenApiSource(path: string): boolean;
10
- export interface ParseOpenApiOptions {
11
- /** When the importer wants nav hrefs to start with this prefix. */
12
- hrefPrefix?: string;
13
- /** Sub-path within the namespace for emitted pages, e.g. "api". */
14
- section?: string;
15
- }
16
12
  export declare function parseOpenApiSpec(sourcePath: string, options?: ParseOpenApiOptions): Promise<{
17
13
  pages: ExportPage[];
18
14
  nav: NavItem[];
19
15
  }>;
20
- //# sourceMappingURL=parse.d.ts.map
16
+ /** Read a spec file (or URL) and prepare it (core.ts `prepareSpec`). */
17
+ export declare function loadSpec(specPath: string): Promise<any>;
18
+ /**
19
+ * A whole spec as the BODY of one page (core.ts `embedSpec`), read from a
20
+ * file. Each call parses the spec; a build that embeds one spec on several
21
+ * pages uses `createSpecCache` and `embedSpec` instead.
22
+ */
23
+ export declare function openApiEmbed(specPath: string, options?: OpenApiEmbedOptions): Promise<OpenApiEmbed>;