Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}