Skip to content
quarry Artifact [5ddb2964c3]
Login

Artifact [5ddb2964c3]

Artifact 5ddb2964c3dc8f51d79439276b4c5fac9f697388101e3d87f81e380c539010c7:


// attach.go resolves `attach:NAME` references — a file attached to the
// same object the body belongs to (RFC 0016 §Reading). The reference is
// by NAME and not by hash, because the name is what an author knows and
// what a fossil client writing the same body would use.
//
// The seam is the Resolver, as every other reference is: markup has no
// idea which object it is rendering, and the scheme never survives to
// the page — resolved it becomes a quarry route, unresolved it becomes
// the text the author wrote, exactly as a dead [[wiki]] link does.
package markup

import (
	gohtml "html"
	"net/url"
	"regexp"
	"strings"
)

// attachScheme is the reference prefix, spelled once.
const attachScheme = "attach:"

var (
	attachAnchor = regexp.MustCompile(`<a href="attach:([^"]*)"([^>]*)>(.*?)</a>`)
	attachImage  = regexp.MustCompile(`<img([^>]*\s)?src="attach:([^"]*)"([^>]*)>`)
	altAttr      = regexp.MustCompile(`\balt="([^"]*)"`)
)

// attachName recovers the filename an author wrote from a rendered
// attribute: goldmark percent-encodes a link destination and the
// sanitizer escapes the attribute, so both layers come off before the
// name is compared against what the index holds.
func attachName(attr string) string {
	name := gohtml.UnescapeString(attr)
	if un, err := url.PathUnescape(name); err == nil {
		name = un
	}
	return name
}

// rewriteAttachments replaces every attach: reference in rendered HTML.
// It runs BEFORE rewriteFossilHrefs so the quarry routes it mints are
// already in that pass's skip set, and it runs even when r is nil, since
// an unresolvable scheme on the page is worse than no link at all.
func rewriteAttachments(html []byte, r Resolver) []byte {
	html = attachImage.ReplaceAllFunc(html, func(m []byte) []byte {
		sub := attachImage.FindSubmatch(m)
		pre, name, post := string(sub[1]), attachName(string(sub[2])), string(sub[3])
		if u := resolveAttach(r, name, true); u != "" {
			return []byte(`<img` + pre + `src="` + escapeAttr(u) + `"` + post + `>`)
		}
		// An image nobody can serve is its own caption. The alt text is
		// what the author meant a reader to get; the filename is the
		// fallback when they wrote none.
		if alt := altAttr.FindStringSubmatch(pre + post); alt != nil && strings.TrimSpace(alt[1]) != "" {
			return []byte(alt[1])
		}
		return []byte(escapeLabel(name))
	})
	return attachAnchor.ReplaceAllFunc(html, func(m []byte) []byte {
		sub := attachAnchor.FindSubmatch(m)
		name, rest, label := attachName(string(sub[1])), string(sub[2]), string(sub[3])
		if u := resolveAttach(r, name, false); u != "" {
			return []byte(`<a href="` + escapeAttr(u) + `"` + rest + `>` + label + `</a>`)
		}
		return []byte(label)
	})
}

// resolveAttach asks the seam, tolerating a resolver that is not there.
func resolveAttach(r Resolver, name string, image bool) string {
	if r == nil || name == "" {
		return ""
	}
	return r.ResolveAttachment(name, image)
}