std.html

Werkzeuge für den Umgang mit HTML als Text: Sonderzeichen maskieren, Tags erkennen und entfernen, Verschachtelung grob prüfen, Leerzeichen für Formularparameter kodieren. Die Unit ist kein Parser — sie arbeitet zeichenweise über den Eingabetext und hat kein Dokumentmodell.

std.xml · std.string · Standard Library

Alle Beispiele dieser Seite sind mit lyxc 1.0.21A übersetzt und ausgeführt; die gezeigten Ausgaben sind echte Programmausgaben.

Einsatz: Template-Ausgabe, HTML-Erzeugung, Textextraktion aus Webseiten, SEO- und Feed-Auswertung.

 
Nicht für Sicherheitsentscheidungen verwenden. Frühere Fassungen dieser Seite nannten „HTML-Sanitisierung (XSS-Schutz)„ als Einsatzbereich — dafür taugt die Unit nicht:

* StripTags entfernt die Tags, nicht deren Inhalt: aus <script>alert(1)</script> wird alert(1).
* Unescape löst derzeit fast keine Entity auf (siehe unten), eine darauf gestützte Prüfung sieht also etwas anderes als der Browser.
* Es gibt keine Attributprüfung: onclick, javascript:-URLs und Data-URLs bleiben unangetastet, solange sie in einem Tag stehen, das entfernt wird — der Text dahinter aber bleibt.

Zum Schutz vor XSS ist Escape beim Ausgeben das richtige Mittel, nicht das Filtern beim Einlesen.

Maskieren

Signatur Beschreibung
Escape(input: pchar, output: pchar): int64 Ersetzt &, <, >, , ' und Byte 160 durch Entities. Rückgabe: Länge ohne Nullbyte.
Unescape(input: pchar, output: pchar): int64 Soll Entities zurückwandeln — funktioniert derzeit nur für &quot;.
NeedsEscape(s: pchar): bool Meldet, ob überhaupt ein zu maskierendes Zeichen vorkommt.
Zeichen Wird zu
& &amp;
< &lt;
> &gt;
&quot;
' &#39; (numerisch, nicht &apos;)
Byte 160 &nbsp;problematisch, siehe Kasten

Der Ausgabepuffer gehört dem Aufrufer. Sechs Byte je Eingabezeichen sind sicher (&quot; und &nbsp; sind die längsten Ersetzungen), plus ein Byte für den Abschluss.

import std.html;
import std.alloc;
import std.string;

fn main(): int64 {
    var o: pchar := alloc(512) as pchar;

    Escape("<a href=\"x\">Tom & Jerry's</a>"c, o);
    PrintLn(StrConcat("Escape:   ", o));

    var back: pchar := alloc(512) as pchar;
    Unescape(o, back);
    PrintLn(StrConcat("Rueckweg: ", back));

    // Einzelne Entities durch den Dekoder
    Unescape("&amp; &lt; &gt; &quot; &apos; &#39; &#x41; &nbsp; &unknown;"c, o);
    PrintLn(StrConcat("Entities: ", o));

    // UTF-8: 'à' ist C3 A0 - das zweite Byte ist 160
    Escape("Caf\xc3\xa0 Grand-M\xc3\xa8re"c, o);
    PrintLn(StrConcat("UTF-8:    ", o));
    return 0;
}

