Skip to content
quarry Artifact [dde5b5bca5]
Login

Artifact [dde5b5bca5]

Artifact dde5b5bca590993d598ae438d63b29a32fe144f13699b612090172b1814cc954:


// refpop.go is the preview half of RFC 0011 §14: the shared popover's
// card fragments, and the two chat rows — the reply-quote and the
// backlink line — that are references without being prose.
//
// A reference anchor carries one attribute — data-ref, the canonical
// token — wherever it is minted. The behaviour is armed once, on the
// layout's container, and reaches every anchor by delegation, so every
// surface previews the same way without any surface paying per link.
package web

import (
	"fmt"
	"html/template"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

// 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"

// refAttr is the whole per-anchor cost of a preview: the canonical
// token, and nothing else. The rows this file draws — the reply-quote
// and the backlink line — are references without being prose, so they
// carry the same one attribute a reference in prose does and are armed
// by the same delegated handlers.
func refAttr(token string) string {
	if token == "" {
		return ""
	}
	return fmt.Sprintf(` data-ref="%s"`, template.HTMLEscapeString(token))
}

// The preview is armed ONCE, on the layout's <body>, and reaches every
// reference on the page by delegation. Arming each anchor instead put
// three near-identical 380-character expressions on every one of them:
// a chat log with two hundred references shipped a fifth of a megabyte
// of duplicated script, which is RFC 0011 Principle 4 — the page pays
// for what it uses — read backwards.
//
// Nothing author-controlled is interpolated here. The expression reads
// the token off `dataset` at event time and encodes it; the token never
// touches the attribute.
//
// refPick resolves a delegated event to the armed anchor it reached and
// the fragment URL for it, or gives up. The repository is the CURRENT
// page's, stated on <body>: a cross-repository token carries its own
// `repo:` prefix inside data-ref, and the fragment route reads that
// against the page it was asked from.
const refPick = `const t=evt.target;if(!t.closest)return;` +
	`const a=t.closest('a[data-ref]');if(!a)return;` +
	`const q=document.body.dataset.refRepo;if(!q)return;` +
	`const u='/r/'+encodeURIComponent(q)+'/ref.frag?ref='+encodeURIComponent(a.dataset.ref);`

// refOpen positions the card AT the link that summoned 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 anchor positioning is
// unknown, so the fallback never overrides the real thing. The probe
// names `anchor-name`, the SAME property the stylesheet's @supports
// gates the anchored rule on: two properties would let a browser that
// implements one and not the other take the CSS path with the JS
// fallback's coordinates, or neither. `refpop` is
// the popover's id reached as the named global every browser defines
// for it; the try/catch covers a body rendered somewhere with no
// popover to open at all.
const refOpen = `try{let p=refpop;if(p.qa)p.qa.style.anchorName='';p.qa=a;` +
	`if(CSS.supports('anchor-name','` + refAnchorName + `')){a.style.anchorName='` + refAnchorName + `'}` +
	`else{let b=a.getBoundingClientRect();` +
	`p.style.left=Math.max(4,Math.min(b.left,innerWidth-340))+'px';` +
	`p.style.top=(b.bottom+6)+'px'}` +
	`p.showPopover()}catch(e){}`

// Hover and focus preview on a pointer that can hover; the tap path is
// for everything else. Click cancels the navigation only there — the
// modifier unconditional 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.
const refHover = refPick + `if(!matchMedia('(hover: hover)').matches)return;@get(u);` + refOpen
const refTap = refPick + `if(matchMedia('(hover: hover)').matches)return;` +
	`evt.preventDefault();@get(u);` + refOpen

// refDelegation is the layout attribute run: three handlers for the
// whole page, whatever it holds.
const refDelegation = ` data-on:mouseover__debounce.300ms="` + refHover + `"` +
	` data-on:focusin="` + refHover + `"` +
	` data-on:click="` + refTap + `"`

// refCard is ref.resolve's answer, as the page reads it.
type refCard struct {
	Kind    string `json:"kind"`
	ID      string `json:"id"`
	Repo    string `json:"repo"`
	Title   string `json:"title"`
	Summary string `json:"summary"`
	State   string `json:"state"`
	Href    string `json:"href"`
	Expired bool   `json:"expired"`
	Author  string `json:"author"`
	At      string `json:"at"`
}

// refCardHTML draws one card: what it is, one to three facts, and the
// way in. Every kind uses the same three slots rather than seven
// bespoke layouts — a reader glancing at a preview is reading a shape,
// not a form, and seven shapes is six too many.
func refCardHTML(c refCard) template.HTML {
	var b strings.Builder
	fmt.Fprintf(&b, `<div class="refcard" data-kind="%s"`, template.HTMLEscapeString(c.Kind))
	if c.Expired {
		b.WriteString(` data-expired`)
	}
	if c.State != "" {
		fmt.Fprintf(&b, ` data-state="%s"`, template.HTMLEscapeString(c.State))
	}
	b.WriteString(">")
	if c.Kind == "person" {
		b.WriteString(string(identicon(c.Title)))
	}
	if c.Title != "" {
		fmt.Fprintf(&b, `<b class="refcard-title">%s</b>`, template.HTMLEscapeString(c.Title))
	}
	if c.State != "" {
		fmt.Fprintf(&b, ` <span class="refcard-state">%s</span>`, template.HTMLEscapeString(c.State))
	}
	if c.Summary != "" {
		fmt.Fprintf(&b, `<br><span class="refcard-sum">%s</span>`, template.HTMLEscapeString(c.Summary))
	}
	if c.Author != "" || c.At != "" {
		b.WriteString(`<br><small class="tl-meta-inline">`)
		if c.Author != "" {
			b.WriteString(template.HTMLEscapeString(c.Author))
		}
		if c.At != "" {
			b.WriteString(" " + string(ago(c.At)))
		}
		b.WriteString(`</small>`)
	}
	if c.Expired {
		b.WriteString(`<br><span class="refcard-sum">aged out of the chat window</span>`)
	}
	fmt.Fprintf(&b, `<br><a class="refcard-open" href="%s">open</a></div>`,
		template.HTMLEscapeString(c.Href))
	return template.HTML(b.String())
}

// refPopHTML wraps a card as the shared popover's whole content: the
// fragment REPLACES #refpop, so the popover attribute has to come back
// with it or the element stops being a popover after the first preview.
func refPopHTML(inner template.HTML) string {
	return `<div id="refpop" popover>` + string(inner) + `</div>`
}

// chatQuoteHTML is the reply-quote block: who is being answered and the
// first line of what they said, above the reply's own text. It is a
// link to the permalink and carries the same preview hooks a reference
// in prose does, because it IS one.
func chatQuoteHTML(m ChatMessage, repo string) template.HTML {
	if m.Ref == 0 || m.RefUser == "" {
		return ""
	}
	return chatQuoteBlock(m.Ref, m.RefUser, m.RefLine, repo)
}

// chatQuoteBlock is the quote itself, spelled once: a reply's quote and
// the quote an inline #seq earns are the same block, so they cannot
// come to look like two different things.
func chatQuoteBlock(seq uint64, user, line, repo string) template.HTML {
	n := strconv.FormatUint(seq, 10)
	href := "/r/" + url.PathEscape(repo) + "/chat/" + n
	return template.HTML(fmt.Sprintf(
		`<a class="chat-quote" data-quote-seq="%d" href="%s"%s><b>%s</b> · #%d · %s</a>`,
		seq, href, refAttr("#"+n),
		template.HTMLEscapeString(user), seq,
		template.HTMLEscapeString(quoteLine(line))))
}

// quoteLine is the quoted message's first line, bounded: a quote is a
// reminder of what is being answered, not a second copy of it.
func quoteLine(s string) string {
	const limit = 120
	if len(s) <= limit {
		return s
	}
	cut := limit
	for cut > 0 && s[cut]&0xC0 == 0x80 {
		cut--
	}
	return strings.TrimSpace(s[:cut]) + "…"
}

// patchElements writes one Datastar element patch — the same two-line
// SSE frame the caps preview fragment writes, spelled once here so the
// reference fragments and it cannot drift.
func patchElements(w http.ResponseWriter, body string) {
	w.Header().Set("Content-Type", "text/event-stream")
	fmt.Fprint(w, "event: datastar-patch-elements\n")
	for _, line := range strings.Split(body, "\n") {
		fmt.Fprintf(w, "data: elements %s\n", line)
	}
	fmt.Fprint(w, "\n")
}

// trailingMention reads the @handle a composer is part-way through
// typing: the token at the very end of the text, and nothing else. A
// mention finished three words ago is not what the reader is asking to
// complete.
func trailingMention(text string) (string, bool) {
	i := strings.LastIndexAny(text, " \t\n")
	last := text[i+1:]
	if !strings.HasPrefix(last, "@") {
		return "", false
	}
	q := last[1:]
	for _, c := range q {
		if !isHandleRune(c) {
			return "", false
		}
	}
	return q, true
}

func isHandleRune(c rune) bool {
	return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' ||
		c == '.' || c == '_' || c == '-'
}

// chatRefsRow is ref-counting's page half: how often the CURRENT chat
// window named this artifact, and the latest message to land on.
type chatRefsRow struct {
	Count int    `json:"count"`
	Seq   uint64 `json:"seq"`
	At    string `json:"at"`
}

// chatRefsHTML draws the "chat" row that sits beside an artifact's
// durable MentionRef rows. It is visibly its own row and says it is
// window-bounded, because a count that silently shrinks reads as data
// loss to anyone who thought it was an index fact (RFC 0011 §14).
func chatRefsHTML(c chatRefsRow, repo string) template.HTML {
	if c.Count == 0 || c.Seq == 0 {
		return ""
	}
	href := "/r/" + url.PathEscape(repo) + "/chat/" + strconv.FormatUint(c.Seq, 10)
	var b strings.Builder
	fmt.Fprintf(&b, `<p class="chatrefs" data-chat-refs data-count="%d" data-seq="%d">`, c.Count, c.Seq)
	fmt.Fprintf(&b, `<a href="%s"%s>%d chat %s</a>`, href,
		refAttr("#"+strconv.FormatUint(c.Seq, 10)),
		c.Count, map[bool]string{true: "message", false: "messages"}[c.Count == 1])
	if c.At != "" {
		b.WriteString(` <small class="tl-meta-inline">last ` + string(ago(c.At)) + `</small>`)
	}
	b.WriteString(` <small class="tl-meta-inline">· within the chat window</small></p>`)
	return template.HTML(b.String())
}

// chatRefsOf lifts the chat row out of any op answer that carries one,
// so four page handlers share one spelling of "did chat mention this".
func chatRefsOf(out any) chatRefsRow {
	var v struct {
		ChatRefs chatRefsRow `json:"chatRefs"`
	}
	remarshal(out, &v)
	return v.ChatRefs
}