Skip to content
heff

std.html

HTML’s escapes of text: escape writes the five characters that would be read as markup, &, <, >, ' and ", as references, and unescape reads every reference back, as Go’s html package does (EscapeString, UnescapeString): the numeric ones, &#233; and &#xE9;, and the named ones, all of HTML5’s. Import it by a name, import std.html as HTML.

import std.html as HTML
-- | a paragraph of the text, whatever is in it
-- | >>> paragraph "1 < 2 & \"so\""
-- | "<p>1 &lt; 2 &amp; &#34;so&#34;</p>"
paragraph : String -> String
paragraph text = "<p>" ^ HTML.escape text ^ "</p>"

escape

escape : String -> String

The text with &, <, >, ' and " written as &amp;, &lt;, &gt;, &#39; and &#34; (as Go’s html.EscapeString writes them), the rest as it is: in an element’s content or a quoted attribute’s value, it is read as the text it is.

>>> escape "<a href=\"x\">Tom & Jerry's</a>"
"&lt;a href=&#34;x&#34;&gt;Tom &amp; Jerry&#39;s&lt;/a&gt;"
>>> List.map escape ["", "plain \u{e9}", "&&", "&amp;"]
["", "plain \u{e9}", "&amp;&amp;", "&amp;amp;"]

unescape

unescape : String -> String

The text with each character reference read, as Go’s html.UnescapeString reads them. &# and decimal digits, or &#x (or &#X) and hex digits, a ; after them or not, is the code point they are: one from 0x80 to 0x9F Windows-1252’s character there, as browsers read it (&#150; is –), and 0, a surrogate or one past U+10FFFF U+FFFD. &, a name — letters and digits, their case told apart — and ; is what HTML5 names so. Where there is no such name, or no ;, the longest of the legacy names that may end without one (amp, lt, copy, eacute, …) that the name starts with is read, the rest left after it, as browsers read &copy2024. Any other & — no digits, no name, a name none is — is left as it is written.

>>> unescape "&lt;p&gt;Tom &amp; Jerry&#39;s &quot;caf&eacute;&quot;&lt;/p&gt;"
"<p>Tom & Jerry's \"caf\u{e9}\"</p>"
>>> List.map unescape ["&#233;", "&#xe9;", "&#XE9", "&#233x", "&#128512;", "&#x1F600;"]
["\u{e9}", "\u{e9}", "\u{e9}", "\u{e9}x", "\u{1F600}", "\u{1F600}"]

(Windows-1252’s, and what is no character)

>>> List.map unescape ["&#150;", "&#x80;", "&#x81;", "&#0;", "&#xD800;", "&#x110000;", "&#99999999999999999999;"]
["\u{2013}", "\u{20AC}", "\u{81}", "\u{FFFD}", "\u{FFFD}", "\u{FFFD}", "\u{FFFD}"]

(rare names, a name of two code points, one of a character that is a name’s syntax)

>>> List.map unescape ["&NotSquareSupersetEqual;", "&fjlig;", "&nGt;", "&bsol;&quot;&NewLine;", "&ThickSpace;"]
["\u{22E3}", "fj", "\u{226B}\u{20D2}", "\\\"\n", "\u{205F}\u{200A}"]

(legacy names without their ;, and the longest of them first)

>>> List.map unescape ["&copy2024", "&amp", "&ltx", "&notit;", "&not;", "&notin;", "&frac34x", "&AMP;&AMP"]
["\u{a9}2024", "&", "<x", "\u{ac}it;", "\u{ac}", "\u{2209}", "\u{be}x", "&&"]

(left as they are written: no name, no digits, a name none is, a name not legacy without its ;, a name in the wrong case)

>>> List.map unescape ["&", "& ", "&;", "&#", "&#;", "&#x;", "&#xg;", "&nosuch;", "&hellip", "&Amp;", "a&b", "&&lt;"]
["&", "& ", "&;", "&#", "&#;", "&#x;", "&#xg;", "&nosuch;", "&hellip", "&Amp;", "a&b", "&<"]