From af9a5d48726d7138174cb941bb5ff8f61030536d Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 24 Jun 2019 08:07:36 +0200 Subject: [PATCH] Render tweet quotes --- public/style.css | 68 ++++++++++++++++++++++++++++++++++++++++++++- src/formatters.nim | 7 +++-- src/parser.nim | 27 +++++++++++------- src/parserutils.nim | 13 +++++++-- src/types.nim | 5 ++-- src/views/tweet.nim | 26 +++++++++++++++++ 6 files changed, 128 insertions(+), 18 deletions(-) diff --git a/public/style.css b/public/style.css index a7ea7ee..5105315 100644 --- a/public/style.css +++ b/public/style.css @@ -3,6 +3,7 @@ body { color: #f8f8f2; margin: 0; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; } #tweets { @@ -575,6 +576,11 @@ nav { font-weight: bold; } +video { + height: 100%; + width: 100%; +} + .video-overlay { width: 100%; height: 100%; @@ -594,7 +600,67 @@ nav { 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%; 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; } diff --git a/src/formatters.nim b/src/formatters.nim index 046d1df..0c1f16d 100644 --- a/src/formatters.nim +++ b/src/formatters.nim @@ -21,7 +21,7 @@ proc toLink*(url, text: string; class="timeline-link"): string = proc reUrlToLink*(m: RegexMatch; s: string): string = let url = s[m.group(0)[0]] - toLink(url, " " & shortLink(url)) + toLink(url, shortLink(url)) proc reEmailToLink*(m: RegexMatch; s: string): string = let url = s[m.group(0)[0]] @@ -44,12 +44,13 @@ proc reUsernameToLink*(m: RegexMatch; s: string): string = pretext & toLink("/" & username, "@" & username) proc linkifyText*(text: string): string = - result = text.replace("\n", "
") + result = text.strip() + result = result.replace("\n", "
") result = result.replace(ellipsisRegex, "") result = result.replace(usernameRegex, reUsernameToLink) result = result.replace(emailRegex, reEmailToLink) result = result.replace(urlRegex, reUrlToLink) - result = result.replace(re"\s+", " ") + result = result.replace(re"([A-z0-9])", "$1 ") result = result.replace(re" ([.,\)])", "$1") proc stripTwitterUrls*(text: string): string = diff --git a/src/parser.nim b/src/parser.nim index 0e0cd38..585334e 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -1,7 +1,7 @@ import xmltree, sequtils, strtabs, strutils, strformat, json import nimquery -import ./types, ./parserutils +import ./types, ./parserutils, ./formatters proc parsePopupProfile*(node: XmlNode): Profile = let profile = node.querySelector(".profile-card") @@ -16,6 +16,7 @@ proc parsePopupProfile*(node: XmlNode): Profile = protected: isProtected(profile), banner: getBanner(profile) ) + result.getPopupStats(profile) proc parseIntentProfile*(profile: XmlNode): Profile = @@ -28,6 +29,7 @@ proc parseIntentProfile*(profile: XmlNode): Profile = protected: not profile.querySelector("li.protected").isNil, banner: getBanner(profile) ) + result.getIntentStats(profile) proc parseTweetProfile*(profile: XmlNode): Profile = @@ -38,20 +40,21 @@ proc parseTweetProfile*(profile: XmlNode): Profile = verified: isVerified(profile) ) -proc parseQuote*(tweet: XmlNode): Tweet = - let tweet = tweet.querySelector(".QuoteTweet-innerContainer") - result = Tweet( - id: tweet.getAttr("data-item-id"), - link: tweet.getAttr("href"), - text: tweet.selectText(".QuoteTweet-text") +proc parseQuote*(quote: XmlNode): Quote = + result = Quote( + id: quote.getAttr("data-item-id"), + link: quote.getAttr("href"), + text: quote.selectText(".QuoteTweet-text").stripTwitterUrls() ) result.profile = Profile( - fullname: tweet.getAttr("data-screen-name"), - username: tweet.selectText(".QuteTweet-fullname"), - verified: isVerified(tweet) + fullname: quote.selectText(".QuoteTweet-fullname"), + username: quote.getAttr("data-screen-name"), + verified: isVerified(quote) ) + result.getQuoteMedia(quote) + proc parseTweet*(tweet: XmlNode): Tweet = result = Tweet( id: tweet.getAttr("data-item-id"), @@ -71,6 +74,10 @@ proc parseTweet*(tweet: XmlNode): Tweet = result.retweetBy = some(by) 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 = if node.isNil: return node.querySelectorAll(".tweet").map(parseTweet) diff --git a/src/parserutils.nim b/src/parserutils.nim index 7fe11a5..c1bfb67 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -34,10 +34,10 @@ proc getUsername*(profile: XmlNode; selector: string): string = proc getTweetText*(tweet: XmlNode): string = let selector = ".tweet-text > a.twitter-timeline-link.u-hidden" 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: - text = text.replace(link, " " & link) + text = text.replace(link, "") stripTwitterUrls(text) @@ -114,3 +114,12 @@ proc getTweetMedia*(tweet: Tweet; node: XmlNode) = tweet.gif = some(getGif(player.querySelector(".PlayableMedia-player"))) elif "video" in player.getAttr("class"): 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()) diff --git a/src/types.nim b/src/types.nim index cca8e8d..85a0b0b 100644 --- a/src/types.nim +++ b/src/types.nim @@ -43,12 +43,13 @@ type url*: string thumb*: string - Quote* = ref object + Quote* = object id*: string profile*: Profile link*: string text*: string - video*: Option[Video] + thumb*: Option[string] + badge*: Option[string] Tweet* = ref object id*: string diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 8f22e37..c5c2461 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -29,6 +29,30 @@ #end proc # +#proc renderQuote(quote: Quote): string = +#let hasMedia = quote.thumb.isSome() +
+
+ + #if hasMedia: +
+
+ + #if quote.badge.isSome: +
${quote.badge.get()}
+ #end if +
+
+ #end if +
+ ${linkUser(quote.profile, "b", class="username", username=false)} + ${linkUser(quote.profile, "span", class="account-name")} +
+
${linkifyText(xmltree.escape(quote.text))}
+
+
+#end proc +# #proc renderMediaGroup(tweet: Tweet): string = #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: "" @@ -105,6 +129,8 @@ ${renderVideo(tweet.video.get())} #elif tweet.gif.isSome: ${renderGif(tweet.gif.get())} + #elif tweet.quote.isSome: + ${renderQuote(tweet.quote.get())} #end if ${renderStats(tweet)}