Merge pull request #3783 from vitorpamplona/claude/highlight-excess-spaces-0678he

Trim highlight context to bounded window, collapse whitespace
This commit is contained in:
Vitor Pamplona
2026-07-28 17:25:17 -04:00
committed by GitHub
4 changed files with 171 additions and 7 deletions
@@ -131,6 +131,13 @@ class HighlightEvent(
* NIP-84 `context` tag and falling back to reconstructing it from a W3C
* `textquoteselector`'s prefix/suffix (as web highlighter clients emit) so the
* in-context rendering still works when no `context` tag is present.
*
* The prefix/suffix are scraped from a web page, so they carry the page's
* block-boundary whitespace — runs of newlines and spaces between DOM nodes — which
* would otherwise render as a stack of blank lines around the highlight. Each run is
* collapsed to a single space so the surrounding context reads as one continuous
* passage; the highlight's own [content] is left verbatim so its offsets inside the
* reconstructed context stay exact for the in-context marker.
*/
fun contextOrReconstructed(): String? {
context()?.let { return it }
@@ -138,7 +145,10 @@ class HighlightEvent(
val selector = textQuoteSelector() ?: return null
if (selector.prefix == null && selector.suffix == null) return null
return (selector.prefix ?: "") + content + (selector.suffix ?: "")
val prefix = selector.prefix?.replace(WHITESPACE_RUN, " ")?.trimStart() ?: ""
val suffix = selector.suffix?.replace(WHITESPACE_RUN, " ")?.trimEnd() ?: ""
return prefix + content + suffix
}
fun inPost() = firstTaggedATag()
@@ -150,6 +160,9 @@ class HighlightEvent(
companion object {
const val KIND = 9802
/** Any run of whitespace (spaces, tabs, newlines) — collapsed to a single space. */
private val WHITESPACE_RUN = Regex("\\s+")
suspend fun create(
msg: String,
signer: NostrSigner,
@@ -64,12 +64,39 @@ class HighlightEventTest {
@Test
fun reconstructsContextFromSelectorWhenNoContextTag() {
// The suffix's leading "\n\n" (a page block boundary) is collapsed to a single space so
// it doesn't render as blank lines; the quote's own content is left verbatim.
assertEquals(
"Your food is prechewed for you. The caged tiger prefers a pot of meat slop to an antelope they have to chase.\n\nAnd its not like theres anyw",
"Your food is prechewed for you. The caged tiger prefers a pot of meat slop to an antelope they have to chase. And its not like theres anyw",
webHighlight.contextOrReconstructed(),
)
}
@Test
fun collapsesRunsOfWhitespaceScrapedFromThePage() {
// A real highlight whose prefix carries five newlines between two paragraphs of the
// source page. Without collapsing, the reconstructed context renders a stack of blank
// lines above the marked quote.
val excessWhitespace =
HighlightEvent(
id = "fc2366a5ac54de837842492e525f8f5d141d4a9bba5b1238e135adaf4225763f",
pubKey = "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
createdAt = 1785270682,
tags =
arrayOf(
arrayOf("r", "https://geohot.github.io//blog/jekyll/update/2026/06/06/our-great-war.html"),
arrayOf("textquoteselector", "-", "n way, the better.\n\n\n\n\nHowever, ", ". A single totalizing control sy"),
),
content = "it will end badly for everyone if the systems of comfort prevent structural exit for the people who dont want it",
sig = "0428ad8aef2a12f36a4dc86105f8b429a4b4161f1fa72b08414fb2dd6c1a2276838b167682cb49ab94aa5c4e1d6351bbec0b7906a4b5e6cec1ab64a9d4c63d9d",
)
assertEquals(
"n way, the better. However, it will end badly for everyone if the systems of comfort prevent structural exit for the people who dont want it. A single totalizing control sy",
excessWhitespace.contextOrReconstructed(),
)
}
@Test
fun prefersExplicitContextTagOverSelectorReconstruction() {
val withContext =