/**
 * Shared parser/rewriter for workspace asset URLs embedded in an artifact
 * bundle. One implementation behind every surface that has to reason about
 * `/api/assets/...` references, so they can't drift:
 *   - mint  → enumerate the exact asset keys an artifact references (allowlist)
 *   - in-app srcdoc + headless PNG/PDF render → append the `?at=` capability token
 *   - public share page → rewrite to `/api/share/<token>/asset/...` paths
 *   - HTML export → rewrite to relative `./assets/...` paths (placeholders for
 *     anything not bundled)
 * Which rewrite applies is decided once per render by the `AssetResolution`
 * contract (artifact-asset-resolution.ts), which derives the CSP from the
 * same value.
 *
 * Matches BOTH forms that appear in bundles:
 *   - root-relative   `/api/assets/<assetId>/<filename>`
 *   - absolute        `https://host/api/assets/<assetId>/<filename>`
 * in any textual context — `<img src>`, `srcset` descriptors, CSS `url(...)`,
 * inline `style=`, and (acceptably) JS string literals. assetId is a UUID and
 * the filename segment is percent-encoded by `publicUrlFor`/`publicUrlForAssetId`
 * (`encodeAssetFilename`) to `[A-Za-z0-9._-]` + `%XX`, so the path never holds a
 * quote/space/paren even when the stored filename does (e.g. a screenshot named
 * `page(Desktop 1920x1080) (1).png`) — we can match the URL token directly
 * without parsing HTML/CSS. (Raw special chars would truncate the match at the
 * first delimiter, splicing `?at=` into a partial URL → broken sandbox images.)
 *
 * Only the IMMUTABLE two-segment scheme is handled. The negative lookahead
 * after the filename means a legacy three-segment `/api/assets/<type>/<id>/<file>`
 * URL is left untouched rather than mis-parsed (legacy is out of scope).
 */
