Render tweet quotes
This commit is contained in:
parent
1213220ef0
commit
af9a5d4872
6 changed files with 128 additions and 18 deletions
|
@ -3,6 +3,7 @@ body {
|
||||||
color: #f8f8f2;
|
color: #f8f8f2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tweets {
|
#tweets {
|
||||||
|
@ -575,6 +576,11 @@ nav {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.video-overlay {
|
.video-overlay {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -594,7 +600,67 @@ nav {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
video {
|
.quote {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: solid 1px #404040;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote:hover {
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-link {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-text {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-media-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 102px;
|
||||||
|
width: 102px;
|
||||||
|
float: left;
|
||||||
|
margin-right: 7px;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-media {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-media img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-badge {
|
||||||
|
background: rgba(0,0,0,0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
bottom: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #fffffff0;
|
||||||
|
left: 8px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ proc toLink*(url, text: string; class="timeline-link"): string =
|
||||||
|
|
||||||
proc reUrlToLink*(m: RegexMatch; s: string): string =
|
proc reUrlToLink*(m: RegexMatch; s: string): string =
|
||||||
let url = s[m.group(0)[0]]
|
let url = s[m.group(0)[0]]
|
||||||
toLink(url, " " & shortLink(url))
|
toLink(url, shortLink(url))
|
||||||
|
|
||||||
proc reEmailToLink*(m: RegexMatch; s: string): string =
|
proc reEmailToLink*(m: RegexMatch; s: string): string =
|
||||||
let url = s[m.group(0)[0]]
|
let url = s[m.group(0)[0]]
|
||||||
|
@ -44,12 +44,13 @@ proc reUsernameToLink*(m: RegexMatch; s: string): string =
|
||||||
pretext & toLink("/" & username, "@" & username)
|
pretext & toLink("/" & username, "@" & username)
|
||||||
|
|
||||||
proc linkifyText*(text: string): string =
|
proc linkifyText*(text: string): string =
|
||||||
result = text.replace("\n", "<br>")
|
result = text.strip()
|
||||||
|
result = result.replace("\n", "<br>")
|
||||||
result = result.replace(ellipsisRegex, "")
|
result = result.replace(ellipsisRegex, "")
|
||||||
result = result.replace(usernameRegex, reUsernameToLink)
|
result = result.replace(usernameRegex, reUsernameToLink)
|
||||||
result = result.replace(emailRegex, reEmailToLink)
|
result = result.replace(emailRegex, reEmailToLink)
|
||||||
result = result.replace(urlRegex, reUrlToLink)
|
result = result.replace(urlRegex, reUrlToLink)
|
||||||
result = result.replace(re"</a>\s+", "</a> ")
|
result = result.replace(re"([A-z0-9])<a>", "$1 <a>")
|
||||||
result = result.replace(re"</a> ([.,\)])", "</a>$1")
|
result = result.replace(re"</a> ([.,\)])", "</a>$1")
|
||||||
|
|
||||||
proc stripTwitterUrls*(text: string): string =
|
proc stripTwitterUrls*(text: string): string =
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import xmltree, sequtils, strtabs, strutils, strformat, json
|
import xmltree, sequtils, strtabs, strutils, strformat, json
|
||||||
import nimquery
|
import nimquery
|
||||||
|
|
||||||
import ./types, ./parserutils
|
import ./types, ./parserutils, ./formatters
|
||||||
|
|
||||||
proc parsePopupProfile*(node: XmlNode): Profile =
|
proc parsePopupProfile*(node: XmlNode): Profile =
|
||||||
let profile = node.querySelector(".profile-card")
|
let profile = node.querySelector(".profile-card")
|
||||||
|
@ -16,6 +16,7 @@ proc parsePopupProfile*(node: XmlNode): Profile =
|
||||||
protected: isProtected(profile),
|
protected: isProtected(profile),
|
||||||
banner: getBanner(profile)
|
banner: getBanner(profile)
|
||||||
)
|
)
|
||||||
|
|
||||||
result.getPopupStats(profile)
|
result.getPopupStats(profile)
|
||||||
|
|
||||||
proc parseIntentProfile*(profile: XmlNode): Profile =
|
proc parseIntentProfile*(profile: XmlNode): Profile =
|
||||||
|
@ -28,6 +29,7 @@ proc parseIntentProfile*(profile: XmlNode): Profile =
|
||||||
protected: not profile.querySelector("li.protected").isNil,
|
protected: not profile.querySelector("li.protected").isNil,
|
||||||
banner: getBanner(profile)
|
banner: getBanner(profile)
|
||||||
)
|
)
|
||||||
|
|
||||||
result.getIntentStats(profile)
|
result.getIntentStats(profile)
|
||||||
|
|
||||||
proc parseTweetProfile*(profile: XmlNode): Profile =
|
proc parseTweetProfile*(profile: XmlNode): Profile =
|
||||||
|
@ -38,20 +40,21 @@ proc parseTweetProfile*(profile: XmlNode): Profile =
|
||||||
verified: isVerified(profile)
|
verified: isVerified(profile)
|
||||||
)
|
)
|
||||||
|
|
||||||
proc parseQuote*(tweet: XmlNode): Tweet =
|
proc parseQuote*(quote: XmlNode): Quote =
|
||||||
let tweet = tweet.querySelector(".QuoteTweet-innerContainer")
|
result = Quote(
|
||||||
result = Tweet(
|
id: quote.getAttr("data-item-id"),
|
||||||
id: tweet.getAttr("data-item-id"),
|
link: quote.getAttr("href"),
|
||||||
link: tweet.getAttr("href"),
|
text: quote.selectText(".QuoteTweet-text").stripTwitterUrls()
|
||||||
text: tweet.selectText(".QuoteTweet-text")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
result.profile = Profile(
|
result.profile = Profile(
|
||||||
fullname: tweet.getAttr("data-screen-name"),
|
fullname: quote.selectText(".QuoteTweet-fullname"),
|
||||||
username: tweet.selectText(".QuteTweet-fullname"),
|
username: quote.getAttr("data-screen-name"),
|
||||||
verified: isVerified(tweet)
|
verified: isVerified(quote)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
result.getQuoteMedia(quote)
|
||||||
|
|
||||||
proc parseTweet*(tweet: XmlNode): Tweet =
|
proc parseTweet*(tweet: XmlNode): Tweet =
|
||||||
result = Tweet(
|
result = Tweet(
|
||||||
id: tweet.getAttr("data-item-id"),
|
id: tweet.getAttr("data-item-id"),
|
||||||
|
@ -71,6 +74,10 @@ proc parseTweet*(tweet: XmlNode): Tweet =
|
||||||
result.retweetBy = some(by)
|
result.retweetBy = some(by)
|
||||||
result.retweetId = some(tweet.getAttr("data-retweet-id"))
|
result.retweetId = some(tweet.getAttr("data-retweet-id"))
|
||||||
|
|
||||||
|
let quote = tweet.querySelector(".QuoteTweet-innerContainer")
|
||||||
|
if not quote.isNil:
|
||||||
|
result.quote = some(parseQuote(quote))
|
||||||
|
|
||||||
proc parseTweets*(node: XmlNode): Tweets =
|
proc parseTweets*(node: XmlNode): Tweets =
|
||||||
if node.isNil: return
|
if node.isNil: return
|
||||||
node.querySelectorAll(".tweet").map(parseTweet)
|
node.querySelectorAll(".tweet").map(parseTweet)
|
||||||
|
|
|
@ -34,10 +34,10 @@ proc getUsername*(profile: XmlNode; selector: string): string =
|
||||||
proc getTweetText*(tweet: XmlNode): string =
|
proc getTweetText*(tweet: XmlNode): string =
|
||||||
let selector = ".tweet-text > a.twitter-timeline-link.u-hidden"
|
let selector = ".tweet-text > a.twitter-timeline-link.u-hidden"
|
||||||
let link = tweet.selectAttr(selector, "data-expanded-url")
|
let link = tweet.selectAttr(selector, "data-expanded-url")
|
||||||
var text =tweet.selectText(".tweet-text")
|
var text = tweet.selectText(".tweet-text")
|
||||||
|
|
||||||
if link.len > 0 and link in text:
|
if link.len > 0 and link in text:
|
||||||
text = text.replace(link, " " & link)
|
text = text.replace(link, "")
|
||||||
|
|
||||||
stripTwitterUrls(text)
|
stripTwitterUrls(text)
|
||||||
|
|
||||||
|
@ -114,3 +114,12 @@ proc getTweetMedia*(tweet: Tweet; node: XmlNode) =
|
||||||
tweet.gif = some(getGif(player.querySelector(".PlayableMedia-player")))
|
tweet.gif = some(getGif(player.querySelector(".PlayableMedia-player")))
|
||||||
elif "video" in player.getAttr("class"):
|
elif "video" in player.getAttr("class"):
|
||||||
tweet.video = some(Video())
|
tweet.video = some(Video())
|
||||||
|
|
||||||
|
proc getQuoteMedia*(quote: var Quote; node: XmlNode) =
|
||||||
|
let media = node.querySelector(".QuoteMedia")
|
||||||
|
if not media.isNil:
|
||||||
|
quote.thumb = some(media.selectAttr("img", "src"))
|
||||||
|
|
||||||
|
let badge = node.querySelector(".AdaptiveMedia-badgeText")
|
||||||
|
if not badge.isNil:
|
||||||
|
quote.badge = some(badge.innerText())
|
||||||
|
|
|
@ -43,12 +43,13 @@ type
|
||||||
url*: string
|
url*: string
|
||||||
thumb*: string
|
thumb*: string
|
||||||
|
|
||||||
Quote* = ref object
|
Quote* = object
|
||||||
id*: string
|
id*: string
|
||||||
profile*: Profile
|
profile*: Profile
|
||||||
link*: string
|
link*: string
|
||||||
text*: string
|
text*: string
|
||||||
video*: Option[Video]
|
thumb*: Option[string]
|
||||||
|
badge*: Option[string]
|
||||||
|
|
||||||
Tweet* = ref object
|
Tweet* = ref object
|
||||||
id*: string
|
id*: string
|
||||||
|
|
|
@ -29,6 +29,30 @@
|
||||||
</div>
|
</div>
|
||||||
#end proc
|
#end proc
|
||||||
#
|
#
|
||||||
|
#proc renderQuote(quote: Quote): string =
|
||||||
|
#let hasMedia = quote.thumb.isSome()
|
||||||
|
<div class="quote">
|
||||||
|
<div class="quote-container" href="${quote.link}">
|
||||||
|
<a class="quote-link" href="${quote.link}"></a>
|
||||||
|
#if hasMedia:
|
||||||
|
<div class="quote-media-container">
|
||||||
|
<div class="quote-media">
|
||||||
|
<img src=${quote.thumb.get().getSigUrl("pic")}>
|
||||||
|
#if quote.badge.isSome:
|
||||||
|
<div class="quote-badge">${quote.badge.get()}</div>
|
||||||
|
#end if
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
#end if
|
||||||
|
<div class="profile-card-name">
|
||||||
|
${linkUser(quote.profile, "b", class="username", username=false)}
|
||||||
|
${linkUser(quote.profile, "span", class="account-name")}
|
||||||
|
</div>
|
||||||
|
<div class="quote-text">${linkifyText(xmltree.escape(quote.text))}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
#end proc
|
||||||
|
#
|
||||||
#proc renderMediaGroup(tweet: Tweet): string =
|
#proc renderMediaGroup(tweet: Tweet): string =
|
||||||
#let groups = if tweet.photos.len > 2: tweet.photos.distribute(2) else: @[tweet.photos]
|
#let groups = if tweet.photos.len > 2: tweet.photos.distribute(2) else: @[tweet.photos]
|
||||||
#let display = if groups.len == 1 and groups[0].len == 1: "display: table-caption;" else: ""
|
#let display = if groups.len == 1 and groups[0].len == 1: "display: table-caption;" else: ""
|
||||||
|
@ -105,6 +129,8 @@
|
||||||
${renderVideo(tweet.video.get())}
|
${renderVideo(tweet.video.get())}
|
||||||
#elif tweet.gif.isSome:
|
#elif tweet.gif.isSome:
|
||||||
${renderGif(tweet.gif.get())}
|
${renderGif(tweet.gif.get())}
|
||||||
|
#elif tweet.quote.isSome:
|
||||||
|
${renderQuote(tweet.quote.get())}
|
||||||
#end if
|
#end if
|
||||||
${renderStats(tweet)}
|
${renderStats(tweet)}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue