Initial Commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { hashedAssetPath } from "../assets/assets.js";
|
||||
import type { ScalarUiOptions } from "../options/options.js";
|
||||
|
||||
/**
|
||||
* Minimal HTML escape — replaces the five characters that have special
|
||||
* meaning in HTML attribute / text contexts. Used to safely embed
|
||||
* caller-supplied title / theme / endpoint strings into the rendered
|
||||
* shell.
|
||||
*/
|
||||
export function escapeHtml(input: string): string {
|
||||
return input
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/** Inputs threaded into the rendered shell beyond the parsed options. */
|
||||
export interface RenderShellInput {
|
||||
readonly options: ScalarUiOptions;
|
||||
/** Document URL the Scalar shell points at (may carry query string). */
|
||||
readonly openapiUrl: string;
|
||||
/** Resolved Scalar runtime URL (CDN or bundled asset path). */
|
||||
readonly runtimeUrl: string;
|
||||
/** Optional per-request CSP nonce embedded on inline JSON config. */
|
||||
readonly nonce?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Scalar HTML shell.
|
||||
*
|
||||
* The shell embeds the configured `openapiUrl` into a
|
||||
* `<script id="scalar-config" type="application/json">` block (NOT
|
||||
* executable JavaScript — keeps the strict `script-src 'self'` posture
|
||||
* intact) and references the Scalar runtime via the resolved
|
||||
* `runtimeUrl` (either the upstream CDN or `<uiEndpoint>/assets/<hash>`).
|
||||
*
|
||||
* In bundled mode the local stylesheet is also referenced; in cdn mode
|
||||
* the upstream bundle ships its own styles so the local sheet is omitted.
|
||||
*
|
||||
* `nonce`, when supplied, is added to the `<script>` tag attributes so
|
||||
* hosts running nonce-based CSP can still embed the config block.
|
||||
*/
|
||||
export function renderShell(input: RenderShellInput): string {
|
||||
const { options, openapiUrl, runtimeUrl, nonce } = input;
|
||||
const config = {
|
||||
openapiUrl,
|
||||
theme: options.theme,
|
||||
layout: options.layout,
|
||||
title: options.title,
|
||||
darkMode: options.darkMode,
|
||||
hideSearch: options.hideSearch,
|
||||
defaultOpenAllTags: options.defaultOpenAllTags,
|
||||
editorOptions: options.editorOptions,
|
||||
};
|
||||
// Inside <script type="application/json"> only the literal "</script>"
|
||||
// is unsafe — JSON syntax doesn't otherwise collide with HTML. We
|
||||
// replace every "<" with its unicode escape so even string values
|
||||
// like `"foo</script>"` cannot terminate the block.
|
||||
const configJson = JSON.stringify(config).replace(/</g, "\\u003c");
|
||||
const themeAttr = escapeHtml(options.theme);
|
||||
const title = escapeHtml(options.title);
|
||||
const nonceAttr =
|
||||
nonce === undefined ? "" : ` nonce="${escapeHtml(nonce)}"`;
|
||||
const bundledCssHref = `${escapeHtml(options.uiEndpoint)}/assets/${escapeHtml(hashedAssetPath("scalar.css"))}`;
|
||||
const linkBlock =
|
||||
options.assetMode === "bundled"
|
||||
? `<link rel="stylesheet" href="${bundledCssHref}" />`
|
||||
: "";
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en" data-theme="${themeAttr}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>${title}</title>
|
||||
${linkBlock}
|
||||
</head>
|
||||
<body data-theme="${themeAttr}">
|
||||
<script id="scalar-config" type="application/json"${nonceAttr}>${configJson}</script>
|
||||
<div id="scalar-root" class="scalar-shell">
|
||||
<header class="scalar-header"><strong>${title}</strong></header>
|
||||
<main class="scalar-body">
|
||||
<p id="scalar-status" class="scalar-status">initialising…</p>
|
||||
<ul id="scalar-paths" class="scalar-paths"></ul>
|
||||
</main>
|
||||
</div>
|
||||
<script src="${escapeHtml(runtimeUrl)}"${nonceAttr} data-configuration-source="#scalar-config"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user