// g1 origin (optional, host has no slash) · g2 assetId · g3 filename · g4 query (incl leading ?)
// The (?![A-Za-z0-9._%/-]) after the filename forbids ANY trailing path char so
// the engine can't backtrack the filename into a partial segment — that both
// stops mid-segment matches and leaves legacy 3-segment URLs untouched.
const ASSET_URL_RE = /(https?:\/\/[^\s"'()<>\/]+)?\/api\/assets\/([A-Za-z0-9._%-]+)\/([A-Za-z0-9._%-]+)(?![A-Za-z0-9._%/-])(\?[^\s"'()<>]*)?/g;
/**
 * Decode a URL filename segment back to the RAW stored filename — the canonical
 * IDENTITY used to match the asset index (`row.filename`), the capability-token
 * allowlist (`artifactAssetKey`), and the decoded serve-route path param. The URL
 * segment itself is percent-encoded by `encodeAssetFilename` (e.g. a screenshot
 * `page(1).png` → `page%281%29.png`); everything that reasons about asset identity
 * must use this decoded form, or it won't match the raw name the index/serve speak
 * (which is exactly the encoded-vs-raw mismatch that left special-char images 404).
 * URL emitters below keep the encoded segment. Falls back to the raw input on a
 * malformed `%` sequence (never throws).
 */
export function decodeAssetFilenameSegment(segment) {
    try {
        return decodeURIComponent(segment);
    }
    catch {
        return segment;
    }
}
/**
 * Percent-encode a RAW stored filename into the URL segment form — the single
 * inverse of {@link decodeAssetFilenameSegment} (encode on emit, decode on
 * parse). `encodeURIComponent` alone is not enough: it leaves `!~*'()`
 * unescaped, and `()` are exactly the delimiters `ASSET_URL_RE` rejects — so
 * encode those too. Result chars ∈ `[A-Za-z0-9._-]` + `%XX`, which the
 * matcher's filename class accepts; the serve route decodes the path param
 * back to the raw stored filename, so the round-trip is exact.
 */
export function encodeAssetFilename(filename) {
    return encodeURIComponent(filename).replace(/[!~*'()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
}
/** Every distinct asset referenced in the text, in first-seen order. `filename`
 *  and `key` are the DECODED (raw) identity — matchable against the asset index
 *  and token allowlist — not the percent-encoded URL segment. */
export function findArtifactAssetRefs(text) {
    const seen = new Map();
    for (const m of text.matchAll(ASSET_URL_RE)) {
        const assetId = m[2];
        const filename = decodeAssetFilenameSegment(m[3]);
        const key = `${assetId}/${filename}`;
        if (!seen.has(key))
            seen.set(key, { key, assetId, filename });
    }
    return [...seen.values()];
}
/**
 * Set the `at=<token>` capability param on every asset URL. Preserves any other
 * existing query params and **replaces** a pre-existing `at` with the fresh
 * token (so a stale or embedded `?at=` in artifact HTML can't shadow a
 * freshly-minted render/preview token). Idempotent: re-applying the same token
 * yields the same string.
 */
export function appendAssetToken(text, token) {
    const enc = encodeURIComponent(token);
    return text.replace(ASSET_URL_RE, (_full, origin, assetId, filename, query) => {
        const base = `${origin ?? ''}/api/assets/${assetId}/${filename}`;
        const params = query ? query.slice(1).split('&').filter((p) => p && !/^at=/i.test(p)) : [];
        params.push(`at=${enc}`);
        return `${base}?${params.join('&')}`;
    });
}
/**
 * Rewrite asset URLs to the public-share scheme:
 * `/api/share/<shareToken>/asset/<assetId>/<filename>`. Used by the share-mode
 * asset resolution so the document's URLs and its CSP allowlist come from the
 * same token. The filename segment stays in its encoded URL form. Idempotent:
 * the output no longer matches the `/api/assets/` grammar, and the server-side
 * share read may have already rewritten the body — re-applying is a no-op.
 * Legacy 3-segment URLs aren't matched and pass through unchanged.
 */
export function rewriteAssetsToShare(text, shareToken) {
    return text.replace(ASSET_URL_RE, (_full, _origin, assetId, filename) => `/api/share/${shareToken}/asset/${assetId}/${filename}`);
}
/** Escape text for embedding inside an SVG source string. */
function escapeXml(s) {
    return s.replace(/[&<>"']/g, (c) => c === '&' ? '&amp;' : c === '<' ? '&lt;' : c === '>' ? '&gt;' : c === '"' ? '&quot;' : '&#39;');
}
/**
 * A labeled inline placeholder for an asset the export could not bundle
 * (exporter lacks access / blob missing): a neutral framed box carrying the
 * filename, as a `data:image/svg+xml` URI so it renders wherever the original
 * URL appeared (`<img src>`, `srcset`, CSS `url(...)`) under the export's
 * no-network CSP. Every output char is `%XX`-escaped past encodeURIComponent's
 * leftovers (`!~*'()`), so the URI never carries a quote/paren/comma that
 * would terminate an attribute, CSS `url()`, or srcset candidate.
 */
export function assetPlaceholderDataUri(filename) {
    const label = filename.length > 48 ? `${filename.slice(0, 45)}…` : filename;
    const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 640 360">' +
        '<rect width="640" height="360" fill="#f4f4f4"/>' +
        '<rect x="0.5" y="0.5" width="639" height="359" fill="none" stroke="#d0d0d0"/>' +
        '<g fill="#8a8a8a" font-family="system-ui, -apple-system, sans-serif" text-anchor="middle">' +
        '<text x="320" y="168" font-size="15">Image not included in export</text>' +
        `<text x="320" y="196" font-size="13">${escapeXml(label)}</text>` +
        '</g></svg>';
    // encodeAssetFilename is the strict percent-encoder (encodeURIComponent +
    // the `!~*'()` leftovers) — exactly the delimiter-free guarantee the URI
    // needs, applied to the whole SVG payload.
    return `data:image/svg+xml,${encodeAssetFilename(svg)}`;
}
/**
 * Rewrite asset URLs for the self-contained HTML export. Refs whose key is in
 * `bundledKeys` (the assets actually written into the zip) become relative
 * `./assets/<id>/<file>` (offline-resolvable); any OTHER `/api/assets/...` ref
 * (exporter lacks access / blob missing — not bundled) becomes an explicit
 * labeled placeholder. NEVER absolutized back to the workspace: an exported
 * file must not phone home, and its no-network CSP would block the load anyway
 * (D11b). With no `bundledKeys` every ref is relativized (caller bundles all
 * of them). Legacy 3-segment URLs aren't matched and pass through unchanged.
 */
export function rewriteAssetsToRelative(text, baseDir = './assets', opts = {}) {
    const prefix = baseDir.replace(/\/+$/, '');
    return text.replace(ASSET_URL_RE, (_full, _origin, assetId, filename) => {
        // `bundledKeys` are DECODED identity keys (artifactAssetKey over the raw
        // filename), so decode the URL segment to look them up; the rewritten URL
        // itself keeps the encoded segment (the browser decodes it back to the
        // raw on-disk path in the export zip).
        const raw = decodeAssetFilenameSegment(filename);
        if (!opts.bundledKeys || opts.bundledKeys.has(`${assetId}/${raw}`)) {
            return `${prefix}/${assetId}/${filename}`;
        }
        return assetPlaceholderDataUri(raw);
    });
}
//# sourceMappingURL=artifact-asset-urls.js.map