mirror of https://github.com/zedeus/nitter
197 lines
6.1 KiB
Nim
197 lines
6.1 KiB
Nim
# SPDX-License-Identifier: AGPL-3.0-only
|
|
import strutils, tables, unicode, bitops
|
|
import karax/[karaxdsl, vdom]
|
|
from jester import Request
|
|
|
|
import renderutils, tweet
|
|
import ".."/[types, utils, formatters]
|
|
|
|
const doctype = "<!DOCTYPE html>\n"
|
|
|
|
proc getSmallPic(url: string): string =
|
|
result = url
|
|
if "?" notin url and not url.endsWith("placeholder.png"):
|
|
result &= "?name=small"
|
|
result = getPicUrl(result)
|
|
|
|
proc renderMiniAvatar(user: User; prefs: Prefs): VNode =
|
|
let url = getPicUrl(user.getUserPic("_mini"))
|
|
buildHtml():
|
|
img(class=(prefs.getAvatarClass & " mini"), src=url)
|
|
|
|
proc renderNoteParagraph(articleParagraph: ArticleParagraph; article: Article; tweets: Table[int64, Tweet]; path: string; prefs: Prefs): VNode =
|
|
if articleParagraph.baseType == ArticleType.atomic:
|
|
let er = articleParagraph.entityRanges[0]
|
|
let entity = article.entities[er.key]
|
|
|
|
case entity.entityType
|
|
of ArticleEntityType.media:
|
|
for id in entity.mediaIds:
|
|
let media = article.media.getOrDefault(id)
|
|
if media.url == "":
|
|
discard
|
|
case media.mediaType:
|
|
of ArticleMediaType.image:
|
|
let image = buildHtml(span(class="image")):
|
|
img(src=media.url.getSmallPic, alt="")
|
|
result = image
|
|
of ArticleMediaType.gif:
|
|
let video = buildHtml(span(class="image")):
|
|
video(src=media.url.getVidUrl, controls="", autoplay="", loop="")
|
|
result = video
|
|
else: discard
|
|
of ArticleEntityType.tweet:
|
|
let tweet = tweets.getOrDefault(entity.tweetId.parseInt, nil)
|
|
if tweet == nil: discard
|
|
result = renderTweet(tweet, prefs, path, mainTweet=true)
|
|
else: discard
|
|
else:
|
|
let text = articleParagraph.text
|
|
|
|
case articleParagraph.baseType
|
|
of ArticleType.headerOne:
|
|
result = h1.newVNode()
|
|
of ArticleType.headerTwo:
|
|
result = h2.newVNode()
|
|
of ArticleType.headerThree:
|
|
result = h3.newVNode()
|
|
of ArticleType.orderedListItem:
|
|
result = li.newVNode()
|
|
of ArticleType.unorderedListItem:
|
|
result = li.newVNode()
|
|
of ArticleType.atomic:
|
|
result = nil
|
|
else:
|
|
result = p.newVNode()
|
|
|
|
proc flushPlainText(target: VNode; start: int; len: int): void =
|
|
if articleParagraph.inlineStyleRanges.len == 0:
|
|
target.add verbatim text.runeSubStr(start, len).replaceHashtagsAndMentions
|
|
else:
|
|
|
|
proc flushInternal(start: int, len: int, style: int): void =
|
|
let content = text.runeSubStr(start, len).replaceHashtagsAndMentions
|
|
if style == 0:
|
|
target.add text content
|
|
else:
|
|
let container = span.newVNode()
|
|
container.add text content
|
|
var styleStr = ""
|
|
if style.testBit(0):
|
|
styleStr.add "font-weight:bold;"
|
|
if style.testBit(1):
|
|
styleStr.add "font-style:italic;"
|
|
if style.testBit(2):
|
|
styleStr.add "text-decoration:line-through;"
|
|
container.setAttr("style", styleStr)
|
|
target.add container
|
|
|
|
var
|
|
lastStyle = 0
|
|
lastStart = start
|
|
|
|
for i in start..(start + len):
|
|
var style = 0
|
|
for styleRange in articleParagraph.inlineStyleRanges:
|
|
let
|
|
styleStart = styleRange.offset
|
|
styleEnd = styleStart + styleRange.length
|
|
if styleStart <= i and styleEnd >= i:
|
|
case styleRange.style:
|
|
of ArticleStyle.bold:
|
|
style.setBit(0)
|
|
of ArticleStyle.italic:
|
|
style.setBit(1)
|
|
of ArticleStyle.strikethrough:
|
|
style.setBit(2)
|
|
else: discard
|
|
|
|
if style != lastStyle:
|
|
if i > lastStart:
|
|
flushInternal(lastStart, i - lastStart - 1, lastStyle)
|
|
lastStart = i - 1
|
|
else:
|
|
lastStart = i
|
|
lastStyle = style
|
|
|
|
if lastStart < len:
|
|
flushInternal(lastStart, len - lastStart, lastStyle)
|
|
|
|
var last = 0
|
|
for er in articleParagraph.entityRanges:
|
|
# prevent karax from inserting whitespaces to fix wrapping
|
|
result.add text ""
|
|
|
|
if er.offset > last:
|
|
flushPlainText(result, last, er.offset - last)
|
|
|
|
let entity = article.entities[er.key]
|
|
case entity.entityType
|
|
of ArticleEntityType.link:
|
|
let link = buildHtml(a(href=entity.url)):
|
|
text text.runeSubStr(er.offset, er.length)
|
|
result.add link
|
|
of ArticleEntityType.twemoji:
|
|
let url = entity.twemoji.getSmallPic
|
|
let emoji = buildHtml(img(class="twemoji", src=url, alt=""))
|
|
result.add emoji
|
|
else: discard
|
|
|
|
last = er.offset + er.length
|
|
|
|
# flush remaining text
|
|
if last < text.len:
|
|
flushPlainText(result, last, text.len - last)
|
|
|
|
proc renderNote*(article: Article; tweets: Table[int64, Tweet]; path: string; prefs: Prefs): VNode =
|
|
let cover = getSmallPic(article.coverImage)
|
|
let author = article.user
|
|
|
|
# build header
|
|
let main = buildHtml(article):
|
|
h1: text article.title
|
|
|
|
tdiv(class="author"):
|
|
renderMiniAvatar(author, prefs)
|
|
linkUser(author, class="fullname")
|
|
linkUser(author, class="username")
|
|
text " · "
|
|
text article.time.getShortTime
|
|
|
|
# add paragraphs
|
|
var listType = ArticleType.unknown
|
|
var list: VNode = nil
|
|
|
|
proc flushList() =
|
|
if list != nil:
|
|
main.add list
|
|
list = nil
|
|
listType = ArticleType.unknown
|
|
|
|
for paragraph in article.paragraphs:
|
|
let node = renderNoteParagraph(paragraph, article, tweets, path, prefs)
|
|
|
|
let currentType = paragraph.baseType
|
|
if currentType in [ArticleType.orderedListItem, ArticleType.unorderedListItem]:
|
|
if currentType != listType:
|
|
flushList()
|
|
|
|
case currentType:
|
|
of ArticleType.orderedListItem:
|
|
list = ol.newVNode()
|
|
of ArticleType.unorderedListItem:
|
|
list = ul.newVNode()
|
|
else: discard
|
|
listType = currentType
|
|
list.add node
|
|
else:
|
|
flushList()
|
|
main.add node
|
|
|
|
flushList()
|
|
|
|
buildHtml(tdiv(class="note")):
|
|
img(class="cover", src=(cover), alt="")
|
|
|
|
main
|