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

text.gno

4.30 Kb · 171 lines
  1package gallery
  2
  3import "strings"
  4
  5// doc builds the plain-text target: a document that reads well as-is, rather
  6// than markdown with the syntax characters deleted.
  7//
  8// This is a prototype, deliberately small and deliberately here rather than in
  9// a p/ package. Porting one realm is not enough to know what the shared
 10// document model should be; porting the fifth will be. What it already pins is
 11// the set of rules that make text good instead of merely stripped:
 12//
 13//  1. A link prints its text inline and collects the URL into a numbered
 14//     footnote. Never "[text](url)", which is the single worst thing markdown
 15//     does to a terminal reader.
 16//  2. A heading is underlined, not prefixed with '#'.
 17//  3. Art is emitted verbatim: never wrapped, never escaped, never fenced.
 18//  4. Prose is wrapped at a fixed width; art and fields are not.
 19type doc struct {
 20	b     strings.Builder
 21	links []string
 22}
 23
 24// width is where prose wraps. 72 leaves room for a quoting prefix in an email
 25// or a diff without re-wrapping.
 26const width = 72
 27
 28func (d *doc) H1(s string) {
 29	d.blank()
 30	d.b.WriteString(s + "\n" + strings.Repeat("=", runeLen(s)) + "\n")
 31}
 32
 33func (d *doc) H2(s string) {
 34	d.blank()
 35	d.b.WriteString(s + "\n" + strings.Repeat("-", runeLen(s)) + "\n")
 36}
 37
 38func (d *doc) Para(s string) {
 39	if s == "" {
 40		return
 41	}
 42	d.blank()
 43	d.b.WriteString(wrap(s, width) + "\n")
 44}
 45
 46func (d *doc) Item(s string) {
 47	d.b.WriteString("  - " + s + "\n")
 48}
 49
 50// Table writes columns padded to their widest cell, under a rule. An unpadded
 51// pipe table is the other thing markdown does to a terminal reader that no
 52// amount of character-stripping fixes: the columns stop lining up and the table
 53// stops being a table.
 54//
 55// Cells are laid out by rune count, not byte count, so a box-drawing glyph or
 56// an accent does not shift the column.
 57func (d *doc) Table(headers []string, rows [][]string) {
 58	if len(headers) == 0 {
 59		return
 60	}
 61	w := make([]int, len(headers))
 62	for i, h := range headers {
 63		w[i] = runeLen(h)
 64	}
 65	for _, r := range rows {
 66		for i, c := range r {
 67			if i < len(w) && runeLen(c) > w[i] {
 68				w[i] = runeLen(c)
 69			}
 70		}
 71	}
 72
 73	d.blank()
 74	d.b.WriteString("  " + joinPadded(headers, w) + "\n")
 75
 76	rule := make([]string, len(headers))
 77	for i := range rule {
 78		rule[i] = strings.Repeat("-", w[i])
 79	}
 80	d.b.WriteString("  " + joinPadded(rule, w) + "\n")
 81
 82	for _, r := range rows {
 83		d.b.WriteString("  " + joinPadded(r, w) + "\n")
 84	}
 85}
 86
 87// joinPadded pads every cell but the last, which would only add trailing space.
 88func joinPadded(cells []string, w []int) string {
 89	var b strings.Builder
 90	for i, c := range cells {
 91		if i > 0 {
 92			b.WriteString("  ")
 93		}
 94		b.WriteString(c)
 95		if i < len(cells)-1 && i < len(w) {
 96			b.WriteString(strings.Repeat(" ", w[i]-runeLen(c)))
 97		}
 98	}
 99	return b.String()
100}
101
102func (d *doc) Field(k, v string) {
103	d.b.WriteString("  " + k + ": " + v + "\n")
104}
105
106// Art writes a block verbatim. No fence, no indent, no escaping: a consumer
107// that asked for text asked for the picture as it is.
108func (d *doc) Art(s string) {
109	d.blank()
110	d.b.WriteString(s + "\n")
111}
112
113// Link prints the text inline and defers the URL to the footnote block.
114func (d *doc) Link(text, url string) {
115	d.links = append(d.links, url)
116	d.b.WriteString("  " + text + " [" + itoa(len(d.links)) + "]\n")
117}
118
119func (d *doc) String() string {
120	out := d.b.String()
121	if len(d.links) == 0 {
122		return out
123	}
124	var b strings.Builder
125	b.WriteString(out)
126	b.WriteString("\nLinks:\n")
127	for i, u := range d.links {
128		b.WriteString("  [" + itoa(i+1) + "] " + u + "\n")
129	}
130	return b.String()
131}
132
133// blank writes a separating newline unless the document is empty or already
134// ends in one, so no block has to know what came before it.
135func (d *doc) blank() {
136	s := d.b.String()
137	if s == "" || strings.HasSuffix(s, "\n\n") {
138		return
139	}
140	d.b.WriteString("\n")
141}
142
143// wrap breaks on spaces at w runes. A word longer than w is left alone rather
144// than cut: a URL or a realm path is worse broken than wide.
145func wrap(s string, w int) string {
146	var out strings.Builder
147	line := 0
148	for i, word := range strings.Fields(s) {
149		n := runeLen(word)
150		switch {
151		case i == 0:
152			out.WriteString(word)
153			line = n
154		case line+1+n > w:
155			out.WriteString("\n" + word)
156			line = n
157		default:
158			out.WriteString(" " + word)
159			line += 1 + n
160		}
161	}
162	return out.String()
163}
164
165func runeLen(s string) int {
166	n := 0
167	for range s {
168		n++
169	}
170	return n
171}