PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/nt/markdown/sanitize/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
nt / markdown / sanitize
Files
4 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/nt/markdown/sanitize/v0
gno
0.9

Files (4)

  • README.mdmarkdown
  • gnomod.tomltoml
  • sanitize.gnogno
  • sanitize_test.gnogno
sanitize_test.gnogno
1package sanitize23import (4	"strings"5	"testing"6)78func TestStripBidiAndZeroWidthRe(t *testing.T) {9	// Re-export sanity check.10	if got := StripBidiAndZeroWidth("a\u200Bb"); got != "ab" {11		t.Errorf("re-export StripBidiAndZeroWidth failed: got %q", got)12	}13}1415func TestNormalizeBreaksRe(t *testing.T) {16	if got := NormalizeBreaks("a\r\nb"); got != "a\nb" {17		t.Errorf("re-export NormalizeBreaks failed: got %q", got)18	}19}2021func TestInlineText(t *testing.T) {22	cases := []struct{ in, want string }{23		{"plain", "plain"},24		{"a*b", `a\*b`},25		{"a|b", "a|b"},                    // | NOT escaped26		{"a=b", "a=b"},                    // = NOT escaped27		{"line1\nline2", "line1 line2"},   // newline folded28		{"line1\r\nline2", "line1 line2"}, // CRLF normalized then folded29		{"a\u2028b", "a b"},               // U+2028 folded30		{"a\u0085b", "a b"},               // NEL folded31		{"a\u200Bb", "ab"},                // ZWSP stripped32		{"hello *world*", `hello \*world\*`},33	}34	for _, c := range cases {35		if got := InlineText(c.in); got != c.want {36			t.Errorf("InlineText(%q) = %q, want %q", c.in, got, c.want)37		}38	}39}4041func TestBlock(t *testing.T) {42	// Block wraps every non-empty output with "\n\n" — CM §4.8 blank43	// lines — on BOTH sides, so user content is paragraph-isolated44	// from any realm chrome that precedes OR follows it. Bounds CM45	// §4.6 HTML block types 6/7 (`<div>`, `<table>`, …) which are not46	// escaped in any mode and would otherwise consume appended chrome.47	cases := []struct{ in, want string }{48		{"hello world\n", "\n\nhello world\n\n"},49		{"# heading\n", "\n\n\\# heading\n\n"},50		{"> quote\n", "\n\n\\> quote\n\n"},51		{"| a | b |\n", "\n\n\\| a | b |\n\n"}, // GFM table-row escaped in strict mode52		// CM §4.6 HTML block types 1-5 — escaped (blank-line-NON-terminating).53		{"<script>x</script>\n", "\n\n\\<script>x</script>\n\n"},54		{"<!-- comment -->\n", "\n\n\\<!-- comment -->\n\n"},55		{"<?php x ?>\n", "\n\n\\<?php x ?>\n\n"},56		{"<!DOCTYPE html>\n", "\n\n\\<!DOCTYPE html>\n\n"},57		{"```\ncode\n", "\n\n```\ncode\n```\n\n"}, // fence auto-close at EOF58		{"[x](&#x6a;avascript:alert)\n", "\n\n[x](%26#x6a;avascript:alert)\n\n"},59		{"[x](&#0000152;avascript:alert)\n", "\n\n[x](%26#0000152;avascript:alert)\n\n"}, // octal reference60		// A renderer unescapes `\#` before resolving references, so this61		// reaches it as `&#x6a;` unless the `&` is encoded here.62		{"[x](&\\#x6a;avascript:alert)\n", "\n\n[x](%26\\#x6a;avascript:alert)\n\n"},63		// A renderer resolves numeric references before named ones, over64		// the same buffer, so `&#38;colon;` reaches the named pass as65		// `&colon;` and resolves to `:`.66		{"[x](javascript&#38;colon;alert)\n", "\n\n[x](javascript%26#38;colon;alert)\n\n"},67		{"[x](?x=1&y=2)\n", "\n\n[x](?x=1&y=2)\n\n"},68		// Empty / strip-to-empty inputs short-circuit (no stray blank line).69		{"[x]: y\n", ""},70		{"", ""},71	}72	for _, c := range cases {73		got := Block(c.in)74		if got != c.want {75			t.Errorf("Block(%q) = %q, want %q", c.in, got, c.want)76		}77		// Idempotency: Block(Block(in)) must be byte-identical to78		// Block(in) for every input in the table.79		twice := Block(got)80		if twice != got {81			t.Errorf("Block not idempotent for %q: Block(once)=%q, Block(twice)=%q", c.in, got, twice)82		}83	}84}8586func TestBlockRich(t *testing.T) {87	// BlockRich wraps every non-empty output with "\n\n" — CM §4.888	// blank lines — on BOTH sides, so user content is paragraph-89	// isolated from any realm chrome that precedes OR follows it.90	cases := []struct{ in, want string }{91		// Block-level markdown that Block escapes — preserved by BlockRich.92		{"hello world\n", "\n\nhello world\n\n"},93		{"# heading\n", "\n\n# heading\n\n"},94		{"> quote\n", "\n\n> quote\n\n"},95		{"- item\n", "\n\n- item\n\n"},96		{"1. item\n", "\n\n1. item\n\n"},97		{"---\n", "\n\n\\---\n\n"},                       // first-line setext-h2 → escaped by qualifying-setext pre-pass98		{"***\n", "\n\n***\n\n"},                         // thematic break NOT setext-h2 — preserved99		{"text\n===\n", "\n\ntext\n===\n\n"},             // deeper setext preserved (user-authored above)100		{"body\n===\nmore\n", "\n\nbody\n===\nmore\n\n"}, // cross-paragraph backward attack: realm `chrome\n\nbody\n===\nmore` keeps realm in its own paragraph101		// GFM tables PRESERVED in Rich mode.102		{"| a | b |\n", "\n\n| a | b |\n\n"},103		{"| H |\n|---|\n| a |\n", "\n\n| H |\n|---|\n| a |\n\n"}, // full table renders as <table>104		// Realm-binding defenses STILL ON.105		{"<gno-card>\n", "\n\n\\<gno-card>\n\n"},106		// CM §4.6 HTML block types 1-5 — escaped in Rich mode too107		// (defense is mode-independent).108		{"<script>x</script>\n", "\n\n\\<script>x</script>\n\n"},109		{"<!-- comment -->\n", "\n\n\\<!-- comment -->\n\n"},110		{"<?php x ?>\n", "\n\n\\<?php x ?>\n\n"},111		{"<!DOCTYPE html>\n", "\n\n\\<!DOCTYPE html>\n\n"},112		{"[t][l]\n", "\n\n\\[t\\]\\[l\\]\n\n"},113		{"[^name]\n", "\n\n\\[^name\\]\n\n"},114		// LRD-only input strips to nothing; empty-after-escape short-115		// circuits to "" so realm concatenation doesn't leak a stray116		// blank line.117		{"[x]: y\n", ""},118		{"", ""}, // empty input → empty output, no stray blank line119		{"```\ncode\n", "\n\n```\ncode\n```\n\n"},120		{"[x](javascript&colon;alert)\n", "\n\n[x](javascript%26colon;alert)\n\n"},121		// `\;` on a named reference — unescaped to `&colon;` by a renderer.122		{"[x](javascript&colon\\;alert)\n", "\n\n[x](javascript%26colon\\;alert)\n\n"},123		// Numeric reference feeding the named pass — see TestBlock.124		{"[x](javascript&#x26;colon;alert)\n", "\n\n[x](javascript%26#x26;colon;alert)\n\n"},125		{"[x](/r/boards$help&func=DeleteThread&boardID=2)\n", "\n\n[x](/r/boards$help&func=DeleteThread&boardID=2)\n\n"},126	}127	for _, c := range cases {128		got := BlockRich(c.in)129		if got != c.want {130			t.Errorf("BlockRich(%q) = %q, want %q", c.in, got, c.want)131		}132		// Idempotency: BlockRich(BlockRich(in)) must be byte-identical133		// to BlockRich(in) for every input in the table.134		twice := BlockRich(got)135		if twice != got {136			t.Errorf("BlockRich not idempotent for %q: BlockRich(once)=%q, BlockRich(twice)=%q", c.in, got, twice)137		}138	}139}140141func TestBlockRich_LeadingBlankLineGuaranteed(t *testing.T) {142	// Every non-empty result begins with "\n\n" so `chrome +143	// BlockRich(user)` cannot place the user's first line in the same144	// paragraph as the realm's last line (which would let a deeper145	// `===` or `|---|` in user content retroactively promote realm146	// chrome).147	for _, in := range []string{"x", "x\n", "# h", "- i\n- j", "| a |\n|---|\n| 1 |"} {148		got := BlockRich(in)149		if got == "" {150			continue151		}152		if !strings.HasPrefix(got, "\n\n") {153			t.Errorf("BlockRich(%q) = %q; expected leading '\\n\\n'", in, got)154		}155	}156}157158func TestBlockRich_TrailingBlankLineGuaranteed(t *testing.T) {159	// Every non-empty result ends with "\n\n" so `BlockRich(user) +160	// chrome` cannot extend user's last paragraph into the realm's161	// next line (CM §5.2 lazy continuation, or a realm-supplied162	// `|---|` row retroactively promoting user's last line into a163	// `<thead>` header).164	for _, in := range []string{"x", "x\n", "# h", "- i\n- j", "| H |\n|---|\n| a |"} {165		got := BlockRich(in)166		if got == "" {167			continue168		}169		if !strings.HasSuffix(got, "\n\n") {170			t.Errorf("BlockRich(%q) = %q; expected trailing '\\n\\n'", in, got)171		}172	}173}174175func TestBlockquoteRich(t *testing.T) {176	// BlockquoteRich strips BlockRich's leading "\n", line-prefixes177	// each remaining line with "> ", and emits "\n" on both ends:178	// leading "\n" prevents `chrome + BlockquoteRich(user)` from179	// landing the first `>` mid-line; trailing "\n\n" (blank line)180	// prevents `BlockquoteRich(user) + chrome` from pulling chrome181	// into the quote via CM §5.2 lazy continuation.182	cases := []struct{ name, in, want string }{183		{"plain text", "hello world\n", "\n> hello world\n\n"},184		{"atx heading preserved", "# heading\n", "\n> # heading\n\n"},185		{"nested blockquote", "> nested\n", "\n> > nested\n\n"},186		{"list item preserved", "- item\n", "\n> - item\n\n"},187		{"ordered list preserved", "1. item\n", "\n> 1. item\n\n"},188		{"first-line setext escaped", "---\n", "\n> \\---\n\n"},189		{"deeper setext preserved", "text\n===\n", "\n> text\n> ===\n\n"},190		{"thematic break asterisks preserved", "***\n", "\n> ***\n\n"},191		// Realm-binding defenses still on.192		{"gno extension escaped", "<gno-card>\n", "\n> \\<gno-card>\n\n"},193		{"ref-link use escaped", "[t][l]\n", "\n> \\[t\\]\\[l\\]\n\n"},194		{"footnote escaped", "[^name]\n", "\n> \\[^name\\]\n\n"},195		// LRDs are stripped by BlockRich entirely; trimming the196		// resulting bare "\n" leaves empty input and the helper197		// returns "" without emitting a blockquote.198		{"lrd alone strips to empty", "[x]: y\n", ""},199		// Code fence autoclose at EOF lands inside the quote.200		{"unclosed fence autoclosed", "```\ncode\n", "\n> ```\n> code\n> ```\n\n"},201		// Multi-paragraph preserves the inner blank line (rendered202		// as a quoted blank line `> \n`).203		{"multi-paragraph preserves blank", "a\n\nb\n", "\n> a\n> \n> b\n\n"},204		// Empty / blank-only input collapses cleanly to "".205		{"empty input", "", ""},206		// CRLF normalized through BlockRich.207		{"crlf normalized", "a\r\nb\n", "\n> a\n> b\n\n"},208		// NUL replaced with U+FFFD by BlockRich.209		{"nul replaced", "x\x00y\n", "\n> x\uFFFDy\n\n"},210		// Multiple trailing newlines collapse to the single trailing211		// blank line shape — output never ends with more than `\n\n`.212		{"multiple trailing newlines collapse", "foo\n\n\n", "\n> foo\n\n"},213		// Internal tabs are preserved (BlockRich does not touch them).214		{"internal tab preserved", "a\tb\n", "\n> a\tb\n\n"},215		// Whitespace-only input still yields a blockquote (the user216		// wrote literal spaces). The line-prefix loop emits `> ` plus217		// the original two spaces.218		{"whitespace-only input", "  ", "\n>   \n\n"},219	}220	for _, c := range cases {221		got := BlockquoteRich(c.in)222		if got != c.want {223			t.Errorf("%s: BlockquoteRich(%q) = %q, want %q", c.name, c.in, got, c.want)224		}225	}226}227228func TestBlockquoteRich_DoubleWrapNestsQuote(t *testing.T) {229	// Not idempotent: calling twice nests the quote one level230	// deeper. The aggressive TrimRight in BlockquoteRich also231	// collapses the inner trailing blank line, so the second pass232	// produces a clean `> > foo` nesting (no `> ` quoted blank in233	// the middle) plus the outer trailing blank line.234	once := BlockquoteRich("foo\n")235	twice := BlockquoteRich(once)236	if want := "\n> > foo\n\n"; twice != want {237		t.Errorf("BlockquoteRich(BlockquoteRich(%q)) = %q, want %q", "foo\n", twice, want)238	}239}240241func TestBlockquoteRich_LeadingNewlineGuaranteed(t *testing.T) {242	// Every non-empty result starts with "\n" so realm concatenation243	// like `chrome + BlockquoteRich(user)` doesn't place `>` mid-line.244	for _, in := range []string{"x", "x\n", "# h", "- i\n- j"} {245		got := BlockquoteRich(in)246		if got == "" {247			continue248		}249		if got[0] != '\n' {250			t.Errorf("BlockquoteRich(%q) = %q; expected leading '\\n'", in, got)251		}252	}253}254255func TestBlockquoteRich_TrailingBlankLineGuaranteed(t *testing.T) {256	// Every non-empty result ends with "\n\n" so realm concatenation257	// like `BlockquoteRich(user) + "more text"` doesn't pull "more258	// text" into the quote via CM §5.2 lazy continuation.259	for _, in := range []string{"x", "x\n", "# h", "- i\n- j", "para\n\nmore"} {260		got := BlockquoteRich(in)261		if got == "" {262			continue263		}264		if !strings.HasSuffix(got, "\n\n") {265			t.Errorf("BlockquoteRich(%q) = %q; expected trailing '\\n\\n'", in, got)266		}267	}268}269270func TestBlockquoteRich_NoStrayEmptyQuotedLine(t *testing.T) {271	// BlockRich's own leading "\n" must be stripped before272	// line-prefixing — otherwise the output would start with a273	// useless `> \n` empty quoted line.274	got := BlockquoteRich("foo\n")275	if strings.HasPrefix(got, "\n> \n") {276		t.Errorf("BlockquoteRich leaked BlockRich's leading '\\n' as `> \\n`: %q", got)277	}278}279280func TestNeuterLeadingSetextIfQualifying(t *testing.T) {281	cases := []struct{ name, in, want string }{282		{"leading-setext-h1", "===\nbody\n", "\\===\nbody\n"},283		{"leading-setext-h2", "---\nbody\n", "\\---\nbody\n"},284		{"leading-setext-indented", "   ===\nbody\n", "   \\===\nbody\n"},285		{"leading-setext-indented-4", "    ===\nbody\n", "    ===\nbody\n"}, // indented code286		{"leading-setext-trailing-ws", "===   \nbody\n", "\\===   \nbody\n"},287		{"leading-setext-mixed-chars", "=-=-\nbody\n", "=-=-\nbody\n"},         // mixed; not setext288		{"leading-setext-has-content", "=== text\nbody\n", "=== text\nbody\n"}, // mixed content289		{"setext-deeper-untouched", "title\n===\nfoo\n", "title\n===\nfoo\n"},290		{"leading-thematic-asterisk", "***\nbody\n", "***\nbody\n"}, // not setext, untouched291		{"leading-thematic-underscore", "___\nbody\n", "___\nbody\n"},292		{"leading-blank-then-setext", "\n===\nbody\n", "\n\\===\nbody\n"},293		{"leading-blank-ws-then-setext", "   \n===\nbody\n", "   \n\\===\nbody\n"},294		{"text-first", "hello\n===\n", "hello\n===\n"}, // text before === — user-authored295		{"empty", "", ""},296		{"all-blank", "   \n\n", "   \n\n"},297		{"only-equals", "===", "\\==="},298	}299	for _, c := range cases {300		if got := neuterLeadingSetextIfQualifying(c.in); got != c.want {301			t.Errorf("%s: neuterLeadingSetextIfQualifying(%q) = %q, want %q", c.name, c.in, got, c.want)302		}303	}304}305306func TestLinkTitle(t *testing.T) {307	cases := []struct{ in, want string }{308		{`he said "hi"`, `he said \"hi\"`},309		{`it's nice`, `it\'s nice`},310		{"line1\nline2", "line1 line2"},311	}312	for _, c := range cases {313		if got := LinkTitle(c.in); got != c.want {314			t.Errorf("LinkTitle(%q) = %q, want %q", c.in, got, c.want)315		}316	}317}318319func TestTableCell(t *testing.T) {320	cases := []struct{ in, want string }{321		{"plain cell", "plain cell"},322		{"a|b", `a\|b`},323		{"a\tb", "a b"},324		{"a*b|c", `a\*b\|c`},325	}326	for _, c := range cases {327		if got := TableCell(c.in); got != c.want {328			t.Errorf("TableCell(%q) = %q, want %q", c.in, got, c.want)329		}330	}331}332333func TestHTMLEscape(t *testing.T) {334	cases := []struct{ in, want string }{335		{"plain", "plain"},336		{"<script>", "&lt;script&gt;"},337		{`a & b`, "a &amp; b"},338		{`"quoted"`, "&#34;quoted&#34;"},339	}340	for _, c := range cases {341		if got := HTMLEscape(c.in); got != c.want {342			t.Errorf("HTMLEscape(%q) = %q, want %q", c.in, got, c.want)343		}344	}345}346347func TestURL(t *testing.T) {348	cases := []struct{ in, want string }{349		{"https://example.com/x", "https://example.com/x"},350		{"http://example.com", "http://example.com"},351		{"mailto:a@b.com", "mailto:a@b.com"},352		{"mailto:a@b.com?body=phish", ""},            // any query is a prefill-phishing vector353		{"mailto:a@b.com?BODY=phish", ""},            // header name case is irrelevant354		{"mailto:a@b.com?Body=phish", ""},            // mixed case355		{"mailto:a@b.com?%62ody=phish", ""},          // percent-encoded header (%62 == b)356		{"mailto:a@b.com?subject=hi&body=phish", ""}, // body as second param357		{"mailto:a@b.com?subject=hi&BODY=phish", ""}, // mixed case second param358		{"mailto:a@b.com?subject=hello", ""},         // subject alone still rejected359		{"mailto:a@b.com?cc=x@y.com", ""},            // cc rejected360		{"mailto:a@b.com?bcc=x@y.com", ""},           // bcc rejected361		{"mailto:a@b.com?", ""},                      // bare trailing query marker rejected362		{"mailto:a@b.com&#x3f;body=phish", ""},       // hex char-ref ? — renderer decodes to ?body=363		{"mailto:a@b.com&#63;body=phish", ""},        // decimal char-ref ? — same bypass364		{"mailto:a@b.com&quest;body=phish", ""},      // named char-ref ? — same bypass365		{"mailto:a@b.com&body=phish", ""},            // bare & gateway rejected366		{"//evil.com", ""},                           // protocol-relative rejected367		{"javascript:alert(1)", ""},                  // bad scheme368		{"/r/foo", "/r/foo"},                         // relative369		{"./local", "./local"},370		{"#section", "#section"}, // fragment-only371		{"", ""},372		{"   ", ""},373		{"https://a.com/path with space", "https://a.com/path%20with%20space"},374	}375	for _, c := range cases {376		if got := URL(c.in); got != c.want {377			t.Errorf("URL(%q) = %q, want %q", c.in, got, c.want)378		}379	}380}381382func TestImageURL(t *testing.T) {383	cases := []struct{ in, want string }{384		{"https://example.com/img.png", "https://example.com/img.png"},385		{"mailto:a@b.com", ""}, // mailto rejected for images386		{"data:image/svg+xml,<svg/>", "data:image/svg+xml,%3Csvg/%3E"},387		{"data:image/png;base64,XXX", "data:image/png;base64,XXX"},388		{"data:text/html,<script>", ""}, // bad data subset389		{"javascript:alert(1)", ""},390		{"", ""},391	}392	for _, c := range cases {393		if got := ImageURL(c.in); got != c.want {394			t.Errorf("ImageURL(%q) = %q, want %q", c.in, got, c.want)395		}396	}397}398399func TestUserName(t *testing.T) {400	cases := []struct{ in, want string }{401		{"alice", "alice"},402		{"alice123", "alice123"},403		{"alice_bob-cat", "alice_bob-cat"},404		{"Alice", ""},  // uppercase first405		{"1alice", ""}, // digit first406		{"", ""},407		{"a\u200Blice", "alice"}, // bidi stripped, then matches408	}409	for _, c := range cases {410		if got := UserName(c.in); got != c.want {411			t.Errorf("UserName(%q) = %q, want %q", c.in, got, c.want)412		}413	}414}415416func TestBechString(t *testing.T) {417	addrG := "g1abc123def456ghi789jkl012mno345p"418	cases := []struct {419		s, prefix string420		want      string421	}{422		{addrG, "g", addrG},423		{addrG, "", addrG},  // any prefix424		{addrG, "gpub", ""}, // wrong prefix425		{"gpub1abc123def456ghijklmn", "gpub", "gpub1abc123def456ghijklmn"},426		{"gpub1abc123def456ghijklmn", "", "gpub1abc123def456ghijklmn"},427		{"b1xyz789abc123def456", "", "b1xyz789abc123def456"}, // any-prefix mode allows b1...428		{"g1ABC", "g", ""}, // uppercase rejected429		{"x", "g", ""},430		{"", "g", ""},431	}432	for _, c := range cases {433		if got := BechString(c.s, c.prefix); got != c.want {434			t.Errorf("BechString(%q,%q) = %q, want %q", c.s, c.prefix, got, c.want)435		}436	}437}438439func TestFootnoteLabel(t *testing.T) {440	cases := []struct{ in, want string }{441		{"note1", "note1"},442		{"Note_A-1", "Note_A-1"},443		{"with space", ""},444		{"", ""},445	}446	for _, c := range cases {447		if got := FootnoteLabel(c.in); got != c.want {448			t.Errorf("FootnoteLabel(%q) = %q, want %q", c.in, got, c.want)449		}450	}451}452453func TestLanguageName(t *testing.T) {454	cases := []struct{ in, want string }{455		{"go", "go"},456		{"c++", "c++"},457		{"python3", "python3"},458		{"objective-c", "objective-c"},459		{"with space", ""},460		{"", ""},461	}462	for _, c := range cases {463		if got := LanguageName(c.in); got != c.want {464			t.Errorf("LanguageName(%q) = %q, want %q", c.in, got, c.want)465		}466	}467}468469func TestNestedPrefix(t *testing.T) {470	cases := []struct{ in, want string }{471		{"", ""},472		{"  ", "  "},473		{"\t", "\t"},474		{"> ", "> "},475		{"> > ", "> > "},476		{"## ", ""}, // markdown-active prefix rejected477		{"- ", ""},478	}479	for _, c := range cases {480		if got := NestedPrefix(c.in); got != c.want {481			t.Errorf("NestedPrefix(%q) = %q, want %q", c.in, got, c.want)482		}483	}484}485486func TestCodeFence(t *testing.T) {487	if got := CodeFence("```", 3); got != "````" {488		t.Errorf("CodeFence: got %q, want %q", got, "````")489	}490	if got := CodeFence("", 3); got != "```" {491		t.Errorf("CodeFence empty: got %q, want %q", got, "```")492	}493}494

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.