Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

text.gno

6.42 Kb · 184 lines
  1package bolo
  2
  3import (
  4	"strings"
  5	"unicode"
  6	"unicode/utf8"
  7
  8	"gno.land/p/nt/markdown/sanitize/v0"
  9)
 10
 11// Text limits in characters — Unicode code points — after normalization (API
 12// §2.3). Every text also has a byte cap of four times its character limit, on
 13// the argument as sent.
 14const (
 15	titleMin, titleMax   = 4, 80
 16	descriptionMax       = 5000
 17	messageMax           = 140
 18	updateMin, updateMax = 1, 2000
 19	noticeMin, noticeMax = 1, 280
 20	altMin, altMax       = 1, 250
 21	cidMaxBytes          = 100
 22)
 23
 24// normalize prepares user text for checking and storing (API C7, §2.3). Bidi and
 25// zero-width characters go first, through the very function sanitize.Block calls
 26// when it renders, so that no check can be dodged by what rendering removes. Line
 27// breaks are unified — CR-LF, CR, NEL and the Unicode line and paragraph
 28// separators all become "\n" — and kept in multiline text; elsewhere they become
 29// a space. Tabs and the other whitespace controls (a vertical tab, a form feed, the
 30// information separators) become a space everywhere, so words never join. Then
 31// every character that only pads a text goes (invisible), and surrounding
 32// whitespace is trimmed. Stripping more than Block does only makes the checks
 33// stricter, and the stripped text is what gets stored.
 34func normalize(s string, multiline bool) string {
 35	s = sanitize.StripBidiAndZeroWidth(s)
 36	s = sanitize.NormalizeBreaks(s)
 37	s = strings.Map(func(r rune) rune {
 38		switch {
 39		case r == '\n' || r == '\u0085' || r == '\u2028' || r == '\u2029':
 40			if multiline {
 41				return '\n'
 42			}
 43			return ' '
 44		case r == '\t' || r == '\v' || r == '\f' || (r >= '\x1c' && r <= '\x1f'):
 45			return ' '
 46		case invisible(r):
 47			return -1
 48		}
 49		return r
 50	}, s)
 51	return strings.TrimSpace(s)
 52}
 53
 54// invisible reports whether r shows nothing and only pads a text: a control or a
 55// format character (Unicode Cc, Cf: a word joiner, a soft hyphen), one Unicode
 56// lists as Other_Default_Ignorable_Code_Point (a Hangul filler, the combining
 57// grapheme joiner), or a blank symbol (the Braille blank, the musical null
 58// notehead). A Latin-1 character skips the tables, which are costly and which
 59// English or French text seldom leaves: of them, only the controls and the soft
 60// hyphen are invisible (TestLatin1 holds this to the tables). Every control is
 61// Latin-1.
 62func invisible(r rune) bool {
 63	if r <= unicode.MaxLatin1 {
 64		return r < 0x20 || r >= 0x7f && r <= 0x9f || r == 0xad
 65	}
 66	return unicode.Is(unicode.Cf, r) || unicode.Is(unicode.Other_Default_Ignorable_Code_Point, r) ||
 67		r == '\u2800' || r == '\U0001D159'
 68}
 69
 70// checkText checks one text argument and returns it normalized, the form Bolo
 71// stores (API §2.3): valid UTF-8 and at most four bytes per allowed character as
 72// sent; then, normalized, at most max characters, of which at least min show on
 73// their own. It panics with err otherwise.
 74func checkText(s string, min, max int, multiline bool, err Error) string {
 75	if !utf8.ValidString(s) || len(s) > 4*max {
 76		panic(err)
 77	}
 78	s = normalize(s, multiline)
 79	if utf8.RuneCountInString(s) > max || !visible(s, min) {
 80		panic(err)
 81	}
 82	return s
 83}
 84
 85// visible reports whether at least min characters of s show on their own —
 86// letters, digits, punctuation, symbols and spacing marks — counting the spaces
 87// between two of them. A mark that takes no space (an accent, a variation
 88// selector) only changes the character before it, a private-use character or a
 89// code point Unicode hasn't assigned shows nothing a reader can count on, and a
 90// space shows only between characters that do. They all stay in the text, but
 91// don't count toward a minimum. Latin-1 skips the tables, as in invisible: there,
 92// every character shows on its own but the controls, the soft hyphen and the two
 93// spaces, ' ' and U+00A0. Counting stops once min characters have shown, so a long
 94// text pays the tables only until then, and a story, whose minimum is 0, never.
 95func visible(s string, min int) bool {
 96	n, spaces, shown := 0, 0, false
 97	for _, r := range s {
 98		if n >= min {
 99			break
100		}
101		switch {
102		case r > ' ' && r < 0x7f, r > 0xa0 && r <= unicode.MaxLatin1 && r != 0xad,
103			r > unicode.MaxLatin1 && unicode.In(r, unicode.L, unicode.Mc, unicode.N, unicode.P, unicode.S):
104			n += 1 + spaces
105			spaces, shown = 0, true
106		case shown && (r == ' ' || r == 0xa0 || r > unicode.MaxLatin1 && unicode.Is(unicode.Zs, r)):
107			spaces++
108		}
109	}
110	return n >= min
111}
112
113// checkStory checks a story or an update, the organizer's own text, and returns it
114// normalized (API §2.3): the limits of checkText, then no image and no raw HTML,
115// judged on the normalized text that sanitize.Block will render.
116func checkStory(s string, min, max int, err Error) string {
117	s = checkText(s, min, max, true, err)
118	checkMarkup(s)
119	return s
120}
121
122// checkMarkup refuses an image or raw HTML (API §2.3): `![`, or `<` followed by a
123// letter, `/`, `!` or `?` — every way CommonMark opens raw HTML. The campaign
124// photo is the only image a campaign shows, and `<3` still passes.
125func checkMarkup(s string) {
126	if strings.Contains(s, "![") {
127		panic(ErrMarkup)
128	}
129	for i := 0; i+1 < len(s); i++ {
130		if s[i] != '<' {
131			continue
132		}
133		c := s[i+1]
134		if c == '/' || c == '!' || c == '?' || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') {
135			panic(ErrMarkup)
136		}
137	}
138}
139
140// checkImage checks a campaign photo's CID and its description (API §2.3, ADR
141// D16), and returns both, the description normalized: no photo, no description;
142// a photo, a description of 1 to 250 characters.
143func checkImage(cid, alt string) (string, string) {
144	if cid == "" {
145		if alt != "" {
146			panic(ErrImage)
147		}
148		return "", ""
149	}
150	if !validCID(cid) {
151		panic(ErrImage)
152	}
153	return cid, checkText(alt, altMin, altMax, false, ErrImage)
154}
155
156// validCID reports whether s has the shape of an IPFS CID Bolo accepts: v0, "Qm"
157// and 44 more base58btc characters; or v1 in lowercase base32, "b" then that
158// alphabet; at most 100 bytes. Render writes the CID into `(ipfs://…)` as it is,
159// so the character set is the escaping (API §2.3).
160func validCID(s string) bool {
161	switch {
162	case len(s) > cidMaxBytes:
163		return false
164	case len(s) == 46 && strings.HasPrefix(s, "Qm"):
165		return allIn(s, base58btc)
166	case len(s) > 1 && s[0] == 'b':
167		return allIn(s[1:], base32Lower)
168	}
169	return false
170}
171
172const (
173	base58btc   = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
174	base32Lower = "abcdefghijklmnopqrstuvwxyz234567"
175)
176
177func allIn(s, set string) bool {
178	for i := 0; i < len(s); i++ {
179		if strings.IndexByte(set, s[i]) < 0 {
180			return false
181		}
182	}
183	return true
184}