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/foreign/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
nt / markdown / foreign
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/foreign/v0
gno
0.9

Files (4)

  • README.mdmarkdown
  • gnomod.tomltoml
  • foreign.gnogno
  • foreign_test.gnogno
foreign.gnogno
1// Package foreign provides the realm-side helper for emitting the2// gno-foreign sandbox block. Realm authors wrap externally-built3// markdown (markdown returned by an interface method on a foreign4// realm, fetched from chain storage owned by another realm, etc.) in5// Foreign before flowing it into rendered output, so gnoweb renders6// the body inside its own goldmark sub-instance with structural7// extensions selectively loaded.8//9// The renderer-side contract lives in10// gno.land/pkg/gnoweb/markdown/ext_foreign.go. This helper produces11// bytes that satisfy the parser's opener requirements (CommonMark12// §4.6 Type-7 HTML block, no attribute fall-through) and neutralizes13// any literal sentinel lines in the body so the foreign markdown14// cannot terminate the outer block prematurely.15package foreign1617import (18	"chain/markdown"19	"strings"20)2122// Foreign wraps body in a `<gno-foreign>` ... `</gno-foreign>` sandbox23// block. The returned string is ready to concatenate into a larger24// markdown document.25//26// Three normalization steps apply to body:27//28//  1. \r\n and bare \r line endings are normalized to \n. The parser29//     uses byte-equal matching against the sentinel close tag, so30//     mixed line endings would otherwise change the match boundary.31//32//  2. Any line whose trimmed content looks like a gno-foreign tag33//     opener OR closer — bare (`<gno-foreign>`, `</gno-foreign>`) or34//     attribute-bearing (`<gno-foreign label="x">`, `</gno-foreign35//     attr="…">`, etc.) — is neutralized by HTML-escaping the leading36//     `<` to `&lt;`. The parser tokenizes line bytes literally, so the37//     escaped form is seen as text and cannot terminate the outer38//     block or open an unintended inner block.39//40//     Crucially, BOTH open-tag and close-tag attribute-bearing forms41//     are neutralized. The parser recognizes a bare `<gno-foreign>`42//     opener, a labeled `<gno-foreign label="x">` opener, and ANY43//     `</gno-foreign…>` closer (golang.org/x/net/html drops attrs on44//     end tags before our recognizer sees them, so attr-bearing45//     closers are sentinel-equivalent). Leaving any of those forms46//     un-neutralized in body bytes would let attacker-supplied47//     markdown adjust the parser's framing-depth counter and either48//     consume the helper's own close (capturing trailing realm49//     content into the sandbox) or close the outer block early50//     (escaping the sandbox entirely).51//52//     There is therefore NO nesting via the helper: Foreign(Foreign(x))53//     escapes the inner call's own `<gno-foreign>`/`</gno-foreign>`54//     lines, so the inner block renders as visible literal text inside55//     one sandbox, not as a nested sandbox. This is intended — wrapping56//     foreign-built markdown that itself contains gno-foreign sentinels57//     must neutralize them, not honor them.58//59//  3. A leading and trailing blank line are emitted around the60//     opener / closer. CommonMark §4.6 forbids Type-7 HTML blocks61//     from interrupting a paragraph; without the blank line, an62//     opener following a non-blank line is absorbed into the63//     preceding paragraph instead of opening a sandbox.64//65// The renderer caps cross-family nesting at 4 levels and per-Convert66// foreign blocks at 256. Beyond those caps, the opener falls through67// to raw HTML and is stripped by the renderer's safe mode.68func Foreign(body string) string {69	return wrapForeign("", body)70}7172// ForeignWithLabel wraps body like Foreign but emits an explicit73// `label="…"` attribute on the opener so the rendered sandbox carries74// a caller-supplied label (e.g., "Pulled from /r/foo") shown as a75// strip above the body. The label is sanitized so it cannot inject76// HTML or break out of the attribute value:77//78//   - NUL bytes are dropped.79//   - Other control characters (U+0000–U+001F, U+007F) become spaces.80//   - `&`, `<`, `>`, and `"` are replaced with their HTML entities.81//   - Leading/trailing whitespace is trimmed.82//83// A label that is empty after sanitization behaves identically to84// Foreign: no attribute is emitted, and the renderer shows the sandbox85// box with NO label strip (there is no default label text).86func ForeignWithLabel(label, body string) string {87	return wrapForeign(label, body)88}8990// MaxBlocksPerRender is gnoweb's per-render cap on the number of91// <gno-foreign> blocks a single page render admits; beyond it, later92// blocks fall through to raw HTML and are dropped. A realm emitting93// many foreign blocks (e.g. one per comment) should keep its rendered94// total under this. Re-exports chain/markdown.MaxForeignBlocksPerConvert95// — the single source of truth the gnoweb renderer also reads — so96// callers get the cap without importing chain/markdown directly.97func MaxBlocksPerRender() int {98	return markdown.MaxForeignBlocksPerConvert()99}100101func wrapForeign(rawLabel, body string) string {102	label := sanitizeLabel(rawLabel)103104	// Normalize line endings (CR/CRLF → LF). The parser matches the105	// sentinel close against \n-delimited lines, so mixed line endings106	// would otherwise shift the match boundary. CR/CRLF → LF ONLY: do107	// not fold Unicode separators here — they must stay verbatim in the108	// body so the inner renderer sees the foreign markdown unaltered.109	body = markdown.NormalizeBreaks(body)110111	// Mangle any line that would terminate the outer block or open112	// an inner one. Covers bare and attribute-bearing forms of both113	// the opener and the closer (see step 2 in the package doc).114	var b strings.Builder115	// b accumulates only the body lines (the opener/closer envelope is116	// concatenated separately below), so len(body) is the exact size in117	// the common case. Sentinel lines that expand `<`→`&lt;` may force118	// one growth — rare enough not to pre-size for.119	b.Grow(len(body))120	lines := strings.Split(body, "\n")121	for i, line := range lines {122		if isForeignSentinelLine(trimSentinel(line)) {123			// Escape just the leading `<` so the html tokenizer124			// sees this as text instead of a tag. Preserve any 0-3125			// leading spaces the parser's trim would have stripped.126			idx := strings.Index(line, "<")127			if idx >= 0 {128				line = line[:idx] + "&lt;" + line[idx+1:]129			}130		}131		b.WriteString(line)132		if i < len(lines)-1 {133			b.WriteByte('\n')134		}135	}136137	opener := "<gno-foreign>"138	if label != "" {139		opener = `<gno-foreign label="` + label + `">`140	}141	return "\n\n" + opener + "\n" + b.String() + "\n</gno-foreign>\n\n"142}143144// sanitizeLabel makes a user-supplied label safe to splice into an145// HTML attribute value on the gno-foreign opener line.146func sanitizeLabel(s string) string {147	// Strip bidi-override and zero-width controls FIRST — same ordering148	// as the sanitize package's HTMLEscape — so invisible reordering or149	// zero-width payloads can't survive into the rendered label.150	s = markdown.StripBidiAndZeroWidth(s)151	// Drop NUL; map other ASCII controls AND the Unicode line/paragraph152	// separators (U+2028, U+2029, U+0085 NEL) to spaces. The opener is a153	// single line, so any of these surviving in the label would either154	// add a control payload or, for the separators, render as a stray155	// line break inside the attribute.156	s = strings.Map(func(r rune) rune {157		if r == 0 {158			return -1159		}160		if r < 0x20 || r == 0x7f || r == 0x2028 || r == 0x2029 || r == 0x0085 {161			return ' '162		}163		return r164	}, s)165	// Escape `&` first so subsequent entity bytes don't get166	// re-escaped.167	s = strings.ReplaceAll(s, "&", "&amp;")168	s = strings.ReplaceAll(s, `"`, "&quot;")169	s = strings.ReplaceAll(s, "<", "&lt;")170	s = strings.ReplaceAll(s, ">", "&gt;")171	return strings.TrimSpace(s)172}173174// isForeignSentinelLine reports whether s (already trimmed via175// trimSentinel) begins with the gno-foreign tag prefix and so must be176// neutralized before it can reach the renderer-side parser.177//178// Deliberately OVER-INCLUSIVE: it matches any line whose trimmed form179// starts (case-INSENSITIVELY) with `<gno-foreign` or `</gno-foreign`,180// regardless of what follows. This is a strict superset of every line181// goldmark's html.Tokenizer can recognize as a <gno-foreign> opener or182// closer, which is what makes it safe:183//184//   - The tokenizer lowercases tag names, so `<GNO-FOREIGN>` etc. are185//     sentinels; the prefix match is case-folded to mirror that.186//   - The tokenizer ends a tag name at ANY of several terminators187//     (`>`, space, tab, form-feed, `/`). A precise check that188//     enumerates terminators keeps missing variants — e.g.189//     `</gno-foreign/>` and `</gno-foreign\f>` are both recognized as190//     closers by the parser. Matching on the prefix alone cannot miss191//     one: if a body line could be parsed as a sentinel, it starts with192//     this prefix and is escaped here.193//194// The only cost is that an unrelated longer tag like `<gno-foreignx>`195// (a different tag name, not a sentinel) is also escaped — rendered as196// visible literal text instead of being raw-HTML-stripped — which is197// harmless for foreign body bytes.198func isForeignSentinelLine(s string) bool {199	return hasASCIIFoldPrefix(s, "</gno-foreign") || hasASCIIFoldPrefix(s, "<gno-foreign")200}201202// hasASCIIFoldPrefix reports whether s begins with prefix, comparing203// ASCII letters case-insensitively. prefix must be lowercase ASCII;204// folding is ASCII-only on purpose (Unicode case folding would205// over-match, and the sentinel envelope is pure ASCII anyway).206func hasASCIIFoldPrefix(s, prefix string) bool {207	if len(s) < len(prefix) {208		return false209	}210	for i := 0; i < len(prefix); i++ {211		c := s[i]212		if c >= 'A' && c <= 'Z' {213			c += 'a' - 'A'214		}215		if c != prefix[i] {216			return false217		}218	}219	return true220}221222// trimSentinel returns line with the leading 0-3 spaces and trailing223// ASCII whitespace that the parser's trimForeignLine strips. Mirrors224// the byte-level trim the parser performs so this helper detects the225// same sentinel match the parser would.226func trimSentinel(s string) string {227	i := 0228	for i < len(s) && i < 3 && s[i] == ' ' {229		i++230	}231	s = s[i:]232	for len(s) > 0 {233		c := s[len(s)-1]234		if c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r' {235			s = s[:len(s)-1]236			continue237		}238		break239	}240	return s241}242

Functions

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

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