@copilotkit/web-inspector 2.0.0-rc.10 → 2.0.0-rc.12
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.
- package/dist/index.umd.js +234 -12
- package/dist/index.umd.js.map +1 -1
- package/dist/lib/hud-config.cjs +4 -4
- package/dist/lib/hud-config.cjs.map +1 -1
- package/dist/lib/hud-config.mjs +3 -3
- package/dist/lib/hud-config.mjs.map +1 -1
- package/dist/lib/markdown.cjs +2 -2
- package/dist/lib/markdown.cjs.map +1 -1
- package/dist/lib/markdown.mjs +1 -1
- package/dist/lib/markdown.mjs.map +1 -1
- package/dist/lib/notifications.cjs +4 -4
- package/dist/lib/notifications.cjs.map +1 -1
- package/dist/lib/notifications.mjs +4 -4
- package/dist/lib/notifications.mjs.map +1 -1
- package/dist/lib/semver.cjs +224 -0
- package/dist/lib/semver.cjs.map +1 -0
- package/dist/lib/semver.mjs +221 -0
- package/dist/lib/semver.mjs.map +1 -0
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/package.json +6 -6
package/dist/lib/hud-config.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const require_semver = require('./semver.cjs');
|
|
2
2
|
|
|
3
3
|
//#region src/lib/hud-config.ts
|
|
4
4
|
const HUD_FEATURE_IDS = ["threads", "learning"];
|
|
@@ -37,7 +37,7 @@ function parseRule(value) {
|
|
|
37
37
|
"framework",
|
|
38
38
|
"sdkVersion",
|
|
39
39
|
"features"
|
|
40
|
-
]) || typeof value.framework !== "string" || !frameworkPattern.test(value.framework) || !text(value.sdkVersion, 256) ||
|
|
40
|
+
]) || typeof value.framework !== "string" || !frameworkPattern.test(value.framework) || !text(value.sdkVersion, 256) || !require_semver.isValidRange(value.sdkVersion) || !record(value.features) || !Object.keys(value.features).length) return null;
|
|
41
41
|
const features = {};
|
|
42
42
|
for (const [id, entry] of Object.entries(value.features)) {
|
|
43
43
|
if (!featureIdPattern.test(id)) return null;
|
|
@@ -68,8 +68,8 @@ function parseHudFeed(value) {
|
|
|
68
68
|
/** Return the first rule for a development client with a known stable SDK. */
|
|
69
69
|
function matchHudRule(feed, context) {
|
|
70
70
|
const { development, framework, sdkVersion } = context;
|
|
71
|
-
if (!development || !framework || !sdkVersion ||
|
|
72
|
-
return feed.rules.find((rule) => rule.framework === framework &&
|
|
71
|
+
if (!development || !framework || !sdkVersion || !require_semver.isStableVersion(sdkVersion)) return null;
|
|
72
|
+
return feed.rules.find((rule) => rule.framework === framework && require_semver.satisfies(sdkVersion, rule.sdkVersion)) ?? null;
|
|
73
73
|
}
|
|
74
74
|
/** Overlay the matching rule on the built-in content, field by field. */
|
|
75
75
|
function resolveHudContent(feed, context, options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hud-config.cjs","names":[],"sources":["../../src/lib/hud-config.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"hud-config.cjs","names":["isValidRange","isStableVersion","satisfies"],"sources":["../../src/lib/hud-config.ts"],"sourcesContent":["import { isStableVersion, isValidRange, satisfies } from \"./semver.js\";\nimport type { NotificationContext } from \"./notifications.js\";\n\n/** Versioned CDN HUD contract. Keep the Intelligence copy and fixtures aligned. */\nexport type HudFramework = \"react\" | \"vue\" | \"angular\";\nexport type HudFeatureId = \"threads\" | \"learning\";\nexport interface HudFeatureContent {\n label: string;\n description: string;\n destination: string;\n}\nexport type HudContent = Record<HudFeatureId, HudFeatureContent>;\nexport type HudFeatureOverride = Partial<HudFeatureContent>;\nexport interface HudRule {\n /** Frameworks unknown to this SDK are kept but never match. */\n framework: string;\n sdkVersion: string;\n /** Keyed by feature ID; IDs unknown to this SDK are kept but never applied. */\n features: Record<string, HudFeatureOverride>;\n}\nexport interface HudFeed {\n schemaVersion: 1;\n rules: HudRule[];\n}\nexport type HudContext = Pick<\n NotificationContext,\n \"development\" | \"framework\" | \"sdkVersion\"\n>;\nexport interface HudResolveOptions {\n defaults: HudContent;\n /** Destination keys this SDK can open; anything else keeps the default. */\n supportedDestinations: readonly string[];\n}\n\nexport const HUD_FEATURE_IDS: readonly HudFeatureId[] = [\"threads\", \"learning\"];\nexport const HUD_LABEL_MAX_LENGTH = 32;\nexport const HUD_DESCRIPTION_MAX_LENGTH = 140;\nconst frameworkPattern = /^[a-z][a-z0-9-]{0,31}$/;\nconst featureIdPattern = /^[a-z][a-z0-9-]{0,31}$/;\nconst destinationPattern = /^[a-z][a-z0-9-]{0,63}$/;\n// eslint-disable-next-line no-control-regex\nconst controlCharacters = /[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/;\n\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max: number): value is string {\n return (\n typeof value === \"string\" &&\n value.trim().length > 0 &&\n value.length <= max &&\n !controlCharacters.test(value)\n );\n}\nfunction parseFeature(value: unknown): HudFeatureOverride | null {\n if (\n !record(value) ||\n !Object.keys(value).length ||\n !onlyKeys(value, [\"label\", \"description\", \"destination\"])\n )\n return null;\n if (value.label !== undefined && !text(value.label, HUD_LABEL_MAX_LENGTH))\n return null;\n if (\n value.description !== undefined &&\n !text(value.description, HUD_DESCRIPTION_MAX_LENGTH)\n )\n return null;\n if (\n value.destination !== undefined &&\n (typeof value.destination !== \"string\" ||\n !destinationPattern.test(value.destination))\n )\n return null;\n return {\n ...(value.label === undefined ? {} : { label: value.label }),\n ...(value.description === undefined\n ? {}\n : { description: value.description }),\n ...(value.destination === undefined\n ? {}\n : { destination: value.destination }),\n };\n}\nfunction parseRule(value: unknown): HudRule | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"framework\", \"sdkVersion\", \"features\"]) ||\n typeof value.framework !== \"string\" ||\n !frameworkPattern.test(value.framework) ||\n !text(value.sdkVersion, 256) ||\n !isValidRange(value.sdkVersion) ||\n !record(value.features) ||\n !Object.keys(value.features).length\n )\n return null;\n const features: Record<string, HudFeatureOverride> = {};\n for (const [id, entry] of Object.entries(value.features)) {\n if (!featureIdPattern.test(id)) return null;\n const feature = parseFeature(entry);\n if (!feature) return null;\n features[id] = feature;\n }\n return {\n framework: value.framework,\n sdkVersion: value.sdkVersion,\n features,\n };\n}\n\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseHudFeed(value: unknown): HudFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"rules\"]) ||\n value.schemaVersion !== 1 ||\n !Array.isArray(value.rules) ||\n value.rules.length > 100\n )\n return null;\n const rules: HudRule[] = [];\n for (const entry of value.rules) {\n const rule = parseRule(entry);\n if (!rule) return null;\n rules.push(rule);\n }\n return { schemaVersion: 1, rules };\n}\n\n/** Return the first rule for a development client with a known stable SDK. */\nexport function matchHudRule(\n feed: HudFeed,\n context: HudContext,\n): HudRule | null {\n const { development, framework, sdkVersion } = context;\n if (!development || !framework || !sdkVersion || !isStableVersion(sdkVersion))\n return null;\n return (\n feed.rules.find(\n (rule) =>\n rule.framework === framework && satisfies(sdkVersion, rule.sdkVersion),\n ) ?? null\n );\n}\n\n/** Overlay the matching rule on the built-in content, field by field. */\nexport function resolveHudContent(\n feed: HudFeed | null,\n context: HudContext,\n options: HudResolveOptions,\n): HudContent {\n const rule = feed ? matchHudRule(feed, context) : null;\n const content = {} as HudContent;\n for (const id of HUD_FEATURE_IDS) {\n const fallback = options.defaults[id];\n const override = rule?.features[id];\n content[id] = {\n label: override?.label ?? fallback.label,\n description: override?.description ?? fallback.description,\n destination:\n override?.destination !== undefined &&\n options.supportedDestinations.includes(override.destination)\n ? override.destination\n : fallback.destination,\n };\n }\n return content;\n}\n"],"mappings":"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"}
|
package/dist/lib/hud-config.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isStableVersion, isValidRange, satisfies } from "./semver.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/lib/hud-config.ts
|
|
4
4
|
const HUD_FEATURE_IDS = ["threads", "learning"];
|
|
@@ -37,7 +37,7 @@ function parseRule(value) {
|
|
|
37
37
|
"framework",
|
|
38
38
|
"sdkVersion",
|
|
39
39
|
"features"
|
|
40
|
-
]) || typeof value.framework !== "string" || !frameworkPattern.test(value.framework) || !text(value.sdkVersion, 256) ||
|
|
40
|
+
]) || typeof value.framework !== "string" || !frameworkPattern.test(value.framework) || !text(value.sdkVersion, 256) || !isValidRange(value.sdkVersion) || !record(value.features) || !Object.keys(value.features).length) return null;
|
|
41
41
|
const features = {};
|
|
42
42
|
for (const [id, entry] of Object.entries(value.features)) {
|
|
43
43
|
if (!featureIdPattern.test(id)) return null;
|
|
@@ -68,7 +68,7 @@ function parseHudFeed(value) {
|
|
|
68
68
|
/** Return the first rule for a development client with a known stable SDK. */
|
|
69
69
|
function matchHudRule(feed, context) {
|
|
70
70
|
const { development, framework, sdkVersion } = context;
|
|
71
|
-
if (!development || !framework || !sdkVersion ||
|
|
71
|
+
if (!development || !framework || !sdkVersion || !isStableVersion(sdkVersion)) return null;
|
|
72
72
|
return feed.rules.find((rule) => rule.framework === framework && satisfies(sdkVersion, rule.sdkVersion)) ?? null;
|
|
73
73
|
}
|
|
74
74
|
/** Overlay the matching rule on the built-in content, field by field. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hud-config.mjs","names":[],"sources":["../../src/lib/hud-config.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"hud-config.mjs","names":[],"sources":["../../src/lib/hud-config.ts"],"sourcesContent":["import { isStableVersion, isValidRange, satisfies } from \"./semver.js\";\nimport type { NotificationContext } from \"./notifications.js\";\n\n/** Versioned CDN HUD contract. Keep the Intelligence copy and fixtures aligned. */\nexport type HudFramework = \"react\" | \"vue\" | \"angular\";\nexport type HudFeatureId = \"threads\" | \"learning\";\nexport interface HudFeatureContent {\n label: string;\n description: string;\n destination: string;\n}\nexport type HudContent = Record<HudFeatureId, HudFeatureContent>;\nexport type HudFeatureOverride = Partial<HudFeatureContent>;\nexport interface HudRule {\n /** Frameworks unknown to this SDK are kept but never match. */\n framework: string;\n sdkVersion: string;\n /** Keyed by feature ID; IDs unknown to this SDK are kept but never applied. */\n features: Record<string, HudFeatureOverride>;\n}\nexport interface HudFeed {\n schemaVersion: 1;\n rules: HudRule[];\n}\nexport type HudContext = Pick<\n NotificationContext,\n \"development\" | \"framework\" | \"sdkVersion\"\n>;\nexport interface HudResolveOptions {\n defaults: HudContent;\n /** Destination keys this SDK can open; anything else keeps the default. */\n supportedDestinations: readonly string[];\n}\n\nexport const HUD_FEATURE_IDS: readonly HudFeatureId[] = [\"threads\", \"learning\"];\nexport const HUD_LABEL_MAX_LENGTH = 32;\nexport const HUD_DESCRIPTION_MAX_LENGTH = 140;\nconst frameworkPattern = /^[a-z][a-z0-9-]{0,31}$/;\nconst featureIdPattern = /^[a-z][a-z0-9-]{0,31}$/;\nconst destinationPattern = /^[a-z][a-z0-9-]{0,63}$/;\n// eslint-disable-next-line no-control-regex\nconst controlCharacters = /[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/;\n\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max: number): value is string {\n return (\n typeof value === \"string\" &&\n value.trim().length > 0 &&\n value.length <= max &&\n !controlCharacters.test(value)\n );\n}\nfunction parseFeature(value: unknown): HudFeatureOverride | null {\n if (\n !record(value) ||\n !Object.keys(value).length ||\n !onlyKeys(value, [\"label\", \"description\", \"destination\"])\n )\n return null;\n if (value.label !== undefined && !text(value.label, HUD_LABEL_MAX_LENGTH))\n return null;\n if (\n value.description !== undefined &&\n !text(value.description, HUD_DESCRIPTION_MAX_LENGTH)\n )\n return null;\n if (\n value.destination !== undefined &&\n (typeof value.destination !== \"string\" ||\n !destinationPattern.test(value.destination))\n )\n return null;\n return {\n ...(value.label === undefined ? {} : { label: value.label }),\n ...(value.description === undefined\n ? {}\n : { description: value.description }),\n ...(value.destination === undefined\n ? {}\n : { destination: value.destination }),\n };\n}\nfunction parseRule(value: unknown): HudRule | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"framework\", \"sdkVersion\", \"features\"]) ||\n typeof value.framework !== \"string\" ||\n !frameworkPattern.test(value.framework) ||\n !text(value.sdkVersion, 256) ||\n !isValidRange(value.sdkVersion) ||\n !record(value.features) ||\n !Object.keys(value.features).length\n )\n return null;\n const features: Record<string, HudFeatureOverride> = {};\n for (const [id, entry] of Object.entries(value.features)) {\n if (!featureIdPattern.test(id)) return null;\n const feature = parseFeature(entry);\n if (!feature) return null;\n features[id] = feature;\n }\n return {\n framework: value.framework,\n sdkVersion: value.sdkVersion,\n features,\n };\n}\n\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseHudFeed(value: unknown): HudFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"rules\"]) ||\n value.schemaVersion !== 1 ||\n !Array.isArray(value.rules) ||\n value.rules.length > 100\n )\n return null;\n const rules: HudRule[] = [];\n for (const entry of value.rules) {\n const rule = parseRule(entry);\n if (!rule) return null;\n rules.push(rule);\n }\n return { schemaVersion: 1, rules };\n}\n\n/** Return the first rule for a development client with a known stable SDK. */\nexport function matchHudRule(\n feed: HudFeed,\n context: HudContext,\n): HudRule | null {\n const { development, framework, sdkVersion } = context;\n if (!development || !framework || !sdkVersion || !isStableVersion(sdkVersion))\n return null;\n return (\n feed.rules.find(\n (rule) =>\n rule.framework === framework && satisfies(sdkVersion, rule.sdkVersion),\n ) ?? null\n );\n}\n\n/** Overlay the matching rule on the built-in content, field by field. */\nexport function resolveHudContent(\n feed: HudFeed | null,\n context: HudContext,\n options: HudResolveOptions,\n): HudContent {\n const rule = feed ? matchHudRule(feed, context) : null;\n const content = {} as HudContent;\n for (const id of HUD_FEATURE_IDS) {\n const fallback = options.defaults[id];\n const override = rule?.features[id];\n content[id] = {\n label: override?.label ?? fallback.label,\n description: override?.description ?? fallback.description,\n destination:\n override?.destination !== undefined &&\n options.supportedDestinations.includes(override.destination)\n ? override.destination\n : fallback.destination,\n };\n }\n return content;\n}\n"],"mappings":"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"}
|
package/dist/lib/markdown.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
let
|
|
1
|
+
let _copilotkit_markdown = require("@copilotkit/markdown");
|
|
2
2
|
|
|
3
3
|
//#region src/lib/markdown.ts
|
|
4
4
|
const ESCAPED = {
|
|
@@ -27,7 +27,7 @@ function escapeText(value) {
|
|
|
27
27
|
* reach attributes through the caller's `linkHref` / `imageSrc` policy.
|
|
28
28
|
*/
|
|
29
29
|
function renderMarkdownToHtml(markdown, options) {
|
|
30
|
-
const state = (0,
|
|
30
|
+
const state = (0, _copilotkit_markdown.finalizeStreamingMarkdown)((0, _copilotkit_markdown.parseStreamingMarkdownChunk)((0, _copilotkit_markdown.createStreamingMarkdownParserState)({ segmenter: false }), markdown));
|
|
31
31
|
const nodes = new Map(state.nodes.map((node) => [node.id, node]));
|
|
32
32
|
const root = state.rootId === null ? void 0 : nodes.get(state.rootId);
|
|
33
33
|
if (!root || root.type !== "document") return "";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.cjs","names":[],"sources":["../../src/lib/markdown.ts"],"sourcesContent":["import {\n createStreamingMarkdownParserState,\n finalizeStreamingMarkdown,\n parseStreamingMarkdownChunk,\n} from \"@copilotkit/markdown-renderer\";\nimport type { StreamingMarkdownAstNode } from \"@copilotkit/markdown-renderer\";\n\n/** Inspector policy the HTML renderer applies to URLs and code blocks. */\nexport type MarkdownHtmlOptions = Readonly<{\n /** The href to emit for a link or autolink. Return \"#\" to neuter it. */\n linkHref: (url: string) => string;\n /** The src to emit for an image, or null to render its alt text instead. */\n imageSrc: (url: string) => string | null;\n /** The `data-copy` payload for a code block's Copy button. */\n copyPayload: (code: string) => string;\n}>;\n\ntype MarkdownNode = StreamingMarkdownAstNode;\ntype NodeOfType<T extends MarkdownNode[\"type\"]> = Extract<\n MarkdownNode,\n { type: T }\n>;\n\ntype RenderContext = Readonly<{\n nodes: ReadonlyMap<number, MarkdownNode>;\n /** Normalized Markdown source; node ranges index into it. */\n source: string;\n options: MarkdownHtmlOptions;\n}>;\n\nconst ESCAPED: Readonly<Record<string, string>> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** Escape every markup-significant character (attributes, code). */\nfunction escapeAll(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ESCAPED[char]!);\n}\n\n/**\n * Escape prose text but leave existing character references such as `&`\n * or `©` alone, as Markdown (and the previous marked renderer) does. A\n * character reference cannot open a tag, so this does not weaken escaping.\n */\nfunction escapeText(value: string): string {\n return value.replace(\n /[<>\"']|&(?!#\\d{1,7};|#[Xx][a-fA-F0-9]{1,6};|\\w+;)/g,\n (char) => ESCAPED[char]!,\n );\n}\n\n/**\n * Render a complete Markdown document to an HTML string for the What's new\n * article. The output is meant for `unsafeHTML`, so every piece of source text\n * is escaped: raw HTML in the Markdown renders as visible text, and URLs only\n * reach attributes through the caller's `linkHref` / `imageSrc` policy.\n */\nexport function renderMarkdownToHtml(\n markdown: string,\n options: MarkdownHtmlOptions,\n): string {\n const state = finalizeStreamingMarkdown(\n parseStreamingMarkdownChunk(\n createStreamingMarkdownParserState({ segmenter: false }),\n markdown,\n ),\n );\n const nodes = new Map(state.nodes.map((node) => [node.id, node]));\n const root = state.rootId === null ? undefined : nodes.get(state.rootId);\n if (!root || root.type !== \"document\") return \"\";\n return renderChildren(root.children, {\n nodes,\n source: state.source,\n options,\n });\n}\n\nfunction childNodes(ids: readonly number[], context: RenderContext) {\n return ids.flatMap((id) => {\n const node = context.nodes.get(id);\n return node ? [node] : [];\n });\n}\n\nfunction renderChildren(\n ids: readonly number[],\n context: RenderContext,\n): string {\n return childNodes(ids, context)\n .map((node) => renderNode(node, context))\n .join(\"\");\n}\n\nfunction renderNode(node: MarkdownNode, context: RenderContext): string {\n switch (node.type) {\n case \"document\":\n case \"table-row\":\n case \"table-cell\":\n return renderChildren(node.children, context);\n case \"paragraph\":\n return `<p>${renderChildren(node.children, context)}</p>\\n`;\n case \"heading\":\n return `<h${node.level}>${renderChildren(node.children, context)}</h${node.level}>\\n`;\n case \"blockquote\":\n return `<blockquote>\\n${renderChildren(node.children, context)}</blockquote>\\n`;\n case \"list\":\n return renderList(node, context);\n case \"list-item\":\n return `<li>${renderChildren(node.children, context)}</li>\\n`;\n case \"code-block\":\n return renderCodeBlock(node, context);\n case \"table\":\n return renderTable(node, context);\n case \"thematic-break\":\n return \"<hr>\\n\";\n case \"text\":\n return escapeText(node.text);\n case \"em\":\n return `<em>${renderChildren(node.children, context)}</em>`;\n case \"strong\":\n return `<strong>${renderChildren(node.children, context)}</strong>`;\n case \"strikethrough\":\n return `<del>${renderChildren(node.children, context)}</del>`;\n case \"inline-code\":\n return `<code>${escapeAll(node.text)}</code>`;\n case \"soft-break\":\n return \"\\n\";\n case \"hard-break\":\n return \"<br>\";\n case \"link\":\n return renderLink(\n node.url,\n node.title,\n renderChildren(node.children, context),\n context,\n );\n case \"autolink\":\n return renderLink(node.url, undefined, escapeText(node.text), context);\n case \"image\":\n return renderImage(node, context);\n case \"citation\":\n // The article has no footnotes; show the reference as it was typed.\n return escapeText(`[^${node.idRef}]`);\n default: {\n const unhandled: never = node;\n return unhandled;\n }\n }\n}\n\n/** A piece of list item content: one authored paragraph, or another block. */\ntype ItemPart =\n | { kind: \"paragraph\"; nodes: MarkdownNode[] }\n | { kind: \"block\"; node: MarkdownNode };\n\n/**\n * The parser keeps each list item as a single paragraph, so a blank line\n * inside an item survives only as two soft breaks in a row. Split there to\n * recover the paragraphs the author wrote.\n */\nfunction itemParts(\n item: NodeOfType<\"list-item\">,\n context: RenderContext,\n): ItemPart[] {\n return childNodes(item.children, context).flatMap((block): ItemPart[] =>\n block.type === \"paragraph\"\n ? splitAtBlankLines(childNodes(block.children, context)).map((nodes) => ({\n kind: \"paragraph\",\n nodes,\n }))\n : [{ kind: \"block\", node: block }],\n );\n}\n\nfunction splitAtBlankLines(nodes: readonly MarkdownNode[]): MarkdownNode[][] {\n const paragraphs: MarkdownNode[][] = [[]];\n let breaks: MarkdownNode[] = [];\n for (const node of nodes) {\n const isBlankText = node.type === \"text\" && node.text.trim() === \"\";\n if (node.type === \"soft-break\" || (breaks.length > 0 && isBlankText)) {\n breaks.push(node);\n continue;\n }\n const current = paragraphs[paragraphs.length - 1]!;\n if (breaks.filter((n) => n.type === \"soft-break\").length >= 2) {\n paragraphs.push([node]);\n } else {\n current.push(...breaks, node);\n }\n breaks = [];\n }\n return paragraphs.filter((paragraph) => paragraph.length > 0);\n}\n\n/**\n * A list is loose when a blank line separates two of its items or two\n * paragraphs inside one item. The blank line that ends the list does not\n * count, so a list followed by more prose still renders tight.\n */\nfunction isLooseList(\n items: readonly NodeOfType<\"list-item\">[],\n parts: readonly ItemPart[][],\n context: RenderContext,\n): boolean {\n const hasSplitItem = parts.some(\n (item) => item.filter((part) => part.kind === \"paragraph\").length > 1,\n );\n return (\n hasSplitItem ||\n items.slice(0, -1).some((item, index) => {\n const between = context.source.slice(\n item.range.start,\n items[index + 1]!.range.start,\n );\n return /\\n[ \\t]*\\n/.test(between);\n })\n );\n}\n\nconst TASK_MARKER = /^\\[([ xX])\\][ \\t]+/;\n\n/** Render inline nodes, turning a leading `[ ]` / `[x]` into a checkbox. */\nfunction renderTaskAwareInline(\n nodes: readonly MarkdownNode[],\n context: RenderContext,\n): string {\n const [first, ...rest] = nodes;\n const task = first?.type === \"text\" ? TASK_MARKER.exec(first.text) : null;\n if (!first || first.type !== \"text\" || !task) {\n return nodes.map((node) => renderNode(node, context)).join(\"\");\n }\n const checked = task[1] === \" \" ? \"\" : 'checked=\"\" ';\n const label = escapeText(first.text.slice(task[0].length));\n const tail = rest.map((node) => renderNode(node, context)).join(\"\");\n return `<input ${checked}disabled=\"\" type=\"checkbox\"> ${label}${tail}`;\n}\n\nfunction renderList(node: NodeOfType<\"list\">, context: RenderContext): string {\n const tag = node.ordered ? \"ol\" : \"ul\";\n const start =\n node.ordered && node.start !== null && node.start !== 1\n ? ` start=\"${node.start}\"`\n : \"\";\n const items = childNodes(node.children, context).filter(\n (item): item is NodeOfType<\"list-item\"> => item.type === \"list-item\",\n );\n const parts = items.map((item) => itemParts(item, context));\n const loose = isLooseList(items, parts, context);\n const body = parts\n .map((itemContent) => {\n const content = itemContent\n .map((part, index) => {\n if (part.kind === \"block\") return renderNode(part.node, context);\n const inline =\n index === 0\n ? renderTaskAwareInline(part.nodes, context)\n : part.nodes.map((n) => renderNode(n, context)).join(\"\");\n return loose ? `<p>${inline}</p>\\n` : inline;\n })\n .join(\"\");\n return `<li>${content}</li>\\n`;\n })\n .join(\"\");\n return `<${tag}${start}>\\n${body}</${tag}>\\n`;\n}\n\nfunction renderCodeBlock(\n node: NodeOfType<\"code-block\">,\n context: RenderContext,\n): string {\n const language = (node.info ?? \"\").replace(/[^a-z0-9-]/gi, \"\");\n const languageClass = language ? ` class=\"language-${language}\"` : \"\";\n const payload = escapeAll(context.options.copyPayload(node.text));\n return `<div class=\"announcement-code\"><pre><code${languageClass}>${escapeAll(node.text)}</code></pre><div class=\"announcement-code__copy-shield\"><button type=\"button\" class=\"announcement-code__copy\" data-copy=\"${payload}\" aria-label=\"Copy code\">Copy</button></div></div>`;\n}\n\nfunction renderTable(\n node: NodeOfType<\"table\">,\n context: RenderContext,\n): string {\n const rows = childNodes(node.children, context).filter(\n (row): row is NodeOfType<\"table-row\"> => row.type === \"table-row\",\n );\n const renderRow = (row: NodeOfType<\"table-row\">) => {\n const tag = row.isHeader ? \"th\" : \"td\";\n const cells = childNodes(row.children, context)\n .map((cell, index) => {\n const align = node.align[index];\n const alignAttr = align && align !== \"none\" ? ` align=\"${align}\"` : \"\";\n return `<${tag}${alignAttr}>${renderNode(cell, context)}</${tag}>\\n`;\n })\n .join(\"\");\n return `<tr>\\n${cells}</tr>\\n`;\n };\n const header = rows.filter((row) => row.isHeader).map(renderRow);\n const body = rows.filter((row) => !row.isHeader).map(renderRow);\n return `<table>\\n<thead>\\n${header.join(\"\")}</thead>\\n${\n body.length > 0 ? `<tbody>${body.join(\"\")}</tbody>` : \"\"\n }</table>\\n`;\n}\n\nfunction renderImage(\n node: NodeOfType<\"image\">,\n context: RenderContext,\n): string {\n const src = context.options.imageSrc(node.url);\n if (src === null) return escapeText(node.alt);\n const title = node.title ? ` title=\"${escapeAll(node.title)}\"` : \"\";\n return `<img src=\"${escapeAll(src)}\" alt=\"${escapeAll(node.alt)}\"${title}>`;\n}\n\nfunction renderLink(\n url: string,\n title: string | undefined,\n content: string,\n context: RenderContext,\n): string {\n const href = escapeAll(context.options.linkHref(url));\n const titleAttr = title ? ` title=\"${escapeAll(title)}\"` : \"\";\n return `<a href=\"${href}\" target=\"_blank\" rel=\"noopener\"${titleAttr}>${content}</a>`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"markdown.cjs","names":[],"sources":["../../src/lib/markdown.ts"],"sourcesContent":["import {\n createStreamingMarkdownParserState,\n finalizeStreamingMarkdown,\n parseStreamingMarkdownChunk,\n} from \"@copilotkit/markdown\";\nimport type { StreamingMarkdownAstNode } from \"@copilotkit/markdown\";\n\n/** Inspector policy the HTML renderer applies to URLs and code blocks. */\nexport type MarkdownHtmlOptions = Readonly<{\n /** The href to emit for a link or autolink. Return \"#\" to neuter it. */\n linkHref: (url: string) => string;\n /** The src to emit for an image, or null to render its alt text instead. */\n imageSrc: (url: string) => string | null;\n /** The `data-copy` payload for a code block's Copy button. */\n copyPayload: (code: string) => string;\n}>;\n\ntype MarkdownNode = StreamingMarkdownAstNode;\ntype NodeOfType<T extends MarkdownNode[\"type\"]> = Extract<\n MarkdownNode,\n { type: T }\n>;\n\ntype RenderContext = Readonly<{\n nodes: ReadonlyMap<number, MarkdownNode>;\n /** Normalized Markdown source; node ranges index into it. */\n source: string;\n options: MarkdownHtmlOptions;\n}>;\n\nconst ESCAPED: Readonly<Record<string, string>> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** Escape every markup-significant character (attributes, code). */\nfunction escapeAll(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ESCAPED[char]!);\n}\n\n/**\n * Escape prose text but leave existing character references such as `&`\n * or `©` alone, as Markdown (and the previous marked renderer) does. A\n * character reference cannot open a tag, so this does not weaken escaping.\n */\nfunction escapeText(value: string): string {\n return value.replace(\n /[<>\"']|&(?!#\\d{1,7};|#[Xx][a-fA-F0-9]{1,6};|\\w+;)/g,\n (char) => ESCAPED[char]!,\n );\n}\n\n/**\n * Render a complete Markdown document to an HTML string for the What's new\n * article. The output is meant for `unsafeHTML`, so every piece of source text\n * is escaped: raw HTML in the Markdown renders as visible text, and URLs only\n * reach attributes through the caller's `linkHref` / `imageSrc` policy.\n */\nexport function renderMarkdownToHtml(\n markdown: string,\n options: MarkdownHtmlOptions,\n): string {\n const state = finalizeStreamingMarkdown(\n parseStreamingMarkdownChunk(\n createStreamingMarkdownParserState({ segmenter: false }),\n markdown,\n ),\n );\n const nodes = new Map(state.nodes.map((node) => [node.id, node]));\n const root = state.rootId === null ? undefined : nodes.get(state.rootId);\n if (!root || root.type !== \"document\") return \"\";\n return renderChildren(root.children, {\n nodes,\n source: state.source,\n options,\n });\n}\n\nfunction childNodes(ids: readonly number[], context: RenderContext) {\n return ids.flatMap((id) => {\n const node = context.nodes.get(id);\n return node ? [node] : [];\n });\n}\n\nfunction renderChildren(\n ids: readonly number[],\n context: RenderContext,\n): string {\n return childNodes(ids, context)\n .map((node) => renderNode(node, context))\n .join(\"\");\n}\n\nfunction renderNode(node: MarkdownNode, context: RenderContext): string {\n switch (node.type) {\n case \"document\":\n case \"table-row\":\n case \"table-cell\":\n return renderChildren(node.children, context);\n case \"paragraph\":\n return `<p>${renderChildren(node.children, context)}</p>\\n`;\n case \"heading\":\n return `<h${node.level}>${renderChildren(node.children, context)}</h${node.level}>\\n`;\n case \"blockquote\":\n return `<blockquote>\\n${renderChildren(node.children, context)}</blockquote>\\n`;\n case \"list\":\n return renderList(node, context);\n case \"list-item\":\n return `<li>${renderChildren(node.children, context)}</li>\\n`;\n case \"code-block\":\n return renderCodeBlock(node, context);\n case \"table\":\n return renderTable(node, context);\n case \"thematic-break\":\n return \"<hr>\\n\";\n case \"text\":\n return escapeText(node.text);\n case \"em\":\n return `<em>${renderChildren(node.children, context)}</em>`;\n case \"strong\":\n return `<strong>${renderChildren(node.children, context)}</strong>`;\n case \"strikethrough\":\n return `<del>${renderChildren(node.children, context)}</del>`;\n case \"inline-code\":\n return `<code>${escapeAll(node.text)}</code>`;\n case \"soft-break\":\n return \"\\n\";\n case \"hard-break\":\n return \"<br>\";\n case \"link\":\n return renderLink(\n node.url,\n node.title,\n renderChildren(node.children, context),\n context,\n );\n case \"autolink\":\n return renderLink(node.url, undefined, escapeText(node.text), context);\n case \"image\":\n return renderImage(node, context);\n case \"citation\":\n // The article has no footnotes; show the reference as it was typed.\n return escapeText(`[^${node.idRef}]`);\n default: {\n const unhandled: never = node;\n return unhandled;\n }\n }\n}\n\n/** A piece of list item content: one authored paragraph, or another block. */\ntype ItemPart =\n | { kind: \"paragraph\"; nodes: MarkdownNode[] }\n | { kind: \"block\"; node: MarkdownNode };\n\n/**\n * The parser keeps each list item as a single paragraph, so a blank line\n * inside an item survives only as two soft breaks in a row. Split there to\n * recover the paragraphs the author wrote.\n */\nfunction itemParts(\n item: NodeOfType<\"list-item\">,\n context: RenderContext,\n): ItemPart[] {\n return childNodes(item.children, context).flatMap((block): ItemPart[] =>\n block.type === \"paragraph\"\n ? splitAtBlankLines(childNodes(block.children, context)).map((nodes) => ({\n kind: \"paragraph\",\n nodes,\n }))\n : [{ kind: \"block\", node: block }],\n );\n}\n\nfunction splitAtBlankLines(nodes: readonly MarkdownNode[]): MarkdownNode[][] {\n const paragraphs: MarkdownNode[][] = [[]];\n let breaks: MarkdownNode[] = [];\n for (const node of nodes) {\n const isBlankText = node.type === \"text\" && node.text.trim() === \"\";\n if (node.type === \"soft-break\" || (breaks.length > 0 && isBlankText)) {\n breaks.push(node);\n continue;\n }\n const current = paragraphs[paragraphs.length - 1]!;\n if (breaks.filter((n) => n.type === \"soft-break\").length >= 2) {\n paragraphs.push([node]);\n } else {\n current.push(...breaks, node);\n }\n breaks = [];\n }\n return paragraphs.filter((paragraph) => paragraph.length > 0);\n}\n\n/**\n * A list is loose when a blank line separates two of its items or two\n * paragraphs inside one item. The blank line that ends the list does not\n * count, so a list followed by more prose still renders tight.\n */\nfunction isLooseList(\n items: readonly NodeOfType<\"list-item\">[],\n parts: readonly ItemPart[][],\n context: RenderContext,\n): boolean {\n const hasSplitItem = parts.some(\n (item) => item.filter((part) => part.kind === \"paragraph\").length > 1,\n );\n return (\n hasSplitItem ||\n items.slice(0, -1).some((item, index) => {\n const between = context.source.slice(\n item.range.start,\n items[index + 1]!.range.start,\n );\n return /\\n[ \\t]*\\n/.test(between);\n })\n );\n}\n\nconst TASK_MARKER = /^\\[([ xX])\\][ \\t]+/;\n\n/** Render inline nodes, turning a leading `[ ]` / `[x]` into a checkbox. */\nfunction renderTaskAwareInline(\n nodes: readonly MarkdownNode[],\n context: RenderContext,\n): string {\n const [first, ...rest] = nodes;\n const task = first?.type === \"text\" ? TASK_MARKER.exec(first.text) : null;\n if (!first || first.type !== \"text\" || !task) {\n return nodes.map((node) => renderNode(node, context)).join(\"\");\n }\n const checked = task[1] === \" \" ? \"\" : 'checked=\"\" ';\n const label = escapeText(first.text.slice(task[0].length));\n const tail = rest.map((node) => renderNode(node, context)).join(\"\");\n return `<input ${checked}disabled=\"\" type=\"checkbox\"> ${label}${tail}`;\n}\n\nfunction renderList(node: NodeOfType<\"list\">, context: RenderContext): string {\n const tag = node.ordered ? \"ol\" : \"ul\";\n const start =\n node.ordered && node.start !== null && node.start !== 1\n ? ` start=\"${node.start}\"`\n : \"\";\n const items = childNodes(node.children, context).filter(\n (item): item is NodeOfType<\"list-item\"> => item.type === \"list-item\",\n );\n const parts = items.map((item) => itemParts(item, context));\n const loose = isLooseList(items, parts, context);\n const body = parts\n .map((itemContent) => {\n const content = itemContent\n .map((part, index) => {\n if (part.kind === \"block\") return renderNode(part.node, context);\n const inline =\n index === 0\n ? renderTaskAwareInline(part.nodes, context)\n : part.nodes.map((n) => renderNode(n, context)).join(\"\");\n return loose ? `<p>${inline}</p>\\n` : inline;\n })\n .join(\"\");\n return `<li>${content}</li>\\n`;\n })\n .join(\"\");\n return `<${tag}${start}>\\n${body}</${tag}>\\n`;\n}\n\nfunction renderCodeBlock(\n node: NodeOfType<\"code-block\">,\n context: RenderContext,\n): string {\n const language = (node.info ?? \"\").replace(/[^a-z0-9-]/gi, \"\");\n const languageClass = language ? ` class=\"language-${language}\"` : \"\";\n const payload = escapeAll(context.options.copyPayload(node.text));\n return `<div class=\"announcement-code\"><pre><code${languageClass}>${escapeAll(node.text)}</code></pre><div class=\"announcement-code__copy-shield\"><button type=\"button\" class=\"announcement-code__copy\" data-copy=\"${payload}\" aria-label=\"Copy code\">Copy</button></div></div>`;\n}\n\nfunction renderTable(\n node: NodeOfType<\"table\">,\n context: RenderContext,\n): string {\n const rows = childNodes(node.children, context).filter(\n (row): row is NodeOfType<\"table-row\"> => row.type === \"table-row\",\n );\n const renderRow = (row: NodeOfType<\"table-row\">) => {\n const tag = row.isHeader ? \"th\" : \"td\";\n const cells = childNodes(row.children, context)\n .map((cell, index) => {\n const align = node.align[index];\n const alignAttr = align && align !== \"none\" ? ` align=\"${align}\"` : \"\";\n return `<${tag}${alignAttr}>${renderNode(cell, context)}</${tag}>\\n`;\n })\n .join(\"\");\n return `<tr>\\n${cells}</tr>\\n`;\n };\n const header = rows.filter((row) => row.isHeader).map(renderRow);\n const body = rows.filter((row) => !row.isHeader).map(renderRow);\n return `<table>\\n<thead>\\n${header.join(\"\")}</thead>\\n${\n body.length > 0 ? `<tbody>${body.join(\"\")}</tbody>` : \"\"\n }</table>\\n`;\n}\n\nfunction renderImage(\n node: NodeOfType<\"image\">,\n context: RenderContext,\n): string {\n const src = context.options.imageSrc(node.url);\n if (src === null) return escapeText(node.alt);\n const title = node.title ? ` title=\"${escapeAll(node.title)}\"` : \"\";\n return `<img src=\"${escapeAll(src)}\" alt=\"${escapeAll(node.alt)}\"${title}>`;\n}\n\nfunction renderLink(\n url: string,\n title: string | undefined,\n content: string,\n context: RenderContext,\n): string {\n const href = escapeAll(context.options.linkHref(url));\n const titleAttr = title ? ` title=\"${escapeAll(title)}\"` : \"\";\n return `<a href=\"${href}\" target=\"_blank\" rel=\"noopener\"${titleAttr}>${content}</a>`;\n}\n"],"mappings":"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"}
|
package/dist/lib/markdown.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createStreamingMarkdownParserState, finalizeStreamingMarkdown, parseStreamingMarkdownChunk } from "@copilotkit/markdown
|
|
1
|
+
import { createStreamingMarkdownParserState, finalizeStreamingMarkdown, parseStreamingMarkdownChunk } from "@copilotkit/markdown";
|
|
2
2
|
|
|
3
3
|
//#region src/lib/markdown.ts
|
|
4
4
|
const ESCAPED = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.mjs","names":[],"sources":["../../src/lib/markdown.ts"],"sourcesContent":["import {\n createStreamingMarkdownParserState,\n finalizeStreamingMarkdown,\n parseStreamingMarkdownChunk,\n} from \"@copilotkit/markdown-renderer\";\nimport type { StreamingMarkdownAstNode } from \"@copilotkit/markdown-renderer\";\n\n/** Inspector policy the HTML renderer applies to URLs and code blocks. */\nexport type MarkdownHtmlOptions = Readonly<{\n /** The href to emit for a link or autolink. Return \"#\" to neuter it. */\n linkHref: (url: string) => string;\n /** The src to emit for an image, or null to render its alt text instead. */\n imageSrc: (url: string) => string | null;\n /** The `data-copy` payload for a code block's Copy button. */\n copyPayload: (code: string) => string;\n}>;\n\ntype MarkdownNode = StreamingMarkdownAstNode;\ntype NodeOfType<T extends MarkdownNode[\"type\"]> = Extract<\n MarkdownNode,\n { type: T }\n>;\n\ntype RenderContext = Readonly<{\n nodes: ReadonlyMap<number, MarkdownNode>;\n /** Normalized Markdown source; node ranges index into it. */\n source: string;\n options: MarkdownHtmlOptions;\n}>;\n\nconst ESCAPED: Readonly<Record<string, string>> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** Escape every markup-significant character (attributes, code). */\nfunction escapeAll(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ESCAPED[char]!);\n}\n\n/**\n * Escape prose text but leave existing character references such as `&`\n * or `©` alone, as Markdown (and the previous marked renderer) does. A\n * character reference cannot open a tag, so this does not weaken escaping.\n */\nfunction escapeText(value: string): string {\n return value.replace(\n /[<>\"']|&(?!#\\d{1,7};|#[Xx][a-fA-F0-9]{1,6};|\\w+;)/g,\n (char) => ESCAPED[char]!,\n );\n}\n\n/**\n * Render a complete Markdown document to an HTML string for the What's new\n * article. The output is meant for `unsafeHTML`, so every piece of source text\n * is escaped: raw HTML in the Markdown renders as visible text, and URLs only\n * reach attributes through the caller's `linkHref` / `imageSrc` policy.\n */\nexport function renderMarkdownToHtml(\n markdown: string,\n options: MarkdownHtmlOptions,\n): string {\n const state = finalizeStreamingMarkdown(\n parseStreamingMarkdownChunk(\n createStreamingMarkdownParserState({ segmenter: false }),\n markdown,\n ),\n );\n const nodes = new Map(state.nodes.map((node) => [node.id, node]));\n const root = state.rootId === null ? undefined : nodes.get(state.rootId);\n if (!root || root.type !== \"document\") return \"\";\n return renderChildren(root.children, {\n nodes,\n source: state.source,\n options,\n });\n}\n\nfunction childNodes(ids: readonly number[], context: RenderContext) {\n return ids.flatMap((id) => {\n const node = context.nodes.get(id);\n return node ? [node] : [];\n });\n}\n\nfunction renderChildren(\n ids: readonly number[],\n context: RenderContext,\n): string {\n return childNodes(ids, context)\n .map((node) => renderNode(node, context))\n .join(\"\");\n}\n\nfunction renderNode(node: MarkdownNode, context: RenderContext): string {\n switch (node.type) {\n case \"document\":\n case \"table-row\":\n case \"table-cell\":\n return renderChildren(node.children, context);\n case \"paragraph\":\n return `<p>${renderChildren(node.children, context)}</p>\\n`;\n case \"heading\":\n return `<h${node.level}>${renderChildren(node.children, context)}</h${node.level}>\\n`;\n case \"blockquote\":\n return `<blockquote>\\n${renderChildren(node.children, context)}</blockquote>\\n`;\n case \"list\":\n return renderList(node, context);\n case \"list-item\":\n return `<li>${renderChildren(node.children, context)}</li>\\n`;\n case \"code-block\":\n return renderCodeBlock(node, context);\n case \"table\":\n return renderTable(node, context);\n case \"thematic-break\":\n return \"<hr>\\n\";\n case \"text\":\n return escapeText(node.text);\n case \"em\":\n return `<em>${renderChildren(node.children, context)}</em>`;\n case \"strong\":\n return `<strong>${renderChildren(node.children, context)}</strong>`;\n case \"strikethrough\":\n return `<del>${renderChildren(node.children, context)}</del>`;\n case \"inline-code\":\n return `<code>${escapeAll(node.text)}</code>`;\n case \"soft-break\":\n return \"\\n\";\n case \"hard-break\":\n return \"<br>\";\n case \"link\":\n return renderLink(\n node.url,\n node.title,\n renderChildren(node.children, context),\n context,\n );\n case \"autolink\":\n return renderLink(node.url, undefined, escapeText(node.text), context);\n case \"image\":\n return renderImage(node, context);\n case \"citation\":\n // The article has no footnotes; show the reference as it was typed.\n return escapeText(`[^${node.idRef}]`);\n default: {\n const unhandled: never = node;\n return unhandled;\n }\n }\n}\n\n/** A piece of list item content: one authored paragraph, or another block. */\ntype ItemPart =\n | { kind: \"paragraph\"; nodes: MarkdownNode[] }\n | { kind: \"block\"; node: MarkdownNode };\n\n/**\n * The parser keeps each list item as a single paragraph, so a blank line\n * inside an item survives only as two soft breaks in a row. Split there to\n * recover the paragraphs the author wrote.\n */\nfunction itemParts(\n item: NodeOfType<\"list-item\">,\n context: RenderContext,\n): ItemPart[] {\n return childNodes(item.children, context).flatMap((block): ItemPart[] =>\n block.type === \"paragraph\"\n ? splitAtBlankLines(childNodes(block.children, context)).map((nodes) => ({\n kind: \"paragraph\",\n nodes,\n }))\n : [{ kind: \"block\", node: block }],\n );\n}\n\nfunction splitAtBlankLines(nodes: readonly MarkdownNode[]): MarkdownNode[][] {\n const paragraphs: MarkdownNode[][] = [[]];\n let breaks: MarkdownNode[] = [];\n for (const node of nodes) {\n const isBlankText = node.type === \"text\" && node.text.trim() === \"\";\n if (node.type === \"soft-break\" || (breaks.length > 0 && isBlankText)) {\n breaks.push(node);\n continue;\n }\n const current = paragraphs[paragraphs.length - 1]!;\n if (breaks.filter((n) => n.type === \"soft-break\").length >= 2) {\n paragraphs.push([node]);\n } else {\n current.push(...breaks, node);\n }\n breaks = [];\n }\n return paragraphs.filter((paragraph) => paragraph.length > 0);\n}\n\n/**\n * A list is loose when a blank line separates two of its items or two\n * paragraphs inside one item. The blank line that ends the list does not\n * count, so a list followed by more prose still renders tight.\n */\nfunction isLooseList(\n items: readonly NodeOfType<\"list-item\">[],\n parts: readonly ItemPart[][],\n context: RenderContext,\n): boolean {\n const hasSplitItem = parts.some(\n (item) => item.filter((part) => part.kind === \"paragraph\").length > 1,\n );\n return (\n hasSplitItem ||\n items.slice(0, -1).some((item, index) => {\n const between = context.source.slice(\n item.range.start,\n items[index + 1]!.range.start,\n );\n return /\\n[ \\t]*\\n/.test(between);\n })\n );\n}\n\nconst TASK_MARKER = /^\\[([ xX])\\][ \\t]+/;\n\n/** Render inline nodes, turning a leading `[ ]` / `[x]` into a checkbox. */\nfunction renderTaskAwareInline(\n nodes: readonly MarkdownNode[],\n context: RenderContext,\n): string {\n const [first, ...rest] = nodes;\n const task = first?.type === \"text\" ? TASK_MARKER.exec(first.text) : null;\n if (!first || first.type !== \"text\" || !task) {\n return nodes.map((node) => renderNode(node, context)).join(\"\");\n }\n const checked = task[1] === \" \" ? \"\" : 'checked=\"\" ';\n const label = escapeText(first.text.slice(task[0].length));\n const tail = rest.map((node) => renderNode(node, context)).join(\"\");\n return `<input ${checked}disabled=\"\" type=\"checkbox\"> ${label}${tail}`;\n}\n\nfunction renderList(node: NodeOfType<\"list\">, context: RenderContext): string {\n const tag = node.ordered ? \"ol\" : \"ul\";\n const start =\n node.ordered && node.start !== null && node.start !== 1\n ? ` start=\"${node.start}\"`\n : \"\";\n const items = childNodes(node.children, context).filter(\n (item): item is NodeOfType<\"list-item\"> => item.type === \"list-item\",\n );\n const parts = items.map((item) => itemParts(item, context));\n const loose = isLooseList(items, parts, context);\n const body = parts\n .map((itemContent) => {\n const content = itemContent\n .map((part, index) => {\n if (part.kind === \"block\") return renderNode(part.node, context);\n const inline =\n index === 0\n ? renderTaskAwareInline(part.nodes, context)\n : part.nodes.map((n) => renderNode(n, context)).join(\"\");\n return loose ? `<p>${inline}</p>\\n` : inline;\n })\n .join(\"\");\n return `<li>${content}</li>\\n`;\n })\n .join(\"\");\n return `<${tag}${start}>\\n${body}</${tag}>\\n`;\n}\n\nfunction renderCodeBlock(\n node: NodeOfType<\"code-block\">,\n context: RenderContext,\n): string {\n const language = (node.info ?? \"\").replace(/[^a-z0-9-]/gi, \"\");\n const languageClass = language ? ` class=\"language-${language}\"` : \"\";\n const payload = escapeAll(context.options.copyPayload(node.text));\n return `<div class=\"announcement-code\"><pre><code${languageClass}>${escapeAll(node.text)}</code></pre><div class=\"announcement-code__copy-shield\"><button type=\"button\" class=\"announcement-code__copy\" data-copy=\"${payload}\" aria-label=\"Copy code\">Copy</button></div></div>`;\n}\n\nfunction renderTable(\n node: NodeOfType<\"table\">,\n context: RenderContext,\n): string {\n const rows = childNodes(node.children, context).filter(\n (row): row is NodeOfType<\"table-row\"> => row.type === \"table-row\",\n );\n const renderRow = (row: NodeOfType<\"table-row\">) => {\n const tag = row.isHeader ? \"th\" : \"td\";\n const cells = childNodes(row.children, context)\n .map((cell, index) => {\n const align = node.align[index];\n const alignAttr = align && align !== \"none\" ? ` align=\"${align}\"` : \"\";\n return `<${tag}${alignAttr}>${renderNode(cell, context)}</${tag}>\\n`;\n })\n .join(\"\");\n return `<tr>\\n${cells}</tr>\\n`;\n };\n const header = rows.filter((row) => row.isHeader).map(renderRow);\n const body = rows.filter((row) => !row.isHeader).map(renderRow);\n return `<table>\\n<thead>\\n${header.join(\"\")}</thead>\\n${\n body.length > 0 ? `<tbody>${body.join(\"\")}</tbody>` : \"\"\n }</table>\\n`;\n}\n\nfunction renderImage(\n node: NodeOfType<\"image\">,\n context: RenderContext,\n): string {\n const src = context.options.imageSrc(node.url);\n if (src === null) return escapeText(node.alt);\n const title = node.title ? ` title=\"${escapeAll(node.title)}\"` : \"\";\n return `<img src=\"${escapeAll(src)}\" alt=\"${escapeAll(node.alt)}\"${title}>`;\n}\n\nfunction renderLink(\n url: string,\n title: string | undefined,\n content: string,\n context: RenderContext,\n): string {\n const href = escapeAll(context.options.linkHref(url));\n const titleAttr = title ? ` title=\"${escapeAll(title)}\"` : \"\";\n return `<a href=\"${href}\" target=\"_blank\" rel=\"noopener\"${titleAttr}>${content}</a>`;\n}\n"],"mappings":";;;AA8BA,MAAM,UAA4C;CAChD,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAK;CACL,KAAK;CACN;;AAGD,SAAS,UAAU,OAAuB;AACxC,QAAO,MAAM,QAAQ,aAAa,SAAS,QAAQ,MAAO;;;;;;;AAQ5D,SAAS,WAAW,OAAuB;AACzC,QAAO,MAAM,QACX,uDACC,SAAS,QAAQ,MACnB;;;;;;;;AASH,SAAgB,qBACd,UACA,SACQ;CACR,MAAM,QAAQ,0BACZ,4BACE,mCAAmC,EAAE,WAAW,OAAO,CAAC,EACxD,SACD,CACF;CACD,MAAM,QAAQ,IAAI,IAAI,MAAM,MAAM,KAAK,SAAS,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC;CACjE,MAAM,OAAO,MAAM,WAAW,OAAO,SAAY,MAAM,IAAI,MAAM,OAAO;AACxE,KAAI,CAAC,QAAQ,KAAK,SAAS,WAAY,QAAO;AAC9C,QAAO,eAAe,KAAK,UAAU;EACnC;EACA,QAAQ,MAAM;EACd;EACD,CAAC;;AAGJ,SAAS,WAAW,KAAwB,SAAwB;AAClE,QAAO,IAAI,SAAS,OAAO;EACzB,MAAM,OAAO,QAAQ,MAAM,IAAI,GAAG;AAClC,SAAO,OAAO,CAAC,KAAK,GAAG,EAAE;GACzB;;AAGJ,SAAS,eACP,KACA,SACQ;AACR,QAAO,WAAW,KAAK,QAAQ,CAC5B,KAAK,SAAS,WAAW,MAAM,QAAQ,CAAC,CACxC,KAAK,GAAG;;AAGb,SAAS,WAAW,MAAoB,SAAgC;AACtE,SAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK;EACL,KAAK,aACH,QAAO,eAAe,KAAK,UAAU,QAAQ;EAC/C,KAAK,YACH,QAAO,MAAM,eAAe,KAAK,UAAU,QAAQ,CAAC;EACtD,KAAK,UACH,QAAO,KAAK,KAAK,MAAM,GAAG,eAAe,KAAK,UAAU,QAAQ,CAAC,KAAK,KAAK,MAAM;EACnF,KAAK,aACH,QAAO,iBAAiB,eAAe,KAAK,UAAU,QAAQ,CAAC;EACjE,KAAK,OACH,QAAO,WAAW,MAAM,QAAQ;EAClC,KAAK,YACH,QAAO,OAAO,eAAe,KAAK,UAAU,QAAQ,CAAC;EACvD,KAAK,aACH,QAAO,gBAAgB,MAAM,QAAQ;EACvC,KAAK,QACH,QAAO,YAAY,MAAM,QAAQ;EACnC,KAAK,iBACH,QAAO;EACT,KAAK,OACH,QAAO,WAAW,KAAK,KAAK;EAC9B,KAAK,KACH,QAAO,OAAO,eAAe,KAAK,UAAU,QAAQ,CAAC;EACvD,KAAK,SACH,QAAO,WAAW,eAAe,KAAK,UAAU,QAAQ,CAAC;EAC3D,KAAK,gBACH,QAAO,QAAQ,eAAe,KAAK,UAAU,QAAQ,CAAC;EACxD,KAAK,cACH,QAAO,SAAS,UAAU,KAAK,KAAK,CAAC;EACvC,KAAK,aACH,QAAO;EACT,KAAK,aACH,QAAO;EACT,KAAK,OACH,QAAO,WACL,KAAK,KACL,KAAK,OACL,eAAe,KAAK,UAAU,QAAQ,EACtC,QACD;EACH,KAAK,WACH,QAAO,WAAW,KAAK,KAAK,QAAW,WAAW,KAAK,KAAK,EAAE,QAAQ;EACxE,KAAK,QACH,QAAO,YAAY,MAAM,QAAQ;EACnC,KAAK,WAEH,QAAO,WAAW,KAAK,KAAK,MAAM,GAAG;EACvC,QAEE,QADyB;;;;;;;;AAgB/B,SAAS,UACP,MACA,SACY;AACZ,QAAO,WAAW,KAAK,UAAU,QAAQ,CAAC,SAAS,UACjD,MAAM,SAAS,cACX,kBAAkB,WAAW,MAAM,UAAU,QAAQ,CAAC,CAAC,KAAK,WAAW;EACrE,MAAM;EACN;EACD,EAAE,GACH,CAAC;EAAE,MAAM;EAAS,MAAM;EAAO,CAAC,CACrC;;AAGH,SAAS,kBAAkB,OAAkD;CAC3E,MAAM,aAA+B,CAAC,EAAE,CAAC;CACzC,IAAI,SAAyB,EAAE;AAC/B,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,cAAc,KAAK,SAAS,UAAU,KAAK,KAAK,MAAM,KAAK;AACjE,MAAI,KAAK,SAAS,gBAAiB,OAAO,SAAS,KAAK,aAAc;AACpE,UAAO,KAAK,KAAK;AACjB;;EAEF,MAAM,UAAU,WAAW,WAAW,SAAS;AAC/C,MAAI,OAAO,QAAQ,MAAM,EAAE,SAAS,aAAa,CAAC,UAAU,EAC1D,YAAW,KAAK,CAAC,KAAK,CAAC;MAEvB,SAAQ,KAAK,GAAG,QAAQ,KAAK;AAE/B,WAAS,EAAE;;AAEb,QAAO,WAAW,QAAQ,cAAc,UAAU,SAAS,EAAE;;;;;;;AAQ/D,SAAS,YACP,OACA,OACA,SACS;AAIT,QAHqB,MAAM,MACxB,SAAS,KAAK,QAAQ,SAAS,KAAK,SAAS,YAAY,CAAC,SAAS,EACrE,IAGC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,UAAU;EACvC,MAAM,UAAU,QAAQ,OAAO,MAC7B,KAAK,MAAM,OACX,MAAM,QAAQ,GAAI,MAAM,MACzB;AACD,SAAO,aAAa,KAAK,QAAQ;GACjC;;AAIN,MAAM,cAAc;;AAGpB,SAAS,sBACP,OACA,SACQ;CACR,MAAM,CAAC,OAAO,GAAG,QAAQ;CACzB,MAAM,OAAO,OAAO,SAAS,SAAS,YAAY,KAAK,MAAM,KAAK,GAAG;AACrE,KAAI,CAAC,SAAS,MAAM,SAAS,UAAU,CAAC,KACtC,QAAO,MAAM,KAAK,SAAS,WAAW,MAAM,QAAQ,CAAC,CAAC,KAAK,GAAG;AAKhE,QAAO,UAHS,KAAK,OAAO,MAAM,KAAK,gBAGd,+BAFX,WAAW,MAAM,KAAK,MAAM,KAAK,GAAG,OAAO,CAAC,GAC7C,KAAK,KAAK,SAAS,WAAW,MAAM,QAAQ,CAAC,CAAC,KAAK,GAAG;;AAIrE,SAAS,WAAW,MAA0B,SAAgC;CAC5E,MAAM,MAAM,KAAK,UAAU,OAAO;CAClC,MAAM,QACJ,KAAK,WAAW,KAAK,UAAU,QAAQ,KAAK,UAAU,IAClD,WAAW,KAAK,MAAM,KACtB;CACN,MAAM,QAAQ,WAAW,KAAK,UAAU,QAAQ,CAAC,QAC9C,SAA0C,KAAK,SAAS,YAC1D;CACD,MAAM,QAAQ,MAAM,KAAK,SAAS,UAAU,MAAM,QAAQ,CAAC;CAC3D,MAAM,QAAQ,YAAY,OAAO,OAAO,QAAQ;AAgBhD,QAAO,IAAI,MAAM,MAAM,KAfV,MACV,KAAK,gBAAgB;AAWpB,SAAO,OAVS,YACb,KAAK,MAAM,UAAU;AACpB,OAAI,KAAK,SAAS,QAAS,QAAO,WAAW,KAAK,MAAM,QAAQ;GAChE,MAAM,SACJ,UAAU,IACN,sBAAsB,KAAK,OAAO,QAAQ,GAC1C,KAAK,MAAM,KAAK,MAAM,WAAW,GAAG,QAAQ,CAAC,CAAC,KAAK,GAAG;AAC5D,UAAO,QAAQ,MAAM,OAAO,UAAU;IACtC,CACD,KAAK,GAAG,CACW;GACtB,CACD,KAAK,GAAG,CACsB,IAAI,IAAI;;AAG3C,SAAS,gBACP,MACA,SACQ;CACR,MAAM,YAAY,KAAK,QAAQ,IAAI,QAAQ,gBAAgB,GAAG;CAC9D,MAAM,gBAAgB,WAAW,oBAAoB,SAAS,KAAK;CACnE,MAAM,UAAU,UAAU,QAAQ,QAAQ,YAAY,KAAK,KAAK,CAAC;AACjE,QAAO,4CAA4C,cAAc,GAAG,UAAU,KAAK,KAAK,CAAC,4HAA4H,QAAQ;;AAG/N,SAAS,YACP,MACA,SACQ;CACR,MAAM,OAAO,WAAW,KAAK,UAAU,QAAQ,CAAC,QAC7C,QAAwC,IAAI,SAAS,YACvD;CACD,MAAM,aAAa,QAAiC;EAClD,MAAM,MAAM,IAAI,WAAW,OAAO;AAQlC,SAAO,SAPO,WAAW,IAAI,UAAU,QAAQ,CAC5C,KAAK,MAAM,UAAU;GACpB,MAAM,QAAQ,KAAK,MAAM;AAEzB,UAAO,IAAI,MADO,SAAS,UAAU,SAAS,WAAW,MAAM,KAAK,GACzC,GAAG,WAAW,MAAM,QAAQ,CAAC,IAAI,IAAI;IAChE,CACD,KAAK,GAAG,CACW;;CAExB,MAAM,SAAS,KAAK,QAAQ,QAAQ,IAAI,SAAS,CAAC,IAAI,UAAU;CAChE,MAAM,OAAO,KAAK,QAAQ,QAAQ,CAAC,IAAI,SAAS,CAAC,IAAI,UAAU;AAC/D,QAAO,qBAAqB,OAAO,KAAK,GAAG,CAAC,YAC1C,KAAK,SAAS,IAAI,UAAU,KAAK,KAAK,GAAG,CAAC,YAAY,GACvD;;AAGH,SAAS,YACP,MACA,SACQ;CACR,MAAM,MAAM,QAAQ,QAAQ,SAAS,KAAK,IAAI;AAC9C,KAAI,QAAQ,KAAM,QAAO,WAAW,KAAK,IAAI;CAC7C,MAAM,QAAQ,KAAK,QAAQ,WAAW,UAAU,KAAK,MAAM,CAAC,KAAK;AACjE,QAAO,aAAa,UAAU,IAAI,CAAC,SAAS,UAAU,KAAK,IAAI,CAAC,GAAG,MAAM;;AAG3E,SAAS,WACP,KACA,OACA,SACA,SACQ;AAGR,QAAO,YAFM,UAAU,QAAQ,QAAQ,SAAS,IAAI,CAAC,CAE7B,kCADN,QAAQ,WAAW,UAAU,MAAM,CAAC,KAAK,GACS,GAAG,QAAQ"}
|
|
1
|
+
{"version":3,"file":"markdown.mjs","names":[],"sources":["../../src/lib/markdown.ts"],"sourcesContent":["import {\n createStreamingMarkdownParserState,\n finalizeStreamingMarkdown,\n parseStreamingMarkdownChunk,\n} from \"@copilotkit/markdown\";\nimport type { StreamingMarkdownAstNode } from \"@copilotkit/markdown\";\n\n/** Inspector policy the HTML renderer applies to URLs and code blocks. */\nexport type MarkdownHtmlOptions = Readonly<{\n /** The href to emit for a link or autolink. Return \"#\" to neuter it. */\n linkHref: (url: string) => string;\n /** The src to emit for an image, or null to render its alt text instead. */\n imageSrc: (url: string) => string | null;\n /** The `data-copy` payload for a code block's Copy button. */\n copyPayload: (code: string) => string;\n}>;\n\ntype MarkdownNode = StreamingMarkdownAstNode;\ntype NodeOfType<T extends MarkdownNode[\"type\"]> = Extract<\n MarkdownNode,\n { type: T }\n>;\n\ntype RenderContext = Readonly<{\n nodes: ReadonlyMap<number, MarkdownNode>;\n /** Normalized Markdown source; node ranges index into it. */\n source: string;\n options: MarkdownHtmlOptions;\n}>;\n\nconst ESCAPED: Readonly<Record<string, string>> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** Escape every markup-significant character (attributes, code). */\nfunction escapeAll(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ESCAPED[char]!);\n}\n\n/**\n * Escape prose text but leave existing character references such as `&`\n * or `©` alone, as Markdown (and the previous marked renderer) does. A\n * character reference cannot open a tag, so this does not weaken escaping.\n */\nfunction escapeText(value: string): string {\n return value.replace(\n /[<>\"']|&(?!#\\d{1,7};|#[Xx][a-fA-F0-9]{1,6};|\\w+;)/g,\n (char) => ESCAPED[char]!,\n );\n}\n\n/**\n * Render a complete Markdown document to an HTML string for the What's new\n * article. The output is meant for `unsafeHTML`, so every piece of source text\n * is escaped: raw HTML in the Markdown renders as visible text, and URLs only\n * reach attributes through the caller's `linkHref` / `imageSrc` policy.\n */\nexport function renderMarkdownToHtml(\n markdown: string,\n options: MarkdownHtmlOptions,\n): string {\n const state = finalizeStreamingMarkdown(\n parseStreamingMarkdownChunk(\n createStreamingMarkdownParserState({ segmenter: false }),\n markdown,\n ),\n );\n const nodes = new Map(state.nodes.map((node) => [node.id, node]));\n const root = state.rootId === null ? undefined : nodes.get(state.rootId);\n if (!root || root.type !== \"document\") return \"\";\n return renderChildren(root.children, {\n nodes,\n source: state.source,\n options,\n });\n}\n\nfunction childNodes(ids: readonly number[], context: RenderContext) {\n return ids.flatMap((id) => {\n const node = context.nodes.get(id);\n return node ? [node] : [];\n });\n}\n\nfunction renderChildren(\n ids: readonly number[],\n context: RenderContext,\n): string {\n return childNodes(ids, context)\n .map((node) => renderNode(node, context))\n .join(\"\");\n}\n\nfunction renderNode(node: MarkdownNode, context: RenderContext): string {\n switch (node.type) {\n case \"document\":\n case \"table-row\":\n case \"table-cell\":\n return renderChildren(node.children, context);\n case \"paragraph\":\n return `<p>${renderChildren(node.children, context)}</p>\\n`;\n case \"heading\":\n return `<h${node.level}>${renderChildren(node.children, context)}</h${node.level}>\\n`;\n case \"blockquote\":\n return `<blockquote>\\n${renderChildren(node.children, context)}</blockquote>\\n`;\n case \"list\":\n return renderList(node, context);\n case \"list-item\":\n return `<li>${renderChildren(node.children, context)}</li>\\n`;\n case \"code-block\":\n return renderCodeBlock(node, context);\n case \"table\":\n return renderTable(node, context);\n case \"thematic-break\":\n return \"<hr>\\n\";\n case \"text\":\n return escapeText(node.text);\n case \"em\":\n return `<em>${renderChildren(node.children, context)}</em>`;\n case \"strong\":\n return `<strong>${renderChildren(node.children, context)}</strong>`;\n case \"strikethrough\":\n return `<del>${renderChildren(node.children, context)}</del>`;\n case \"inline-code\":\n return `<code>${escapeAll(node.text)}</code>`;\n case \"soft-break\":\n return \"\\n\";\n case \"hard-break\":\n return \"<br>\";\n case \"link\":\n return renderLink(\n node.url,\n node.title,\n renderChildren(node.children, context),\n context,\n );\n case \"autolink\":\n return renderLink(node.url, undefined, escapeText(node.text), context);\n case \"image\":\n return renderImage(node, context);\n case \"citation\":\n // The article has no footnotes; show the reference as it was typed.\n return escapeText(`[^${node.idRef}]`);\n default: {\n const unhandled: never = node;\n return unhandled;\n }\n }\n}\n\n/** A piece of list item content: one authored paragraph, or another block. */\ntype ItemPart =\n | { kind: \"paragraph\"; nodes: MarkdownNode[] }\n | { kind: \"block\"; node: MarkdownNode };\n\n/**\n * The parser keeps each list item as a single paragraph, so a blank line\n * inside an item survives only as two soft breaks in a row. Split there to\n * recover the paragraphs the author wrote.\n */\nfunction itemParts(\n item: NodeOfType<\"list-item\">,\n context: RenderContext,\n): ItemPart[] {\n return childNodes(item.children, context).flatMap((block): ItemPart[] =>\n block.type === \"paragraph\"\n ? splitAtBlankLines(childNodes(block.children, context)).map((nodes) => ({\n kind: \"paragraph\",\n nodes,\n }))\n : [{ kind: \"block\", node: block }],\n );\n}\n\nfunction splitAtBlankLines(nodes: readonly MarkdownNode[]): MarkdownNode[][] {\n const paragraphs: MarkdownNode[][] = [[]];\n let breaks: MarkdownNode[] = [];\n for (const node of nodes) {\n const isBlankText = node.type === \"text\" && node.text.trim() === \"\";\n if (node.type === \"soft-break\" || (breaks.length > 0 && isBlankText)) {\n breaks.push(node);\n continue;\n }\n const current = paragraphs[paragraphs.length - 1]!;\n if (breaks.filter((n) => n.type === \"soft-break\").length >= 2) {\n paragraphs.push([node]);\n } else {\n current.push(...breaks, node);\n }\n breaks = [];\n }\n return paragraphs.filter((paragraph) => paragraph.length > 0);\n}\n\n/**\n * A list is loose when a blank line separates two of its items or two\n * paragraphs inside one item. The blank line that ends the list does not\n * count, so a list followed by more prose still renders tight.\n */\nfunction isLooseList(\n items: readonly NodeOfType<\"list-item\">[],\n parts: readonly ItemPart[][],\n context: RenderContext,\n): boolean {\n const hasSplitItem = parts.some(\n (item) => item.filter((part) => part.kind === \"paragraph\").length > 1,\n );\n return (\n hasSplitItem ||\n items.slice(0, -1).some((item, index) => {\n const between = context.source.slice(\n item.range.start,\n items[index + 1]!.range.start,\n );\n return /\\n[ \\t]*\\n/.test(between);\n })\n );\n}\n\nconst TASK_MARKER = /^\\[([ xX])\\][ \\t]+/;\n\n/** Render inline nodes, turning a leading `[ ]` / `[x]` into a checkbox. */\nfunction renderTaskAwareInline(\n nodes: readonly MarkdownNode[],\n context: RenderContext,\n): string {\n const [first, ...rest] = nodes;\n const task = first?.type === \"text\" ? TASK_MARKER.exec(first.text) : null;\n if (!first || first.type !== \"text\" || !task) {\n return nodes.map((node) => renderNode(node, context)).join(\"\");\n }\n const checked = task[1] === \" \" ? \"\" : 'checked=\"\" ';\n const label = escapeText(first.text.slice(task[0].length));\n const tail = rest.map((node) => renderNode(node, context)).join(\"\");\n return `<input ${checked}disabled=\"\" type=\"checkbox\"> ${label}${tail}`;\n}\n\nfunction renderList(node: NodeOfType<\"list\">, context: RenderContext): string {\n const tag = node.ordered ? \"ol\" : \"ul\";\n const start =\n node.ordered && node.start !== null && node.start !== 1\n ? ` start=\"${node.start}\"`\n : \"\";\n const items = childNodes(node.children, context).filter(\n (item): item is NodeOfType<\"list-item\"> => item.type === \"list-item\",\n );\n const parts = items.map((item) => itemParts(item, context));\n const loose = isLooseList(items, parts, context);\n const body = parts\n .map((itemContent) => {\n const content = itemContent\n .map((part, index) => {\n if (part.kind === \"block\") return renderNode(part.node, context);\n const inline =\n index === 0\n ? renderTaskAwareInline(part.nodes, context)\n : part.nodes.map((n) => renderNode(n, context)).join(\"\");\n return loose ? `<p>${inline}</p>\\n` : inline;\n })\n .join(\"\");\n return `<li>${content}</li>\\n`;\n })\n .join(\"\");\n return `<${tag}${start}>\\n${body}</${tag}>\\n`;\n}\n\nfunction renderCodeBlock(\n node: NodeOfType<\"code-block\">,\n context: RenderContext,\n): string {\n const language = (node.info ?? \"\").replace(/[^a-z0-9-]/gi, \"\");\n const languageClass = language ? ` class=\"language-${language}\"` : \"\";\n const payload = escapeAll(context.options.copyPayload(node.text));\n return `<div class=\"announcement-code\"><pre><code${languageClass}>${escapeAll(node.text)}</code></pre><div class=\"announcement-code__copy-shield\"><button type=\"button\" class=\"announcement-code__copy\" data-copy=\"${payload}\" aria-label=\"Copy code\">Copy</button></div></div>`;\n}\n\nfunction renderTable(\n node: NodeOfType<\"table\">,\n context: RenderContext,\n): string {\n const rows = childNodes(node.children, context).filter(\n (row): row is NodeOfType<\"table-row\"> => row.type === \"table-row\",\n );\n const renderRow = (row: NodeOfType<\"table-row\">) => {\n const tag = row.isHeader ? \"th\" : \"td\";\n const cells = childNodes(row.children, context)\n .map((cell, index) => {\n const align = node.align[index];\n const alignAttr = align && align !== \"none\" ? ` align=\"${align}\"` : \"\";\n return `<${tag}${alignAttr}>${renderNode(cell, context)}</${tag}>\\n`;\n })\n .join(\"\");\n return `<tr>\\n${cells}</tr>\\n`;\n };\n const header = rows.filter((row) => row.isHeader).map(renderRow);\n const body = rows.filter((row) => !row.isHeader).map(renderRow);\n return `<table>\\n<thead>\\n${header.join(\"\")}</thead>\\n${\n body.length > 0 ? `<tbody>${body.join(\"\")}</tbody>` : \"\"\n }</table>\\n`;\n}\n\nfunction renderImage(\n node: NodeOfType<\"image\">,\n context: RenderContext,\n): string {\n const src = context.options.imageSrc(node.url);\n if (src === null) return escapeText(node.alt);\n const title = node.title ? ` title=\"${escapeAll(node.title)}\"` : \"\";\n return `<img src=\"${escapeAll(src)}\" alt=\"${escapeAll(node.alt)}\"${title}>`;\n}\n\nfunction renderLink(\n url: string,\n title: string | undefined,\n content: string,\n context: RenderContext,\n): string {\n const href = escapeAll(context.options.linkHref(url));\n const titleAttr = title ? ` title=\"${escapeAll(title)}\"` : \"\";\n return `<a href=\"${href}\" target=\"_blank\" rel=\"noopener\"${titleAttr}>${content}</a>`;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const require_semver = require('./semver.cjs');
|
|
2
2
|
|
|
3
3
|
//#region src/lib/notifications.ts
|
|
4
4
|
function emptyNotificationState() {
|
|
@@ -46,7 +46,7 @@ function validNotificationConditions(value) {
|
|
|
46
46
|
if (value.sdkVersion !== void 0 && value.framework === void 0) return false;
|
|
47
47
|
return Object.entries(value).every(([key, expected]) => {
|
|
48
48
|
if (!text(expected)) return false;
|
|
49
|
-
if (key === "sdkVersion") return
|
|
49
|
+
if (key === "sdkVersion") return require_semver.isValidRange(expected);
|
|
50
50
|
if (key === "plan") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);
|
|
51
51
|
return Object.prototype.hasOwnProperty.call(conditionValues, key) && (conditionValues[key]?.includes(expected) ?? false);
|
|
52
52
|
});
|
|
@@ -144,7 +144,7 @@ function matchNotification(notice, context) {
|
|
|
144
144
|
matches: false,
|
|
145
145
|
reasons: ["Development only"]
|
|
146
146
|
};
|
|
147
|
-
if (!context.sdkVersion || !
|
|
147
|
+
if (!context.sdkVersion || !require_semver.isStableVersion(context.sdkVersion)) return {
|
|
148
148
|
matches: false,
|
|
149
149
|
reasons: ["Known stable SDK required"]
|
|
150
150
|
};
|
|
@@ -152,7 +152,7 @@ function matchNotification(notice, context) {
|
|
|
152
152
|
const matches = notice.audiences.some((conditions) => {
|
|
153
153
|
return Object.entries(conditions).every(([field, expected]) => {
|
|
154
154
|
const actual = context[field];
|
|
155
|
-
const matches = field === "sdkVersion" ? actual !== void 0 &&
|
|
155
|
+
const matches = field === "sdkVersion" ? actual !== void 0 && require_semver.isStableVersion(actual) && require_semver.satisfies(actual, expected) : actual === expected;
|
|
156
156
|
if (!matches) reasons.push(`${field} ${actual === void 0 ? "is unknown" : `requires ${expected} (got ${actual})`}`);
|
|
157
157
|
return matches;
|
|
158
158
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.cjs","names":[],"sources":["../../src/lib/notifications.ts"],"sourcesContent":["import { satisfies, valid, validRange, prerelease } from \"semver\";\n\n/** Versioned CDN notification contract. Keep the Intelligence copy and fixtures aligned. */\nexport type NotificationFramework = \"react\" | \"vue\" | \"angular\";\nexport type NotificationPriority = \"Low\" | \"Normal\" | \"High\" | \"Urgent\";\nexport interface NotificationConditions {\n framework?: NotificationFramework;\n sdkVersion?: string;\n intelligence?: \"enabled\" | \"disabled\";\n deployment?: \"managed\" | \"self-hosted\";\n plan?: string;\n}\nexport interface CohortNotification {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n audiences: NotificationConditions[];\n priority: NotificationPriority;\n priorityOverride?: number;\n}\nexport interface NotificationFeed {\n schemaVersion: 1;\n notifications: CohortNotification[];\n}\nexport interface NotificationContext extends Omit<\n NotificationConditions,\n \"sdkVersion\"\n> {\n development: boolean;\n sdkVersion?: string;\n}\nexport interface NotificationState {\n schemaVersion: 1;\n eligibleIds: string[];\n readIds: string[];\n suppressedIds: string[];\n activeId: string | null;\n}\nexport function emptyNotificationState(): NotificationState {\n return {\n schemaVersion: 1,\n eligibleIds: [],\n readIds: [],\n suppressedIds: [],\n activeId: null,\n };\n}\nconst priorities: Record<NotificationPriority, number> = {\n Low: 10,\n Normal: 20,\n High: 30,\n Urgent: 40,\n};\nconst conditionValues: Record<string, readonly string[]> = {\n framework: [\"react\", \"vue\", \"angular\"],\n intelligence: [\"enabled\", \"disabled\"],\n deployment: [\"managed\", \"self-hosted\"],\n};\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max = 256): value is string {\n return (\n typeof value === \"string\" && value.trim().length > 0 && value.length <= max\n );\n}\nexport function isNotificationId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n value,\n )\n );\n}\nfunction ids(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.every(isNotificationId) &&\n new Set(value).size === value.length\n );\n}\n/** Validate authored conditions; unsupported predicates cannot broaden the audience. */\nexport function validNotificationConditions(\n value: unknown,\n): value is NotificationConditions {\n if (!record(value)) return false;\n if (value.sdkVersion !== undefined && value.framework === undefined)\n return false;\n return Object.entries(value).every(([key, expected]) => {\n if (!text(expected)) return false;\n if (key === \"sdkVersion\") return validRange(expected) !== null;\n if (key === \"plan\") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);\n return (\n Object.prototype.hasOwnProperty.call(conditionValues, key) &&\n (conditionValues[key]?.includes(expected) ?? false)\n );\n });\n}\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseNotificationFeed(value: unknown): NotificationFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"notifications\"]) ||\n value.schemaVersion !== 1\n )\n return null;\n if (!Array.isArray(value.notifications) || value.notifications.length > 100)\n return null;\n const notifications: CohortNotification[] = [];\n for (const notice of value.notifications) {\n if (\n !record(notice) ||\n !onlyKeys(notice, [\n \"id\",\n \"title\",\n \"body\",\n \"publishedAt\",\n \"audiences\",\n \"priority\",\n \"priorityOverride\",\n ])\n )\n return null;\n if (\n !isNotificationId(notice.id) ||\n !text(notice.title) ||\n !text(notice.body, 100_000) ||\n !text(notice.publishedAt)\n )\n return null;\n if (\n !/^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d{3}Z$/.test(notice.publishedAt) ||\n !Number.isFinite(Date.parse(notice.publishedAt)) ||\n new Date(notice.publishedAt).toISOString() !== notice.publishedAt\n )\n return null;\n if (\n !Array.isArray(notice.audiences) ||\n !notice.audiences.length ||\n notice.audiences.length > 100 ||\n !notice.audiences.every(validNotificationConditions)\n )\n return null;\n if (\n !text(notice.priority) ||\n !Object.prototype.hasOwnProperty.call(priorities, notice.priority)\n )\n return null;\n if (\n notice.priorityOverride !== undefined &&\n (typeof notice.priorityOverride !== \"number\" ||\n !Number.isSafeInteger(notice.priorityOverride) ||\n notice.priorityOverride < 0)\n )\n return null;\n if (notifications.some((n) => n.id === notice.id)) return null;\n notifications.push({\n id: notice.id,\n title: notice.title,\n body: notice.body,\n publishedAt: notice.publishedAt,\n audiences: notice.audiences,\n priority: notice.priority as NotificationPriority,\n ...(notice.priorityOverride === undefined\n ? {}\n : { priorityOverride: notice.priorityOverride }),\n });\n }\n return { schemaVersion: 1, notifications };\n}\n/** Ignore malformed or incompatible browser state. */\nexport function parseNotificationState(\n value: unknown,\n): NotificationState | null {\n if (\n !record(value) ||\n value.schemaVersion !== 1 ||\n !ids(value.eligibleIds) ||\n !ids(value.readIds) ||\n !ids(value.suppressedIds) ||\n (value.activeId !== null && !isNotificationId(value.activeId))\n )\n return null;\n return {\n schemaVersion: 1,\n eligibleIds: value.eligibleIds,\n readIds: value.readIds,\n suppressedIds: value.suppressedIds,\n activeId: value.activeId,\n };\n}\n/** Internal priority never becomes a developer-facing badge. */\nexport function notificationPriority(notice: CohortNotification): number {\n return notice.priorityOverride ?? priorities[notice.priority];\n}\n/** Stable ordering used by authoring reports and first-time selection. */\nexport function compareNotifications(\n a: CohortNotification,\n b: CohortNotification,\n): number {\n return (\n notificationPriority(b) - notificationPriority(a) ||\n Date.parse(b.publishedAt) - Date.parse(a.publishedAt) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)\n );\n}\n/** Refresh eligibility without promoting the previously delivered backlog. */\nexport function reconcileNotifications(\n state: NotificationState,\n feed: NotificationFeed,\n context: NotificationContext,\n): NotificationState {\n const eligible = feed.notifications.filter(\n (n) => matchNotification(n, context).matches,\n );\n const quiet = (n: CohortNotification) =>\n state.readIds.includes(n.id) || state.suppressedIds.includes(n.id);\n const active =\n eligible.find((n) => n.id === state.activeId && !quiet(n)) ??\n feed.notifications.find(\n (n) =>\n n.id === state.activeId &&\n !quiet(n) &&\n n.audiences.some((conditions) => {\n const entries = Object.entries(conditions);\n if (\n !entries.some(\n ([key]) =>\n context[key as keyof NotificationConditions] === undefined,\n )\n )\n return false;\n const known = Object.fromEntries(\n entries.filter(\n ([key]) =>\n context[key as keyof NotificationConditions] !== undefined,\n ),\n );\n return matchNotification({ ...n, audiences: [known] }, context)\n .matches;\n }),\n );\n const newcomer = eligible\n .filter((n) => !quiet(n) && !state.eligibleIds.includes(n.id))\n .sort(compareNotifications)[0];\n const winner =\n active &&\n (!newcomer ||\n notificationPriority(active) >= notificationPriority(newcomer))\n ? active\n : newcomer;\n return {\n ...state,\n eligibleIds: eligible.map((n) => n.id),\n activeId: winner?.id ?? null,\n };\n}\n/** Reading or closing the highlight quiets its delivered backlog, not future IDs. */\nexport function acknowledgeNotification(\n state: NotificationState,\n noticeId: string,\n): NotificationState {\n if (!state.eligibleIds.includes(noticeId)) return state;\n const highlighted = state.activeId === noticeId;\n return {\n ...state,\n readIds: [...new Set([...state.readIds, noticeId])],\n suppressedIds: [\n ...new Set([\n ...state.suppressedIds,\n ...(highlighted ? state.eligibleIds : [noticeId]),\n ]),\n ],\n activeId: highlighted ? null : state.activeId,\n };\n}\n/** Explain targeting using the same rules in CI and the Inspector. */\nexport function matchNotification(\n notice: CohortNotification,\n context: NotificationContext,\n): { matches: boolean; reasons: string[] } {\n if (!context.development)\n return { matches: false, reasons: [\"Development only\"] };\n if (\n !context.sdkVersion ||\n !valid(context.sdkVersion) ||\n prerelease(context.sdkVersion) !== null\n )\n return {\n matches: false,\n reasons: [\"Known stable SDK required\"],\n };\n const reasons: string[] = [];\n const matches = notice.audiences.some((conditions) => {\n return Object.entries(conditions).every(([field, expected]) => {\n const actual = context[field as keyof NotificationConditions];\n const matches =\n field === \"sdkVersion\"\n ? actual !== undefined &&\n valid(actual) !== null &&\n prerelease(actual) === null &&\n satisfies(actual, expected)\n : actual === expected;\n if (!matches)\n reasons.push(\n `${field} ${actual === undefined ? \"is unknown\" : `requires ${expected} (got ${actual})`}`,\n );\n return matches;\n });\n });\n return {\n matches,\n reasons: matches ? [] : reasons,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"notifications.cjs","names":["isValidRange","isStableVersion","satisfies"],"sources":["../../src/lib/notifications.ts"],"sourcesContent":["import { isStableVersion, isValidRange, satisfies } from \"./semver.js\";\n\n/** Versioned CDN notification contract. Keep the Intelligence copy and fixtures aligned. */\nexport type NotificationFramework = \"react\" | \"vue\" | \"angular\";\nexport type NotificationPriority = \"Low\" | \"Normal\" | \"High\" | \"Urgent\";\nexport interface NotificationConditions {\n framework?: NotificationFramework;\n sdkVersion?: string;\n intelligence?: \"enabled\" | \"disabled\";\n deployment?: \"managed\" | \"self-hosted\";\n plan?: string;\n}\nexport interface CohortNotification {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n audiences: NotificationConditions[];\n priority: NotificationPriority;\n priorityOverride?: number;\n}\nexport interface NotificationFeed {\n schemaVersion: 1;\n notifications: CohortNotification[];\n}\nexport interface NotificationContext extends Omit<\n NotificationConditions,\n \"sdkVersion\"\n> {\n development: boolean;\n sdkVersion?: string;\n}\nexport interface NotificationState {\n schemaVersion: 1;\n eligibleIds: string[];\n readIds: string[];\n suppressedIds: string[];\n activeId: string | null;\n}\nexport function emptyNotificationState(): NotificationState {\n return {\n schemaVersion: 1,\n eligibleIds: [],\n readIds: [],\n suppressedIds: [],\n activeId: null,\n };\n}\nconst priorities: Record<NotificationPriority, number> = {\n Low: 10,\n Normal: 20,\n High: 30,\n Urgent: 40,\n};\nconst conditionValues: Record<string, readonly string[]> = {\n framework: [\"react\", \"vue\", \"angular\"],\n intelligence: [\"enabled\", \"disabled\"],\n deployment: [\"managed\", \"self-hosted\"],\n};\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max = 256): value is string {\n return (\n typeof value === \"string\" && value.trim().length > 0 && value.length <= max\n );\n}\nexport function isNotificationId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n value,\n )\n );\n}\nfunction ids(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.every(isNotificationId) &&\n new Set(value).size === value.length\n );\n}\n/** Validate authored conditions; unsupported predicates cannot broaden the audience. */\nexport function validNotificationConditions(\n value: unknown,\n): value is NotificationConditions {\n if (!record(value)) return false;\n if (value.sdkVersion !== undefined && value.framework === undefined)\n return false;\n return Object.entries(value).every(([key, expected]) => {\n if (!text(expected)) return false;\n if (key === \"sdkVersion\") return isValidRange(expected);\n if (key === \"plan\") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);\n return (\n Object.prototype.hasOwnProperty.call(conditionValues, key) &&\n (conditionValues[key]?.includes(expected) ?? false)\n );\n });\n}\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseNotificationFeed(value: unknown): NotificationFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"notifications\"]) ||\n value.schemaVersion !== 1\n )\n return null;\n if (!Array.isArray(value.notifications) || value.notifications.length > 100)\n return null;\n const notifications: CohortNotification[] = [];\n for (const notice of value.notifications) {\n if (\n !record(notice) ||\n !onlyKeys(notice, [\n \"id\",\n \"title\",\n \"body\",\n \"publishedAt\",\n \"audiences\",\n \"priority\",\n \"priorityOverride\",\n ])\n )\n return null;\n if (\n !isNotificationId(notice.id) ||\n !text(notice.title) ||\n !text(notice.body, 100_000) ||\n !text(notice.publishedAt)\n )\n return null;\n if (\n !/^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d{3}Z$/.test(notice.publishedAt) ||\n !Number.isFinite(Date.parse(notice.publishedAt)) ||\n new Date(notice.publishedAt).toISOString() !== notice.publishedAt\n )\n return null;\n if (\n !Array.isArray(notice.audiences) ||\n !notice.audiences.length ||\n notice.audiences.length > 100 ||\n !notice.audiences.every(validNotificationConditions)\n )\n return null;\n if (\n !text(notice.priority) ||\n !Object.prototype.hasOwnProperty.call(priorities, notice.priority)\n )\n return null;\n if (\n notice.priorityOverride !== undefined &&\n (typeof notice.priorityOverride !== \"number\" ||\n !Number.isSafeInteger(notice.priorityOverride) ||\n notice.priorityOverride < 0)\n )\n return null;\n if (notifications.some((n) => n.id === notice.id)) return null;\n notifications.push({\n id: notice.id,\n title: notice.title,\n body: notice.body,\n publishedAt: notice.publishedAt,\n audiences: notice.audiences,\n priority: notice.priority as NotificationPriority,\n ...(notice.priorityOverride === undefined\n ? {}\n : { priorityOverride: notice.priorityOverride }),\n });\n }\n return { schemaVersion: 1, notifications };\n}\n/** Ignore malformed or incompatible browser state. */\nexport function parseNotificationState(\n value: unknown,\n): NotificationState | null {\n if (\n !record(value) ||\n value.schemaVersion !== 1 ||\n !ids(value.eligibleIds) ||\n !ids(value.readIds) ||\n !ids(value.suppressedIds) ||\n (value.activeId !== null && !isNotificationId(value.activeId))\n )\n return null;\n return {\n schemaVersion: 1,\n eligibleIds: value.eligibleIds,\n readIds: value.readIds,\n suppressedIds: value.suppressedIds,\n activeId: value.activeId,\n };\n}\n/** Internal priority never becomes a developer-facing badge. */\nexport function notificationPriority(notice: CohortNotification): number {\n return notice.priorityOverride ?? priorities[notice.priority];\n}\n/** Stable ordering used by authoring reports and first-time selection. */\nexport function compareNotifications(\n a: CohortNotification,\n b: CohortNotification,\n): number {\n return (\n notificationPriority(b) - notificationPriority(a) ||\n Date.parse(b.publishedAt) - Date.parse(a.publishedAt) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)\n );\n}\n/** Refresh eligibility without promoting the previously delivered backlog. */\nexport function reconcileNotifications(\n state: NotificationState,\n feed: NotificationFeed,\n context: NotificationContext,\n): NotificationState {\n const eligible = feed.notifications.filter(\n (n) => matchNotification(n, context).matches,\n );\n const quiet = (n: CohortNotification) =>\n state.readIds.includes(n.id) || state.suppressedIds.includes(n.id);\n const active =\n eligible.find((n) => n.id === state.activeId && !quiet(n)) ??\n feed.notifications.find(\n (n) =>\n n.id === state.activeId &&\n !quiet(n) &&\n n.audiences.some((conditions) => {\n const entries = Object.entries(conditions);\n if (\n !entries.some(\n ([key]) =>\n context[key as keyof NotificationConditions] === undefined,\n )\n )\n return false;\n const known = Object.fromEntries(\n entries.filter(\n ([key]) =>\n context[key as keyof NotificationConditions] !== undefined,\n ),\n );\n return matchNotification({ ...n, audiences: [known] }, context)\n .matches;\n }),\n );\n const newcomer = eligible\n .filter((n) => !quiet(n) && !state.eligibleIds.includes(n.id))\n .sort(compareNotifications)[0];\n const winner =\n active &&\n (!newcomer ||\n notificationPriority(active) >= notificationPriority(newcomer))\n ? active\n : newcomer;\n return {\n ...state,\n eligibleIds: eligible.map((n) => n.id),\n activeId: winner?.id ?? null,\n };\n}\n/** Reading or closing the highlight quiets its delivered backlog, not future IDs. */\nexport function acknowledgeNotification(\n state: NotificationState,\n noticeId: string,\n): NotificationState {\n if (!state.eligibleIds.includes(noticeId)) return state;\n const highlighted = state.activeId === noticeId;\n return {\n ...state,\n readIds: [...new Set([...state.readIds, noticeId])],\n suppressedIds: [\n ...new Set([\n ...state.suppressedIds,\n ...(highlighted ? state.eligibleIds : [noticeId]),\n ]),\n ],\n activeId: highlighted ? null : state.activeId,\n };\n}\n/** Explain targeting using the same rules in CI and the Inspector. */\nexport function matchNotification(\n notice: CohortNotification,\n context: NotificationContext,\n): { matches: boolean; reasons: string[] } {\n if (!context.development)\n return { matches: false, reasons: [\"Development only\"] };\n if (!context.sdkVersion || !isStableVersion(context.sdkVersion))\n return {\n matches: false,\n reasons: [\"Known stable SDK required\"],\n };\n const reasons: string[] = [];\n const matches = notice.audiences.some((conditions) => {\n return Object.entries(conditions).every(([field, expected]) => {\n const actual = context[field as keyof NotificationConditions];\n const matches =\n field === \"sdkVersion\"\n ? actual !== undefined &&\n isStableVersion(actual) &&\n satisfies(actual, expected)\n : actual === expected;\n if (!matches)\n reasons.push(\n `${field} ${actual === undefined ? \"is unknown\" : `requires ${expected} (got ${actual})`}`,\n );\n return matches;\n });\n });\n return {\n matches,\n reasons: matches ? [] : reasons,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isStableVersion, isValidRange, satisfies } from "./semver.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/lib/notifications.ts
|
|
4
4
|
function emptyNotificationState() {
|
|
@@ -46,7 +46,7 @@ function validNotificationConditions(value) {
|
|
|
46
46
|
if (value.sdkVersion !== void 0 && value.framework === void 0) return false;
|
|
47
47
|
return Object.entries(value).every(([key, expected]) => {
|
|
48
48
|
if (!text(expected)) return false;
|
|
49
|
-
if (key === "sdkVersion") return
|
|
49
|
+
if (key === "sdkVersion") return isValidRange(expected);
|
|
50
50
|
if (key === "plan") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);
|
|
51
51
|
return Object.prototype.hasOwnProperty.call(conditionValues, key) && (conditionValues[key]?.includes(expected) ?? false);
|
|
52
52
|
});
|
|
@@ -144,7 +144,7 @@ function matchNotification(notice, context) {
|
|
|
144
144
|
matches: false,
|
|
145
145
|
reasons: ["Development only"]
|
|
146
146
|
};
|
|
147
|
-
if (!context.sdkVersion || !
|
|
147
|
+
if (!context.sdkVersion || !isStableVersion(context.sdkVersion)) return {
|
|
148
148
|
matches: false,
|
|
149
149
|
reasons: ["Known stable SDK required"]
|
|
150
150
|
};
|
|
@@ -152,7 +152,7 @@ function matchNotification(notice, context) {
|
|
|
152
152
|
const matches = notice.audiences.some((conditions) => {
|
|
153
153
|
return Object.entries(conditions).every(([field, expected]) => {
|
|
154
154
|
const actual = context[field];
|
|
155
|
-
const matches = field === "sdkVersion" ? actual !== void 0 &&
|
|
155
|
+
const matches = field === "sdkVersion" ? actual !== void 0 && isStableVersion(actual) && satisfies(actual, expected) : actual === expected;
|
|
156
156
|
if (!matches) reasons.push(`${field} ${actual === void 0 ? "is unknown" : `requires ${expected} (got ${actual})`}`);
|
|
157
157
|
return matches;
|
|
158
158
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.mjs","names":[],"sources":["../../src/lib/notifications.ts"],"sourcesContent":["import { satisfies, valid, validRange, prerelease } from \"semver\";\n\n/** Versioned CDN notification contract. Keep the Intelligence copy and fixtures aligned. */\nexport type NotificationFramework = \"react\" | \"vue\" | \"angular\";\nexport type NotificationPriority = \"Low\" | \"Normal\" | \"High\" | \"Urgent\";\nexport interface NotificationConditions {\n framework?: NotificationFramework;\n sdkVersion?: string;\n intelligence?: \"enabled\" | \"disabled\";\n deployment?: \"managed\" | \"self-hosted\";\n plan?: string;\n}\nexport interface CohortNotification {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n audiences: NotificationConditions[];\n priority: NotificationPriority;\n priorityOverride?: number;\n}\nexport interface NotificationFeed {\n schemaVersion: 1;\n notifications: CohortNotification[];\n}\nexport interface NotificationContext extends Omit<\n NotificationConditions,\n \"sdkVersion\"\n> {\n development: boolean;\n sdkVersion?: string;\n}\nexport interface NotificationState {\n schemaVersion: 1;\n eligibleIds: string[];\n readIds: string[];\n suppressedIds: string[];\n activeId: string | null;\n}\nexport function emptyNotificationState(): NotificationState {\n return {\n schemaVersion: 1,\n eligibleIds: [],\n readIds: [],\n suppressedIds: [],\n activeId: null,\n };\n}\nconst priorities: Record<NotificationPriority, number> = {\n Low: 10,\n Normal: 20,\n High: 30,\n Urgent: 40,\n};\nconst conditionValues: Record<string, readonly string[]> = {\n framework: [\"react\", \"vue\", \"angular\"],\n intelligence: [\"enabled\", \"disabled\"],\n deployment: [\"managed\", \"self-hosted\"],\n};\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max = 256): value is string {\n return (\n typeof value === \"string\" && value.trim().length > 0 && value.length <= max\n );\n}\nexport function isNotificationId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n value,\n )\n );\n}\nfunction ids(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.every(isNotificationId) &&\n new Set(value).size === value.length\n );\n}\n/** Validate authored conditions; unsupported predicates cannot broaden the audience. */\nexport function validNotificationConditions(\n value: unknown,\n): value is NotificationConditions {\n if (!record(value)) return false;\n if (value.sdkVersion !== undefined && value.framework === undefined)\n return false;\n return Object.entries(value).every(([key, expected]) => {\n if (!text(expected)) return false;\n if (key === \"sdkVersion\") return validRange(expected) !== null;\n if (key === \"plan\") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);\n return (\n Object.prototype.hasOwnProperty.call(conditionValues, key) &&\n (conditionValues[key]?.includes(expected) ?? false)\n );\n });\n}\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseNotificationFeed(value: unknown): NotificationFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"notifications\"]) ||\n value.schemaVersion !== 1\n )\n return null;\n if (!Array.isArray(value.notifications) || value.notifications.length > 100)\n return null;\n const notifications: CohortNotification[] = [];\n for (const notice of value.notifications) {\n if (\n !record(notice) ||\n !onlyKeys(notice, [\n \"id\",\n \"title\",\n \"body\",\n \"publishedAt\",\n \"audiences\",\n \"priority\",\n \"priorityOverride\",\n ])\n )\n return null;\n if (\n !isNotificationId(notice.id) ||\n !text(notice.title) ||\n !text(notice.body, 100_000) ||\n !text(notice.publishedAt)\n )\n return null;\n if (\n !/^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d{3}Z$/.test(notice.publishedAt) ||\n !Number.isFinite(Date.parse(notice.publishedAt)) ||\n new Date(notice.publishedAt).toISOString() !== notice.publishedAt\n )\n return null;\n if (\n !Array.isArray(notice.audiences) ||\n !notice.audiences.length ||\n notice.audiences.length > 100 ||\n !notice.audiences.every(validNotificationConditions)\n )\n return null;\n if (\n !text(notice.priority) ||\n !Object.prototype.hasOwnProperty.call(priorities, notice.priority)\n )\n return null;\n if (\n notice.priorityOverride !== undefined &&\n (typeof notice.priorityOverride !== \"number\" ||\n !Number.isSafeInteger(notice.priorityOverride) ||\n notice.priorityOverride < 0)\n )\n return null;\n if (notifications.some((n) => n.id === notice.id)) return null;\n notifications.push({\n id: notice.id,\n title: notice.title,\n body: notice.body,\n publishedAt: notice.publishedAt,\n audiences: notice.audiences,\n priority: notice.priority as NotificationPriority,\n ...(notice.priorityOverride === undefined\n ? {}\n : { priorityOverride: notice.priorityOverride }),\n });\n }\n return { schemaVersion: 1, notifications };\n}\n/** Ignore malformed or incompatible browser state. */\nexport function parseNotificationState(\n value: unknown,\n): NotificationState | null {\n if (\n !record(value) ||\n value.schemaVersion !== 1 ||\n !ids(value.eligibleIds) ||\n !ids(value.readIds) ||\n !ids(value.suppressedIds) ||\n (value.activeId !== null && !isNotificationId(value.activeId))\n )\n return null;\n return {\n schemaVersion: 1,\n eligibleIds: value.eligibleIds,\n readIds: value.readIds,\n suppressedIds: value.suppressedIds,\n activeId: value.activeId,\n };\n}\n/** Internal priority never becomes a developer-facing badge. */\nexport function notificationPriority(notice: CohortNotification): number {\n return notice.priorityOverride ?? priorities[notice.priority];\n}\n/** Stable ordering used by authoring reports and first-time selection. */\nexport function compareNotifications(\n a: CohortNotification,\n b: CohortNotification,\n): number {\n return (\n notificationPriority(b) - notificationPriority(a) ||\n Date.parse(b.publishedAt) - Date.parse(a.publishedAt) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)\n );\n}\n/** Refresh eligibility without promoting the previously delivered backlog. */\nexport function reconcileNotifications(\n state: NotificationState,\n feed: NotificationFeed,\n context: NotificationContext,\n): NotificationState {\n const eligible = feed.notifications.filter(\n (n) => matchNotification(n, context).matches,\n );\n const quiet = (n: CohortNotification) =>\n state.readIds.includes(n.id) || state.suppressedIds.includes(n.id);\n const active =\n eligible.find((n) => n.id === state.activeId && !quiet(n)) ??\n feed.notifications.find(\n (n) =>\n n.id === state.activeId &&\n !quiet(n) &&\n n.audiences.some((conditions) => {\n const entries = Object.entries(conditions);\n if (\n !entries.some(\n ([key]) =>\n context[key as keyof NotificationConditions] === undefined,\n )\n )\n return false;\n const known = Object.fromEntries(\n entries.filter(\n ([key]) =>\n context[key as keyof NotificationConditions] !== undefined,\n ),\n );\n return matchNotification({ ...n, audiences: [known] }, context)\n .matches;\n }),\n );\n const newcomer = eligible\n .filter((n) => !quiet(n) && !state.eligibleIds.includes(n.id))\n .sort(compareNotifications)[0];\n const winner =\n active &&\n (!newcomer ||\n notificationPriority(active) >= notificationPriority(newcomer))\n ? active\n : newcomer;\n return {\n ...state,\n eligibleIds: eligible.map((n) => n.id),\n activeId: winner?.id ?? null,\n };\n}\n/** Reading or closing the highlight quiets its delivered backlog, not future IDs. */\nexport function acknowledgeNotification(\n state: NotificationState,\n noticeId: string,\n): NotificationState {\n if (!state.eligibleIds.includes(noticeId)) return state;\n const highlighted = state.activeId === noticeId;\n return {\n ...state,\n readIds: [...new Set([...state.readIds, noticeId])],\n suppressedIds: [\n ...new Set([\n ...state.suppressedIds,\n ...(highlighted ? state.eligibleIds : [noticeId]),\n ]),\n ],\n activeId: highlighted ? null : state.activeId,\n };\n}\n/** Explain targeting using the same rules in CI and the Inspector. */\nexport function matchNotification(\n notice: CohortNotification,\n context: NotificationContext,\n): { matches: boolean; reasons: string[] } {\n if (!context.development)\n return { matches: false, reasons: [\"Development only\"] };\n if (\n !context.sdkVersion ||\n !valid(context.sdkVersion) ||\n prerelease(context.sdkVersion) !== null\n )\n return {\n matches: false,\n reasons: [\"Known stable SDK required\"],\n };\n const reasons: string[] = [];\n const matches = notice.audiences.some((conditions) => {\n return Object.entries(conditions).every(([field, expected]) => {\n const actual = context[field as keyof NotificationConditions];\n const matches =\n field === \"sdkVersion\"\n ? actual !== undefined &&\n valid(actual) !== null &&\n prerelease(actual) === null &&\n satisfies(actual, expected)\n : actual === expected;\n if (!matches)\n reasons.push(\n `${field} ${actual === undefined ? \"is unknown\" : `requires ${expected} (got ${actual})`}`,\n );\n return matches;\n });\n });\n return {\n matches,\n reasons: matches ? [] : reasons,\n };\n}\n"],"mappings":";;;AAuCA,SAAgB,yBAA4C;AAC1D,QAAO;EACL,eAAe;EACf,aAAa,EAAE;EACf,SAAS,EAAE;EACX,eAAe,EAAE;EACjB,UAAU;EACX;;AAEH,MAAM,aAAmD;CACvD,KAAK;CACL,QAAQ;CACR,MAAM;CACN,QAAQ;CACT;AACD,MAAM,kBAAqD;CACzD,WAAW;EAAC;EAAS;EAAO;EAAU;CACtC,cAAc,CAAC,WAAW,WAAW;CACrC,YAAY,CAAC,WAAW,cAAc;CACvC;AACD,SAAS,OAAO,OAAkD;AAChE,QAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM;;AAE7E,SAAS,SAAS,OAAgC,MAAyB;AACzE,QAAO,OAAO,KAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,SAAS,IAAI,CAAC;;AAE9D,SAAS,KAAK,OAAgB,MAAM,KAAsB;AACxD,QACE,OAAO,UAAU,YAAY,MAAM,MAAM,CAAC,SAAS,KAAK,MAAM,UAAU;;AAG5E,SAAgB,iBAAiB,OAAiC;AAChE,QACE,OAAO,UAAU,YACjB,6EAA6E,KAC3E,MACD;;AAGL,SAAS,IAAI,OAAmC;AAC9C,QACE,MAAM,QAAQ,MAAM,IACpB,MAAM,MAAM,iBAAiB,IAC7B,IAAI,IAAI,MAAM,CAAC,SAAS,MAAM;;;AAIlC,SAAgB,4BACd,OACiC;AACjC,KAAI,CAAC,OAAO,MAAM,CAAE,QAAO;AAC3B,KAAI,MAAM,eAAe,UAAa,MAAM,cAAc,OACxD,QAAO;AACT,QAAO,OAAO,QAAQ,MAAM,CAAC,OAAO,CAAC,KAAK,cAAc;AACtD,MAAI,CAAC,KAAK,SAAS,CAAE,QAAO;AAC5B,MAAI,QAAQ,aAAc,QAAO,WAAW,SAAS,KAAK;AAC1D,MAAI,QAAQ,OAAQ,QAAO,6BAA6B,KAAK,SAAS;AACtE,SACE,OAAO,UAAU,eAAe,KAAK,iBAAiB,IAAI,KACzD,gBAAgB,MAAM,SAAS,SAAS,IAAI;GAE/C;;;AAGJ,SAAgB,sBAAsB,OAAyC;AAC7E,KACE,CAAC,OAAO,MAAM,IACd,CAAC,SAAS,OAAO,CAAC,iBAAiB,gBAAgB,CAAC,IACpD,MAAM,kBAAkB,EAExB,QAAO;AACT,KAAI,CAAC,MAAM,QAAQ,MAAM,cAAc,IAAI,MAAM,cAAc,SAAS,IACtE,QAAO;CACT,MAAM,gBAAsC,EAAE;AAC9C,MAAK,MAAM,UAAU,MAAM,eAAe;AACxC,MACE,CAAC,OAAO,OAAO,IACf,CAAC,SAAS,QAAQ;GAChB;GACA;GACA;GACA;GACA;GACA;GACA;GACD,CAAC,CAEF,QAAO;AACT,MACE,CAAC,iBAAiB,OAAO,GAAG,IAC5B,CAAC,KAAK,OAAO,MAAM,IACnB,CAAC,KAAK,OAAO,MAAM,IAAQ,IAC3B,CAAC,KAAK,OAAO,YAAY,CAEzB,QAAO;AACT,MACE,CAAC,2CAA2C,KAAK,OAAO,YAAY,IACpE,CAAC,OAAO,SAAS,KAAK,MAAM,OAAO,YAAY,CAAC,IAChD,IAAI,KAAK,OAAO,YAAY,CAAC,aAAa,KAAK,OAAO,YAEtD,QAAO;AACT,MACE,CAAC,MAAM,QAAQ,OAAO,UAAU,IAChC,CAAC,OAAO,UAAU,UAClB,OAAO,UAAU,SAAS,OAC1B,CAAC,OAAO,UAAU,MAAM,4BAA4B,CAEpD,QAAO;AACT,MACE,CAAC,KAAK,OAAO,SAAS,IACtB,CAAC,OAAO,UAAU,eAAe,KAAK,YAAY,OAAO,SAAS,CAElE,QAAO;AACT,MACE,OAAO,qBAAqB,WAC3B,OAAO,OAAO,qBAAqB,YAClC,CAAC,OAAO,cAAc,OAAO,iBAAiB,IAC9C,OAAO,mBAAmB,GAE5B,QAAO;AACT,MAAI,cAAc,MAAM,MAAM,EAAE,OAAO,OAAO,GAAG,CAAE,QAAO;AAC1D,gBAAc,KAAK;GACjB,IAAI,OAAO;GACX,OAAO,OAAO;GACd,MAAM,OAAO;GACb,aAAa,OAAO;GACpB,WAAW,OAAO;GAClB,UAAU,OAAO;GACjB,GAAI,OAAO,qBAAqB,SAC5B,EAAE,GACF,EAAE,kBAAkB,OAAO,kBAAkB;GAClD,CAAC;;AAEJ,QAAO;EAAE,eAAe;EAAG;EAAe;;;AAG5C,SAAgB,uBACd,OAC0B;AAC1B,KACE,CAAC,OAAO,MAAM,IACd,MAAM,kBAAkB,KACxB,CAAC,IAAI,MAAM,YAAY,IACvB,CAAC,IAAI,MAAM,QAAQ,IACnB,CAAC,IAAI,MAAM,cAAc,IACxB,MAAM,aAAa,QAAQ,CAAC,iBAAiB,MAAM,SAAS,CAE7D,QAAO;AACT,QAAO;EACL,eAAe;EACf,aAAa,MAAM;EACnB,SAAS,MAAM;EACf,eAAe,MAAM;EACrB,UAAU,MAAM;EACjB;;;AAGH,SAAgB,qBAAqB,QAAoC;AACvE,QAAO,OAAO,oBAAoB,WAAW,OAAO;;;AAGtD,SAAgB,qBACd,GACA,GACQ;AACR,QACE,qBAAqB,EAAE,GAAG,qBAAqB,EAAE,IACjD,KAAK,MAAM,EAAE,YAAY,GAAG,KAAK,MAAM,EAAE,YAAY,KACpD,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI;;;AAI1C,SAAgB,uBACd,OACA,MACA,SACmB;CACnB,MAAM,WAAW,KAAK,cAAc,QACjC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,QACtC;CACD,MAAM,SAAS,MACb,MAAM,QAAQ,SAAS,EAAE,GAAG,IAAI,MAAM,cAAc,SAAS,EAAE,GAAG;CACpE,MAAM,SACJ,SAAS,MAAM,MAAM,EAAE,OAAO,MAAM,YAAY,CAAC,MAAM,EAAE,CAAC,IAC1D,KAAK,cAAc,MAChB,MACC,EAAE,OAAO,MAAM,YACf,CAAC,MAAM,EAAE,IACT,EAAE,UAAU,MAAM,eAAe;EAC/B,MAAM,UAAU,OAAO,QAAQ,WAAW;AAC1C,MACE,CAAC,QAAQ,MACN,CAAC,SACA,QAAQ,SAAyC,OACpD,CAED,QAAO;EACT,MAAM,QAAQ,OAAO,YACnB,QAAQ,QACL,CAAC,SACA,QAAQ,SAAyC,OACpD,CACF;AACD,SAAO,kBAAkB;GAAE,GAAG;GAAG,WAAW,CAAC,MAAM;GAAE,EAAE,QAAQ,CAC5D;GACH,CACL;CACH,MAAM,WAAW,SACd,QAAQ,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,YAAY,SAAS,EAAE,GAAG,CAAC,CAC7D,KAAK,qBAAqB,CAAC;CAC9B,MAAM,SACJ,WACC,CAAC,YACA,qBAAqB,OAAO,IAAI,qBAAqB,SAAS,IAC5D,SACA;AACN,QAAO;EACL,GAAG;EACH,aAAa,SAAS,KAAK,MAAM,EAAE,GAAG;EACtC,UAAU,QAAQ,MAAM;EACzB;;;AAGH,SAAgB,wBACd,OACA,UACmB;AACnB,KAAI,CAAC,MAAM,YAAY,SAAS,SAAS,CAAE,QAAO;CAClD,MAAM,cAAc,MAAM,aAAa;AACvC,QAAO;EACL,GAAG;EACH,SAAS,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,MAAM,SAAS,SAAS,CAAC,CAAC;EACnD,eAAe,CACb,GAAG,IAAI,IAAI,CACT,GAAG,MAAM,eACT,GAAI,cAAc,MAAM,cAAc,CAAC,SAAS,CACjD,CAAC,CACH;EACD,UAAU,cAAc,OAAO,MAAM;EACtC;;;AAGH,SAAgB,kBACd,QACA,SACyC;AACzC,KAAI,CAAC,QAAQ,YACX,QAAO;EAAE,SAAS;EAAO,SAAS,CAAC,mBAAmB;EAAE;AAC1D,KACE,CAAC,QAAQ,cACT,CAAC,MAAM,QAAQ,WAAW,IAC1B,WAAW,QAAQ,WAAW,KAAK,KAEnC,QAAO;EACL,SAAS;EACT,SAAS,CAAC,4BAA4B;EACvC;CACH,MAAM,UAAoB,EAAE;CAC5B,MAAM,UAAU,OAAO,UAAU,MAAM,eAAe;AACpD,SAAO,OAAO,QAAQ,WAAW,CAAC,OAAO,CAAC,OAAO,cAAc;GAC7D,MAAM,SAAS,QAAQ;GACvB,MAAM,UACJ,UAAU,eACN,WAAW,UACX,MAAM,OAAO,KAAK,QAClB,WAAW,OAAO,KAAK,QACvB,UAAU,QAAQ,SAAS,GAC3B,WAAW;AACjB,OAAI,CAAC,QACH,SAAQ,KACN,GAAG,MAAM,GAAG,WAAW,SAAY,eAAe,YAAY,SAAS,QAAQ,OAAO,KACvF;AACH,UAAO;IACP;GACF;AACF,QAAO;EACL;EACA,SAAS,UAAU,EAAE,GAAG;EACzB"}
|
|
1
|
+
{"version":3,"file":"notifications.mjs","names":[],"sources":["../../src/lib/notifications.ts"],"sourcesContent":["import { isStableVersion, isValidRange, satisfies } from \"./semver.js\";\n\n/** Versioned CDN notification contract. Keep the Intelligence copy and fixtures aligned. */\nexport type NotificationFramework = \"react\" | \"vue\" | \"angular\";\nexport type NotificationPriority = \"Low\" | \"Normal\" | \"High\" | \"Urgent\";\nexport interface NotificationConditions {\n framework?: NotificationFramework;\n sdkVersion?: string;\n intelligence?: \"enabled\" | \"disabled\";\n deployment?: \"managed\" | \"self-hosted\";\n plan?: string;\n}\nexport interface CohortNotification {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n audiences: NotificationConditions[];\n priority: NotificationPriority;\n priorityOverride?: number;\n}\nexport interface NotificationFeed {\n schemaVersion: 1;\n notifications: CohortNotification[];\n}\nexport interface NotificationContext extends Omit<\n NotificationConditions,\n \"sdkVersion\"\n> {\n development: boolean;\n sdkVersion?: string;\n}\nexport interface NotificationState {\n schemaVersion: 1;\n eligibleIds: string[];\n readIds: string[];\n suppressedIds: string[];\n activeId: string | null;\n}\nexport function emptyNotificationState(): NotificationState {\n return {\n schemaVersion: 1,\n eligibleIds: [],\n readIds: [],\n suppressedIds: [],\n activeId: null,\n };\n}\nconst priorities: Record<NotificationPriority, number> = {\n Low: 10,\n Normal: 20,\n High: 30,\n Urgent: 40,\n};\nconst conditionValues: Record<string, readonly string[]> = {\n framework: [\"react\", \"vue\", \"angular\"],\n intelligence: [\"enabled\", \"disabled\"],\n deployment: [\"managed\", \"self-hosted\"],\n};\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max = 256): value is string {\n return (\n typeof value === \"string\" && value.trim().length > 0 && value.length <= max\n );\n}\nexport function isNotificationId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n value,\n )\n );\n}\nfunction ids(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.every(isNotificationId) &&\n new Set(value).size === value.length\n );\n}\n/** Validate authored conditions; unsupported predicates cannot broaden the audience. */\nexport function validNotificationConditions(\n value: unknown,\n): value is NotificationConditions {\n if (!record(value)) return false;\n if (value.sdkVersion !== undefined && value.framework === undefined)\n return false;\n return Object.entries(value).every(([key, expected]) => {\n if (!text(expected)) return false;\n if (key === \"sdkVersion\") return isValidRange(expected);\n if (key === \"plan\") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);\n return (\n Object.prototype.hasOwnProperty.call(conditionValues, key) &&\n (conditionValues[key]?.includes(expected) ?? false)\n );\n });\n}\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseNotificationFeed(value: unknown): NotificationFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"notifications\"]) ||\n value.schemaVersion !== 1\n )\n return null;\n if (!Array.isArray(value.notifications) || value.notifications.length > 100)\n return null;\n const notifications: CohortNotification[] = [];\n for (const notice of value.notifications) {\n if (\n !record(notice) ||\n !onlyKeys(notice, [\n \"id\",\n \"title\",\n \"body\",\n \"publishedAt\",\n \"audiences\",\n \"priority\",\n \"priorityOverride\",\n ])\n )\n return null;\n if (\n !isNotificationId(notice.id) ||\n !text(notice.title) ||\n !text(notice.body, 100_000) ||\n !text(notice.publishedAt)\n )\n return null;\n if (\n !/^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d{3}Z$/.test(notice.publishedAt) ||\n !Number.isFinite(Date.parse(notice.publishedAt)) ||\n new Date(notice.publishedAt).toISOString() !== notice.publishedAt\n )\n return null;\n if (\n !Array.isArray(notice.audiences) ||\n !notice.audiences.length ||\n notice.audiences.length > 100 ||\n !notice.audiences.every(validNotificationConditions)\n )\n return null;\n if (\n !text(notice.priority) ||\n !Object.prototype.hasOwnProperty.call(priorities, notice.priority)\n )\n return null;\n if (\n notice.priorityOverride !== undefined &&\n (typeof notice.priorityOverride !== \"number\" ||\n !Number.isSafeInteger(notice.priorityOverride) ||\n notice.priorityOverride < 0)\n )\n return null;\n if (notifications.some((n) => n.id === notice.id)) return null;\n notifications.push({\n id: notice.id,\n title: notice.title,\n body: notice.body,\n publishedAt: notice.publishedAt,\n audiences: notice.audiences,\n priority: notice.priority as NotificationPriority,\n ...(notice.priorityOverride === undefined\n ? {}\n : { priorityOverride: notice.priorityOverride }),\n });\n }\n return { schemaVersion: 1, notifications };\n}\n/** Ignore malformed or incompatible browser state. */\nexport function parseNotificationState(\n value: unknown,\n): NotificationState | null {\n if (\n !record(value) ||\n value.schemaVersion !== 1 ||\n !ids(value.eligibleIds) ||\n !ids(value.readIds) ||\n !ids(value.suppressedIds) ||\n (value.activeId !== null && !isNotificationId(value.activeId))\n )\n return null;\n return {\n schemaVersion: 1,\n eligibleIds: value.eligibleIds,\n readIds: value.readIds,\n suppressedIds: value.suppressedIds,\n activeId: value.activeId,\n };\n}\n/** Internal priority never becomes a developer-facing badge. */\nexport function notificationPriority(notice: CohortNotification): number {\n return notice.priorityOverride ?? priorities[notice.priority];\n}\n/** Stable ordering used by authoring reports and first-time selection. */\nexport function compareNotifications(\n a: CohortNotification,\n b: CohortNotification,\n): number {\n return (\n notificationPriority(b) - notificationPriority(a) ||\n Date.parse(b.publishedAt) - Date.parse(a.publishedAt) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)\n );\n}\n/** Refresh eligibility without promoting the previously delivered backlog. */\nexport function reconcileNotifications(\n state: NotificationState,\n feed: NotificationFeed,\n context: NotificationContext,\n): NotificationState {\n const eligible = feed.notifications.filter(\n (n) => matchNotification(n, context).matches,\n );\n const quiet = (n: CohortNotification) =>\n state.readIds.includes(n.id) || state.suppressedIds.includes(n.id);\n const active =\n eligible.find((n) => n.id === state.activeId && !quiet(n)) ??\n feed.notifications.find(\n (n) =>\n n.id === state.activeId &&\n !quiet(n) &&\n n.audiences.some((conditions) => {\n const entries = Object.entries(conditions);\n if (\n !entries.some(\n ([key]) =>\n context[key as keyof NotificationConditions] === undefined,\n )\n )\n return false;\n const known = Object.fromEntries(\n entries.filter(\n ([key]) =>\n context[key as keyof NotificationConditions] !== undefined,\n ),\n );\n return matchNotification({ ...n, audiences: [known] }, context)\n .matches;\n }),\n );\n const newcomer = eligible\n .filter((n) => !quiet(n) && !state.eligibleIds.includes(n.id))\n .sort(compareNotifications)[0];\n const winner =\n active &&\n (!newcomer ||\n notificationPriority(active) >= notificationPriority(newcomer))\n ? active\n : newcomer;\n return {\n ...state,\n eligibleIds: eligible.map((n) => n.id),\n activeId: winner?.id ?? null,\n };\n}\n/** Reading or closing the highlight quiets its delivered backlog, not future IDs. */\nexport function acknowledgeNotification(\n state: NotificationState,\n noticeId: string,\n): NotificationState {\n if (!state.eligibleIds.includes(noticeId)) return state;\n const highlighted = state.activeId === noticeId;\n return {\n ...state,\n readIds: [...new Set([...state.readIds, noticeId])],\n suppressedIds: [\n ...new Set([\n ...state.suppressedIds,\n ...(highlighted ? state.eligibleIds : [noticeId]),\n ]),\n ],\n activeId: highlighted ? null : state.activeId,\n };\n}\n/** Explain targeting using the same rules in CI and the Inspector. */\nexport function matchNotification(\n notice: CohortNotification,\n context: NotificationContext,\n): { matches: boolean; reasons: string[] } {\n if (!context.development)\n return { matches: false, reasons: [\"Development only\"] };\n if (!context.sdkVersion || !isStableVersion(context.sdkVersion))\n return {\n matches: false,\n reasons: [\"Known stable SDK required\"],\n };\n const reasons: string[] = [];\n const matches = notice.audiences.some((conditions) => {\n return Object.entries(conditions).every(([field, expected]) => {\n const actual = context[field as keyof NotificationConditions];\n const matches =\n field === \"sdkVersion\"\n ? actual !== undefined &&\n isStableVersion(actual) &&\n satisfies(actual, expected)\n : actual === expected;\n if (!matches)\n reasons.push(\n `${field} ${actual === undefined ? \"is unknown\" : `requires ${expected} (got ${actual})`}`,\n );\n return matches;\n });\n });\n return {\n matches,\n reasons: matches ? [] : reasons,\n };\n}\n"],"mappings":"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"}
|