Compare commits

..

29 Commits

Author SHA1 Message Date
956a979204 Added deprecated notices
Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
2023-06-02 14:58:26 +01:00
cfd9c7292c Fixed double dash in tweets
Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
2022-11-05 09:10:37 +00:00
7f14289760 Tweak text sizing slightly
Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
2022-02-21 16:57:27 +00:00
091f6a455e Added caching 2021-09-04 16:41:15 +01:00
d061860fa2 fix: update release task to restart deployment 2021-07-24 11:20:38 +01:00
afc1cee5d7 feat: added opengraph image 2021-07-24 11:19:44 +01:00
749758fca4 feat: support quoted tweets 2021-07-24 10:52:07 +01:00
607f063abc docs: added example tweets 2021-07-24 10:12:22 +01:00
8be9ddc40a fix: tweaked sizes to better fit svg 2021-07-24 10:12:06 +01:00
f188873c40 fix: only update image if textbox populated 2021-07-24 06:03:59 +01:00
651f3683d2 feat: handle tweets from suspended accounts 2021-07-24 06:02:47 +01:00
7b4c6cf3fa Better handling of 3 images 2021-04-09 21:50:09 +01:00
80cabdd365 Better handling of character length and image height 2021-04-09 21:33:42 +01:00
79d7d64e98 Added favicon 2021-03-21 10:11:54 +00:00
4572550f77 Merge branch 'master' of https://git.cluster.fun/AverageMarcus/tweetsvg 2021-03-21 09:45:45 +00:00
d9c60b2613 Added logo to readme 2021-03-21 09:43:48 +00:00
075d6c45df Update 'README.md' 2021-03-21 09:29:25 +00:00
bb3278e29b Switch to develop version of milligram 2021-03-21 09:25:20 +00:00
fa281037fb Remove invalid image from readme 2021-03-21 09:17:31 +00:00
f5a356773a Updated webpage 2021-03-21 09:13:33 +00:00
7ba7f8cb5b Added example image to readme 2021-02-23 05:21:43 +00:00
a4efbb3926 Update readme 2021-02-23 04:33:24 +00:00
a804ef1185 Add index to dockerfile 2021-02-22 19:57:41 +00:00
0af5801d4b Added index page 2021-02-22 19:53:25 +00:00
7fe956435a Set SVG height 2021-02-22 19:29:31 +00:00
7d6428b0da Remove duplicate target attr 2021-02-20 16:29:50 +00:00
79cd2adfd4 Remove encoding 2021-02-20 16:26:52 +00:00
05d82767ad Open links in new tab 2021-02-20 16:26:08 +00:00
882080cc51 fix run task 2021-02-20 16:24:54 +00:00
11 changed files with 559 additions and 64 deletions

View File

@@ -1,16 +1,13 @@
FROM golang:alpine AS builder FROM golang:1.16-alpine AS builder
RUN apk update && apk add --no-cache git && apk add -U --no-cache ca-certificates RUN apk update && apk add --no-cache git && apk add -U --no-cache ca-certificates
WORKDIR /app/ WORKDIR /app/
ADD go.mod go.sum ./ ADD go.mod go.sum ./
RUN go mod download RUN go mod download
ADD . . ADD . .
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o tweetsvg main.go RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o tweetsvg .
FROM scratch FROM scratch
WORKDIR /app/ WORKDIR /app/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/tweetsvg /app/tweetsvg COPY --from=builder /app/tweetsvg /app/tweetsvg
ADD tweet.svg.tmpl .
ENTRYPOINT ["/app/tweetsvg"] ENTRYPOINT ["/app/tweetsvg"]

View File

@@ -39,7 +39,7 @@ docker-publish:
.PHONY: run # Run the application .PHONY: run # Run the application
run: run:
@go run main.go@npm start @go run .
.PHONY: ci # Perform CI specific tasks to perform on a pull request .PHONY: ci # Perform CI specific tasks to perform on a pull request
ci: ci:
@@ -47,7 +47,7 @@ ci:
.PHONY: release # Release the latest version of the application .PHONY: release # Release the latest version of the application
release: release:
@kubectl --namespace tweetsvg set image deployment tweetsvg web=docker.cluster.fun/averagemarcus/tweetsvg:$(SHA) @kubectl --namespace tweetsvg rollout restart deployment tweetsvg
.PHONY: help # Show this list of commands .PHONY: help # Show this list of commands
help: help:

View File