Escape:   &lt;a href=&quot;x&quot;&gt;Tom &amp; Jerry&#39;s&lt;/a&gt;
Rueckweg: &lt;a href="x"&gt;Tom &amp; Jerry&#39;s&lt;/a&gt;
Entities: &amp; &lt; &gt; " &apos; &#39; &#x41; &nbsp; &unknown;
UTF-8:    CafM-C&nbsp; Grand-MM-CM-(re

Die letzte Zeile ist durch cat -v sichtbar gemacht: M-C steht für das Byte 0xC3. Aus à (C3 A0) wurde C3 + &nbsp; — ein halbes UTF-8-Zeichen gefolgt von einer Entity. è (C3 A8) blieb dagegen unversehrt, weil nur 0xA0 geprüft wird.

 
Unescape beherrscht seit 1.0.20F den vollen Umfang (#1418): benannte Entities und numerische Angaben. Gemessen wird &lt;a&gt; &amp; &#65; &nbsp; zu &lt;a&gt; & A samt geschütztem Leerzeichen aufgelöst.
 
Escape lässt UTF-8 unangetastet (#1419 behoben, 1.0.21A). Die frühere Latin-1-Regel „Byte 160 → &nbsp;“ zerlegte Zeichen wie à (C3 A0); nachgemessen bleibt à Grüße jetzt unverändert.

Tags erkennen

Signatur Beschreibung
GetTagName(html: pchar, output: pchar): int64 Liest den Tag-Namen ab <; funktioniert auch für &lt;/tag&gt; und &lt;tag/&gt;. 0, wenn das erste Zeichen kein < ist.
IsClosingTag(html: pchar): bool true bei &lt;/…&gt;.
IsSelfClosing(html: pchar): bool true bei <…/>nicht bei Void-Elementen wie &lt;br&gt;.
HasTags(s: pchar): bool Sucht ein < mit passendem > irgendwo im Text.
ValidateBalance(html: pchar): int64 Bilanz aus öffnenden und schließenden Tags: 0 = ausgeglichen, positiv = offen, negativ = zu viele schließende.

import std.html;
import std.alloc;
import std.string;

fn J(b: bool): pchar { if (b) { return "ja"; } return "nein"; }

fn main(): int64 {
    var o: pchar := alloc(512) as pchar;

    // Tag-Analyse
    GetTagName("<div class=\"x\">"c, o); PrintLn(StrConcat("GetTagName('<div class=..>'): ", o));
    GetTagName("</span>"c, o);           PrintLn(StrConcat("GetTagName('</span>'):        ", o));
    GetTagName("<br/>"c, o);             PrintLn(StrConcat("GetTagName('<br/>'):          ", o));
    PrintLn(StrConcat("IsClosingTag('</p>'):  ", J(IsClosingTag("</p>"c))));
    PrintLn(StrConcat("IsSelfClosing('<br/>'):", J(IsSelfClosing("<br/>"c))));
    PrintLn(StrConcat("IsSelfClosing('<br>'): ", J(IsSelfClosing("<br>"c))));
    PrintLn(StrConcat("HasTags('a < b'):      ", J(HasTags("a < b"c))));
    PrintLn(StrConcat("NeedsEscape('a & b'):  ", J(NeedsEscape("a & b"c))));

    // Verschachtelung
    PrintLn(StrConcat("Balance('<p><b>x</b></p>'): ", IntToStr(ValidateBalance("<p><b>x</b></p>"c))));
    PrintLn(StrConcat("Balance('<p><b>x</p>'):     ", IntToStr(ValidateBalance("<p><b>x</p>"c))));
    PrintLn(StrConcat("Balance('<p>a<br>b</p>'):   ", IntToStr(ValidateBalance("<p>a<br>b</p>"c))));
    PrintLn(StrConcat("Balance('<img src=x/>'):    ", IntToStr(ValidateBalance("<img src=x/>"c))));
    PrintLn(StrConcat("Balance('5 < 7 und 9 > 2'): ", IntToStr(ValidateBalance("5 < 7 und 9 > 2"c))));

    // StripTags
    StripTags("<p>Hallo <b>Welt</b></p>"c, o);      PrintLn(StrConcat("Strip normal:  ", o));
    StripTags("<script>alert(1)</script>"c, o);      PrintLn(StrConcat("Strip script:  ", o));
    StripTags("5 < 7 und 9 > 2"c, o);                PrintLn(StrConcat("Strip Text:    ", o));
    StripTags("<p onclick=\"evil()\">Text</p>"c, o); PrintLn(StrConcat("Strip Attribut:", o));

    // URL-Leerzeichen
    EncodeSpaces("a b c"c, o); PrintLn(StrConcat("EncodeSpaces('a b c'): ", o));
    DecodeSpaces(o, o);        PrintLn(StrConcat("DecodeSpaces zurueck:  ", o));
    return 0;
}

GetTagName('<div class=..>'): div
GetTagName('</span>'):        span
GetTagName('<br/>'):          br
IsClosingTag('</p>'):  ja
IsSelfClosing('<br/>'):ja
IsSelfClosing('<br>'): nein
HasTags('a < b'):      ja
NeedsEscape('a & b'):  ja
Balance('<p><b>x</b></p>'): 0
Balance('<p><b>x</p>'):     1
Balance('<p>a<br>b</p>'):   1
Balance('<img src=x/>'):    1
Balance('5 < 7 und 9 > 2'): 1
Strip normal:  Hallo Welt
Strip script:  alert(1)
Strip Text:    5  2
Strip Attribut:Text
EncodeSpaces('a b c'): a+b+c
DecodeSpaces zurueck:  a b c

 
ValidateBalance rechnet naiv. Jedes < zählt als geöffnet, jedes &lt;/ als geschlossen — unabhängig davon, ob überhaupt ein Tag vorliegt:

* Void-Elemente (&lt;br&gt;, &lt;img&gt;, &lt;meta&gt;, &lt;input&gt;, &lt;hr&gt;) gelten als offen; gültiges HTML bekommt eine Bilanz ungleich null.
* Selbstschließende Tags (&lt;img/&gt;) ebenfalls, obwohl IsSelfClosing sie erkennen könnte.
* Ein < im Fließtext (5 < 7) zählt mit.

Brauchbar ist die Funktion damit nur für streng gepaartes, XML-artiges Markup ohne Void-Elemente (#1420).

Tags entfernen

StripTags kopiert alles, was außerhalb von <…> steht. Das Verfahren ist bewusst einfach — und hat zwei Folgen, die man kennen muss:

  • Der Inhalt bleibt. Ein &lt;script&gt;-Block verliert seine Tags, der Code darin steht anschließend im Text.
  • Ein < im Text frisst mit. Aus 5 < 7 und 9 > 2 wird 5 2 — alles zwischen < und dem nächsten > gilt als Tag. Wer Text mit Vergleichszeichen verarbeitet, maskiert ihn vorher mit Escape.

Für die Textextraktion aus wohlgeformtem HTML — der eigentliche Zweck — arbeitet die Funktion zuverlässig.


Leerzeichen für Formularparameter

Signatur Beschreibung
EncodeSpaces(input: pchar, output: pchar): int64 Ersetzt Leerzeichen durch +.
DecodeSpaces(input: pchar, output: pchar): int64 Ersetzt + durch Leerzeichen.
 
Das ist +, nicht %20. Frühere Fassungen dieser Seite sprachen von „%20 für URLs„ — tatsächlich erzeugt EncodeSpaces die Form aus application/x-www-form-urlencoded, wie sie in Formulardaten und Abfrageparametern üblich ist. DecodeSpaces kennt umgekehrt nur + und lässt %20 unverändert stehen.

Beide Funktionen behandeln ausschließlich das Leerzeichen. &, =, ?, # und Nicht-ASCII bleiben unangetastet — für vollständige URL-Kodierung ist std.net.http zuständig.

Fallstricke

  • Ausgabepuffer selbst anlegen und großzügig bemessen — keine Funktion prüft die Zielgröße. Für Escape sind sechs Byte je Eingabezeichen sicher.
  • Unescape löst benannte und numerische Entities auf (#1418 behoben).
  • Escape lässt UTF-8 unangetastet (#1419 behoben).
  • Doppelte Maskierung merkt niemand: Escape(Escape(x)) ergibt &amp;lt; statt &lt;. Nur einmal maskieren, unmittelbar vor der Ausgabe.
  • Kein Dokumentmodell. GetTagName und Verwandte erwarten einen Text, der am < beginnt; einen Tag-Iterator gibt es nicht. Wer Struktur braucht, nimmt std.xml.
  • Kommentare, CDATA und Skriptinhalte kennt die Unit nicht — &lt;!– a &gt; b –&gt; endet für StripTags beim ersten >.
  • Die exportierten Entity-Konstanten sind unit-privat; von außen sichtbar sind nur die elf Funktionen.

Weiterführend: std.xml — Parser mit Dokumentmodell · std.string · std.net.http · Standard Library

Quelle: std/html.lyx (459 Zeilen) · Autor: Andreas Röne · Copyright: 2024–2025 Andreas Röne

Letzte Aktualisierung: 2026-08-13 — Seite gegen std/html.lyx überarbeitet: leere Konstanten-Tabelle entfernt, zwei lauffähige Beispiele mit echter Ausgabe, Entity-Tabelle, Fallstricke. Die Nennung als Mittel zur „HTML-Sanitisierung (XSS-Schutz)“ ist gestrichen — StripTags entfernt Tags, nicht deren Inhalt. Belegt und als Issue erfasst: Unescape erkennt nur &quot; (#1418), Escape zerstört UTF-8-Zeichen mit Folgebyte 0xA0 (#1419), ValidateBalance zählt Void-Elemente und Text-Kleinerzeichen mit (#1420). EncodeSpaces erzeugt +, nicht %20. Geprüft mit lyxc 1.0.21A.