mirror of
https://github.com/zedeus/nitter
synced 2026-09-05 22:59:31 +00:00
248 lines
7.9 KiB
Nim
248 lines
7.9 KiB
Nim
# SPDX-License-Identifier: AGPL-3.0-only
|
|
import strutils, strformat, tables, unicode, bitops, uri
|
|
import karax/[karaxdsl, vdom]
|
|
|
|
import renderutils, tweet, timeline
|
|
import ".."/[types, utils, formatters]
|
|
|
|
proc renderAtomicParagraph(paragraph: ArticleParagraph; article: Article;
|
|
tweets: Table[int64, Tweet]; path: string;
|
|
prefs: Prefs): VNode =
|
|
if paragraph.entityRanges.len == 0:
|
|
return text ""
|
|
|
|
let er = paragraph.entityRanges[0]
|
|
if er.key notin article.entities:
|
|
return text ""
|
|
|
|
let entity = article.entities[er.key]
|
|
|
|
case entity.kind
|
|
of "MEDIA":
|
|
buildHtml(tdiv(class="article-media")):
|
|
for id in entity.mediaIds:
|
|
let media = article.media.getOrDefault(id)
|
|
if media.url.len == 0:
|
|
continue
|
|
case media.kind
|
|
of "ApiGif":
|
|
video(src=getVidUrl(media.url), controls="", autoplay="", loop="",
|
|
muted="")
|
|
of "ApiVideo":
|
|
video(src=getVidUrl(media.url), controls="")
|
|
else:
|
|
a(href=getOrigPicUrl(media.url), target="_blank"):
|
|
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":
|
|
let tweet = tweets.getOrDefault(
|
|
try: parseBiggestInt(entity.tweetId)
|
|
except ValueError: 0, nil)
|
|
if tweet != nil:
|
|
renderTweet(tweet, prefs, path)
|
|
else:
|
|
text ""
|
|
of "MARKDOWN":
|
|
var content = entity.markdown
|
|
if content.startsWith("```"):
|
|
let firstNl = content.find('\n')
|
|
if firstNl >= 0: content = content[firstNl + 1 .. ^1]
|
|
if content.endsWith("```"): content = content[0 .. ^4]
|
|
content = content.strip
|
|
buildHtml(pre()):
|
|
code(): text content
|
|
of "DIVIDER":
|
|
buildHtml(hr(class="article-divider"))
|
|
else:
|
|
text ""
|
|
|
|
proc wrapStyle(node: VNode; style: int): VNode =
|
|
result = node
|
|
if style.testBit(4): result = buildHtml(code()): result
|
|
if style.testBit(0): result = buildHtml(strong()): result
|
|
if style.testBit(1): result = buildHtml(em()): result
|
|
if style.testBit(2): result = buildHtml(del()): result
|
|
if style.testBit(3): result = buildHtml(underlined()): result
|
|
|
|
proc addContent(target: VNode; content: string; style = 0) =
|
|
var first = true
|
|
for line in content.split('\n'):
|
|
if not first:
|
|
target.add VNode(kind: VNodeKind.br)
|
|
first = false
|
|
var pos = 0
|
|
while pos < line.len:
|
|
let atPos = line.find('@', pos)
|
|
if atPos == -1:
|
|
target.add wrapStyle(text line[pos .. ^1], style)
|
|
break
|
|
if atPos > 0 and line[atPos - 1] in Letters + Digits + {'_'}:
|
|
target.add wrapStyle(text line[pos .. atPos], style)
|
|
pos = atPos + 1
|
|
continue
|
|
var j = atPos + 1
|
|
while j < line.len and j - atPos - 1 < 15 and
|
|
line[j] in Letters + Digits + {'_'}:
|
|
inc j
|
|
if j == atPos + 1:
|
|
target.add wrapStyle(text line[pos .. atPos], style)
|
|
pos = atPos + 1
|
|
continue
|
|
if atPos > pos:
|
|
target.add wrapStyle(text line[pos ..< atPos], style)
|
|
let username = line[atPos + 1 ..< j]
|
|
let link = a.newVNode()
|
|
link.setAttr("href", "/" & username)
|
|
link.add wrapStyle(text ("@" & username), style)
|
|
target.add link
|
|
pos = j
|
|
|
|
proc applyInlineStyles(target: VNode; runes: seq[Rune]; start, length: int;
|
|
styles: seq[ArticleStyle]) =
|
|
if styles.len == 0:
|
|
target.addContent($runes[start ..< start + length])
|
|
return
|
|
|
|
var
|
|
lastStyle = 0
|
|
lastStart = start
|
|
let endPos = start + length
|
|
|
|
for i in start ..< endPos:
|
|
var style = 0
|
|
for sr in styles:
|
|
let
|
|
sStart = sr.offset
|
|
sEnd = sStart + sr.length
|
|
if sStart <= i and sEnd > i:
|
|
case sr.style
|
|
of "Bold": style.setBit(0)
|
|
of "Italic": style.setBit(1)
|
|
of "Strikethrough": style.setBit(2)
|
|
of "Underline": style.setBit(3)
|
|
of "Code": style.setBit(4)
|
|
else: discard
|
|
|
|
if style != lastStyle:
|
|
if i > lastStart:
|
|
addContent(target, $runes[lastStart ..< i], lastStyle)
|
|
lastStyle = style
|
|
lastStart = i
|
|
|
|
if lastStart < endPos:
|
|
addContent(target, $runes[lastStart ..< endPos], lastStyle)
|
|
|
|
proc renderTextParagraph(paragraph: ArticleParagraph; article: Article): VNode =
|
|
let text = paragraph.text
|
|
|
|
result = case paragraph.kind
|
|
of "header-one": h1.newVNode()
|
|
of "header-two": h2.newVNode()
|
|
of "header-three": h3.newVNode()
|
|
of "ordered-list-item", "unordered-list-item": li.newVNode()
|
|
of "blockquote": VNode(kind: VNodeKind.blockquote)
|
|
of "code-block":
|
|
let pre = pre.newVNode()
|
|
let code = code.newVNode()
|
|
code.add text text
|
|
pre.add code
|
|
return pre
|
|
else: p.newVNode()
|
|
|
|
let
|
|
runes = text.toRunes
|
|
textLen = runes.len
|
|
var last = 0
|
|
for er in paragraph.entityRanges:
|
|
if er.offset > last:
|
|
applyInlineStyles(result, runes, last, er.offset - last,
|
|
paragraph.inlineStyles)
|
|
|
|
last = er.offset + er.length
|
|
|
|
var target = result
|
|
if er.key in article.entities:
|
|
let entity = article.entities[er.key]
|
|
if entity.kind == "LINK":
|
|
let parsed = parseUri(entity.url)
|
|
if parsed.scheme in ["http", "https"]:
|
|
target = a.newVNode()
|
|
if parsed.isTwitterUrl:
|
|
target.setAttr("href", parsed.path)
|
|
else:
|
|
target.setAttr("href", entity.url)
|
|
|
|
applyInlineStyles(target, runes, er.offset, er.length,
|
|
paragraph.inlineStyles)
|
|
if target != result:
|
|
result.add target
|
|
|
|
if last < textLen:
|
|
applyInlineStyles(result, runes, last, textLen - last,
|
|
paragraph.inlineStyles)
|
|
|
|
if paragraph.kind == "blockquote" and result.len > 0:
|
|
let lastChild = result[result.len - 1]
|
|
if lastChild.kind == VNodeKind.strong and lastChild.len > 0 and
|
|
lastChild[0].kind == VNodeKind.text:
|
|
lastChild.setAttr("class", "blockquote-attribution")
|
|
|
|
proc renderArticle*(article: Article; tweets: Table[int64, Tweet];
|
|
path: string; prefs: Prefs; tweetId=""): VNode =
|
|
let author = article.user
|
|
|
|
let main = buildHtml(article(class="article-body")):
|
|
h1(class="article-title"): text article.title
|
|
|
|
tdiv(class="article-author"):
|
|
tdiv(class="article-author-row"):
|
|
a(class="article-avatar", href=("/" & author.username)):
|
|
genImg(author.getUserPic("_bigger"), class=prefs.getAvatarClass)
|
|
tdiv(class="article-author-info"):
|
|
tdiv(class="article-author-name"):
|
|
linkUser(author, class="fullname")
|
|
verifiedIcon(author)
|
|
tdiv(class="article-author-meta"):
|
|
linkUser(author, class="username")
|
|
span(class="article-date-sep"): text " · "
|
|
a(class="article-date",
|
|
href=("/" & author.username & "/status/" & tweetId)):
|
|
text article.time.getShortTime
|
|
if not prefs.hideTweetStats:
|
|
renderStats(article.stats)
|
|
|
|
var listKind = ""
|
|
var list: VNode = nil
|
|
|
|
for paragraph in article.paragraphs:
|
|
let isListItem = paragraph.kind in [
|
|
"ordered-list-item", "unordered-list-item"]
|
|
|
|
if not isListItem and list != nil:
|
|
main.add list
|
|
list = nil
|
|
listKind = ""
|
|
|
|
if paragraph.kind == "atomic":
|
|
main.add renderAtomicParagraph(paragraph, article, tweets, path, prefs)
|
|
elif isListItem:
|
|
if paragraph.kind != listKind:
|
|
if list != nil:
|
|
main.add list
|
|
list = if paragraph.kind == "ordered-list-item": ol.newVNode()
|
|
else: ul.newVNode()
|
|
listKind = paragraph.kind
|
|
list.add renderTextParagraph(paragraph, article)
|
|
else:
|
|
main.add renderTextParagraph(paragraph, article)
|
|
|
|
if list != nil:
|
|
main.add list
|
|
|
|
buildHtml(tdiv(class="article-page")):
|
|
if article.coverImage.len > 0:
|
|
a(href=getOrigPicUrl(article.coverImage), target="_blank"):
|
|
img(class="article-cover", src=getSmallPic(article.coverImage), alt="")
|
|
main
|
|
renderToTop()
|