// Package gemtext provides a variety of tools for processing gemtext files. package gemtext import ( "fmt" "strings" ) type ObjectType int64 const ( TEXT ObjectType = iota LINK PREFORMATTED_TOGGLE PREFORMATTED_TEXT HEADING LIST QUOTE ) // type GemtextObject represents a gemtext object. type GemtextObject struct { Type ObjectType Text string // Contains the text of the element. For a link this is the label. Literal string // Contains the line as it exists in the file Path string // Populated if object is a link Level int // Populated if object is a heading } // type GemtextPage represents a gemtext page. It is equivalent to a slice of GemtextObjects. type GemtextPage []GemtextObject // ParseLink takes a gemini link as a string and returns a GemtextObject and an error. func ParseLink(l string) (GemtextObject, error) { if !strings.HasPrefix(l, "=>") { return GemtextObject{}, fmt.Errorf("Not a gemtext link!") } else { f := strings.Fields(l[2:]) link := GemtextObject{Type: LINK, Literal: l} link.Path = f[0] link.Text = strings.TrimSpace(l[3+len(f[0]):]) // text that remains after removing the URL and link marker return link, nil } } // ParseHeading parses a gemtext heading, returns a HEADING GemtextObject if it is between levels 1-3 and TEXT otherwise. func ParseHeading(l string) (GemtextObject, error) { switch { case strings.HasPrefix(l, "####"): // Fake headings return GemtextObject{Type: TEXT, Text: l, Literal: l}, nil case strings.HasPrefix(l, "###"): return GemtextObject{Type: HEADING, Level: 3, Text: strings.TrimSpace(l[3:]), Literal: l}, nil case strings.HasPrefix(l, "##"): return GemtextObject{Type: HEADING, Level: 2, Text: strings.TrimSpace(l[2:]), Literal: l}, nil case strings.HasPrefix(l, "#"): return GemtextObject{Type: HEADING, Level: 1, Text: strings.TrimSpace(l[1:]), Literal: l}, nil } return GemtextObject{Type: TEXT, Text: l, Literal: l}, nil } // ParseLine parses a gemtext line, and returns a GemtextObject representing it. // "isPreformatted" takes a true if the line is in a preformatted block, else otherwise. func ParseLine(line string, isPreformatted bool) (GemtextObject, error) { if !isPreformatted { switch { case strings.HasPrefix(line, "=>"): return ParseLink(line) case strings.HasPrefix(line, "```"): return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: line, Text: line[3:]}, nil case strings.HasPrefix(line, "#"): return ParseHeading(line) case strings.HasPrefix(line, ">"): return GemtextObject{Type: QUOTE, Text: strings.TrimSpace(line[1:]), Literal: line}, nil case strings.HasPrefix(line, "* "): // Bullets require a space in the spec return GemtextObject{Type: LIST, Text: line[2:], Literal: line}, nil } return GemtextObject{Type: TEXT, Text: line, Literal: line}, nil } else { if strings.HasPrefix(line, "```") { // toggles are the only special line type in preformatted mode return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: line, Text: line[3:]}, nil } return GemtextObject{Type: PREFORMATTED_TEXT, Text: line, Literal: line}, nil } } // ParsePage takes a string containing the contents of a gemtext page and returns a GemtextPage. func ParsePage(p string) (GemtextPage, error) { preformatted := false lines := strings.Split(p, "\n") var page GemtextPage for _, line := range lines { l, err := ParseLine(line, preformatted) page = append(page, l) if err != nil { return GemtextPage{}, err } if l.Type == PREFORMATTED_TOGGLE { preformatted = !preformatted } } return page, nil }