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 }