AI-Studio/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.cs
2026-07-28 20:44:33 +02:00

1893 lines
96 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using AIStudio.Tools.Metadata;
using HtmlAgilityPack;
namespace AIStudio.Assistants.VisualBriefing;
/// <summary>
/// Defines <c>VisualBriefingArtifactService</c> for the visual briefing feature.
/// </summary>
public sealed partial class VisualBriefingArtifactService
{
/// <summary>
/// Marks the Base64 compatibility manifest embedded in standalone HTML.
/// </summary>
private const string MANIFEST_MARKER = "MWAI_VISUAL_BRIEFING_MANIFEST:";
/// <summary>
/// Identifies the canonical JSON script element.
/// </summary>
private const string DATA_ELEMENT_ID = "mwai-briefing-data";
/// <summary>
/// Gets the shared compact JSON configuration.
/// </summary>
private static readonly JsonSerializerOptions JSON_OPTIONS = VisualBriefingJson.Compact;
/// <summary>
/// Lists declarative elements allowed in model-generated templates.
/// </summary>
private static readonly HashSet<string> ALLOWED_ELEMENTS = new(StringComparer.OrdinalIgnoreCase)
{
"a", "article", "aside", "button", "canvas", "caption", "dd", "details", "div", "dl", "dt",
"figcaption", "figure", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "header", "img",
"input", "label", "li", "main", "nav", "ol", "option", "p", "progress", "section", "select",
"small", "span", "strong", "summary", "table", "tbody", "td", "template", "tfoot", "th",
"thead", "tr", "ul",
};
/// <summary>
/// Lists ordinary attributes allowed in model-generated templates.
/// </summary>
private static readonly HashSet<string> ALLOWED_ATTRIBUTES = new(StringComparer.OrdinalIgnoreCase)
{
"aria-atomic", "aria-controls", "aria-describedby", "aria-expanded", "aria-hidden", "aria-label",
"aria-labelledby", "aria-live", "aria-selected", "class", "colspan", "disabled", "for", "height",
"hidden", "href", "id", "max", "min", "name", "open", "placeholder", "role", "rowspan", "scope", "step",
"tabindex", "type", "value", "width",
};
/// <summary>
/// Lists supported AI Studio runtime bindings.
/// </summary>
private static readonly HashSet<string> ALLOWED_DATA_ATTRIBUTES = new(StringComparer.OrdinalIgnoreCase)
{
"data-mwai-asset", "data-mwai-chart", "data-mwai-direction", "data-mwai-each", "data-mwai-expr",
"data-mwai-filter", "data-mwai-filter-value", "data-mwai-if", "data-mwai-model", "data-mwai-reset",
"data-mwai-region", "data-mwai-search", "data-mwai-set", "data-mwai-sort", "data-mwai-tab-panel", "data-mwai-tab-target",
"data-mwai-tabs", "data-mwai-text", "data-mwai-toggle", "data-mwai-value",
};
/// <summary>
/// Lists bindings whose values are canonical data paths.
/// </summary>
private static readonly HashSet<string> PATH_BINDINGS = new(StringComparer.OrdinalIgnoreCase)
{
"data-mwai-chart", "data-mwai-each", "data-mwai-expr", "data-mwai-filter", "data-mwai-filter-value",
"data-mwai-if", "data-mwai-model", "data-mwai-set", "data-mwai-text", "data-mwai-toggle",
};
/// <summary>
/// Lists supported safe formula operators.
/// </summary>
private static readonly HashSet<string> FORMULA_OPERATORS = new(StringComparer.Ordinal)
{
"add", "subtract", "multiply", "divide", "power", "eq", "ne", "gt", "gte", "lt", "lte", "if",
"min", "max", "round", "sqrt", "log", "exp",
};
/// <summary>
/// Defines <c>CssProhibitedRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex CSS_PROHIBITED = CssProhibitedRegex();
/// <summary>
/// Defines <c>CssProtectedTargetRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex CSS_PROTECTED_TARGET = CssProtectedTargetRegex();
/// <summary>
/// Defines <c>DataPathRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex DATA_PATH = DataPathRegex();
/// <summary>
/// Defines <c>LocalDataPathRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex LOCAL_DATA_PATH = LocalDataPathRegex();
/// <summary>
/// Defines <c>HtmlLanguageTagRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex HTML_LANGUAGE_TAG = HtmlLanguageTagRegex();
/// <summary>
/// Defines <c>SafeSelectorRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex SAFE_SELECTOR = SafeSelectorRegex();
/// <summary>
/// Defines <c>ManifestRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex MANIFEST_REGEX = ManifestRegex();
/// <summary>
/// Defines <c>StyleRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex STYLE_REGEX = StyleRegex();
/// <summary>
/// Defines <c>RuntimeRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex RUNTIME_REGEX = RuntimeRegex();
/// <summary>
/// Defines <c>RuntimeAIVersionRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex RUNTIME_AI_VERSION_REGEX = RuntimeAIVersionRegex();
/// <summary>
/// Defines <c>EChartsRegex</c> for the visual briefing feature.
/// </summary>
private static readonly Regex ECHARTS_REGEX = EChartsRegex();
/// <summary>
/// Lazily loads the pinned ECharts common distribution.
/// </summary>
private static readonly Lazy<string?> ECHARTS_SCRIPT = new(LoadECharts);
/// <summary>
/// Defines the protected, app-owned static footer template.
/// </summary>
private const string STATIC_FOOTER_TEMPLATE = """
<span data-mwai-text="_mwai.footer.createdWith"></span>
<span data-mwai-text="_mwai.footer.models"></span>
<span data-mwai-text="_mwai.footer.createdAt"></span>
<span data-mwai-text="_mwai.footer.authors"></span>
<span data-mwai-text="_mwai.footer.protection"></span>
""";
/// <summary>
/// Defines protected footer styles that model CSS cannot override.
/// </summary>
private const string PROTECTED_FOOTER_CSS = """
#mwai-static-footer {
display: flex !important;
flex-wrap: wrap !important;
gap: .5rem 1.25rem !important;
position: relative !important;
z-index: 2147483647 !important;
visibility: visible !important;
opacity: 1 !important;
padding: 1rem !important;
font: 13px/1.5 system-ui, sans-serif !important;
}
#mwai-static-footer span {
display: inline !important;
visibility: visible !important;
opacity: 1 !important;
}
""";
/// <summary>
/// Defines <c>AIStudioVersion</c> for the visual briefing feature.
/// </summary>
private string AIStudioVersion { get; } = Assembly.GetExecutingAssembly().GetCustomAttribute<MetaDataAttribute>()?.Version ?? "unknown";
/// <summary>
/// Defines <c>RuntimeScript</c> for the visual briefing feature.
/// </summary>
private string RuntimeScript => BuildRuntimeScript(this.AIStudioVersion);
/// <summary>
/// Defines <c>GetContentSecurityPolicy</c> for the visual briefing feature.
/// </summary>
public static string GetContentSecurityPolicy(VisualBriefingArtifactParts parts)
{
var echartsHash = string.IsNullOrWhiteSpace(parts.EChartsScript)
? string.Empty
: $" {ScriptCspHash(parts.EChartsScript)}";
return $"default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src {ScriptCspHash(parts.RuntimeScript)}{echartsHash}; font-src 'none'; media-src 'none'; frame-src 'none'; connect-src 'none'; form-action 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'self'";
}
/// <summary>
/// Defines <c>BuildAsync</c> for the visual briefing feature.
/// </summary>
public async Task<string> BuildAsync(
VisualBriefingManifest manifest,
VisualBriefingRevisionRequest request,
string? lockedRuntimeScript = null,
string? lockedEChartsScript = null,
CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
var data = AddProtectedArtifactData(manifest, request);
var usesCharts = ContainsChartBinding(request.TemplateHtml);
var validationIssue = ValidateGeneratedParts(manifest, data, request.TemplateHtml, request.Css, usesCharts);
if (!string.IsNullOrEmpty(validationIssue))
throw new InvalidDataException(validationIssue);
var dataJson = JsonSerializer.Serialize(data, JSON_OPTIONS);
var template = CanonicalizeTemplate(request.TemplateHtml);
var css = request.Css.Trim();
var runtime = lockedRuntimeScript ?? this.RuntimeScript;
var runtimeAIStudioVersion = ExtractRuntimeAIStudioVersion(runtime)
?? throw new InvalidDataException("The AI Studio runtime does not contain a valid originating app version.");
var echarts = usesCharts ? lockedEChartsScript ?? ECHARTS_SCRIPT.Value : null;
if (usesCharts && string.IsNullOrWhiteSpace(echarts))
throw new InvalidOperationException("Apache ECharts 6.1.0 common is not available in this AI Studio build.");
await Task.CompletedTask;
var payloadHash = ComputePayloadHash(dataJson, template, css, runtime, echarts);
var exportManifest = CreateExportManifest(
manifest,
request,
payloadHash,
this.AIStudioVersion,
runtimeAIStudioVersion);
var encodedManifest = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(exportManifest, JSON_OPTIONS)));
var csp = GetContentSecurityPolicy(new(exportManifest, data, template, css, runtime, echarts, payloadHash));
return $"""
<!doctype html>
<html lang="{GetHtmlLanguage(manifest.Settings)}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="{csp}">
<meta name="referrer" content="no-referrer">
<title>{HtmlEncode(manifest.Name)}</title>
<style id="mwai-briefing-style">{css}
{PROTECTED_FOOTER_CSS}</style>
</head>
<body>
<!--{MANIFEST_MARKER}{encodedManifest}-->
<script id="{DATA_ELEMENT_ID}" type="application/json">{dataJson}</script>
<div id="mwai-briefing-root">{template}</div>
<footer id="mwai-static-footer" class="mwai-footer">
{STATIC_FOOTER_TEMPLATE}
</footer>
{BuildScriptTag(echarts, "mwai-echarts-runtime")}
<script id="mwai-briefing-runtime">{runtime}</script>
</body>
</html>
""";
}
/// <summary>
/// Defines <c>TryParse</c> for the visual briefing feature.
/// </summary>
public bool TryParse(string html, out VisualBriefingArtifactParts parts, out string issue)
{
parts = null!;
issue = string.Empty;
if (string.IsNullOrWhiteSpace(html))
{
issue = "The briefing file is empty.";
return false;
}
if (!html.StartsWith("<!doctype html>\n", StringComparison.Ordinal) ||
!html.EndsWith("</html>", StringComparison.Ordinal))
{
issue = "The briefing document wrapper is invalid or modified.";
return false;
}
var manifestMatch = MANIFEST_REGEX.Match(html);
if (!manifestMatch.Success)
{
issue = "The briefing compatibility manifest is missing.";
return false;
}
VisualBriefingExportManifest? exportManifest;
try
{
var json = Encoding.UTF8.GetString(Convert.FromBase64String(manifestMatch.Groups["value"].Value));
using var manifestDocument = JsonDocument.Parse(json);
exportManifest = HasDuplicateProperties(manifestDocument.RootElement)
? null
: manifestDocument.RootElement.Deserialize<VisualBriefingExportManifest>(JSON_OPTIONS);
}
catch (Exception exception) when (exception is FormatException or JsonException)
{
issue = "The briefing compatibility manifest is invalid.";
return false;
}
if (exportManifest is null ||
exportManifest.ArtifactVersion != VisualBriefingVersions.ARTIFACT ||
exportManifest.SchemaVersion != VisualBriefingVersions.SCHEMA ||
exportManifest.RuntimeVersion != VisualBriefingVersions.RUNTIME ||
exportManifest.BriefingId == Guid.Empty ||
exportManifest.RevisionId == Guid.Empty ||
string.IsNullOrWhiteSpace(exportManifest.Name) ||
string.IsNullOrWhiteSpace(exportManifest.AIStudioVersion) ||
string.IsNullOrWhiteSpace(exportManifest.RuntimeAIStudioVersion) ||
string.IsNullOrWhiteSpace(exportManifest.PayloadHash) ||
exportManifest.PayloadHash.Length != 64 ||
!exportManifest.PayloadHash.All(Uri.IsHexDigit) ||
exportManifest.TargetLanguage is CommonLanguages.OTHER &&
string.IsNullOrWhiteSpace(exportManifest.CustomTargetLanguage) ||
exportManifest.ProtectionLevel is VisualBriefingProtectionLevel.OTHER &&
string.IsNullOrWhiteSpace(exportManifest.CustomProtectionLevel))
{
issue = "The briefing uses an unsupported or invalid artifact version.";
return false;
}
var document = new HtmlDocument();
document.LoadHtml(html);
var dataNode = FindElementById(document, DATA_ELEMENT_ID);
var rootNode = FindElementById(document, "mwai-briefing-root");
var footerNode = FindElementById(document, "mwai-static-footer");
var headNode = FindNode(document.DocumentNode, "//head");
var bodyNode = FindNode(document.DocumentNode, "//body");
var htmlNode = FindNode(document.DocumentNode, "//html");
var styleMatch = STYLE_REGEX.Match(html);
var runtimeMatch = RUNTIME_REGEX.Match(html);
if (dataNode is null || rootNode is null || footerNode is null || headNode is null || bodyNode is null ||
htmlNode is null || !styleMatch.Success || !runtimeMatch.Success)
{
issue = "The briefing structure is incomplete.";
return false;
}
var headChildren = headNode.ChildNodes.Where(node => node.NodeType is HtmlNodeType.Element).ToArray();
var metaNodes = headChildren.Where(node => node.Name.Equals("meta", StringComparison.OrdinalIgnoreCase)).ToArray();
var styleNodes = headChildren.Where(node => node.Name.Equals("style", StringComparison.OrdinalIgnoreCase)).ToArray();
var titleNodes = headChildren.Where(node => node.Name.Equals("title", StringComparison.OrdinalIgnoreCase)).ToArray();
if (headChildren.Length != 6 ||
metaNodes.Length != 4 ||
styleNodes.Length != 1 ||
titleNodes.Length != 1 ||
metaNodes.Count(node => string.Equals(node.GetAttributeValue("charset", string.Empty), "utf-8", StringComparison.OrdinalIgnoreCase)) != 1 ||
metaNodes.Count(node => string.Equals(node.GetAttributeValue("name", string.Empty), "viewport", StringComparison.OrdinalIgnoreCase) &&
string.Equals(node.GetAttributeValue("content", string.Empty), "width=device-width,initial-scale=1", StringComparison.Ordinal)) != 1 ||
metaNodes.Count(node => string.Equals(node.GetAttributeValue("http-equiv", string.Empty), "Content-Security-Policy", StringComparison.OrdinalIgnoreCase)) != 1 ||
metaNodes.Count(node => string.Equals(node.GetAttributeValue("name", string.Empty), "referrer", StringComparison.OrdinalIgnoreCase) &&
string.Equals(node.GetAttributeValue("content", string.Empty), "no-referrer", StringComparison.OrdinalIgnoreCase)) != 1 ||
metaNodes.Any(node => FindAttribute(node, "charset") is not null
? !HasExactAttributes(node, "charset")
: !HasExactAttributes(node, FindAttribute(node, "http-equiv") is not null ? "http-equiv" : "name", "content")) ||
styleNodes[0].Id != "mwai-briefing-style" ||
!HasExactAttributes(styleNodes[0], "id") ||
!HasExactAttributes(titleNodes[0]) ||
!string.Equals(titleNodes[0].InnerText, exportManifest.Name, StringComparison.Ordinal) ||
!HasExactAttributes(headNode) ||
!HasExactAttributes(bodyNode) ||
!HasExactAttributes(htmlNode, "lang") ||
!string.Equals(
htmlNode.GetAttributeValue("lang", string.Empty),
GetHtmlLanguage(exportManifest.TargetLanguage, exportManifest.CustomTargetLanguage),
StringComparison.Ordinal))
{
issue = "The briefing head or document structure was modified.";
return false;
}
var bodyChildren = FindNodes(document.DocumentNode, "//body/*")?.ToArray() ?? [];
var bodyComments = bodyNode.ChildNodes.Where(node => node.NodeType is HtmlNodeType.Comment).ToArray();
var allowedBodyIds = new HashSet<string>(StringComparer.Ordinal)
{
DATA_ELEMENT_ID,
"mwai-briefing-root",
"mwai-static-footer",
"mwai-echarts-runtime",
"mwai-briefing-runtime",
};
if (bodyChildren.Any(node => !allowedBodyIds.Contains(node.Id)) ||
bodyChildren.Select(node => node.Id).Distinct(StringComparer.Ordinal).Count() != bodyChildren.Length ||
bodyComments.Length != 1 ||
!string.Equals(
bodyComments[0].OuterHtml,
$"<!--{MANIFEST_MARKER}{manifestMatch.Groups["value"].Value}-->",
StringComparison.Ordinal) ||
bodyNode.ChildNodes.Any(node =>
node.NodeType is HtmlNodeType.Text && !string.IsNullOrWhiteSpace(node.InnerText)) ||
CanonicalizeTemplate(footerNode.InnerHtml) != CanonicalizeTemplate(STATIC_FOOTER_TEMPLATE) ||
!HasExactAttributes(dataNode, "id", "type") ||
!HasExactAttributes(rootNode, "id") ||
!HasExactAttributes(footerNode, "id", "class") ||
!string.Equals(footerNode.GetAttributeValue("class", string.Empty), "mwai-footer", StringComparison.Ordinal))
{
issue = "The briefing body or static footer structure was modified.";
return false;
}
var scriptNodes = FindNodes(document.DocumentNode, "//script")?.ToArray() ?? [];
if (scriptNodes.Any(node => node.Id is not DATA_ELEMENT_ID and not "mwai-echarts-runtime" and not "mwai-briefing-runtime") ||
scriptNodes.Count(node => node.Id == DATA_ELEMENT_ID) != 1 ||
scriptNodes.Count(node => node.Id == "mwai-briefing-runtime") != 1 ||
scriptNodes.Any(node => node.Id != DATA_ELEMENT_ID && !HasExactAttributes(node, "id")) ||
!string.Equals(dataNode.GetAttributeValue("type", string.Empty), "application/json", StringComparison.OrdinalIgnoreCase))
{
issue = "The briefing contains an unknown or duplicated script element.";
return false;
}
JsonElement data;
try
{
using var parsedData = JsonDocument.Parse(dataNode.InnerText);
data = parsedData.RootElement.Clone();
}
catch (JsonException)
{
issue = "The briefing data block is invalid.";
return false;
}
var protectedDataIssue = ValidateProtectedData(exportManifest, data);
if (!string.IsNullOrEmpty(protectedDataIssue))
{
issue = protectedDataIssue;
return false;
}
var template = CanonicalizeTemplate(rootNode.InnerHtml);
var combinedCss = styleMatch.Groups["value"].Value.Trim();
const string PROTECTED_CSS_SUFFIX = $"\n{PROTECTED_FOOTER_CSS}";
if (!combinedCss.EndsWith(PROTECTED_CSS_SUFFIX, StringComparison.Ordinal))
{
issue = "The protected briefing footer stylesheet is missing or modified.";
return false;
}
var css = combinedCss[..^PROTECTED_CSS_SUFFIX.Length].Trim();
var runtime = runtimeMatch.Groups["value"].Value;
var echartsMatch = ECHARTS_REGEX.Match(html);
var echarts = echartsMatch.Success ? echartsMatch.Groups["value"].Value : null;
if (echarts is not null && !string.Equals(echarts, ECHARTS_SCRIPT.Value, StringComparison.Ordinal))
{
issue = "The briefing contains an unknown or modified ECharts runtime.";
return false;
}
var validationIssue = ValidateGeneratedParts(null, data, template, css, !string.IsNullOrWhiteSpace(echarts));
if (!string.IsNullOrEmpty(validationIssue))
{
issue = validationIssue;
return false;
}
if (!string.Equals(runtime, BuildRuntimeScript(exportManifest.RuntimeAIStudioVersion), StringComparison.Ordinal))
{
issue = "The briefing contains an unknown or modified AI Studio runtime.";
return false;
}
var dataJson = JsonSerializer.Serialize(data, JSON_OPTIONS);
var payloadHash = ComputePayloadHash(dataJson, template, css, runtime, echarts);
if (!string.Equals(payloadHash, exportManifest.PayloadHash, StringComparison.OrdinalIgnoreCase))
{
issue = "The briefing payload hash does not match its manifest.";
return false;
}
var expectedCsp = GetContentSecurityPolicy(new(exportManifest, data, template, css, runtime, echarts, payloadHash));
var actualCsp = FindNode(document.DocumentNode, "//meta[@http-equiv='Content-Security-Policy']")
?.GetAttributeValue("content", string.Empty);
if (!string.Equals(actualCsp, expectedCsp, StringComparison.Ordinal))
{
issue = "The briefing Content Security Policy is missing or modified.";
return false;
}
parts = new(exportManifest, data, template, css, runtime, echarts, payloadHash);
return true;
}
/// <summary>
/// Defines <c>ValidateGeneratedParts</c> for the visual briefing feature.
/// </summary>
public static string ValidateGeneratedParts(
VisualBriefingManifest? manifest,
JsonElement data,
string templateHtml,
string css,
bool usesCharts)
{
if (data.ValueKind is not JsonValueKind.Object)
return "The briefing data block must be one JSON object.";
if (HasDuplicateProperties(data))
return "The briefing data block contains duplicated JSON property names.";
if (HasUnsafePropertyNames(data))
return "The briefing data block contains an unsafe JSON property name.";
if (ContainsLocalOrInternalValue(data, manifest))
return "The briefing data block contains a local path or an internal project reference.";
if (string.IsNullOrWhiteSpace(templateHtml))
return "The briefing template is empty.";
if (CSS_PROHIBITED.IsMatch(css) ||
CSS_PROTECTED_TARGET.IsMatch(css) ||
css.Contains("</style", StringComparison.OrdinalIgnoreCase))
return "The briefing CSS contains an external or unsafe construct.";
var document = new HtmlDocument();
document.LoadHtml($"<div id=\"validation-root\">{templateHtml}</div>");
var root = FindElementById(document, "validation-root");
if (root is null)
return "The briefing template could not be parsed.";
var elementIds = root.Descendants()
.Where(node => node.NodeType is HtmlNodeType.Element)
.Select(node => node.GetAttributeValue("id", string.Empty))
.Where(id => !string.IsNullOrWhiteSpace(id))
.ToArray();
if (elementIds.Any(id => id.StartsWith("mwai-", StringComparison.OrdinalIgnoreCase)) ||
elementIds.Distinct(StringComparer.Ordinal).Count() != elementIds.Length)
return "The briefing template contains a reserved or duplicated element ID.";
foreach (var node in root.Descendants())
{
if (node.NodeType is HtmlNodeType.Comment)
return "Briefing template HTML comments are not allowed.";
if (node.NodeType is HtmlNodeType.Text)
{
if (!string.IsNullOrWhiteSpace(node.InnerText))
return "All visible model-generated text must use a data-mwai binding.";
continue;
}
if (node.NodeType is not HtmlNodeType.Element)
continue;
if (!ALLOWED_ELEMENTS.Contains(node.Name))
return $"The briefing template contains the prohibited element '{node.Name}'.";
foreach (var attribute in node.Attributes)
{
if (attribute.Name.StartsWith("on", StringComparison.OrdinalIgnoreCase) ||
attribute.Name.Equals("style", StringComparison.OrdinalIgnoreCase) ||
!ALLOWED_ATTRIBUTES.Contains(attribute.Name) && !attribute.Name.StartsWith("data-mwai-", StringComparison.OrdinalIgnoreCase))
return $"The briefing template contains the prohibited attribute '{attribute.Name}'.";
if (attribute.Name.Equals("href", StringComparison.OrdinalIgnoreCase) &&
!attribute.Value.StartsWith('#'))
return "Only fragment links are allowed in briefing templates.";
if (attribute.Name.StartsWith("data-mwai-attr-", StringComparison.OrdinalIgnoreCase))
{
var targetAttribute = attribute.Name["data-mwai-attr-".Length..];
if (targetAttribute is not "alt" and not "aria-label" and not "aria-describedby" and not "title" and not "placeholder" and not "value" and not "max" and not "min")
return $"The briefing template contains an unsafe bound attribute '{targetAttribute}'.";
}
else if (attribute.Name.StartsWith("data-mwai-", StringComparison.OrdinalIgnoreCase) &&
!ALLOWED_DATA_ATTRIBUTES.Contains(attribute.Name))
{
return $"The briefing template contains the unknown binding '{attribute.Name}'.";
}
}
if (node.Name.Equals("img", StringComparison.OrdinalIgnoreCase) &&
FindAttribute(node, "data-mwai-asset") is null)
return "Every briefing image must use a data-mwai asset binding.";
if (node.Name.Equals("img", StringComparison.OrdinalIgnoreCase) &&
FindAttribute(node, "data-mwai-attr-alt") is null)
return "Every briefing image must use a bound text alternative.";
if (FindAttribute(node, "aria-label") is not null &&
FindAttribute(node, "data-mwai-attr-aria-label") is null ||
FindAttribute(node, "placeholder") is not null &&
FindAttribute(node, "data-mwai-attr-placeholder") is null ||
FindAttribute(node, "title") is not null &&
FindAttribute(node, "data-mwai-attr-title") is null)
return "Visible accessibility labels, placeholders, and titles must use data bindings.";
if (node.Name.Equals("input", StringComparison.OrdinalIgnoreCase) &&
FindAttribute(node, "value") is not null &&
FindAttribute(node, "data-mwai-attr-value") is null &&
FindAttribute(node, "data-mwai-model") is null)
return "A visible input value must use a data binding.";
if (node.Name.Equals("table", StringComparison.OrdinalIgnoreCase) &&
(FindNode(node, "./caption") is not { } caption ||
FindAttribute(caption, "data-mwai-text") is null && FindAttribute(caption, "data-mwai-expr") is null &&
FindNode(caption, ".//*[@data-mwai-text or @data-mwai-expr]") is null ||
FindNode(node, ".//th") is null ||
FindNodes(node, ".//th")?.Any(header =>
header.GetAttributeValue("scope", string.Empty) is not "row" and not "col") == true))
return "Every table must have a bound caption and scoped row or column headers.";
var bindingIssue = ValidateNodeBindings(node, data);
if (!string.IsNullOrEmpty(bindingIssue))
return bindingIssue;
}
var assets = GetDataAtPath(data, "_mwai.assets");
var boundAssetIds = root.Descendants()
.Where(node => node.NodeType is HtmlNodeType.Element && FindAttribute(node, "data-mwai-asset") is not null)
.Select(node => node.GetAttributeValue("data-mwai-asset", string.Empty))
.ToArray();
if (boundAssetIds.Any(assetId => string.IsNullOrWhiteSpace(assetId) ||
assets is not { ValueKind: JsonValueKind.Object } ||
!assets.Value.TryGetProperty(assetId, out var assetValue) ||
assetValue.ValueKind is not JsonValueKind.String ||
!assetValue.GetString()!.StartsWith("data:image/", StringComparison.Ordinal)))
return "The briefing template contains an unknown or invalid visual asset binding.";
if (manifest is not null)
{
foreach (var asset in manifest.Sources.Where(source => source.Kind is VisualBriefingSourceKind.VISUAL_ASSET))
{
var assetNode = root.Descendants()
.FirstOrDefault(node =>
node.NodeType is HtmlNodeType.Element &&
string.Equals(
node.GetAttributeValue("data-mwai-asset", string.Empty),
asset.AssetId,
StringComparison.Ordinal));
if (string.IsNullOrWhiteSpace(asset.AssetId) ||
assetNode is null ||
FindAttribute(assetNode, "hidden") is not null ||
string.Equals(
assetNode.GetAttributeValue("aria-hidden", string.Empty),
"true",
StringComparison.OrdinalIgnoreCase) ||
assetNode.Ancestors().TakeWhile(ancestor => ancestor != root)
.Take(1)
.Any(ancestor =>
FindAttribute(ancestor, "hidden") is not null ||
string.Equals(
ancestor.GetAttributeValue("aria-hidden", string.Empty),
"true",
StringComparison.OrdinalIgnoreCase)) ||
IsHiddenByCss(assetNode, css) ||
assetNode.ParentNode != root &&
IsHiddenByCss(assetNode.ParentNode, css))
return $"The visual asset '{asset.AssetId}' is not visibly bound in the template.";
}
}
var hasCharts = FindNode(root, ".//*[@data-mwai-chart]") is not null;
if (usesCharts != hasCharts)
return "Chart runtime selection does not match the template's data-mwai-chart bindings.";
return string.Empty;
}
/// <summary>
/// Defines <c>ComputePayloadHash</c> for the visual briefing feature.
/// </summary>
private static string ComputePayloadHash(string dataJson, string template, string css, string runtime, string? echarts) =>
VisualBriefingHashing.ComputeSections(dataJson, template, css, runtime, echarts);
/// <summary>
/// Defines <c>ScriptCspHash</c> for the visual briefing feature.
/// </summary>
private static string ScriptCspHash(string script) =>
$"'sha256-{Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(script)))}'";
/// <summary>
/// Defines <c>BuildRuntimeScript</c> for the visual briefing feature.
/// </summary>
private static string BuildRuntimeScript(string aiStudioVersion) =>
RUNTIME_SCRIPT.Replace(
"\"__MWAI_AI_STUDIO_VERSION__\"",
JsonSerializer.Serialize(aiStudioVersion, JSON_OPTIONS),
StringComparison.Ordinal);
/// <summary>
/// Defines <c>ExtractRuntimeAIStudioVersion</c> for the visual briefing feature.
/// </summary>
private static string? ExtractRuntimeAIStudioVersion(string runtime)
{
var match = RUNTIME_AI_VERSION_REGEX.Match(runtime);
if (!match.Success)
return null;
try
{
return JsonSerializer.Deserialize<string>(match.Groups["value"].Value, JSON_OPTIONS);
}
catch (JsonException)
{
return null;
}
}
/// <summary>
/// Defines <c>BuildScriptTag</c> for the visual briefing feature.
/// </summary>
private static string BuildScriptTag(string? script, string id) => string.IsNullOrWhiteSpace(script)
? string.Empty
: $"<script id=\"{id}\">{script}</script>";
/// <summary>
/// Defines <c>NormalizeTemplate</c> for the visual briefing feature.
/// </summary>
private static string NormalizeTemplate(string template) => template.Trim().Replace("\r\n", "\n", StringComparison.Ordinal);
// HtmlAgilityPack's public annotations declare these lookup APIs as non-null even though
// they return null for missing nodes and attributes. Keep that behavior explicit here.
// ReSharper disable once ReturnTypeCanBeNotNullable
/// <summary>
/// Defines <c>FindElementById</c> for the visual briefing feature.
/// </summary>
private static HtmlNode? FindElementById(HtmlDocument document, string id) => document.GetElementbyId(id);
// ReSharper disable once ReturnTypeCanBeNotNullable
/// <summary>
/// Defines <c>FindNode</c> for the visual briefing feature.
/// </summary>
private static HtmlNode? FindNode(HtmlNode node, string xpath) => node.SelectSingleNode(xpath);
// ReSharper disable once ReturnTypeCanBeNotNullable
/// <summary>
/// Defines <c>FindNodes</c> for the visual briefing feature.
/// </summary>
private static HtmlNodeCollection? FindNodes(HtmlNode node, string xpath) => node.SelectNodes(xpath);
// ReSharper disable once ReturnTypeCanBeNotNullable
/// <summary>
/// Defines <c>FindAttribute</c> for the visual briefing feature.
/// </summary>
private static HtmlAttribute? FindAttribute(HtmlNode node, string name) => node.Attributes[name];
/// <summary>
/// Defines <c>CanonicalizeTemplate</c> for the visual briefing feature.
/// </summary>
private static string CanonicalizeTemplate(string template)
{
var document = new HtmlDocument();
document.LoadHtml($"<div id=\"mwai-canonical-root\">{NormalizeTemplate(template)}</div>");
return NormalizeTemplate(FindElementById(document, "mwai-canonical-root")?.InnerHtml ?? string.Empty);
}
/// <summary>
/// Defines <c>HtmlEncode</c> for the visual briefing feature.
/// </summary>
private static string HtmlEncode(string value) => System.Net.WebUtility.HtmlEncode(value);
/// <summary>
/// Defines <c>HasExactAttributes</c> for the visual briefing feature.
/// </summary>
private static bool HasExactAttributes(HtmlNode node, params string[] expectedNames)
{
if (node.Attributes.Count != expectedNames.Length)
return false;
return expectedNames.All(expectedName =>
node.Attributes.Any(attribute => attribute.Name.Equals(expectedName, StringComparison.OrdinalIgnoreCase)));
}
/// <summary>
/// Defines <c>ValidateNodeBindings</c> for the visual briefing feature.
/// </summary>
private static string ValidateNodeBindings(HtmlNode node, JsonElement data)
{
var isRepeatedContext = node.Ancestors().Any(ancestor => FindAttribute(ancestor, "data-mwai-each") is not null);
foreach (var attribute in node.Attributes)
{
if (attribute.Name.StartsWith("data-mwai-attr-", StringComparison.OrdinalIgnoreCase) ||
PATH_BINDINGS.Contains(attribute.Name))
{
var path = attribute.Value;
if (!IsSafeBindingPath(path, isRepeatedContext))
return $"The briefing binding '{attribute.Name}' contains an invalid data path.";
var isRootPath = path.StartsWith("$root.", StringComparison.Ordinal);
if (isRepeatedContext &&
attribute.Name is "data-mwai-model" or "data-mwai-set" or "data-mwai-toggle" or "data-mwai-filter" &&
!isRootPath)
return $"The interactive binding '{attribute.Name}' inside a repeated area must use a $root path.";
var value = ResolveBindingValue(node, data, path, out var canValidateValue);
if (canValidateValue)
{
if (value is null)
return $"The briefing binding '{attribute.Name}' references a missing data path.";
if (attribute.Name.Equals("data-mwai-each", StringComparison.OrdinalIgnoreCase) &&
value.Value.ValueKind is not JsonValueKind.Array)
return "A data-mwai-each binding must reference an array.";
if (attribute.Name.Equals("data-mwai-expr", StringComparison.OrdinalIgnoreCase) &&
!IsValidFormula(value.Value, 0, isRoot: true))
return "A data-mwai-expr binding references an invalid formula tree.";
if (attribute.Name.Equals("data-mwai-if", StringComparison.OrdinalIgnoreCase) &&
value.Value.ValueKind is JsonValueKind.Object &&
!IsValidFormula(value.Value, 0, isRoot: true))
return "A data-mwai-if binding references an invalid formula tree.";
if (attribute.Name.Equals("data-mwai-chart", StringComparison.OrdinalIgnoreCase) &&
(value.Value.ValueKind is not JsonValueKind.Object ||
!IsValidChartOption(value.Value)))
return "A data-mwai-chart binding must reference a whitelisted chart option object.";
}
}
}
var hasFilter = FindAttribute(node, "data-mwai-filter") is not null;
var hasFilterValue = FindAttribute(node, "data-mwai-filter-value") is not null;
if (hasFilter != hasFilterValue)
return "A data-mwai-filter binding must have a matching data-mwai-filter-value binding.";
var selector = node.GetAttributeValue("data-mwai-search", string.Empty);
if (FindAttribute(node, "data-mwai-search") is not null && !SAFE_SELECTOR.IsMatch(selector))
return "A data-mwai-search binding contains an invalid selector.";
if (FindAttribute(node, "data-mwai-set") is not null)
{
var serializedValue = node.GetAttributeValue("data-mwai-value", string.Empty);
try
{
using var parsedValue = JsonDocument.Parse(serializedValue);
}
catch (JsonException)
{
return "A data-mwai-set binding must contain a valid JSON data-mwai-value.";
}
}
var tabTarget = node.GetAttributeValue("data-mwai-tab-target", string.Empty);
if (FindAttribute(node, "data-mwai-tab-target") is not null)
{
if (!IsSafeDataPath(tabTarget))
return "A data-mwai-tab-target binding contains an invalid identifier.";
var tabs = node.AncestorsAndSelf().FirstOrDefault(candidate => FindAttribute(candidate, "data-mwai-tabs") is not null);
if (tabs is null || FindNode(tabs, $".//*[@data-mwai-tab-panel='{tabTarget}']") is null)
return "A data-mwai-tab-target binding has no matching panel.";
}
if (FindAttribute(node, "data-mwai-chart") is not null &&
FindAttribute(node, "aria-describedby") is null &&
FindAttribute(node, "data-mwai-attr-aria-describedby") is null)
return "Every chart must reference a visible text or table alternative with aria-describedby.";
if (FindAttribute(node, "data-mwai-chart") is not null)
{
var descriptionIds = node.GetAttributeValue("aria-describedby", string.Empty)
.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (FindAttribute(node, "data-mwai-attr-aria-describedby") is { } boundDescription)
{
var value = ResolveBindingValue(node, data, boundDescription.Value, out _);
descriptionIds = value is { ValueKind: JsonValueKind.String }
? value.Value.GetString()!.Split(' ', StringSplitOptions.RemoveEmptyEntries)
: [];
}
if (descriptionIds.Length == 0 ||
descriptionIds.Any(id => FindElementById(node.OwnerDocument, id) is null))
return "A chart's aria-describedby binding must reference an existing text or table alternative.";
}
return string.Empty;
}
/// <summary>
/// Defines <c>ResolveBindingValue</c> for the visual briefing feature.
/// </summary>
private static JsonElement? ResolveBindingValue(
HtmlNode node,
JsonElement root,
string path,
out bool canValidateValue)
{
if (path.StartsWith("$root.", StringComparison.Ordinal))
{
canValidateValue = true;
return GetDataAtPath(root, path[6..]);
}
var context = root;
foreach (var repeat in node.Ancestors()
.Where(ancestor => FindAttribute(ancestor, "data-mwai-each") is not null)
.Reverse())
{
var repeatPath = repeat.GetAttributeValue("data-mwai-each", string.Empty);
var collection = ResolveRelativePath(root, context, repeatPath);
if (collection is not { ValueKind: JsonValueKind.Array })
{
canValidateValue = true;
return null;
}
if (collection.Value.GetArrayLength() == 0)
{
canValidateValue = false;
return null;
}
context = collection.Value[0];
}
canValidateValue = true;
return ResolveRelativePath(root, context, path);
}
/// <summary>
/// Defines <c>ResolveRelativePath</c> for the visual briefing feature.
/// </summary>
private static JsonElement? ResolveRelativePath(JsonElement root, JsonElement context, string path)
{
if (path is "$root")
return root;
if (path.StartsWith("$root.", StringComparison.Ordinal))
return GetDataAtPath(root, path[6..]);
if (path is "." or "$value")
return context;
if (path is "$index")
return JsonSerializer.SerializeToElement(0);
if (path.StartsWith(".", StringComparison.Ordinal))
return GetDataAtPath(context, path[1..]);
return GetDataAtPath(root, path);
}
/// <summary>
/// Defines <c>GetDataAtPath</c> for the visual briefing feature.
/// </summary>
private static JsonElement? GetDataAtPath(JsonElement data, string path)
{
var current = data;
foreach (var segment in path.Split('.', StringSplitOptions.RemoveEmptyEntries))
{
if (current.ValueKind is JsonValueKind.Object && current.TryGetProperty(segment, out var property))
{
current = property;
continue;
}
if (current.ValueKind is JsonValueKind.Array &&
int.TryParse(segment, out var index) &&
index >= 0 &&
index < current.GetArrayLength())
{
current = current[index];
continue;
}
return null;
}
return current;
}
/// <summary>
/// Defines <c>IsValidFormula</c> for the visual briefing feature.
/// </summary>
private static bool IsValidFormula(JsonElement node, int depth, bool isRoot)
{
if (depth > 32)
return false;
if (node.ValueKind is JsonValueKind.Number or JsonValueKind.String or JsonValueKind.True or JsonValueKind.False or JsonValueKind.Null)
return !isRoot;
if (node.ValueKind is not JsonValueKind.Object)
return false;
if (isRoot &&
(!node.TryGetProperty("formulaVersion", out var version) ||
version.ValueKind is not JsonValueKind.Number ||
!version.TryGetInt32(out var parsedVersion) ||
parsedVersion != VisualBriefingVersions.FORMULA))
return false;
// Formula paths are always absolute, see VisualBriefingValidation.ValidateFormulaNode.
// Therefore, relative paths and the context-self path are not allowed here:
if (node.TryGetProperty("path", out var path))
return node.EnumerateObject().All(property =>
property.Name is "formulaVersion" or "path") &&
path.ValueKind is JsonValueKind.String &&
IsSafeBindingPath(path.GetString() ?? string.Empty, repeatedContext: false);
if (node.TryGetProperty("value", out _))
return node.EnumerateObject().All(property =>
property.Name is "formulaVersion" or "value");
if (!node.TryGetProperty("op", out var operation) ||
operation.ValueKind is not JsonValueKind.String ||
!FORMULA_OPERATORS.Contains(operation.GetString() ?? string.Empty) ||
!node.TryGetProperty("args", out var arguments) ||
arguments.ValueKind is not JsonValueKind.Array)
return false;
var argumentCount = arguments.GetArrayLength();
var validArity = operation.GetString() switch
{
"sqrt" or "log" or "exp" => argumentCount == 1,
"subtract" or "divide" or "power" or "eq" or "ne" or "gt" or "gte" or "lt" or "lte" => argumentCount == 2,
"if" => argumentCount == 3,
"round" => argumentCount is 1 or 2,
_ => argumentCount > 0,
};
return validArity &&
node.EnumerateObject().All(property =>
property.Name is "formulaVersion" or "op" or "args") &&
arguments.EnumerateArray().All(argument => IsValidFormula(argument, depth + 1, isRoot: false));
}
/// <summary>
/// Defines <c>IsValidChartOption</c> for the visual briefing feature.
/// </summary>
private static bool IsValidChartOption(JsonElement option)
{
if (!option.TryGetProperty("series", out var series) ||
series.ValueKind is not JsonValueKind.Array ||
series.GetArrayLength() == 0)
return false;
HashSet<string> allowedSeries = new(StringComparer.Ordinal)
{
"line",
"bar",
"scatter",
"pie",
"radar",
};
return series.EnumerateArray().All(item =>
item.ValueKind is JsonValueKind.Object &&
item.TryGetProperty("type", out var type) &&
type.ValueKind is JsonValueKind.String &&
allowedSeries.Contains(type.GetString() ?? string.Empty));
}
/// <summary>
/// Defines <c>HasDuplicateProperties</c> for the visual briefing feature.
/// </summary>
private static bool HasDuplicateProperties(JsonElement value)
{
if (value.ValueKind is JsonValueKind.Array)
return value.EnumerateArray().Any(HasDuplicateProperties);
if (value.ValueKind is not JsonValueKind.Object)
return false;
var properties = value.EnumerateObject().ToArray();
return properties.Select(property => property.Name).Distinct(StringComparer.Ordinal).Count() != properties.Length ||
properties.Any(property => HasDuplicateProperties(property.Value));
}
/// <summary>
/// Defines <c>HasUnsafePropertyNames</c> for the visual briefing feature.
/// </summary>
private static bool HasUnsafePropertyNames(JsonElement value)
{
if (value.ValueKind is JsonValueKind.Array)
return value.EnumerateArray().Any(HasUnsafePropertyNames);
if (value.ValueKind is not JsonValueKind.Object)
return false;
return value.EnumerateObject().Any(property =>
property.Name is "__proto__" or "prototype" or "constructor" ||
HasUnsafePropertyNames(property.Value));
}
/// <summary>
/// Defines <c>ContainsLocalOrInternalValue</c> for the visual briefing feature.
/// </summary>
private static bool ContainsLocalOrInternalValue(JsonElement value, VisualBriefingManifest? manifest)
{
if (value.ValueKind is JsonValueKind.Array)
return value.EnumerateArray().Any(item => ContainsLocalOrInternalValue(item, manifest));
if (value.ValueKind is JsonValueKind.Object)
return value.EnumerateObject().Any(property =>
property.Name is not "_mwai" &&
ContainsLocalOrInternalValue(property.Value, manifest));
if (value.ValueKind is not JsonValueKind.String)
return false;
var text = value.GetString() ?? string.Empty;
if (text.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
return true;
if (manifest is null)
return false;
var pathComparison = OperatingSystem.IsWindows()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
if (manifest.Sources.Any(source =>
text.Contains(source.Path, pathComparison) ||
text.Contains(source.Path.Replace('\\', '/'), pathComparison)))
return true;
var sensitiveValues = new[]
{
manifest.Settings.ProviderId,
manifest.Settings.ProfileId,
manifest.Settings.ModelId,
}
.Where(candidate => !string.IsNullOrWhiteSpace(candidate));
return sensitiveValues.Any(candidate => text.Contains(candidate, StringComparison.Ordinal));
}
/// <summary>
/// Defines <c>IsSafeDataPath</c> for the visual briefing feature.
/// </summary>
private static bool IsSafeDataPath(string path) =>
DATA_PATH.IsMatch(path) &&
path.Split('.').All(segment => segment is not "__proto__" and not "prototype" and not "constructor");
/// <summary>
/// Defines <c>IsSafeBindingPath</c> for the visual briefing feature.
/// </summary>
private static bool IsSafeBindingPath(string path, bool repeatedContext)
{
if (path is "$root")
return true;
if (IsSafeDataPath(path))
return true;
// Inside a repeated area, "." addresses the current item itself. ResolveRelativePath
// resolves it, so the safety check must accept it as well:
if (repeatedContext && path is ".")
return true;
if (!repeatedContext || !LOCAL_DATA_PATH.IsMatch(path))
return false;
return path.Split('.', StringSplitOptions.RemoveEmptyEntries)
.All(segment => segment is not "__proto__" and not "prototype" and not "constructor");
}
/// <summary>
/// Defines <c>ContainsChartBinding</c> for the visual briefing feature.
/// </summary>
private static bool ContainsChartBinding(string templateHtml)
{
var document = new HtmlDocument();
document.LoadHtml($"<div id=\"chart-detection-root\">{templateHtml}</div>");
var root = FindElementById(document, "chart-detection-root");
return root is not null && FindNode(root, ".//*[@data-mwai-chart]") is not null;
}
/// <summary>
/// Determines whether a simple stylesheet rule hides an element.
/// </summary>
/// <param name="node">The element to inspect.</param>
/// <param name="css">The validated model stylesheet.</param>
/// <returns><see langword="true"/> when a matching rule hides the element.</returns>
private static bool IsHiddenByCss(HtmlNode node, string css)
{
foreach (Match rule in CssRuleRegex().Matches(css))
{
if (!CssHiddenDeclarationRegex().IsMatch(rule.Groups["declarations"].Value))
continue;
foreach (var selector in rule.Groups["selectors"].Value.Split(','))
{
if (SimpleSelectorMatches(node, selector))
return true;
}
}
return false;
}
/// <summary>
/// Matches the final simple component of a CSS selector against one element.
/// </summary>
/// <param name="node">The element.</param>
/// <param name="selector">The stylesheet selector.</param>
/// <returns>Whether the selector targets the element.</returns>
private static bool SimpleSelectorMatches(HtmlNode node, string selector)
{
var candidate = selector.Trim();
if (candidate.Length == 0)
return false;
var finalSeparator = candidate.LastIndexOfAny([' ', '>', '+', '~']);
if (finalSeparator >= 0)
candidate = candidate[(finalSeparator + 1)..].Trim();
var pseudo = candidate.IndexOf(':');
if (pseudo >= 0)
candidate = candidate[..pseudo];
if (candidate.Contains("[data-mwai-asset", StringComparison.OrdinalIgnoreCase))
return FindAttribute(node, "data-mwai-asset") is not null;
var idMatch = IdRegex().Match(candidate);
if (idMatch.Success &&
!string.Equals(node.Id, idMatch.Groups["id"].Value, StringComparison.Ordinal))
return false;
var requiredClasses = RequiredClassRegex().Matches(candidate)
.Select(match => match.Groups["class"].Value)
.ToArray();
var classes = node.GetAttributeValue("class", string.Empty)
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.ToHashSet(StringComparer.Ordinal);
if (requiredClasses.Any(requiredClass => !classes.Contains(requiredClass)))
return false;
var tag = TagRegex().Match(candidate);
return !tag.Success ||
string.Equals(node.Name, tag.Groups["tag"].Value, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Defines <c>ValidateProtectedData</c> for the visual briefing feature.
/// </summary>
private static string ValidateProtectedData(VisualBriefingExportManifest exportManifest, JsonElement data)
{
if (!data.TryGetProperty("_mwai", out var protectedData) ||
protectedData.ValueKind is not JsonValueKind.Object ||
!protectedData.TryGetProperty("schemaVersion", out var schemaVersion) ||
schemaVersion.ValueKind is not JsonValueKind.Number ||
!schemaVersion.TryGetInt32(out var parsedSchemaVersion) ||
parsedSchemaVersion != VisualBriefingVersions.SCHEMA ||
!protectedData.TryGetProperty("runtimeVersion", out var runtimeVersion) ||
runtimeVersion.ValueKind is not JsonValueKind.Number ||
!runtimeVersion.TryGetInt32(out var parsedRuntimeVersion) ||
parsedRuntimeVersion != VisualBriefingVersions.RUNTIME ||
!protectedData.TryGetProperty("aiStudioVersion", out var aiStudioVersion) ||
aiStudioVersion.ValueKind is not JsonValueKind.String ||
!string.Equals(aiStudioVersion.GetString(), exportManifest.AIStudioVersion, StringComparison.Ordinal) ||
!protectedData.TryGetProperty("assets", out var protectedAssets) ||
protectedAssets.ValueKind is not JsonValueKind.Object ||
data.TryGetProperty("assets", out _))
return "The protected briefing data block is incomplete or inconsistent.";
var protectedAssetProperties = protectedAssets.EnumerateObject().ToArray();
if (protectedAssetProperties.Any(property =>
property.Value.ValueKind is not JsonValueKind.String ||
!property.Value.GetString()!.StartsWith("data:image/", StringComparison.Ordinal)) ||
protectedAssetProperties.Select(property => property.Name).Distinct(StringComparer.Ordinal).Count() != protectedAssetProperties.Length)
return "The protected embedded asset map contains invalid or duplicated entries.";
if (!protectedData.TryGetProperty("assetMetadata", out var assetMetadata) ||
assetMetadata.ValueKind is not JsonValueKind.Object)
return "The protected visual asset metadata is missing.";
var metadataProperties = assetMetadata.EnumerateObject().ToArray();
if (metadataProperties.Length != protectedAssetProperties.Length ||
metadataProperties.Any(property =>
!protectedAssets.TryGetProperty(property.Name, out _) ||
property.Value.ValueKind is not JsonValueKind.Object ||
!property.Value.TryGetProperty("description", out var description) ||
description.ValueKind is not JsonValueKind.String ||
string.IsNullOrWhiteSpace(description.GetString()) ||
!property.Value.TryGetProperty("altText", out var altText) ||
altText.ValueKind is not JsonValueKind.String ||
string.IsNullOrWhiteSpace(altText.GetString())))
return "The protected visual asset metadata is invalid or incomplete.";
if (!protectedData.TryGetProperty("footer", out var footer) ||
footer.ValueKind is not JsonValueKind.Object)
return "The protected briefing footer data is missing.";
string[] footerFields = ["createdWith", "models", "createdAt", "authors", "protection"];
return footerFields.Any(field =>
!footer.TryGetProperty(field, out var value) ||
value.ValueKind is not JsonValueKind.String ||
string.IsNullOrWhiteSpace(value.GetString()))
? "The protected briefing footer data is incomplete."
: string.Empty;
}
/// <summary>
/// Defines <c>CreateExportManifest</c> for the visual briefing feature.
/// </summary>
private static VisualBriefingExportManifest CreateExportManifest(
VisualBriefingManifest manifest,
VisualBriefingRevisionRequest request,
string payloadHash,
string aiStudioVersion,
string runtimeAIStudioVersion) => new()
{
BriefingId = manifest.BriefingId,
RevisionId = request.RevisionId ?? Guid.NewGuid(),
ParentRevisionId = request.ParentRevisionId,
Name = manifest.Name,
Author = manifest.Author,
CreatedAtUtc = request.CreatedAtUtc ?? DateTimeOffset.UtcNow,
TargetLanguage = manifest.Settings.TargetLanguage,
CustomTargetLanguage = manifest.Settings.CustomTargetLanguage,
AudienceProfile = manifest.Settings.AudienceProfile,
AudienceAgeGroup = manifest.Settings.AudienceAgeGroup,
AudienceOrganizationalLevel = manifest.Settings.AudienceOrganizationalLevel,
AudienceExpertise = manifest.Settings.AudienceExpertise,
ShowSourceReferences = manifest.Settings.ShowSourceReferences,
ProtectionLevel = manifest.Settings.ProtectionLevel,
CustomProtectionLevel = manifest.Settings.CustomProtectionLevel,
AIStudioVersion = aiStudioVersion,
RuntimeAIStudioVersion = runtimeAIStudioVersion,
PayloadHash = payloadHash,
};
/// <summary>
/// Defines <c>AddProtectedArtifactData</c> for the visual briefing feature.
/// </summary>
private static JsonElement AddProtectedArtifactData(VisualBriefingManifest manifest, VisualBriefingRevisionRequest request)
{
var source = request.Data;
var dictionary = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(source.GetRawText(), JSON_OPTIONS) ?? [];
dictionary.Remove("assets");
dictionary.Remove("footerTemplates");
dictionary.Remove("protectionLabel");
dictionary.Remove("_mwai");
dictionary["_mwai"] = JsonSerializer.SerializeToElement(new
{
schemaVersion = VisualBriefingVersions.SCHEMA,
runtimeVersion = VisualBriefingVersions.RUNTIME,
aiStudioVersion = Assembly.GetExecutingAssembly().GetCustomAttribute<MetaDataAttribute>()?.Version ?? "unknown",
assets = request.EmbeddedAssets ?? new Dictionary<string, string>(StringComparer.Ordinal),
assetMetadata = (request.AssetPlan ?? []).ToDictionary(
asset => asset.AssetId,
asset => new { asset.Description, asset.AltText },
StringComparer.Ordinal),
footer = BuildFooter(manifest, request),
}, JSON_OPTIONS);
return JsonSerializer.SerializeToElement(dictionary, JSON_OPTIONS);
}
/// <summary>
/// Defines <c>BuildFooter</c> for the visual briefing feature.
/// </summary>
private static object BuildFooter(VisualBriefingManifest manifest, VisualBriefingRevisionRequest request)
{
var labels = FooterLabelsFor(manifest.Settings, request.CustomLanguageLabels);
var protection = manifest.Settings.ProtectionLevel is VisualBriefingProtectionLevel.OTHER
? manifest.Settings.CustomProtectionLevel
: labels.ProtectionLevel;
var created = (request.CreatedAtUtc ?? DateTimeOffset.UtcNow).ToString("yyyy-MM-dd");
var author = string.IsNullOrWhiteSpace(manifest.Author) ? "—" : manifest.Author;
var version = Assembly.GetExecutingAssembly().GetCustomAttribute<MetaDataAttribute>()?.Version ?? "unknown";
var contributions = request.ModelContributions?.Where(contribution => !string.IsNullOrWhiteSpace(contribution.Model))
.Distinct()
.ToArray() ?? [];
if (contributions.Length == 0 && !string.IsNullOrWhiteSpace(request.ModelDisplayName))
contributions = [new(VisualBriefingModelRole.CONTENT, request.ModelDisplayName)];
var models = contributions.Length == 0
? "—"
: string.Join(
"; ",
contributions
.GroupBy(contribution => contribution.Model, StringComparer.Ordinal)
.Select(group =>
{
var roles = group.Select(contribution => contribution.Role)
.Distinct()
.Select(role => role is VisualBriefingModelRole.DESIGN
? labels.PresentationRole
: labels.ContentRole);
return $"{group.Key} ({string.Join(", ", roles)})";
}));
return new Dictionary<string, string>(StringComparer.Ordinal)
{
["createdWith"] = ApplyFooterTemplate(labels.CreatedWith, "version", version),
["models"] = ApplyFooterTemplate(labels.Models, "models", models),
["createdAt"] = ApplyFooterTemplate(labels.CreatedAt, "date", created),
["authors"] = ApplyFooterTemplate(labels.Authors, "authors", author),
["protection"] = ApplyFooterTemplate(labels.Protection, "level", protection),
};
}
/// <summary>
/// Defines <c>FooterLabelsFor</c> for the visual briefing feature.
/// </summary>
private static FooterLabels FooterLabelsFor(
VisualBriefingLocalSettings settings,
IReadOnlyDictionary<string, string>? customLabels)
{
if (settings.TargetLanguage is CommonLanguages.OTHER &&
customLabels is not null)
{
return new(
customLabels["createdWith"],
customLabels["models"],
customLabels["createdAt"],
customLabels["authors"],
customLabels["protection"],
customLabels["contentRole"],
customLabels["designRole"],
customLabels["protectionLevel"]);
}
var protection = LocalizedProtectionLevel(settings.TargetLanguage, settings.ProtectionLevel);
return settings.TargetLanguage switch
{
CommonLanguages.ZH_CN => new(
"使用 MindWork AI Studio v{version} 创建。",
"贡献模型:{models}。",
"版本创建日期:{date}。",
"作者:{authors}。",
"保护级别:{level}。",
"内容",
"演示",
protection),
CommonLanguages.HI_IN => new(
"MindWork AI Studio v{version} से बनाया गया।",
"योगदान देने वाले मॉडल: {models}।",
"संस्करण निर्माण तिथि: {date}।",
"लेखक: {authors}।",
"सुरक्षा स्तर: {level}।",
"सामग्री",
"प्रस्तुति",
protection),
CommonLanguages.ES_ES => new(
"Creado con MindWork AI Studio v{version}.",
"Modelos participantes: {models}.",
"Versión creada el {date}.",
"Autoría: {authors}.",
"Nivel de protección: {level}.",
"Contenido",
"Presentación",
protection),
CommonLanguages.FR_FR => new(
"Créé avec MindWork AI Studio v{version}.",
"Modèles contributeurs : {models}.",
"Version créée le {date}.",
"Auteur(s) : {authors}.",
"Niveau de protection : {level}.",
"Contenu",
"Présentation",
protection),
CommonLanguages.DE_DE or CommonLanguages.DE_AT or CommonLanguages.DE_CH => new(
"Erstellt mit MindWork AI Studio v{version}.",
"Beitragende Modelle: {models}.",
"Version erstellt am {date}.",
"Autorinnen und Autoren: {authors}.",
"Schutzniveau: {level}.",
"Inhalt",
"Darstellung",
protection),
CommonLanguages.JA_JP => new(
"MindWork AI Studio v{version} で作成。",
"使用モデル: {models}。",
"バージョン作成日: {date}。",
"作成者: {authors}。",
"保護レベル: {level}。",
"コンテンツ",
"プレゼンテーション",
protection),
CommonLanguages.RU_RU => new(
"Создано с помощью MindWork AI Studio v{version}.",
"Использованные модели: {models}.",
"Версия создана {date}.",
"Автор(ы): {authors}.",
"Уровень защиты: {level}.",
"Содержание",
"Представление",
protection),
_ => new(
"Created with MindWork AI Studio v{version}.",
"Contributing models: {models}.",
"Revision created on {date}.",
"Author(s): {authors}.",
"Protection level: {level}.",
"Content",
"Presentation",
protection),
};
}
/// <summary>
/// Defines <c>LocalizedProtectionLevel</c> for the visual briefing feature.
/// </summary>
private static string LocalizedProtectionLevel(
CommonLanguages language,
VisualBriefingProtectionLevel level) => (language, level) switch
{
(CommonLanguages.DE_DE or CommonLanguages.DE_AT or CommonLanguages.DE_CH, VisualBriefingProtectionLevel.PUBLIC) => "öffentlich",
(CommonLanguages.DE_DE or CommonLanguages.DE_AT or CommonLanguages.DE_CH, VisualBriefingProtectionLevel.INTERNAL) => "intern",
(CommonLanguages.DE_DE or CommonLanguages.DE_AT or CommonLanguages.DE_CH, VisualBriefingProtectionLevel.PRIVATE) => "privat",
(CommonLanguages.DE_DE or CommonLanguages.DE_AT or CommonLanguages.DE_CH, VisualBriefingProtectionLevel.CONFIDENTIAL) => "vertraulich",
(CommonLanguages.DE_DE or CommonLanguages.DE_AT or CommonLanguages.DE_CH, VisualBriefingProtectionLevel.STRICTLY_CONFIDENTIAL) => "streng vertraulich",
(CommonLanguages.DE_DE or CommonLanguages.DE_AT or CommonLanguages.DE_CH, VisualBriefingProtectionLevel.SECRET) => "geheim",
(CommonLanguages.DE_DE or CommonLanguages.DE_AT or CommonLanguages.DE_CH, VisualBriefingProtectionLevel.TOP_SECRET) => "streng geheim",
(CommonLanguages.ZH_CN, VisualBriefingProtectionLevel.PUBLIC) => "公开",
(CommonLanguages.ZH_CN, VisualBriefingProtectionLevel.INTERNAL) => "内部",
(CommonLanguages.ZH_CN, VisualBriefingProtectionLevel.PRIVATE) => "私有",
(CommonLanguages.ZH_CN, VisualBriefingProtectionLevel.CONFIDENTIAL) => "机密",
(CommonLanguages.ZH_CN, VisualBriefingProtectionLevel.STRICTLY_CONFIDENTIAL) => "严格保密",
(CommonLanguages.ZH_CN, VisualBriefingProtectionLevel.SECRET) => "秘密",
(CommonLanguages.ZH_CN, VisualBriefingProtectionLevel.TOP_SECRET) => "绝密",
(CommonLanguages.HI_IN, VisualBriefingProtectionLevel.PUBLIC) => "सार्वजनिक",
(CommonLanguages.HI_IN, VisualBriefingProtectionLevel.INTERNAL) => "आंतरिक",
(CommonLanguages.HI_IN, VisualBriefingProtectionLevel.PRIVATE) => "निजी",
(CommonLanguages.HI_IN, VisualBriefingProtectionLevel.CONFIDENTIAL) => "गोपनीय",
(CommonLanguages.HI_IN, VisualBriefingProtectionLevel.STRICTLY_CONFIDENTIAL) => "अत्यंत गोपनीय",
(CommonLanguages.HI_IN, VisualBriefingProtectionLevel.SECRET) => "गुप्त",
(CommonLanguages.HI_IN, VisualBriefingProtectionLevel.TOP_SECRET) => "परम गुप्त",
(CommonLanguages.ES_ES, VisualBriefingProtectionLevel.PUBLIC) => "público",
(CommonLanguages.ES_ES, VisualBriefingProtectionLevel.INTERNAL) => "interno",
(CommonLanguages.ES_ES, VisualBriefingProtectionLevel.PRIVATE) => "privado",
(CommonLanguages.ES_ES, VisualBriefingProtectionLevel.CONFIDENTIAL) => "confidencial",
(CommonLanguages.ES_ES, VisualBriefingProtectionLevel.STRICTLY_CONFIDENTIAL) => "estrictamente confidencial",
(CommonLanguages.ES_ES, VisualBriefingProtectionLevel.SECRET) => "secreto",
(CommonLanguages.ES_ES, VisualBriefingProtectionLevel.TOP_SECRET) => "alto secreto",
(CommonLanguages.FR_FR, VisualBriefingProtectionLevel.PUBLIC) => "public",
(CommonLanguages.FR_FR, VisualBriefingProtectionLevel.INTERNAL) => "interne",
(CommonLanguages.FR_FR, VisualBriefingProtectionLevel.PRIVATE) => "privé",
(CommonLanguages.FR_FR, VisualBriefingProtectionLevel.CONFIDENTIAL) => "confidentiel",
(CommonLanguages.FR_FR, VisualBriefingProtectionLevel.STRICTLY_CONFIDENTIAL) => "strictement confidentiel",
(CommonLanguages.FR_FR, VisualBriefingProtectionLevel.SECRET) => "secret",
(CommonLanguages.FR_FR, VisualBriefingProtectionLevel.TOP_SECRET) => "très secret",
(CommonLanguages.JA_JP, VisualBriefingProtectionLevel.PUBLIC) => "公開",
(CommonLanguages.JA_JP, VisualBriefingProtectionLevel.INTERNAL) => "社内",
(CommonLanguages.JA_JP, VisualBriefingProtectionLevel.PRIVATE) => "非公開",
(CommonLanguages.JA_JP, VisualBriefingProtectionLevel.CONFIDENTIAL) => "機密",
(CommonLanguages.JA_JP, VisualBriefingProtectionLevel.STRICTLY_CONFIDENTIAL) => "厳秘",
(CommonLanguages.JA_JP, VisualBriefingProtectionLevel.SECRET) => "秘密",
(CommonLanguages.JA_JP, VisualBriefingProtectionLevel.TOP_SECRET) => "最高機密",
(CommonLanguages.RU_RU, VisualBriefingProtectionLevel.PUBLIC) => "общедоступно",
(CommonLanguages.RU_RU, VisualBriefingProtectionLevel.INTERNAL) => "для внутреннего использования",
(CommonLanguages.RU_RU, VisualBriefingProtectionLevel.PRIVATE) => "частное",
(CommonLanguages.RU_RU, VisualBriefingProtectionLevel.CONFIDENTIAL) => "конфиденциально",
(CommonLanguages.RU_RU, VisualBriefingProtectionLevel.STRICTLY_CONFIDENTIAL) => "строго конфиденциально",
(CommonLanguages.RU_RU, VisualBriefingProtectionLevel.SECRET) => "секретно",
(CommonLanguages.RU_RU, VisualBriefingProtectionLevel.TOP_SECRET) => "совершенно секретно",
_ => level.ToString().Replace('_', ' ').ToLowerInvariant(),
};
/// <summary>
/// Defines <c>FooterLabels</c> for the visual briefing feature.
/// </summary>
private sealed record FooterLabels(
string CreatedWith,
string Models,
string CreatedAt,
string Authors,
string Protection,
string ContentRole,
string PresentationRole,
string ProtectionLevel);
/// <summary>
/// Defines <c>ApplyFooterTemplate</c> for the visual briefing feature.
/// </summary>
private static string ApplyFooterTemplate(string template, string token, string value) =>
template.Replace($"{{{token}}}", value, StringComparison.Ordinal);
/// <summary>
/// Defines <c>GetHtmlLanguage</c> for the visual briefing feature.
/// </summary>
private static string GetHtmlLanguage(VisualBriefingLocalSettings settings) =>
GetHtmlLanguage(settings.TargetLanguage, settings.CustomTargetLanguage);
/// <summary>
/// Defines <c>GetHtmlLanguage</c> for the visual briefing feature.
/// </summary>
private static string GetHtmlLanguage(CommonLanguages language, string customLanguage) => language switch
{
CommonLanguages.DE_DE => "de-DE",
CommonLanguages.DE_AT => "de-AT",
CommonLanguages.DE_CH => "de-CH",
CommonLanguages.ZH_CN => "zh-CN",
CommonLanguages.HI_IN => "hi-IN",
CommonLanguages.ES_ES => "es-ES",
CommonLanguages.FR_FR => "fr-FR",
CommonLanguages.JA_JP => "ja-JP",
CommonLanguages.RU_RU => "ru-RU",
CommonLanguages.EN_GB => "en-GB",
CommonLanguages.EN_US => "en-US",
CommonLanguages.OTHER when HTML_LANGUAGE_TAG.IsMatch(customLanguage.Trim()) => customLanguage.Trim(),
_ => "und",
};
/// <summary>
/// Defines <c>LoadECharts</c> for the visual briefing feature.
/// </summary>
private static string? LoadECharts()
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(name => name.EndsWith("Assistants.VisualBriefing.Runtime.echarts.common.min.js", StringComparison.Ordinal));
if (resourceName is null)
return null;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
return null;
using var reader = new StreamReader(stream, Encoding.UTF8);
return reader.ReadToEnd();
}
/// <summary>
/// Defines <c>CssProhibitedRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"(?:@import|@font-face|url\s*\(|expression\s*\(|javascript\s*:|behavior\s*:|-moz-binding|content\s*:|<\s*/?\s*script)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex CssProhibitedRegex();
/// <summary>
/// Defines <c>CssProtectedTargetRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"(?:#mwai-static-footer|\.mwai-footer|(?:^|[^A-Za-z0-9_-])(?:html|body|footer|:root)(?=[^A-Za-z0-9_-]))", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline)]
private static partial Regex CssProtectedTargetRegex();
/// <summary>
/// Defines <c>DataPathRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"^(?:\$root\.)?(?:\$index|\$value|[A-Za-z_][A-Za-z0-9_-]*)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*$", RegexOptions.CultureInvariant)]
private static partial Regex DataPathRegex();
/// <summary>
/// Defines <c>LocalDataPathRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"^\.(?:[A-Za-z_][A-Za-z0-9_-]*)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*$", RegexOptions.CultureInvariant)]
private static partial Regex LocalDataPathRegex();
/// <summary>
/// Defines <c>SafeSelectorRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"^[.#]?[A-Za-z][A-Za-z0-9_-]*(?:\s+[.#]?[A-Za-z][A-Za-z0-9_-]*)*$", RegexOptions.CultureInvariant)]
private static partial Regex SafeSelectorRegex();
/// <summary>
/// Defines <c>HtmlLanguageTagRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$", RegexOptions.CultureInvariant)]
private static partial Regex HtmlLanguageTagRegex();
/// <summary>
/// Matches simple CSS rules for visibility checks.
/// </summary>
/// <returns>The generated regular expression.</returns>
[GeneratedRegex(@"(?<selectors>[^{}]+)\{(?<declarations>[^{}]*)\}", RegexOptions.CultureInvariant)]
private static partial Regex CssRuleRegex();
/// <summary>
/// Matches declarations that visually hide an element.
/// </summary>
/// <returns>The generated regular expression.</returns>
[GeneratedRegex(@"(?:display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0(?:\.0+)?)(?:\s*!important)?\s*(?:;|$)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex CssHiddenDeclarationRegex();
/// <summary>
/// Defines <c>ManifestRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"<!--MWAI_VISUAL_BRIEFING_MANIFEST:(?<value>[A-Za-z0-9+/=]+)-->", RegexOptions.CultureInvariant)]
private static partial Regex ManifestRegex();
/// <summary>
/// Defines <c>StyleRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"<style\s+id=""mwai-briefing-style"">(?<value>[\s\S]*?)</style>", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex StyleRegex();
/// <summary>
/// Defines <c>RuntimeRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"<script\s+id=""mwai-briefing-runtime"">(?<value>[\s\S]*?)</script>", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex RuntimeRegex();
/// <summary>
/// Defines <c>RuntimeAIVersionRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"const AI_STUDIO_VERSION = (?<value>""(?:\\.|[^""\\])*"");", RegexOptions.CultureInvariant)]
private static partial Regex RuntimeAIVersionRegex();
/// <summary>
/// Defines <c>EChartsRegex</c> for the visual briefing feature.
/// </summary>
[GeneratedRegex(@"<script\s+id=""mwai-echarts-runtime"">(?<value>[\s\S]*?)</script>", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex EChartsRegex();
/// <summary>
/// Defines the pinned declarative AI Studio briefing runtime.
/// </summary>
private const string RUNTIME_SCRIPT = """
(() => {
"use strict";
const VERSION = 1;
const AI_STUDIO_VERSION = "__MWAI_AI_STUDIO_VERSION__";
const dataElement = document.getElementById("mwai-briefing-data");
const root = document.getElementById("mwai-briefing-root");
if (!dataElement || !root) return;
const state = JSON.parse(dataElement.textContent || "{}");
const contexts = new WeakMap();
const get = (path, context = state) => {
if (!path) return undefined;
if (path === "$root") return state;
if (path === ".") return context && Object.hasOwn(context, "$value") ? context.$value : context;
if (path === "$index") return context && context.$index;
if (path === "$value") return context && context.$value;
const isRoot = path.startsWith("$root.");
const normalized = isRoot ? path.slice(6) : path.startsWith(".") ? path.slice(1) : path;
return normalized.split(".").filter(Boolean).reduce((value, key) => value == null ? undefined : value[key], isRoot ? state : path.startsWith(".") ? context : state);
};
const set = (path, value) => {
const parts = (path.startsWith("$root.") ? path.slice(6) : path).split(".").filter(Boolean);
let target = state;
for (let index = 0; index < parts.length - 1; index++) target = target[parts[index]] ??= {};
target[parts.at(-1)] = value;
};
const expression = (node, context) => {
if (node == null || typeof node !== "object") return node;
if ("path" in node) return get(node.path, context);
if ("value" in node) return node.value;
const args = (node.args || []).map(value => expression(value, context));
switch (node.op) {
case "add": return args.reduce((a, b) => a + b, 0);
case "subtract": return args[0] - args[1];
case "multiply": return args.reduce((a, b) => a * b, 1);
case "divide": return args[1] === 0 ? null : args[0] / args[1];
case "power": return Math.pow(args[0], args[1]);
case "eq": return args[0] === args[1];
case "ne": return args[0] !== args[1];
case "gt": return args[0] > args[1];
case "gte": return args[0] >= args[1];
case "lt": return args[0] < args[1];
case "lte": return args[0] <= args[1];
case "if": return args[0] ? args[1] : args[2];
case "min": return Math.min(...args);
case "max": return Math.max(...args);
case "round": return Math.round(args[0] * Math.pow(10, args[1] || 0)) / Math.pow(10, args[1] || 0);
case "sqrt": return Math.sqrt(args[0]);
case "log": return Math.log(args[0]);
case "exp": return Math.exp(args[0]);
default: return null;
}
};
const bind = (container, context = state) => {
container.querySelectorAll("[data-mwai-text]").forEach(element => {
const value = get(element.dataset.mwaiText, contexts.get(element) || context);
element.textContent = value == null ? "" : String(value);
});
container.querySelectorAll("[data-mwai-expr]").forEach(element => {
const localContext = contexts.get(element) || context;
const tree = get(element.dataset.mwaiExpr, localContext);
const value = expression(tree, localContext);
element.textContent = value == null ? "" : String(value);
});
container.querySelectorAll("[data-mwai-if],[data-mwai-filter]").forEach(element => {
const localContext = contexts.get(element) || context;
const conditionValue = element.dataset.mwaiIf ? get(element.dataset.mwaiIf, localContext) : true;
const conditionMatches = Boolean(conditionValue && typeof conditionValue === "object" ? expression(conditionValue, localContext) : conditionValue);
const selected = element.dataset.mwaiFilter ? get(element.dataset.mwaiFilter, localContext) : "";
const filterValue = element.dataset.mwaiFilterValue ? get(element.dataset.mwaiFilterValue, localContext) : "";
const filterMatches = selected == null || selected === "" || selected === "*" || String(selected) === String(filterValue);
element.hidden = !conditionMatches || !filterMatches;
});
container.querySelectorAll("[data-mwai-asset]").forEach(element => {
const asset = state._mwai?.assets?.[element.dataset.mwaiAsset];
if (asset && element.tagName === "IMG") element.src = asset;
});
container.querySelectorAll("*").forEach(element => {
for (const attribute of [...element.attributes]) {
if (!attribute.name.startsWith("data-mwai-attr-")) continue;
const name = attribute.name.slice("data-mwai-attr-".length);
const value = get(attribute.value, contexts.get(element) || context);
if (value == null) element.removeAttribute(name); else element.setAttribute(name, String(value));
}
});
container.querySelectorAll("template[data-mwai-each]").forEach(template => {
const values = get(template.dataset.mwaiEach, context);
if (!Array.isArray(values)) return;
const fragment = document.createDocumentFragment();
values.forEach((value, index) => {
const clone = template.content.cloneNode(true);
const itemContext = value != null && typeof value === "object"
? Object.assign(Object.create(value), value, { $index: index })
: { $value: value, $index: index };
clone.querySelectorAll("*").forEach(element => contexts.set(element, itemContext));
bind(clone, itemContext);
fragment.appendChild(clone);
});
template.replaceWith(fragment);
});
};
bind(document);
root.querySelectorAll("[data-mwai-tab-target]").forEach(button => button.addEventListener("click", () => {
const group = button.closest("[data-mwai-tabs]") || root;
group.querySelectorAll("[data-mwai-tab-panel]").forEach(panel => panel.hidden = panel.dataset.mwaiTabPanel !== button.dataset.mwaiTabTarget);
group.querySelectorAll("[data-mwai-tab-target]").forEach(tab => tab.setAttribute("aria-selected", tab === button ? "true" : "false"));
}));
root.querySelectorAll("[data-mwai-model]").forEach(control => {
const path = control.dataset.mwaiModel;
const value = get(path);
if (control.type === "checkbox") control.checked = Boolean(value); else if (value != null) control.value = value;
control.addEventListener("input", () => {
set(path, control.type === "checkbox" ? control.checked : control.type === "number" || control.type === "range" ? Number(control.value) : control.value);
bind(root);
});
});
root.querySelectorAll("[data-mwai-set]").forEach(button => button.addEventListener("click", () => {
set(button.dataset.mwaiSet, JSON.parse(button.dataset.mwaiValue || "null"));
bind(root);
}));
root.querySelectorAll("[data-mwai-toggle]").forEach(button => button.addEventListener("click", () => {
const path = button.dataset.mwaiToggle;
set(path, !get(path));
bind(root);
}));
root.querySelectorAll("[data-mwai-reset]").forEach(button => button.addEventListener("click", () => {
const componentId = button.dataset.mwaiReset;
(state.interactions?.controls || [])
.filter(control => control.componentId === componentId)
.forEach(control => set(`interactions.state.${control.controlId}`, control.initialValue));
root.querySelectorAll("[data-mwai-model]").forEach(control => {
const value = get(control.dataset.mwaiModel);
if (control.type === "checkbox") control.checked = Boolean(value); else if (value != null) control.value = value;
});
bind(root);
}));
root.querySelectorAll("[data-mwai-search]").forEach(input => input.addEventListener("input", () => {
const selector = input.dataset.mwaiSearch;
root.querySelectorAll(selector).forEach(item => item.hidden = !item.textContent.toLocaleLowerCase().includes(input.value.toLocaleLowerCase()));
}));
root.querySelectorAll("th[data-mwai-sort]").forEach(header => header.addEventListener("click", () => {
const table = header.closest("table");
const body = table?.tBodies[0];
if (!body) return;
const column = header.cellIndex;
const direction = header.dataset.mwaiDirection === "asc" ? -1 : 1;
[...body.rows].sort((a, b) => a.cells[column].textContent.localeCompare(b.cells[column].textContent, undefined, { numeric: true }) * direction).forEach(row => body.appendChild(row));
header.dataset.mwaiDirection = direction === 1 ? "asc" : "desc";
}));
root.querySelectorAll("[data-mwai-chart]").forEach(element => {
const option = get(element.dataset.mwaiChart, contexts.get(element) || state);
if (!option || !window.echarts) return;
const chart = window.echarts.init(element);
chart.setOption(option);
new ResizeObserver(() => chart.resize()).observe(element);
});
document.documentElement.dataset.mwaiRuntimeVersion = String(VERSION);
document.documentElement.dataset.mwaiAiStudioVersion = AI_STUDIO_VERSION;
})();
""";
[GeneratedRegex(@"#(?<id>[A-Za-z][A-Za-z0-9_-]*)", RegexOptions.CultureInvariant)]
private static partial Regex IdRegex();
[GeneratedRegex(@"\.(?<class>[A-Za-z][A-Za-z0-9_-]*)", RegexOptions.CultureInvariant)]
private static partial Regex RequiredClassRegex();
[GeneratedRegex(@"^(?<tag>[A-Za-z][A-Za-z0-9-]*)", RegexOptions.CultureInvariant)]
private static partial Regex TagRegex();
}