diff --git a/assets/bootstrap.js b/assets/bootstrap.js index 71f0f99..ea1a1c0 100644 --- a/assets/bootstrap.js +++ b/assets/bootstrap.js @@ -2,6 +2,7 @@ import { startStimulusApp } from '@symfony/stimulus-bundle'; import ArticleCommentsController from './controllers/article_comments_controller.js'; import CommentReplyController from './controllers/comment_reply_controller.js'; import CopyTextController from './controllers/copy_text_controller.js'; +import UserHighlightTooltipController from './controllers/user_highlight_tooltip_controller.js'; const app = startStimulusApp(); if (typeof app.debug === 'boolean') { app.debug = false; @@ -23,3 +24,8 @@ try { } catch { /* already registered by the bundle */ } +try { + app.register('user-highlight-tooltip', UserHighlightTooltipController); +} catch { + /* already registered by the bundle */ +} diff --git a/assets/controllers/nostr_preview_controller.js b/assets/controllers/nostr_preview_controller.js index b5b5f24..757ccc6 100644 --- a/assets/controllers/nostr_preview_controller.js +++ b/assets/controllers/nostr_preview_controller.js @@ -3,6 +3,62 @@ import { Controller } from '@hotwired/stimulus'; const LOADING_HTML = `
'.self::markHtml($hi).'
'; } + /** + * For narrow list layouts (e.g. home aside with {@see buildHighlightedBodyHtml} + line-clamp): if the + * `content` is not at the start of the passage, drop the text before the highlight so the + * clamped block begins at (or a few characters before) the mark and the user actually sees + * the highlight. + * + * @param int $includeCharsOfContextBeforeHighlight Extra characters to keep before the + * highlight (0 = passage starts with `content`) + */ + public static function buildHighlightedBodyHtmlForNarrowList( + string $contextQuote, + string $contentField, + int $includeCharsOfContextBeforeHighlight = 0, + ): string { + $q = \trim(self::normalizeLineEndingsForHighlight((string) $contextQuote)); + $hi = \trim(self::normalizeLineEndingsForHighlight((string) $contentField)); + if ($q === '' && $hi === '') { + return ''; + } + if ($q === '' || $hi === '') { + return self::buildHighlightedBodyHtml($q, $hi); + } + if ($q === $hi) { + return self::buildHighlightedBodyHtml($q, $hi); + } + $span = self::findContentSpanInContext($q, $hi); + if (null === $span) { + return self::buildHighlightedBodyHtml($q, $hi); + } + [$st] = $span; + if (0 === $st) { + return self::buildHighlightedBodyHtml($q, $hi); + } + $lead = \max(0, $includeCharsOfContextBeforeHighlight); + $offset = \max(0, $st - $lead); + if (0 === $offset) { + return self::buildHighlightedBodyHtml($q, $hi); + } + $q2 = \mb_substr($q, $offset, null, 'UTF-8'); + if ($q2 === '') { + return self::buildHighlightedBodyHtml($q, $hi); + } + $html = self::buildHighlightedBodyHtml($q2, $hi); + + return self::omittedTextPrefixHtml().$html; + } + + /** + * Safe “earlier text omitted” marker before a truncated passage in list cards. + */ + public static function omittedTextPrefixHtml(): string + { + return ' '; + } + public static function escapeWithNl2br(string $s): string { return \nl2br(\htmlspecialchars($s, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'), false); @@ -341,7 +461,7 @@ final class HighlightEventTags if (null === $row || \count($row) < 2) { continue; } - if (strtolower($row[0]) !== 'textquoteselector') { + if (self::normalizeNostrTagKey($row[0]) !== 'textquoteselector') { continue; } for ($i = 1, $c = \count($row); $i < $c; ++$i) { diff --git a/templates/pages/article.html.twig b/templates/pages/article.html.twig index 737b2ad..9aaeb9a 100644 --- a/templates/pages/article.html.twig +++ b/templates/pages/article.html.twig @@ -59,7 +59,14 @@ {% endblock %} {% block body %} -#}
{# {{ article.content }}#}
{# #}
- {% set article_coordinate = (article.kind ? article.kind.value : 30023) ~ ':' ~ article.pubkey ~ ':' ~ article.slug %}
{% set comments_query = { coordinate: article_coordinate, title: article.title|default('') }|merge(article.eventId ? { e: article.eventId } : {}) %}
{% set _reply_ctx = comments_data.comment_reply_context|default(comment_reply_context|default(null)) %}
{% include 'components/Molecules/ArticleReplyComposer.html.twig' with { comment_reply_context: _reply_ctx } only %}