CodeRigor

LESSON

Escape text and authorize link destinations

Render only controlled tags, escape text/attribute contexts separately and reject unsafe or ambiguous links.

The renderer is a trust boundary. It chooses a few fixed tags and inserts escaped source values. It does not accept a “raw HTML” token. Text that looks like <script> must remain visible text in the generated page.

Two checks are independent: is a link destination permitted, and is its value escaped correctly for an HTML attribute? Performing only one leaves the other problem unsolved.

Separate permission from output context
start / 3
A link destination passes URL policy before being escaped for href. Its label is separately escaped as text.
Separate permission from output context
A link destination passes URL policy before being escaped for href. Its label is separately escaped as text.

Use html.escape(text,quote=False) for ordinary text, code text, emphasis text and labels. Use quote=True for a double-quoted href. A quote in a text node is ordinary text; a quote inside an attribute can otherwise terminate the value.

The supplied link policy rejects javascript/data/http schemes, protocol-relative origins, embedded credentials, unsupported characters and malformed percent escapes. Root-relative paths stay on the current origin. HTTPS destinations use a deliberately supported ASCII-hostname subset; this helper is not a complete browser URL validator.

Python
import html
label = '<img src=x onerror=bad>'
destination = 'https://example.test/?q="one"&x=two'
print("label:", html.escape(label,quote=False))
print("href:", html.escape(destination,quote=True))

Validate first, then escape. A javascript: destination can be perfectly quoted HTML and still execute when clicked. Conversely, an allowed HTTPS destination can contain a quote that must remain attribute data. Both boundaries need tests.

The renderer emits fixed code/em/anchor tags based on known token kinds. It never builds a tag name or attribute name from source text. Unknown kinds fail with their source location. Raw entities are not decoded into markup before validation; source text remains data through rendering.

The course’s browser verification opens generated attack fixtures with JavaScript enabled. They verify that source-like script/img text becomes inert text, no event attribute appears and unsafe links produce diagnostics. The inline course preview also uses a sandboxed frame as an extra display boundary.

Only fixed tags become elements
start / 3
Source text is escaped inside controlled wrappers. The renderer has no pass-through HTML branch.
Only fixed tags become elements
Source text is escaped inside controlled wrappers. The renderer has no pass-through HTML branch.
Python
from urllib.parse import urlsplit
for destination in ["https://example.test/docs","javascript:evil","//other.test/docs"]:
    value = urlsplit(destination)
    print(destination,"scheme",value.scheme,"origin",value.netloc)

Implement rendering and test text-like HTML, quotes in a link query, ampersands, unsafe schemes, protocol-relative links and unknown token kinds. Confirm allowed local links retain their query and fragment.

Next the document renderer will assemble these safe fragments inside a trusted responsive template without recursively substituting user text.

Follow the sample through the compiler.

Inspect the authored source, its located block records and the actual styled output from the downloadable generator.

Markdown source

Markdown
# A small page, built deliberately

This page started as *plain text*. Its structure was parsed before any HTML was generated.

## A useful boundary

- Keep source text as data.
- Give links an explicit policy.
- Publish only after the complete page succeeds.

Literal HTML-like input stays visible: <script>this is displayed, never run</script>.

```python
from pathlib import Path

message = "<source is data>"
print(message)
```

## Continue the project

Inspect [the HTML escaping reference](https://docs.python.org/3.14/library/html.html) and then change one paragraph and rebuild.

The larger [Boot.dev Static Site Generator project](https://www.boot.dev/courses/build-static-site-generator-python) continues into richer nodes and a complete website after classes, recursion and functional programming.

Located block records

JSON
[
  {
    "kind": "h1",
    "lines": [
      [
        "A small page, built deliberately",
        1,
        3
      ]
    ],
    "line": 1,
    "language": ""
  },
  {
    "kind": "paragraph",
    "lines": [
      [
        "This page started as *plain text*. Its structure was parsed before any HTML was generated.",
        3,
        1
      ]
    ],
    "line": 3,
    "language": ""
  },
  {
    "kind": "h2",
    "lines": [
      [
        "A useful boundary",
        5,
        4
      ]
    ],
    "line": 5,
    "language": ""
  },
  {
    "kind": "list",
    "lines": [
      [
        "Keep source text as data.",
        7,
        3
      ],
      [
        "Give links an explicit policy.",
        8,
        3
      ],
      [
        "Publish only after the complete page succeeds.",
        9,
        3
      ]
    ],
    "line": 7,
    "language": ""
  },
  {
    "kind": "paragraph",
    "lines": [
      [
        "Literal HTML-like input stays visible: <script>this is displayed, never run</script>.",
        11,
        1
      ]
    ],
    "line": 11,
    "language": ""
  },
  {
    "kind": "code",
    "lines": [
      [
        "from pathlib import Path",
        14,
        1
      ],
      [
        "",
        15,
        1
      ],
      [
        "message = \"<source is data>\"",
        16,
        1
      ],
      [
        "print(message)",
        17,
        1
      ]
    ],
    "line": 13,
    "language": "python"
  },
  {
    "kind": "h2",
    "lines": [
      [
        "Continue the project",
        20,
        4
      ]
    ],
    "line": 20,
    "language": ""
  },
  {
    "kind": "paragraph",
    "lines": [
      [
        "Inspect [the HTML escaping reference](https://docs.python.org/3.14/library/html.html) and then change one paragraph and rebuild.",
        22,
        1
      ]
    ],
    "line": 22,
    "language": ""
  },
  {
    "kind": "paragraph",
    "lines": [
      [
        "The larger [Boot.dev Static Site Generator project](https://www.boot.dev/courses/build-static-site-generator-python) continues into richer nodes and a complete website after classes, recursion and functional programming.",
        24,
        1
      ]
    ],
    "line": 24,
    "language": ""
  }
]

The generated page

9 blocks · 2,977 UTF-8 bytes · Repeatable source and output included in the lab.

Download the generator and this sample →

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Optional file-I/O companion for the local build command. The original parser and HTML-escaping rules are taught in the authored lessons.