@@ -1,14 +1,26 @@
# TweetSVG # ⚠️ DEPRECATED
# Twitter has killed off its free API so this no longer works
---
![TweetSVG](logo.png)
Generate an SVG for a given Tweet ID Generate an SVG for a given Tweet ID
## Features Available at https://tweet.cluster.fun/
## Install ## Example
```sh
``` ```
<img src="https://tweet.cluster.fun/1363048182020792325" />
```
![](https://tweet.cluster.fun/1363048182020792325)
## Features
* Provide the ID of a tweet and have it render as an SVG for use in an `<img>` tag.
* No JavaScript required
## Building from source ## Building from source
@@ -24,8 +36,6 @@ Standalone:
make build make build
``` ```
## Resources
## Contributing ## Contributing
If you find a bug or have an idea for a new feature please [raise an issue](issues/new) to discuss it. If you find a bug or have an idea for a new feature please [raise an issue](issues/new) to discuss it.

111
chars.go Normal file
View File

@@ -0,0 +1,111 @@
package main
import emoji "github.com/tmdvs/Go-Emoji-Utils"
var charWidths = map[string]float64{
" ": 6,
"0": 14.333328247070312,
"1": 10.583328247070312,
"2": 13.433334350585938,
"3": 14.01666259765625,
"4": 14.333328247070312,
"5": 13.866668701171875,
"6": 14.199996948242188,
"7": 12.966659545898438,
"8": 14.199996948242188,
"9": 14.199996948242188,
"A": 14.949996948242188,
"B": 14.366668701171875,
"C": 16.183334350585938,
"D": 16.050003051757812,
"E": 13.133331298828125,
"F": 12.566665649414062,
"G": 16.666671752929688,
"H": 16.51666259765625,
"I": 5.600006103515625,
"J": 11.816665649414062,
"K": 14.583328247070312,
"L": 12.5,
"M": 19.550003051757812,
"N": 16.51666259765625,
"O": 17.25,
"P": 13.75,
"Q": 17.25,
"R": 14.23333740234375,
"S": 14.083328247070312,
"T": 13.816665649414062,
"U": 16.433334350585938,
"V": 14.949996948242188,
"W": 21.683334350585938,
"X": 15.033340454101562,
"Y": 14.51666259765625,
"Z": 14.616668701171875,
"a": 11.98333740234375,
"b": 13.216659545898438,
"c": 11.98333740234375,
"d": 13.199996948242188,
"e": 12.25,
"f": 7.4499969482421875,
"g": 13.100006103515625,
"h": 12.850006103515625,
"i": 5.133331298828125,
"j": 5.133331298828125,
"k": 11.633331298828125,
"l": 5.133331298828125,
"m": 18.966659545898438,
"n": 12.583328247070312,
"o": 12.683334350585938,
"p": 13.100006103515625,
"q": 13.100006103515625,
"r": 7.5833282470703125,
"s": 11.166671752929688,
"t": 7.4166717529296875,
"u": 12.583328247070312,
"v": 11.583328247070312,
"w": 16.916671752929688,
"x": 11.25,
"y": 11.649993896484375,
"z": 11.26666259765625,
"!": 6.5666656494140625,
"@": 20.550003051757812,
"£": 14.333328247070312,
"#": 14.300003051757812,
"$": 14.333328247070312,
"%": 18.416671752929688,
"^": 9.683334350585938,
"*": 9.683334350585938,
"(": 7.8,
")": 7.8,
"-": 10.300003051757812,
"—": 10.300003051757812,
"_": 9.73333740234375,
"=": 14.333328247070312,
"+": 14.333328247070312,
"[": 7.883331298828125,
"]": 7.883331298828125,
"{": 7.883331298828125,
"}": 7.883331298828125,
";": 6.26666259765625,
":": 6.26666259765625,
"'": 5.26666259765625,
"\"": 9.5,
",": 6.26666259765625,
".": 5,
"/": 7.76666259765625,
"?": 11.649993896484375,
"`": 11.833328247070312,
"”": 9,
}
func getCharWidth(char string) float64 {
width, ok := charWidths[char]
if !ok {
if len(emoji.FindAll(char)) > 0 {
width = 24
} else {
width = 8
}
}
return width
}

6
go.mod
View File

@@ -1,6 +1,6 @@
module tweetsvg module tweetsvg
go 1.15 go 1.16
require ( require (
github.com/ChimeraCoder/anaconda v2.0.0+incompatible github.com/ChimeraCoder/anaconda v2.0.0+incompatible
@@ -9,6 +9,10 @@ require (
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect
github.com/grokify/html-strip-tags-go v0.0.1
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/rivo/uniseg v0.2.0
github.com/tmdvs/Go-Emoji-Utils v1.1.0
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
) )

8
go.sum
View File

@@ -10,8 +10,16 @@ github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKB
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ= github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ=
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE=
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ=
github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0=
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/tmdvs/Go-Emoji-Utils v1.1.0 h1:gtPix7HZPrd49+MNDcuRLvv4xVNxCE5wgjqyuvmbyYg=
github.com/tmdvs/Go-Emoji-Utils v1.1.0/go.mod h1:J82i2WeGn+Kz+T3s5v9+i/OJlvevIVfGZ6qXgqiNWBc=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

191
index.html Normal file
View File

@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TweetSVG</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' style='height: 50px;'%3E%3Cg%3E%3Cpath d='M61 140c-8 0-14-6-14-14V61c0-8 6-14 14-14h65a14 14 0 110 28H75v51c0 8-6 14-14 14z' fill='%23ef71a8'/%3E%3Cpath d='M451 465h-65a14 14 0 110-28h51v-51a14 14 0 1128 0v65c0 8-6 14-14 14z' fill='%23ef71a8'/%3E%3Cpath d='M498 512H14c-8 0-14-6-14-14V14C0 6 6 0 14 0h484c8 0 14 6 14 14v484c0 8-6 14-14 14zM28 484h456V28H28v456z' fill='%23ef71a8'/%3E%3C/g%3E%3Cpath d='M369 266c8-21 12-44 12-65v-1c0-4 2-7 5-9 10-9 26-30 26-30l-40-6c-2 0-12-11-14-12a68 68 0 00-66-14c-22 7-39 25-44 47-2 8-3 17-2 25v2a3 3 0 01-2 1 178 178 0 01-122-65c-2-2-5-2-6 0a68 68 0 0011 82c-5-1-11-3-15-6-3-1-6 1-6 3 0 28 16 52 40 63a70 70 0 01-13-1c-3-1-5 2-4 4 8 24 29 42 53 46-20 14-44 21-69 21h-8c-3 0-5 2-5 4-1 3 0 5 2 6 29 17 61 25 94 25a176 176 0 00138-61' fill='%2371cad1'/%3E%3Cpath d='M196 401c-36 0-70-10-101-28-7-4-11-13-9-21 2-9 10-15 19-15h8c12 0 24-2 35-5a82 82 0 01-33-44c-1-3-1-8 1-11a82 82 0 01-24-59c0-5 3-10 7-14a82 82 0 015-72 18 18 0 0129-2 164 164 0 0099 58l2-15a80 80 0 0179-62 82 82 0 0157 23l7 7 38 6a14 14 0 019 23c-2 2-17 21-29 31 0 23-4 47-13 70a14 14 0 11-26-10c7-20 11-40 11-60v-1c0-8 4-15 10-20l9-9-15-2c-6-1-11-5-21-14l-1-2a53 53 0 00-53-11c-17 6-30 20-34 37-2 7-2 14-2 20a17 17 0 01-18 19 191 191 0 01-120-57 54 54 0 0015 50 14 14 0 01-14 24c4 14 15 26 29 33a14 14 0 01-2 26c8 12 21 20 35 22a14 14 0 015 26c-15 10-32 17-50 21a174 174 0 00129-6c21-10 39-23 54-41a14 14 0 1121 19 190 190 0 01-148 66zm49-211zm127-21zm-261-21zm262-8z' fill='%23ef71a8'/%3E%3C/svg%3E">
<meta property="og:title" content="TweetSVG">
<meta property="og:site_name" content="TweetSVG">
<meta property="og:url" content="https://tweet.cluster.fun">
<meta property="og:description" content="Generate an SVG for a given Tweet ID">
<meta property="og:type" content="website">
<meta property="og:image" content="https://opengraph.cluster.fun/opengraph/?siteTitle=&title=&tags=&image=https%3A%2F%2Fcdn.githubraw.com%2FAverageMarcus%2Ftweetsvg%2Fmaster%2Flogo.png&twitter=Marcus_Noble_&github=AverageMarcus%2Ftweetsvg&website=https%3A%2F%2Ftweet.cluster.fun&bgColor=%23ffffff&fgColor=%23263943">
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@Marcus_Noble_" />
<meta name="twitter:image" content="https://opengraph.cluster.fun/opengraph/?siteTitle=&title=&tags=&image=https%3A%2F%2Fcdn.githubraw.com%2FAverageMarcus%2Ftweetsvg%2Fmaster%2Flogo.png&twitter=Marcus_Noble_&github=AverageMarcus%2Ftweetsvg&website=https%3A%2F%2Ftweet.cluster.fun&bgColor=%23ffffff&fgColor=%23263943">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css">
<link rel="stylesheet" href="https://githubraw.com/AverageMarcus/milligram/master/dist/milligram.min.css">
<style>
body {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
}
img {
max-width: 100%;
}
textarea {
height: 200px;
}
#examples {
align-items: start;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#examples figure {
margin: 2px;
margin-bottom: 50px;
}
figcaption {
text-align: center;
}
.announcement {
font-size: 3em;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1 class="heading-fancy">
TweetSVG
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" style="height: 50px;"><g><path d="M61 140c-8 0-14-6-14-14V61c0-8 6-14 14-14h65a14 14 0 110 28H75v51c0 8-6 14-14 14z" fill="#ef71a8"/><path d="M451 465h-65a14 14 0 110-28h51v-51a14 14 0 1128 0v65c0 8-6 14-14 14z" fill="#ef71a8"/><path d="M498 512H14c-8 0-14-6-14-14V14C0 6 6 0 14 0h484c8 0 14 6 14 14v484c0 8-6 14-14 14zM28 484h456V28H28v456z" fill="#ef71a8"/></g><path d="M369 266c8-21 12-44 12-65v-1c0-4 2-7 5-9 10-9 26-30 26-30l-40-6c-2 0-12-11-14-12a68 68 0 00-66-14c-22 7-39 25-44 47-2 8-3 17-2 25v2a3 3 0 01-2 1 178 178 0 01-122-65c-2-2-5-2-6 0a68 68 0 0011 82c-5-1-11-3-15-6-3-1-6 1-6 3 0 28 16 52 40 63a70 70 0 01-13-1c-3-1-5 2-4 4 8 24 29 42 53 46-20 14-44 21-69 21h-8c-3 0-5 2-5 4-1 3 0 5 2 6 29 17 61 25 94 25a176 176 0 00138-61" fill="#71cad1"/><path d="M196 401c-36 0-70-10-101-28-7-4-11-13-9-21 2-9 10-15 19-15h8c12 0 24-2 35-5a82 82 0 01-33-44c-1-3-1-8 1-11a82 82 0 01-24-59c0-5 3-10 7-14a82 82 0 015-72 18 18 0 0129-2 164 164 0 0099 58l2-15a80 80 0 0179-62 82 82 0 0157 23l7 7 38 6a14 14 0 019 23c-2 2-17 21-29 31 0 23-4 47-13 70a14 14 0 11-26-10c7-20 11-40 11-60v-1c0-8 4-15 10-20l9-9-15-2c-6-1-11-5-21-14l-1-2a53 53 0 00-53-11c-17 6-30 20-34 37-2 7-2 14-2 20a17 17 0 01-18 19 191 191 0 01-120-57 54 54 0 0015 50 14 14 0 01-14 24c4 14 15 26 29 33a14 14 0 01-2 26c8 12 21 20 35 22a14 14 0 015 26c-15 10-32 17-50 21a174 174 0 00129-6c21-10 39-23 54-41a14 14 0 1121 19 190 190 0 01-148 66zm49-211zm127-21zm-261-21zm262-8z" fill="#ef71a8"/></svg>
</h1>
<blockquote>
Generate an SVG for a given Tweet ID
</blockquote>
<p class="announcement">
⚠️ DEPRECATED - Twitter has killed off its free API so this no longer works
</p>
<p>
Enter the URL or ID of a tweet to have an SVG generated for it, no JavaScript required!
</p>
<div class="row">
<form class="column column-80 column-offset-10">
<fieldset>
<label>
Tweet URL:
<input id="tweetURL" type="text" value="https://twitter.com/Marcus_Noble_/status/1363048182020792325" />
</label>
<figure class="text-center">
<img id="exampleImage" src="/1363048182020792325" />
<figcaption>Preview</figcaption>
</figure>
<label>
Image Tag:
<input type="text" id="imageTagExample" readonly />
</label>
<label>
SVG Tag:
<textarea id="svgTagExample" readonly></textarea>
</label>
</fieldset>
</form>
</div>
<div>
<h3>Example Tweets</h3>
<div id="examples">
<figure>
<img src="/1285788484280545280" />
<figcaption>Text with Emoji</figcaption>
</figure>
<figure>
<img src="/1417089885647761409" />
<figcaption>Tweet with 1 image</figcaption>
</figure>
<figure>
<img src="/1400358030869372928" />
<figcaption>Tweet with 2 images</figcaption>
</figure>
<figure>
<img src="/1401103668267556868" />
<figcaption>Tweet with 3 images</figcaption>
</figure>
<figure>
<img src="/1396802436052836360" />
<figcaption>Tweet with 4 images</figcaption>
</figure>
<figure>
<img src="/1370521139684970496" />
<figcaption>Tweet with quoted Tweet</figcaption>
</figure>
<figure>
<img src="/1281166762394791937" />
<figcaption>Tweet with link</figcaption>
</figure>
<figure>
<img src="/1396979104944201741" />
<figcaption>Reply Tweet</figcaption>
</figure>
</div>
</div>
<div>
Source code available on <a href="https://github.com/AverageMarcus/tweetsvg" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://gitlab.com/AverageMarcus/tweetsvg" target="_blank" rel="noopener noreferrer">GitLab</a>, <a href="https://bitbucket.org/AverageMarcus/tweetsvg/" target="_blank" rel="noopener noreferrer">Bitbucket</a> & <a href="https://git.cluster.fun/AverageMarcus/tweetsvg" target="_blank" rel="noopener noreferrer">my own Gitea server</a>.
</div>
</div>
<div class="container">
<div class="row">
<div class="column column-60 column-offset-20">
<footer>
Made with
<svg height="20" class="fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 449.3 449.3" xmlns:xlink="http://www.w3.org/1999/xlink"><title>love</title><g><path d="M0 162.7c1.5-7.7 2.7-15.4 4.5-23A125.5 125.5 0 0132 88a136.3 136.3 0 0162.7-40.6c8.3-2.9 17.7-3.2 26.6-3.7a134 134 0 0155.6 6.6c14.9 5.7 30 11 41 23.6 17-20 36.4-36.3 60-46.4 12-5.2 25.7-6.9 38.7-9.4a79.4 79.4 0 0140.3 3.2 96.4 96.4 0 0143.2 26 209.8 209.8 0 0137.8 55.4 133.2 133.2 0 0111 65.7c-3.2 42.2-21 79-41.5 114.8a431.2 431.2 0 01-47.6 64.3c-19.6 23-39.7 45.7-59.6 68.5-3.7 4.3-7.2 9-11.7 12.4-7.3 5.4-15.9 4.9-23.8 1.5-21.9-9.2-43.8-18.5-65.3-28.5a520.1 520.1 0 01-98-58.7c-28.2-21.5-55.5-44.3-74-75.3a183.8 183.8 0 01-25-61.4c-1-6.2-1.6-12.6-2.4-18.8v-24.5zM138 281l2.5.1c0 2.1.3 4.3 0 6.3l-9 55.5c-.3 1.6 0 4.2 1 5 5.8 4.1 11.8 7.8 17.9 11.8l11-59.7 2.5.3c3.7 21.6-6.3 42-7.6 63.5l19.4 9.6c.5-18 2.2-35 6.9-51.6 1.3 4 1.5 8 1.2 12-.7 11.2-1 22.5-2.6 33.7-1 7.3 1.9 10.6 8 13.3 27 11.7 54.1 23.5 81 35.7 5.5 2.5 8.8 1.5 12.6-2.9 12.5-14.4 25.7-28.1 38-42.7 17.2-20.2 34.5-40.5 50.5-61.7 29.5-39.2 52.7-81.7 61-131 3.6-21.7 2-43-5.6-63.5a176 176 0 00-33.9-54.5 84.8 84.8 0 00-38-26 91.2 91.2 0 00-44.2-2.5c-13 2-25.6 5.1-37.2 12.3a208.6 208.6 0 00-45.9 37c-5.7 6.3-7.8 6.4-15 1.4-5.7-4-11-8.8-17.2-11.9-15.1-7.5-30.7-14.4-48-14.9-13.7-.4-28-2-41.2 1-36 8.4-62.7 30-80.3 62.8a111 111 0 00-11.7 56.6c.3 10 1.4 20 2.2 29.9 7-18.9 11-38.3 19.7-56.3.6 3 .6 6 0 8.7l-14.5 53.2c-.7 2.6-2 5.7-1.2 8 2.7 8.4 6.2 16.6 9.4 24.8 0-20 13.7-63.9 21.8-66.8 0 .6.3 1.1.2 1.6a11936 11936 0 01-17 64c-.3 1.4-1.6 2.6-2.9 4.5l9.7 17a573 573 0 0120-67.3l2.9.7c-1 5-1.5 10-2.8 14.9-4.7 17.2-9.6 34.3-14.2 51.5-.7 2.5-1.5 6-.3 7.8 3.6 5.6 8 10.7 12.4 16.1 1.8-8.6 3.1-16.6 5.2-24.4 3.1-11.5 6.6-22.9 10.2-34.3.8-2.6 2.7-5 4-7.4l2 .8c-.1 1.6-.1 3.4-.5 5l-12.2 47.4c-4.6 17.9-4.3 18.9 7.8 29.4 2.2 1.8 4.5 3.5 7 5.3 3.7-31 12.5-64.7 18.4-68-.3 3.5-.3 6.6-.9 9.6l-12.7 58.8c-.3 1.2-.7 3-.1 3.5 4.7 4.4 9.6 8.6 14.5 12.9 3.5-21.4 7.6-41.7 13.6-61.6 3.4 8.7.6 17-.8 25.2-1.9 11.6-4.5 23.1-6.5 34.7-.4 2.3-.4 6 1 7 4.8 4.2 10.3 7.4 16 11.3 3.9-21.5 5.5-42.6 12.6-62.5z"/><path d="M323.2 180.5a24 24 0 01-24.5-19.7c-2-9.7.7-20.2 15-27 16.6-7.8 38.3 2.3 41.6 19.5 1.2 6.6-1.6 12.1-6 16.3a35.1 35.1 0 01-26 11z"/><path d="M138.9 167.1a31 31 0 0125 12.7 22 22 0 01-13.6 34.9 29.9 29.9 0 01-31-9.9c-6-7-7.6-15.3-4.1-24.1 3.7-9.5 12-12 21-13.5.8-.2 1.8 0 2.7 0z"/><path d="M233.2 202c-18.5-.4-33.7-13.6-34-29.7 0-4.2.4-8 5.4-8.7 4.5-.7 6.6 2.3 8 6 2.3 6.6 5.5 12.4 12.4 15.4 7.5 3.2 14 1.5 20.4-3.3 6.2-4.7 6.4-11 4.7-17.5-1.4-5.2.4-8.4 4.7-10.2 4.3-1.7 9 1.3 10.9 6.2 7.2 19.8-5.7 34.6-22.8 40.2-3 1-6.5 1-9.7 1.6z"/><path d="M201.2 384.7c-.9-2-2.7-4.2-2.5-6.2 1.2-14 2.8-28.1 4.4-42.2 0-.9.7-1.7 1-2.6l2.6 1.1-3.1 49.1-2.4.8z"/><path d="M222 387.4c-4.8-5.7-5-15-1-37.8 3.1 3 4 30.5 1 37.8z"/><path d="M240.9 361.7l-1.4 20.2c-4.8-3.3-4.6-11.9-.2-20.4l1.6.2z"/><path d="M257.4 380.4c-4.1-5.2-3.7-9.7 1-14.7l-1 14.7z"/></g></svg>
by <a href="https://marcusnoble.co.uk" class="fancy-link">Marcus Noble</a>
</footer>
</div>
</div>
</div>
<script>
function loadTweet(tweetID) {
document.getElementById('exampleImage').src = `/${tweetID}`;
document.getElementById('imageTagExample').value = `<img src="https://tweet.cluster.fun/${tweetID}" />`;
fetch(`/${tweetID}`)
.then(res => res.text())
.then(svgSrc => {
document.getElementById('svgTagExample').innerText = svgSrc;
})
}
document.getElementById('tweetURL').addEventListener('change', function(e) {
if (e.target.value.trim() != "") {
let parts = e.target.value.split("/");
let tweetID = parts[parts.length-1];
loadTweet(tweetID);
}
});
document.getElementById('imageTagExample').addEventListener('click', function(e) {
e.target.select();
document.execCommand("copy");
});
document.getElementById('svgTagExample').addEventListener('click', function(e) {
e.target.select();
document.execCommand("copy");
});
// Load example tweet
loadTweet('1363048182020792325')
</script>
</body>
</html>

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

217
main.go
View File

@@ -2,10 +2,12 @@ package main
import ( import (
"bytes" "bytes"
"embed"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"html/template" "html/template"
"log" "log"
"math"
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
@@ -14,9 +16,17 @@ import (
"time" "time"
"github.com/ChimeraCoder/anaconda" "github.com/ChimeraCoder/anaconda"
strip "github.com/grokify/html-strip-tags-go"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/patrickmn/go-cache"
"github.com/rivo/uniseg"
emoji "github.com/tmdvs/Go-Emoji-Utils"
) )
//go:embed index.html tweet.svg.tmpl suspendedTweet.svg
var content embed.FS
var ( var (
api *anaconda.TwitterApi api *anaconda.TwitterApi
@@ -27,6 +37,8 @@ var (
accessTokenSecret string accessTokenSecret string
consumerKey string consumerKey string
consumerSecret string consumerSecret string
ch *cache.Cache
) )
func init() { func init() {
@@ -37,6 +49,8 @@ func init() {
accessTokenSecret = os.Getenv("ACCESS_TOKEN_SECRET") accessTokenSecret = os.Getenv("ACCESS_TOKEN_SECRET")
consumerKey = os.Getenv("CONSUMER_KEY") consumerKey = os.Getenv("CONSUMER_KEY")
consumerSecret = os.Getenv("CONSUMER_SECRET") consumerSecret = os.Getenv("CONSUMER_SECRET")
ch = cache.New(24*time.Hour, 48*time.Hour)
} }
func main() { func main() {
@@ -50,7 +64,14 @@ func main() {
port = "8080" port = "8080"
} }
http.HandleFunc("/", getTweet) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if len(r.URL.Path) > 1 {
getTweet(w, r)
} else {
body, _ := content.ReadFile("index.html")
w.Write(body)
}
})
fmt.Println("Server started at port " + port) fmt.Println("Server started at port " + port)
log.Fatal(http.ListenAndServe(":"+port, nil)) log.Fatal(http.ListenAndServe(":"+port, nil))
} }
@@ -62,34 +83,72 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400) w.WriteHeader(400)
return return
} }
tweet, err := api.GetTweet(i, nil)
result, found := ch.Get(id)
if !found {
fmt.Println("No cached tweet found, generating new...")
tweet, err := api.GetTweet(i, nil)
if err != nil {
switch err := err.(type) {
case *anaconda.ApiError:
switch err.Decoded.Errors[0].Code {
case 63:
fmt.Printf("Generating suspended tweet image for %s\n", id)
suspendedTweet(w)
return
}
}
fmt.Println(err)
w.WriteHeader(404)
return
}
processTweet(&tweet)
result = renderTemplate(tweet, false)
ch.Set(id, result, cache.DefaultExpiration)
}
w.Header().Set("Content-type", "image/svg+xml")
_, err = w.Write(result.([]byte))
if err != nil { if err != nil {
w.WriteHeader(404) fmt.Println(err)
w.WriteHeader(500)
return return
} }
}
re := regexp.MustCompile(`[\x{1F300}-\x{1F6FF}]`) func processTweet(tweet *anaconda.Tweet) {
emojis := re.FindAllString(tweet.FullText, -1) gr := uniseg.NewGraphemes(tweet.FullText)
count := 0
emojiCount := 0 displayText := ""
for _, emoji := range emojis { for gr.Next() {
emojiCount += len([]byte(emoji)) - 1 if count >= tweet.DisplayTextRange[0] && count < tweet.DisplayTextRange[1] {
displayText += gr.Str()
}
count += 1
} }
tweet.FullText = displayText
fmt.Println(tweet.FullText)
tweet.FullText = tweet.FullText[tweet.DisplayTextRange[0] : tweet.DisplayTextRange[1]+emojiCount]
for _, user := range tweet.Entities.User_mentions { for _, user := range tweet.Entities.User_mentions {
tweet.FullText = strings.ReplaceAll(tweet.FullText, "@"+user.Screen_name, fmt.Sprintf("<a href=\"https://twitter.com/%s/\">@%s</a>", user.Screen_name, user.Screen_name)) tweet.FullText = strings.ReplaceAll(tweet.FullText, "@"+user.Screen_name, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/%s/\">@%s</a>", user.Screen_name, user.Screen_name))
} }
for _, url := range tweet.Entities.Urls { for _, url := range tweet.Entities.Urls {
tweet.FullText = strings.ReplaceAll(tweet.FullText, url.Url, fmt.Sprintf("<a href=\"https://twitter.com/%s/\">%s</a>", url.Expanded_url, url.Display_url)) tweet.FullText = strings.ReplaceAll(tweet.FullText, url.Url, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"%s\">%s</a>", url.Expanded_url, url.Display_url))
} }
for _, hashtag := range tweet.Entities.Hashtags { for _, hashtag := range tweet.Entities.Hashtags {
tweet.FullText = strings.ReplaceAll(tweet.FullText, "#"+hashtag.Text, fmt.Sprintf("<a href=\"https://twitter.com/hashtag/%s\">#%s</a>", hashtag.Text, hashtag.Text)) tweet.FullText = strings.ReplaceAll(tweet.FullText, "#"+hashtag.Text, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/hashtag/%s\">#%s</a>", hashtag.Text, hashtag.Text))
} }
tweet.FullText = strings.ReplaceAll(tweet.FullText, "\n", "<br />")
if tweet.QuotedStatus != nil {
processTweet(tweet.QuotedStatus)
}
}
func renderTemplate(tweet anaconda.Tweet, isQuoted bool) []byte {
templateFuncs := template.FuncMap{ templateFuncs := template.FuncMap{
"base64": func(url string) string { "base64": func(url string) string {
res, err := http.Get(url) res, err := http.Get(url)
@@ -111,19 +170,127 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
"html": func(in string) template.HTML { "html": func(in string) template.HTML {
return template.HTML(in) return template.HTML(in)
}, },
"calculateHeight": func(tweet anaconda.Tweet) string {
return fmt.Sprintf("%dpx", calculateHeight(tweet))
},
"renderTweet": func(tweet anaconda.Tweet) template.HTML {
return template.HTML(string(renderTemplate(tweet, true)))
},
"tweetWidth": func() string {
if isQuoted {
return "450px"
}
return "499px"
},
"className": func() string {
if isQuoted {
return "subtweet"
}
return "tweetsvg"
},
} }
t := template.Must( t := template.Must(
template.New("tweet.svg.tmpl"). template.New("tweet.svg.tmpl").
Funcs(templateFuncs). Funcs(templateFuncs).
ParseFiles("tweet.svg.tmpl")) ParseFS(content, "tweet.svg.tmpl"))
w.Header().Set("Content-type", "image/svg+xml") var buf bytes.Buffer
w.Header().Set("content-encoding", "br") t.Execute(&buf, tweet)
err = t.Execute(w, tweet)
if err != nil { return buf.Bytes()
fmt.Println(err) }
w.WriteHeader(500)
return func calculateHeight(tweet anaconda.Tweet) int64 {
} height := 64.0 /* Avatar */ + 20 /* footer */ + 46 /* text margin */ + 22 /* margin */
lineWidth := 0.0
lineHeight := 28.0
tweetText := strings.ReplaceAll(tweet.FullText, "<br />", " \n")
tweetText = strip.StripTags(tweetText)
tweetText = strings.ReplaceAll(tweetText, "--", "——")
words := regexp.MustCompile(`[ |-]`).Split(tweetText, -1)
for _, word := range words {
word = strings.ReplaceAll(word, "——", "--")
if len(emoji.FindAll(word)) > 0 {
lineHeight = 32.0
}
if strings.HasPrefix(word, "\n") {
height += lineHeight
lineWidth = 0
word = strings.TrimPrefix(word, "\n")
}
if strings.Contains(word, "\n") {
height += lineHeight
lineHeight = 28.0
lineWidth = 0
continue
}
chars := strings.Split(word, "")
wordWidth := 0.0
for _, char := range chars {
wordWidth += getCharWidth(char)
}
if wordWidth > 435 {
height += (lineHeight * (math.Ceil(wordWidth/435) + 1))
lineHeight = 28.0
lineWidth = 0
} else if lineWidth+getCharWidth(" ")+wordWidth > 435 {
height += lineHeight
lineHeight = 28.0
lineWidth = wordWidth
} else {
lineWidth += wordWidth
}
}
if lineWidth > 0 {
height += lineHeight
}
if tweet.InReplyToScreenName != "" {
height += 42
}
for i, img := range tweet.ExtendedEntities.Media {
ratio := float64(img.Sizes.Small.W) / 468
tweet.ExtendedEntities.Media[i].Sizes.Small.W = 468
tweet.ExtendedEntities.Media[i].Sizes.Small.H = int((float64(img.Sizes.Small.H) / ratio) + 5.0)
}
if len(tweet.ExtendedEntities.Media) > 1 {
for i, img := range tweet.ExtendedEntities.Media {
tweet.ExtendedEntities.Media[i].Sizes.Small.W = (img.Sizes.Small.W / 2) - 20
tweet.ExtendedEntities.Media[i].Sizes.Small.H = (img.Sizes.Small.H / 2) - 20
}
}
switch len(tweet.ExtendedEntities.Media) {
case 1:
height += float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H)
case 2:
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 5
case 3:
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 5
height += float64(tweet.ExtendedEntities.Media[2].Sizes.Small.H) + 35
case 4:
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 10
height += math.Max(float64(tweet.ExtendedEntities.Media[2].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[3].Sizes.Small.H)) + 10
height += 7
}
if tweet.QuotedStatus != nil {
height += float64(calculateHeight(*tweet.QuotedStatus)) + 9
}
return int64(height)
}
func suspendedTweet(w http.ResponseWriter) {
w.Header().Set("Content-type", "image/svg+xml")
tweet, _ := content.ReadFile("suspendedTweet.svg")
w.Write(tweet)
} }

