{
  "suite": "geovector-cms-article-html-conformance",
  "version": 1,
  "policy": "article-html-policy.v1.json",
  "summary": "Verdicts the reference receiver returns for each body. Run every case through your own content check: `expect.ok` must match. `expect.rules` is the rule ids our receiver reports, in order — a port that rejects a body for a different documented reason is still correct, so treat a rules mismatch as a warning and an `ok` mismatch as a failure.",
  "cases": [
    {
      "id": "accept-empty",
      "why": "An empty body and plain text are both valid content.",
      "html": "",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-plain-text",
      "why": "Text with no markup at all is inert and must pass.",
      "html": "plain text, no markup",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-article-skeleton",
      "why": "The wrapper every published article carries. Rejecting this rejects everything.",
      "html": "<article>\n<h1>Title</h1>\n<main>\n<p>Body.</p>\n</main>\n</article>",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-rich-body",
      "why": "The full vocabulary the renderer emits: callouts, tables with scope, code blocks, footnote anchors.",
      "html": "<div class=\"article-body prose\"><p>Intro with <strong>bold</strong>, <em>italic</em> and <code>code</code>.</p><aside class=\"callout callout-hero\"><p><strong>Key takeaway</strong> <span class=\"callout-separator\">|</span> Something.</p></aside><h2 id=\"a-section\">A section</h2><table><caption>Prices</caption><thead><tr><th scope=\"col\">Feature</th><th scope=\"col\">Price</th></tr></thead><tbody><tr><th scope=\"row\">Basic</th><td>$1</td></tr></tbody></table><ul><li>one</li></ul><pre><code class=\"language-ts\">const x = 1;</code></pre><blockquote><p>quoted</p></blockquote><hr><p>Cited<sup><a href=\"#fn-1\">[1]</a></sup>.</p></div>",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-escaped-angle-brackets",
      "why": "Text between tags is inert: entities decode to characters, never to markup. A scanner that decodes text before scanning it will wrongly reject this.",
      "html": "<p>Use &lt;script&gt; carefully &amp; sparingly.</p>",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-uppercase-names",
      "why": "Tag and attribute names are case-insensitive. Lowercase before every lookup.",
      "html": "<P CLASS=\"x\">hi</P>",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-single-quotes",
      "why": "Single-quoted attribute values are as valid as double-quoted.",
      "html": "<a href='/docs'>docs</a>",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-void-elements",
      "why": "Void elements appear with and without a self-closing slash; both are correct.",
      "html": "<p>a<br>b<br />c</p><hr><img src=\"/a.png\" alt=\"a\" />",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-relative-and-fragment-urls",
      "why": "Relative paths, fragments and protocol-relative URLs carry no scheme and are allowed.",
      "html": "<p><a href=\"/docs/x\">a</a> <a href=\"#fn-1\">b</a> <img src=\"//cdn.example.com/a.png\" alt=\"c\"></p>",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-json-ld",
      "why": "The one permitted script form. Rejecting it drops the structured data the product exists to deliver.",
      "html": "<p>hi</p>\n<script type=\"application/ld+json\">\n{\"@context\":\"https://schema.org\",\"@type\":\"FAQPage\"}\n</script>",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "accept-json-ld-escaped-breakout",
      "why": "A correctly escaped `</script>` inside JSON-LD contains no `<` and must pass — this is what our renderer emits for an FAQ answer mentioning a script tag.",
      "html": "<script type=\"application/ld+json\">{\"a\":\"\\u003c/script\\u003e\"}</script>",
      "expect": {
        "ok": true
      }
    },
    {
      "id": "reject-script-executable",
      "why": "The base case. A receiver that stores this has stored XSS on its own domain.",
      "html": "<script>alert(1)</script>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-script-wrong-type",
      "why": "Any type other than application/ld+json, exactly.",
      "html": "<script type=\"text/javascript\">alert(1)</script>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-script-body-not-rescanned",
      "why": "Script content is raw text to a browser and must never be fed back to the tag scanner. A port that re-scans it reports the inner <img> and buries the real problem.",
      "html": "<script>var a = \"<img src=x>\";</script>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-json-ld-extra-attribute",
      "why": "Only `type` and `id` may appear on a JSON-LD script.",
      "html": "<script type=\"application/ld+json\" onload=\"alert(1)\">{}</script>",
      "expect": {
        "ok": false,
        "rules": [
          "event-handler-attribute"
        ]
      }
    },
    {
      "id": "reject-json-ld-breakout",
      "why": "The breakout attempt. Forbidding `<` anywhere in the body is what makes an early `</script>` impossible.",
      "html": "<script type=\"application/ld+json\">{\"a\":\"</script><img src=x onerror=alert(1)>\"}</script>",
      "expect": {
        "ok": false,
        "rules": [
          "invalid-json-ld",
          "unquoted-attribute-value",
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-json-ld-not-json",
      "why": "Content claiming to be JSON-LD must parse as JSON.",
      "html": "<script type=\"application/ld+json\">not json</script>",
      "expect": {
        "ok": false,
        "rules": [
          "invalid-json-ld"
        ]
      }
    },
    {
      "id": "reject-script-unterminated",
      "why": "An unterminated script means everything after it is raw text a browser will treat as script source.",
      "html": "<script type=\"application/ld+json\">{}",
      "expect": {
        "ok": false,
        "rules": [
          "malformed-markup"
        ]
      }
    },
    {
      "id": "reject-stray-script-end-tag",
      "why": "A `</script>` with no start tag.",
      "html": "<p>hi</p></script>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-event-handler",
      "why": "Event handlers are rejected on name shape, before the allowlist is consulted, and regardless of case.",
      "html": "<img src=\"/a.png\" OnError=\"alert(1)\">",
      "expect": {
        "ok": false,
        "rules": [
          "event-handler-attribute"
        ]
      }
    },
    {
      "id": "reject-inline-style",
      "why": "Inline style enables clickjacking overlays without any script at all.",
      "html": "<p style=\"position:fixed;top:0\">hi</p>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-attribute"
        ]
      }
    },
    {
      "id": "reject-attribute-not-on-element",
      "why": "Per-element attribute lists are enforced, not just a global set.",
      "html": "<a href=\"/a\" download=\"x\">a</a>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-attribute"
        ]
      }
    },
    {
      "id": "reject-unquoted-value",
      "why": "Unquoted values are where a hand-written tokenizer and a browser most easily disagree — `href=x onclick=y` reads as one attribute or two. Refuse rather than interpret.",
      "html": "<a href=/a>a</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unquoted-attribute-value"
        ]
      }
    },
    {
      "id": "reject-url-javascript",
      "why": "The scheme check itself.",
      "html": "<a href=\"javascript:alert(1)\">click</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-url-javascript-mixed-case",
      "why": "Schemes are case-insensitive.",
      "html": "<a href=\"JaVaScRiPt:alert(1)\">click</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-url-leading-whitespace",
      "why": "Browsers trim leading whitespace before reading the scheme.",
      "html": "<a href=\"  javascript:alert(1)\">click</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-url-embedded-tab",
      "why": "Browsers ignore control characters inside a URL, so `java\\tscript:` is a live scheme to them and must be one to the scanner.",
      "html": "<a href=\"java\tscript:alert(1)\">click</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-url-decimal-entity",
      "why": "Attribute values are entity-decoded by the browser before the URL is parsed. Decode before reading the scheme.",
      "html": "<a href=\"&#106;avascript:alert(1)\">click</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-url-hex-entity",
      "why": "Same as the decimal form, in hex.",
      "html": "<a href=\"&#x6a;avascript:alert(1)\">click</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-url-named-entity",
      "why": "Named entities resolve to control characters too. `&Tab;` is the usual one.",
      "html": "<a href=\"java&Tab;script:alert(1)\">click</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-url-data-html",
      "why": "A data: document runs in the origin that navigated to it in some contexts, and is never needed in an article.",
      "html": "<a href=\"data:text/html,x\">click</a>",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-url-data-svg-image",
      "why": "An SVG is a script container, so data: is refused on img src as well as href.",
      "html": "<img src=\"data:image/svg+xml;base64,PHN2Zz4=\" alt=\"x\">",
      "expect": {
        "ok": false,
        "rules": [
          "unsafe-url"
        ]
      }
    },
    {
      "id": "reject-comment",
      "why": "Comments are a mutation-XSS staple and the renderer emits none.",
      "html": "<p>a</p><!-- hi --><p>b</p>",
      "expect": {
        "ok": false,
        "rules": [
          "malformed-markup"
        ]
      }
    },
    {
      "id": "reject-doctype",
      "why": "Article content is a fragment, never a document.",
      "html": "<!DOCTYPE html><p>a</p>",
      "expect": {
        "ok": false,
        "rules": [
          "malformed-markup"
        ]
      }
    },
    {
      "id": "reject-bare-less-than",
      "why": "An unescaped `<` in text is exactly the gap between what a scanner reads as text and what a browser reads as a tag. Our renderer escapes text, so this is always either a broken template or an attack.",
      "html": "<p>2 < 3</p>",
      "expect": {
        "ok": false,
        "rules": [
          "malformed-markup"
        ]
      }
    },
    {
      "id": "reject-unterminated-tag",
      "why": "A start tag with no `>`.",
      "html": "<p>hi</p><div class=\"x\"",
      "expect": {
        "ok": false,
        "rules": [
          "malformed-markup"
        ]
      }
    },
    {
      "id": "reject-unterminated-attribute-value",
      "why": "An unclosed quote swallows the rest of the document.",
      "html": "<p class=\"x>hi</p>",
      "expect": {
        "ok": false,
        "rules": [
          "malformed-markup"
        ]
      }
    },
    {
      "id": "reject-void-end-tag",
      "why": "A void element has no end tag; `</br>` is malformed.",
      "html": "<p>a<br></br></p>",
      "expect": {
        "ok": false,
        "rules": [
          "malformed-markup"
        ]
      }
    },
    {
      "id": "reject-iframe",
      "why": "Framing another origin inside an article page.",
      "html": "<iframe src=\"https://evil.example\"></iframe>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element",
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-style-element",
      "why": "A style element can restyle the whole page, including hiding it behind an overlay.",
      "html": "<style>body{display:none}</style>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element",
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-form",
      "why": "A form in article content is a credential-phishing surface on the client’s own domain.",
      "html": "<form action=\"https://evil.example\"><input name=\"p\"></form>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element",
          "disallowed-element",
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-svg",
      "why": "SVG carries its own script vocabulary.",
      "html": "<svg><a>x</a></svg>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element",
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-prototype-key-element",
      "why": "If the allowlist is a plain hash, `toString` is inherited from the prototype and this element is admitted. Use a structure with no inherited keys — a Map, or an explicit own-key check.",
      "html": "<toString>x</toString>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element",
          "disallowed-element"
        ]
      }
    },
    {
      "id": "reject-prototype-key-element-with-attribute",
      "why": "The same trap one step further: the inherited value is a function, so an attribute lookup against it throws — turning a 400 into a 500.",
      "html": "<constructor foo=\"1\">x</constructor>",
      "expect": {
        "ok": false,
        "rules": [
          "disallowed-element",
          "disallowed-element"
        ]
      }
    }
  ]
}
