Skip to content
quarry Artifact [0b643774a1]
Login

Artifact [0b643774a1]

Artifact 0b643774a177def4a8feb56e15b65f76a36acc6787a8314150cdbf5c8c6e7efa:


// refhook.go arms a reference anchor with its preview (RFC 0011 §14).
//
// The hooks live HERE, at the one place a reference anchor is minted,
// rather than in a web-layer pass over rendered HTML. A second pass
// only ever sees the anchors it can recognise, which is how forum,
// ticket, landing and wiki bodies ended up previewing differently from
// chat: the same reference, four behaviours. One emission point is one
// behaviour.
//
// Principle 3 still holds: every attribute here rides ON a link that
// already works. With no script the reference navigates to the page it
// names, which is the whole feature.
package markup

import (
	"net/url"
	"strings"
)

// RefHost is an OPTIONAL upgrade on Resolver: the repository whose
// pages serve a reference's preview fragment. A resolver that does not
// know one (a storeless preview render) mints the link and no hooks —
// there is nothing to fetch, and a hook nothing arms is a hook that
// lies.
type RefHost interface {
	RefRepo() string
}

// jsString escapes a value for a JS single-quoted string inside an
// HTML attribute: nothing here may close either quote.
func jsString(s string) string {
	e := url.QueryEscape(s)
	e = strings.ReplaceAll(e, "'", "%27")
	e = strings.ReplaceAll(e, `"`, "%22")
	return e
}

// refAnchorName is the one CSS anchor name the shared popover binds
// to. One name, moved from link to link, because there is one popover:
// a name per reference would be a stylesheet per page.
const refAnchorName = "--qrefpop"

// refOpen positions the card AT the link that opened it and shows it.
//
// CSS anchor positioning is the primary path — the browser keeps the
// card with its anchor through scroll and reflow, which measured
// pixels do not — and the measurement runs only where
// `position-anchor` is unknown, so the fallback never overrides the
// real thing.
//
// It is spelled as tersely as it can be read because it rides on every
// reference anchor on the page, three times over. `refpop` is the
// popover's id reached as the named global every browser defines for
// it; the try/catch covers a body rendered somewhere that has no
// popover to open at all.
const refOpen = `try{let t=evt.currentTarget,p=refpop;` +
	`if(p.qa)p.qa.style.anchorName='';p.qa=t;` +
	`if(CSS.supports('position-anchor','` + refAnchorName + `')){t.style.anchorName='` + refAnchorName + `'}` +
	`else{let r=t.getBoundingClientRect();` +
	`p.style.left=Math.max(4,Math.min(r.left,innerWidth-340))+'px';` +
	`p.style.top=(r.bottom+6)+'px'}` +
	`p.showPopover()}catch(e){}`

// RefPreviewAttrs is the hover/focus/tap enhancement one reference
// anchor carries, as key/value pairs with values unescaped — the
// caller escapes for whichever syntax it is writing into.
//
// Click is bound WITHOUT __prevent and cancels the navigation itself,
// only on a pointer that cannot hover. The modifier is unconditional:
// it would eat a desktop reader's ordinary click — and their
// middle-click and cmd-click with it — to serve a card they already
// get on hover. RFC 0011 §14 asks for the tap path, not for taking the
// click away from everyone.
func RefPreviewAttrs(repo, token string) [][2]string {
	if repo == "" || token == "" {
		return nil
	}
	get := "@get('/r/" + jsString(repo) + "/ref.frag?ref=" + jsString(token) + "')"
	hover := "if(matchMedia('(hover: hover)').matches){" + get + ";" + refOpen + "}"
	tap := "if(!matchMedia('(hover: hover)').matches){evt.preventDefault();" + get + ";" + refOpen + "}"
	return [][2]string{
		{"data-on:mouseenter__debounce.300ms", hover},
		{"data-on:focus", hover},
		{"data-on:click", tap},
	}
}