1
suspendedTweet.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="566" height="46" viewBox="0 0 566 46"><rect width="100%" height="100%" fill="rgba(0, 0, 0, 0)"/><path vector-effect="non-scaling-stroke" stroke="#f7f9f9" stroke-width="20" stroke-linejoin="round" fill="#f7f9f9" d="M-50-50V50H50V-50z" transform="matrix(5.46 0 0 .26 283 23.17)"/><g><text font-family="'Open Sans', sans-serif" font-size="15" style="white-space:pre" stroke-width="0" fill="#536471" transform="translate(221.5 21.91)"><tspan x="-202.5" y="5.34" style="white-space:pre" font-size="17">This Tweet is from a suspended account.</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 652 B

View File

@@ -1,43 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32em"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{ tweetWidth }}" height="{{ calculateHeight . }}">
<foreignObject x="0" y="0" width="32em" height="100%" fill="#eade52"> <foreignObject x="0" y="0" width="{{ tweetWidth }}" height="100%" fill="#eade52">
<style> <style>
.tweetsvg{clear:none;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;} .{{ className }}{clear:none;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;}
.tweetsvg.text{font-size: 23px;} .{{ className }}.text{font-size: 23px;}
a.tweetsvg{color: rgb(27, 149, 224); text-decoration:none;} a.{{ className }}{color: rgb(27, 149, 224); text-decoration:none;}
.tweetsvg a { color: #1da1f2; } .{{ className }} a { color: #1da1f2; }
blockquote.tweetsvg{margin:1px; background-color:#fefefe; border-radius:2%; border-style:solid; border-width:.1em; border-color:#ddd; padding:1em; font-family:sans; width:29rem} blockquote.{{ className }}{margin:1px; background-color:#fefefe; border-radius:2%; border-style:solid; border-width:.1em; border-color:#ddd; padding:1em; font-family:sans; width:29rem}
.avatar-tweetsvg{float:left; width:4rem; height:4rem; border-radius:50%;margin-right:.5rem;;margin-bottom:.5rem;border-style: solid; border-width:.1em; border-color:#ddd;} blockquote.subtweet{width:26rem; padding:0.8em;}
h1.tweetsvg{margin:0;font-size:15px;text-decoration:none;color:#000;} .avatar-{{ className }}{float:left; width:4rem; height:4rem; border-radius:50%;margin-right:.5rem;;margin-bottom:.5rem;border-style: solid; border-width:.1em; border-color:#ddd;}
h2.tweetsvg{margin:0;font-size:15px;font-weight:normal;text-decoration:none;color:rgb(101, 119, 134);} h1.{{ className }}{margin:0;font-size:15px;text-decoration:none;color:#000;}
p.tweetsvg{font-size:1rem; clear:both;} h2.{{ className }}{margin:0;font-size:15px;font-weight:normal;text-decoration:none;color:rgb(101, 119, 134);}
hr.tweetsvg{color:#ddd;} p.{{ className }}{font-size:1rem; clear:both;}
.media-tweetsvg{border-radius:2%; max-width:100%;border-radius: 2%; border-style: solid; border-width: .1em; border-color: #ddd;} hr.{{ className }}{color:#ddd;}
time.tweetsvg{font-size:15px;margin:0;margin-left: 2px;padding-bottom:1rem;color:rgb(101, 119, 134);text-decoration:none;} .media-{{ className }}{border-radius:2%; max-width:100%;border-radius: 2%; border-style: solid; border-width: .1em; border-color: #ddd;}
.tweetsvg.reply{font-size:15px;color:rgb(110, 118, 125);} time.{{ className }}{font-size:15px;margin:0;margin-left: 2px;padding-bottom:1rem;color:rgb(101, 119, 134);text-decoration:none;}
.{{ className }}.reply{font-size:15px;color:rgb(110, 118, 125);}
.{{ className }}.footer{display:block;}
</style> </style>
<blockquote class="tweetsvg" xmlns="http://www.w3.org/1999/xhtml"> <blockquote class="{{ className }}" xmlns="http://www.w3.org/1999/xhtml">
<a class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><img class="avatar-tweetsvg" alt="" src="data:image/jpeg;base64,{{ base64 .User.ProfileImageUrlHttps }}" /></a> <a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><img class="avatar-{{ className }}" alt="" src="data:image/jpeg;base64,{{ base64 .User.ProfileImageUrlHttps }}" /></a>
<a class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><h1 class="tweetsvg">{{ .User.Name }}</h1></a> <a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><h1 class="{{ className }}">{{ .User.Name }}</h1></a>
<a class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><h2 class="tweetsvg">@{{ .User.ScreenName }}</h2></a> <a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><h2 class="{{ className }}">@{{ .User.ScreenName }}</h2></a>
{{ if .InReplyToScreenName }} {{ if .InReplyToScreenName }}
<p class="tweetsvg reply">Replying to <a href="https://twitter.com/{{ .InReplyToScreenName }}/">@{{ .InReplyToScreenName }}</a></p> <p class="{{ className }} reply">Replying to <a rel="noopener" target="_blank" href="https://twitter.com/{{ .InReplyToScreenName }}/">@{{ .InReplyToScreenName }}</a></p>
{{ end }} {{ end }}
<p class="tweetsvg text">{{ html .FullText }}</p> <p class="{{ className }} text">{{ html .FullText }}</p>
{{ if .QuotedStatus }}
{{ renderTweet .QuotedStatus }}
{{ end }}
{{ if .ExtendedEntities }} {{ if .ExtendedEntities }}
{{ range .ExtendedEntities.Media }} {{ range .ExtendedEntities.Media }}
<a href="{{ .Media_url_https }}" target="_blank"><img class="media-tweetsvg" width="{{ .Sizes.Small.W }}" src="data:image/jpeg;base64,{{ base64 .Media_url }}" alt="{{ .ExtAltText }}"/></a> <a rel="noopener" target="_blank" href="{{ .Media_url_https }}"><img class="media-{{ className }}" width="{{ .Sizes.Small.W }}" src="data:image/jpeg;base64,{{ base64 .Media_url }}" alt="{{ .ExtAltText }}"/></a>
{{ end }} {{ end }}
{{ end }} {{ end }}
<a class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/status/{{ .Id }}"> <a rel="noopener" target="_blank" class="{{ className }} footer" href="https://twitter.com/{{ .User.ScreenName }}/status/{{ .Id }}">
<time class="tweetsvg" datetime="{{ isoDate .CreatedAt }}">{{ humanDate .CreatedAt }}</time> <time class="{{ className }}" datetime="{{ isoDate .CreatedAt }}">{{ humanDate .CreatedAt }}</time>
</a> </a>
</blockquote> </blockquote>
</foreignObject> </foreignObject>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB