▍ humdrum codex / glint v1.1.2
license AGPL-3.0
3.5 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package preview

import "strings"

// prop is one frontmatter key and its flattened display value.
type prop struct {
	key string
	val string
}

// listSep joins the items of a YAML sequence into one display value.
const listSep = " · "

// splitFrontmatter peels a leading YAML frontmatter block (--- … ---) off the
// document, returning its properties and the remaining markdown. A block that
// doesn't start on the first line, or never closes, is left in the body: it's a
// horizontal rule, not metadata.
func splitFrontmatter(md string) ([]prop, string) {
	if !strings.HasPrefix(md, "---\n") && !strings.HasPrefix(md, "---\r\n") {
		return nil, md
	}
	lines := strings.Split(md[strings.IndexByte(md, '\n')+1:], "\n")
	for i, ln := range lines {
		if t := strings.TrimRight(ln, "\r"); t == "---" || t == "..." {
			return parseProps(lines[:i]), strings.Join(lines[i+1:], "\n")
		}
	}
	return nil, md
}

// parseProps reads the lines between the frontmatter fences. It handles the
// shapes Obsidian writes — scalars, inline arrays, and indented "- item"
// sequences — flattening each to a single line. Anything more exotic (nested
// maps, block scalars) degrades to its raw text rather than erroring.
func parseProps(lines []string) []prop {
	var props []prop
	var items []string
	flush := func() {
		if len(items) > 0 && len(props) > 0 {
			props[len(props)-1].val = strings.Join(items, listSep)
		}
		items = nil
	}
	for _, raw := range lines {
		ln := strings.TrimRight(raw, "\r")
		t := strings.TrimSpace(ln)
		if t == "" || strings.HasPrefix(t, "#") {
			continue
		}
		// Indented or dashed lines belong to the property above.
		if ln != t || strings.HasPrefix(t, "-") {
			if strings.HasPrefix(t, "-") {
				items = append(items, cleanScalar(strings.TrimSpace(t[1:])))
			} else if len(props) > 0 {
				items = append(items, cleanScalar(t))
			}
			continue
		}
		flush()
		k, v, ok := strings.Cut(ln, ":")
		if !ok {
			continue
		}
		props = append(props, prop{strings.TrimSpace(k), cleanValue(strings.TrimSpace(v))})
	}
	flush()
	return props
}

// cleanValue flattens one scalar or inline array ("[a, b]") to display text.
func cleanValue(v string) string {
	if len(v) >= 2 && strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
		// A wikilink ("[[Note]]") is a scalar, not an array.
		if !strings.HasPrefix(v, "[[") {
			var out []string
			for _, item := range splitItems(v[1 : len(v)-1]) {
				if s := cleanScalar(item); s != "" {
					out = append(out, s)
				}
			}
			return strings.Join(out, listSep)
		}
	}
	return cleanScalar(v)
}

// splitItems splits an inline array's contents on commas outside quotes.
func splitItems(s string) []string {
	var out []string
	var quote rune
	start := 0
	for i, r := range s {
		switch {
		case quote != 0:
			if r == quote {
				quote = 0
			}
		case r == '"' || r == '\'':
			quote = r
		case r == ',':
			out = append(out, s[start:i])
			start = i + 1
		}
	}
	return append(out, s[start:])
}

// cleanScalar trims a value and drops one layer of matching quotes.
func cleanScalar(s string) string {
	s = strings.TrimSpace(s)
	if len(s) >= 2 {
		if q := s[0]; (q == '"' || q == '\'') && s[len(s)-1] == q {
			return s[1 : len(s)-1]
		}
	}
	return s
}

// takeTitle pulls the "title" property out of props, so a doc titled in its
// frontmatter uses that name for the heading instead of repeating it below.
func takeTitle(props []prop) (string, []prop) {
	for i, p := range props {
		if p.key == "title" && p.val != "" {
			return p.val, append(props[:i:i], props[i+1:]...)
		}
	}
	return "", props
}