Support captions in articles

Fixes #1418
This commit is contained in:
Zed 2026-07-01 00:49:27 +02:00
commit 7f7092fbf5
7 changed files with 38 additions and 9 deletions

View file

@ -61,6 +61,7 @@ proc parseGraphArticle*(json: string): Article =
of "MEDIA": of "MEDIA":
for mi in entry.value.data.mediaItems: for mi in entry.value.data.mediaItems:
entity.mediaIds.add mi.mediaId entity.mediaIds.add mi.mediaId
entity.caption = entry.value.data.caption
of "TWEET": entity.tweetId = entry.value.data.tweetId of "TWEET": entity.tweetId = entry.value.data.tweetId
of "MARKDOWN": entity.markdown = entry.value.data.markdown of "MARKDOWN": entity.markdown = entry.value.data.markdown
else: discard else: discard

View file

@ -48,6 +48,7 @@ type
mediaItems*: seq[tuple[mediaId: string]] mediaItems*: seq[tuple[mediaId: string]]
tweetId*: string tweetId*: string
markdown*: string markdown*: string
caption*: string
RawMediaEntity* = object RawMediaEntity* = object
mediaId*: string mediaId*: string

View file

@ -189,6 +189,12 @@
max-width: 100%; max-width: 100%;
border-radius: 12px; border-radius: 12px;
} }
.article-media-caption {
color: var(--fg_faded);
font-size: 0.875rem;
margin-top: 6px;
}
} }
> a, > a,

View file

@ -253,6 +253,7 @@ type
mediaIds*: seq[string] mediaIds*: seq[string]
tweetId*: string tweetId*: string
markdown*: string markdown*: string
caption*: string
ArticleMedia* = object ArticleMedia* = object
kind*: string kind*: string

View file

@ -32,7 +32,9 @@ proc renderAtomicParagraph(paragraph: ArticleParagraph; article: Article;
video(src=getVidUrl(media.url), controls="") video(src=getVidUrl(media.url), controls="")
else: else:
a(href=getOrigPicUrl(media.url), target="_blank"): a(href=getOrigPicUrl(media.url), target="_blank"):
img(src=getSmallPic(media.url), alt="", loading="lazy") img(src=getSmallPic(media.url), alt=entity.caption, loading="lazy")
if entity.caption.len > 0:
p(class="article-media-caption"): text entity.caption
of "TWEET": of "TWEET":
let tweet = tweets.getOrDefault( let tweet = tweets.getOrDefault(
try: parseBiggestInt(entity.tweetId) try: parseBiggestInt(entity.tweetId)

View file

@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch" let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
buildHtml(head): buildHtml(head):
link(rel="stylesheet", type="text/css", href="/css/style.css?v=44") link(rel="stylesheet", type="text/css", href="/css/style.css?v=45")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=7") link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=7")
if theme.len > 0: if theme.len > 0:

View file

@ -14,6 +14,7 @@ class ArticleSelectors:
avatar = '.article-author img.avatar' avatar = '.article-author img.avatar'
verified = '.article-author .verified-icon' verified = '.article-author .verified-icon'
media = '.article-media' media = '.article-media'
caption = '.article-media-caption'
divider = '.article-divider' divider = '.article-divider'
@ -30,10 +31,6 @@ articles = [
'Consciousness and AI: The Problem of Inner Experience', 'Consciousness and AI: The Problem of Inner Experience',
'CosmicOrFun', 'Cosmic Orphan'], 'CosmicOrFun', 'Cosmic Orphan'],
['2064696491948777658',
'NC Push for Data Centers + Stablecoin Crypto= Data Centers are defacto BAILOUT OF Fed Reserve System',
'June_12_1776', 'June_12_1776'],
['2064755789391110154', ['2064755789391110154',
'DeFi Markets Update 2026-06-10', 'DeFi Markets Update 2026-06-10',
'SteakhouseFi', 'Steakhouse Financial'], 'SteakhouseFi', 'Steakhouse Financial'],
@ -126,11 +123,11 @@ class ArticleContentTest(BaseTestCase):
self.assertGreater(len(italic), 0) self.assertGreater(len(italic), 0)
def test_article_has_blockquotes(self): def test_article_has_blockquotes(self):
self.open_nitter('i/article/2064696491948777658') self.open_nitter('i/article/2064166507438059759')
self.assert_element_visible('.article-body blockquote') self.assert_element_visible('.article-body blockquote')
def test_article_has_lists(self): def test_article_has_lists(self):
self.open_nitter('i/article/2064696491948777658') self.open_nitter('i/article/2064166507438059759')
self.assert_element_visible('.article-body ul') self.assert_element_visible('.article-body ul')
def test_article_has_emoji_text(self): def test_article_has_emoji_text(self):
@ -186,6 +183,27 @@ class ArticleMediaTest(BaseTestCase):
tweets = self.find_elements('.article-body .timeline-item') tweets = self.find_elements('.article-body .timeline-item')
self.assertGreaterEqual(len(tweets), 3) self.assertGreaterEqual(len(tweets), 3)
def test_media_caption_displayed(self):
self.open_nitter('i/article/2064689664213041529')
self.assert_element_visible(ArticleSelectors.caption)
captions = self.find_elements(ArticleSelectors.caption)
self.assertGreaterEqual(len(captions), 5)
def test_media_caption_text(self):
self.open_nitter('i/article/2064689664213041529')
self.assert_text_visible('FIGURE 1', ArticleSelectors.caption)
def test_media_caption_alt_attribute(self):
self.open_nitter('i/article/2064689664213041529')
img = self.find_element(f'{ArticleSelectors.media} img')
alt = img.get_attribute('alt')
self.assertGreater(len(alt), 0)
def test_no_caption_when_absent(self):
self.open_nitter('i/article/2062858677149675788')
captions = self.find_elements(ArticleSelectors.caption)
self.assertEqual(len(captions), 0)
class ArticleMentionTest(BaseTestCase): class ArticleMentionTest(BaseTestCase):
def test_mention_linkified(self): def test_mention_linkified(self):
@ -209,7 +227,7 @@ class ArticleMentionTest(BaseTestCase):
def test_no_spurious_whitespace_in_styled_paragraph(self): def test_no_spurious_whitespace_in_styled_paragraph(self):
"""Styled paragraphs should not have extra whitespace from VNode serialization.""" """Styled paragraphs should not have extra whitespace from VNode serialization."""
self.open_nitter('i/article/2064696491948777658') self.open_nitter('i/article/2064166507438059759')
source = self.get_page_source() source = self.get_page_source()
self.assertNotIn('white-space: pre-wrap', source) self.assertNotIn('white-space: pre-wrap', source)
self.assertNotIn('white-space:pre-wrap', source) self.assertNotIn('white-space:pre-wrap', source)