First passage alpha.
Second passage beta.
Third passage gamma.
'; $e1 = '00000000000000000000000000000000000000000000000000000000000000a1'; $e2 = '00000000000000000000000000000000000000000000000000000000000000a2'; $e3 = '00000000000000000000000000000000000000000000000000000000000000a3'; $highlights = [ $this->makeHighlight($e1, 'First passage alpha.', [], 100), $this->makeHighlight($e2, 'Second passage beta.', [], 200), $this->makeHighlight($e3, 'Third passage gamma.', [], 300), ]; $injector = $this->createInjector(); $out = $injector->inject($html, $highlights); $this->assertCount(3, $out['injectedEventIds'], 'Each highlight with a unique matching passage should inject.'); $this->assertHighlightFragmentsPresent($out['html'], [$e1, $e2, $e3]); } public function testSamePassageFromTwoEventsYieldsFragmentIdForEachEventId(): void { $html = 'Shared quote text for two readers.
'; $older = '00000000000000000000000000000000000000000000000000000000000000b1'; $newer = '00000000000000000000000000000000000000000000000000000000000000b2'; $highlights = [ $this->makeHighlight($older, 'Shared quote text for two readers.', [], 10), $this->makeHighlight($newer, 'Shared quote text for two readers.', [], 20), ]; $out = $this->createInjector()->inject($html, $highlights); $this->assertCount(2, $out['injectedEventIds']); $this->assertHighlightFragmentsPresent($out['html'], [$older, $newer]); } public function testContextTagUsedWhenContentIsSubspanOfContext(): void { $html = 'Before the important bit the rest of the sentence.
'; $eid = '00000000000000000000000000000000000000000000000000000000000000c1'; $context = 'Before the important bit the rest of the sentence.'; $content = 'important bit'; $highlights = [ $this->makeHighlight( $eid, $content, [['context', $context]], 100 ), ]; $out = $this->createInjector()->inject($html, $highlights); $this->assertContains($eid, $out['injectedEventIds'], 'NIP-84: highlight `content` as subspan of `context` should still match the body.'); $this->assertHighlightFragmentsPresent($out['html'], [$eid]); } public function testCurlyApostropheAndEllipsisInBodyMatchAsciiNeedleFromEvent(): void { // Landing “highlight” cards use NIP-84 text as stored; the article body is rendered by // CommonMark + SmartPunct, which uses U+2019 (’) and U+2026 (…) in the DOM while clients // often send straight ASCII in 9802 `content`. stringForSearch must fold typography. $apostrophe = "\xE2\x80\x99"; $ellipsis = "\xE2\x80\xA6"; // "Here" + ’ + "s" (SmartPunct), not "it" + ’ + "s" $html = 'Here'.$apostrophe.'s the point'.$ellipsis.'
'; $eid = '00000000000000000000000000000000000000000000000000000000000000d1'; $highlights = [ $this->makeHighlight($eid, "Here's the point...", [], 1), ]; $out = $this->createInjector()->inject($html, $highlights); $this->assertContains($eid, $out['injectedEventIds']); $this->assertHighlightFragmentsPresent($out['html'], [$eid]); } public function testBorisBitcoinIsTimeHighlightWithSoftHyphensInjectsDespiteFootnoteCalloutInBody(): void { // Real kind-9802: `content` uses U+00AD in "informational" (read.withboris / Gigi’s article). // The article body is plain "informational"; footnotes break substring search if we keep text. $content = 'keeping track of things in the infor'."\xC2\xAD".'ma'."\xC2\xAD".'tional realm always implies keeping track of time'; $html = 'keeping track of things in the informational realm' .'1' .' always implies keeping track of time
'; $eid = 'f56a6221e8575b051cd6df34e9b61654e08a241b4c5ced3b48c0b769b24ada7d'; $highlights = [ $this->makeHighlight( $eid, $content, [['a', '30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:bitcoin-is-time']], 1762697677 ), ]; $out = $this->createInjector()->inject($html, $highlights); $this->assertContains($eid, $out['injectedEventIds']); $this->assertHighlightFragmentsPresent($out['html'], [$eid]); } public function testSoftHyphenInBodyMatchesPlainNeedleFromEvent(): void { $softHyphen = "\xC2\xAD"; $html = 'Time and order have a very intimate relation'.$softHyphen.'ship.
'; $eid = '00000000000000000000000000000000000000000000000000000000000000e1'; $highlights = [ $this->makeHighlight($eid, 'Time and order have a very intimate relationship.', [], 1), ]; $out = $this->createInjector()->inject($html, $highlights); $this->assertContains($eid, $out['injectedEventIds']); $this->assertHighlightFragmentsPresent($out['html'], [$eid]); } private function createInjector(): ArticleBodyHighlightInjector { $meta = $this->createMock(HighlightAuthorMetadataProvider::class); $meta->method('getMetadata')->willReturn( (object) [ 'display_name' => 'Test', 'name' => 'Test', 'picture' => '', ] ); return new ArticleBodyHighlightInjector($meta, new NostrKeyHelper()); } /** * @param list