mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
fix: collapse scraped whitespace when reconstructing highlight context
A kind:9802 highlight with no `context` tag falls back to reconstructing the surrounding passage from the W3C `textquoteselector` prefix/suffix. Those fragments are scraped from the source web page, so they carry the page's block-boundary whitespace (runs of newlines/spaces between DOM nodes). Glued in verbatim as `prefix + content + suffix`, they render as a stack of blank lines above the marked quote. Collapse each whitespace run in the prefix/suffix to a single space (and trim the outer edges) in `HighlightEvent.contextOrReconstructed()`. The highlight's own `content` is left verbatim so its offsets inside the reconstructed context stay exact for the in-context marker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApuEseGcFjUFqYoLhCuR91
This commit is contained in:
+14
-1
@@ -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,
|
||||
|
||||
+28
-1
@@ -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 it’s not like there’s anyw",
|
||||
"Your food is prechewed for you. The caged tiger prefers a pot of meat slop to an antelope they have to chase. And it’s not like there’s 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 don’t 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 don’t want it. A single totalizing control sy",
|
||||
excessWhitespace.contextOrReconstructed(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun prefersExplicitContextTagOverSelectorReconstruction() {
|
||||
val withContext =
|
||||
|
||||
Reference in New Issue
Block a user