@ 261101.7 · fetching

261023.3 · 2748f9bb997af01b8c7e62bce2571bc4da28614e

Wiki links

A doc renders [[name]] as a link to name.md in its own directory, and [[sub/name]] to sub/name.md, as a relative link resolves. A name with an extension keeps it. [[name|label]] shows the label and [[name#heading]] opens at the heading, by the id goldmark gives it.

The parser runs before goldmark's link parser and leaves anything that is not a wiki link to it, so [[x]](y) stays a link with the text [x]. Its links are plain links in the tree, so a missing target is marked broken and backlinks count them.

reading declarations

Changes to wikilink.go. All files

wikilink.go

new file mode 100644@@ -0,0 +1,89 @@+package main++import (+	"bytes"+	"net/url"+	"path"++	"github.com/yuin/goldmark/ast"+	"github.com/yuin/goldmark/parser"+	"github.com/yuin/goldmark/text"+	"github.com/yuin/goldmark/util"+)++// wikiLinkParser reads [[name]], [[name|label]] and [[name#heading]] as+// links, resolved as relative links are: name.md in the doc's directory,+// or the name as written when it has an extension. It runs before+// goldmark's link parser, which also starts at '[', and leaves anything+// else to it, so the links it makes are marked broken and counted as+// backlinks like any other.+type wikiLinkParser struct{}++func (wikiLinkParser) Trigger() []byte { return []byte{'['} }++func (wikiLinkParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {+	line, seg := block.PeekLine()+	if len(line) < 5 || line[1] != '[' {+		return nil+	}+	end := bytes.Index(line[2:], []byte("]]"))+	if end <= 0 {+		return nil+	}+	inner := line[2 : 2+end]+	// [[x]](y) is a link whose text is [x].+	if bytes.ContainsAny(inner, "[]\n") || bytes.HasPrefix(line[2+end+2:], []byte("(")) {+		return nil+	}+	target, label, labelled := bytes.Cut(inner, []byte("|"))+	name, heading, _ := bytes.Cut(target, []byte("#"))+	name, heading = bytes.TrimSpace(name), bytes.TrimSpace(heading)+	if len(name) == 0 && len(heading) == 0 {+		return nil+	}+	var dest string+	if len(name) > 0 {+		n := string(name)+		if path.Ext(n) == "" {+			n += ".md"+		}+		dest = (&url.URL{Path: n}).String()+	}+	if len(heading) > 0 {+		dest += "#" + headingID(heading)+	}+	link := ast.NewLink()+	link.Destination = []byte(dest)+	start, stop := seg.Start+2, seg.Start+2+len(target)+	if labelled {+		start, stop = stop+1, stop+1+len(label)+	}+	link.AppendChild(link, ast.NewTextSegment(text.NewSegment(start, stop)))+	block.Advance(2 + end + 2)+	return link+}++// headingID is the id goldmark gives a heading with this text, before+// any suffix that tells repeated headings apart.+func headingID(s []byte) string {+	var out []byte+	for i := 0; i < len(s); {+		c := s[i]+		n := util.UTF8Len(c)+		i += int(n)+		switch {+		case n != 1:+		case util.IsAlphaNumeric(c):+			if 'A' <= c && c <= 'Z' {+				c += 'a' - 'A'+			}+			out = append(out, c)+		case util.IsSpace(c) || c == '-' || c == '_':+			out = append(out, '-')+		}+	}+	if len(out) == 0 {+		return "heading"+	}+	return string(out)+}