// 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},
}
}