implement rendering on medium-formatted data

This commit is contained in:
Aravinth Manivannan 2021-10-31 22:26:42 +05:30
parent 63bd10688c
commit f0853a4a64
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
9 changed files with 333 additions and 68 deletions

112
Cargo.lock generated
View file

@ -381,6 +381,19 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits 0.2.14",
"time 0.1.44",
"winapi",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "3.8.1" version = "3.8.1"
@ -545,6 +558,18 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "filetime"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"winapi",
]
[[package]] [[package]]
name = "firestorm" name = "firestorm"
version = "0.4.6" version = "0.4.6"
@ -764,6 +789,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "home"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.5" version = "0.2.5"
@ -886,6 +920,12 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.24" version = "0.1.24"
@ -943,6 +983,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-web", "actix-web",
"actix-web-codegen 0.5.0-beta.4", "actix-web-codegen 0.5.0-beta.4",
"chrono",
"config", "config",
"derive_more", "derive_more",
"graphql_client", "graphql_client",
@ -951,6 +992,7 @@ dependencies = [
"num_cpus", "num_cpus",
"pretty_env_logger", "pretty_env_logger",
"reqwest", "reqwest",
"sailfish",
"serde 1.0.130", "serde 1.0.130",
"serde_json", "serde_json",
"url", "url",
@ -1086,6 +1128,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits 0.2.14",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.1.43" version = "0.1.43"
@ -1276,9 +1328,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.32" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@ -1444,6 +1496,43 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "sailfish"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "816920a08514d9741242b3efe70c16c350ed548bc4a5ba03426e56faf9d45f77"
dependencies = [
"itoap",
"ryu",
"sailfish-macros",
"version_check",
]
[[package]]
name = "sailfish-compiler"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4276e7b848bde8e7813d534f014bc35ce5acd2b9e2b6b075727113fcf478ba63"
dependencies = [
"filetime",
"home",
"memchr",
"proc-macro2",
"quote",
"syn",
"yaml-rust",
]
[[package]]
name = "sailfish-macros"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bba2458ef07ae12c9aed2edb866c3db2f9c21cf19a2c3f2613b2982bc1a4a46"
dependencies = [
"proc-macro2",
"sailfish-compiler",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.19" version = "0.1.19"
@ -1693,9 +1782,9 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.81" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1737,6 +1826,17 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.2.27" version = "0.2.27"
@ -1973,9 +2073,9 @@ dependencies = [
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.2+wasi-snapshot-preview1" version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"

View file

@ -31,7 +31,10 @@ lazy_static = "1.4"
url = "2.2" url = "2.2"
derive_more = "0.99" derive_more = "0.99"
sailfish = "0.3.2"
num_cpus = "1.13" num_cpus = "1.13"
reqwest = { version = "0.11.6", features = ["json"] } reqwest = { version = "0.11.6", features = ["json"] }
graphql_client = { version = "0.10.0", features = ["reqwest"]} graphql_client = { version = "0.10.0", features = ["reqwest"]}
chrono = "0.4.19"

1
sailfish.yml Normal file
View file

@ -0,0 +1 @@
delimiter: "."

View file

@ -14,11 +14,13 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::ops::{Bound, RangeBounds};
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use graphql_client::{reqwest::post_graphql, GraphQLQuery}; use graphql_client::{reqwest::post_graphql, GraphQLQuery};
use serde::{Deserialize, Serialize}; use sailfish::TemplateOnce;
use crate::SETTINGS; use crate::AppData;
pub mod routes { pub mod routes {
pub struct Proxy { pub struct Proxy {
@ -41,6 +43,58 @@ pub mod routes {
} }
} }
// credits @carlomilanesi:
// https://users.rust-lang.org/t/how-to-get-a-substring-of-a-string/1351/11
trait StringUtils {
fn substring(&self, start: usize, len: usize) -> &str;
fn slice(&self, range: impl RangeBounds<usize>) -> &str;
}
impl StringUtils for str {
fn substring(&self, start: usize, len: usize) -> &str {
let mut char_pos = 0;
let mut byte_start = 0;
let mut it = self.chars();
loop {
if char_pos == start {
break;
}
if let Some(c) = it.next() {
char_pos += 1;
byte_start += c.len_utf8();
} else {
break;
}
}
char_pos = 0;
let mut byte_end = byte_start;
loop {
if char_pos == len {
break;
}
if let Some(c) = it.next() {
char_pos += 1;
byte_end += c.len_utf8();
} else {
break;
}
}
&self[byte_start..byte_end]
}
fn slice(&self, range: impl RangeBounds<usize>) -> &str {
let start = match range.start_bound() {
Bound::Included(bound) | Bound::Excluded(bound) => *bound,
Bound::Unbounded => 0,
};
let len = match range.end_bound() {
Bound::Included(bound) => *bound + 1,
Bound::Excluded(bound) => *bound,
Bound::Unbounded => self.len(),
} - start;
self.substring(start, len)
}
}
#[derive(GraphQLQuery)] #[derive(GraphQLQuery)]
#[graphql( #[graphql(
schema_path = "schemas/schema.graphql", schema_path = "schemas/schema.graphql",
@ -49,44 +103,39 @@ pub mod routes {
)] )]
struct GetPost; struct GetPost;
#[derive(TemplateOnce)]
#[template(path = "index.html")]
pub struct Post {
pub data: get_post::GetPostPost,
pub id: String,
}
#[my_codegen::get(path = "crate::V1_API_ROUTES.proxy.page")] #[my_codegen::get(path = "crate::V1_API_ROUTES.proxy.page")]
async fn page(path: web::Path<(String, String)>) -> impl Responder { async fn page(path: web::Path<(String, String)>, data: AppData) -> impl Responder {
let post_id = path.1.split("-").last(); let post_id = path.1.split("-").last();
if post_id.is_none() { if post_id.is_none() {
return HttpResponse::BadRequest(); return HttpResponse::BadRequest().finish();
} }
let id = post_id.unwrap().to_string(); let id = post_id.unwrap().to_string();
let vars = get_post::Variables { id }; let vars = get_post::Variables { id: id.clone() };
const URL: &str = "https://medium.com/_/graphql"; const URL: &str = "https://medium.com/_/graphql";
let client = reqwest::Client::new(); let res = post_graphql::<GetPost, _>(&data.client, URL, vars)
let res = post_graphql::<GetPost, _>(&client, URL, vars)
.await .await
.unwrap(); .unwrap();
println!("{:?}", res);
let response_data: get_post::ResponseData = res.data.expect("missing response data"); let response_data: get_post::ResponseData = res.data.expect("missing response data");
for p in response_data
.post
.unwrap()
.content
.unwrap()
.body_model
.unwrap()
.paragraphs
.unwrap()
.iter()
{
println!("paragraph content: {:?}", p.as_ref().unwrap());
}
// .bodyModel
// .paragraphs
// .iter();
// println!("{:?}", response_data);
let page = Post {
id,
data: response_data.post.unwrap(),
}
.render_once()
.unwrap();
HttpResponse::Ok() HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(page)
} }
pub fn services(cfg: &mut web::ServiceConfig) { pub fn services(cfg: &mut web::ServiceConfig) {
@ -97,42 +146,22 @@ pub fn services(cfg: &mut web::ServiceConfig) {
mod tests { mod tests {
use actix_web::{http::StatusCode, test, App}; use actix_web::{http::StatusCode, test, App};
use crate::services; use crate::{services, Data};
use crate::*;
use super::*; #[actix_rt::test]
async fn deploy_update_works() {
let data = Data::new();
let app = test::init_service(App::new().app_data(data.clone()).configure(services)).await;
let urls = vec![
"/@ftrain/big-data-small-effort-b62607a43a8c",
"/geekculture/rest-api-best-practices-decouple-long-running-tasks-from-http-request-processing-9fab2921ace8",
"/illumination/5-bugs-that-turned-into-features-e9a0e972a4e7",
];
// #[actix_rt::test] for uri in urls.iter() {
// async fn deploy_update_works() { let resp =
// let app = test::init_service(App::new().configure(services)).await; test::call_service(&app, test::TestRequest::get().uri(uri).to_request()).await;
// assert_eq!(resp.status(), StatusCode::OK);
// let page = page.unwrap(); }
// }
// let mut payload = ProxyEvent {
// secret: page.secret.clone(),
// branch: page.branch.clone(),
// };
//
// let resp = test::call_service(
// &app,
// test::TestRequest::post()
// .uri(V1_API_ROUTES.deploy.update)
// .set_json(&payload)
// .to_request(),
// )
// .await;
// assert_eq!(resp.status(), StatusCode::OK);
//
// payload.secret = page.branch.clone();
//
// let resp = test::call_service(
// &app,
// test::TestRequest::post()
// .uri(V1_API_ROUTES.deploy.update)
// .set_json(&payload)
// .to_request(),
// )
// .await;
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// }
} }

9
templates/img.html Normal file
View file

@ -0,0 +1,9 @@
<. let metadata = p.metadata.as_ref().unwrap(); .>
<!-- TODO deal with layouts
height="<.= metadata.original_height.>"
width="<.= metadata.original_width.>"
-->
<img
src="https://miro.medium.com/<.= metadata.id .>"
alt="<.= p.text .>"
/>

37
templates/index.html Normal file
View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><.= data.title .></title>
</head>
<body>
<main class="container">
<h1><.= data.title .></h1>
<. use chrono::{TimeZone, Utc}; .>
<. let dt = Utc.timestamp_millis(data.created_at); .>
<p class="meta">
<a href="https://medium.com/u/<.= data.creator.id .>" rel="noreferrer">
<.= data.creator.name .></a
>
on <.= dt.format("%b %e, %Y").to_string() .>
</p>
<article>
<. let paragraphs = data.content.body_model.paragraphs; .>
<. for (pindex, p) in paragraphs.iter().enumerate() {.>
<. if pindex == 1 && p.type_ == "H3" {.>
<. continue; .>
<.}.>
<. if p.type_ == "IMG" {.>
<. include!("./img.html"); .>
<.} else if p.type_ == "P" {.>
<. include!("./p.html"); .>
<.}.>
<.}.>
</article>
</main>
</body>
<style>
<. include!("./main.css"); .>
</style>
</html>

27
templates/main.css Normal file
View file

@ -0,0 +1,27 @@
* {
margin: 0;
padding: 0;
}
body {
width: 100%;
display: flex;
flex-direction: column;
}
main {
width: 60%;
margin: auto;
display: flex;
flex-direction: column;
}
p {
margin: 20px 0;
}
img {
margin: auto;
max-width: 80%;
/*! display: block; */
}

15
templates/matcher.rs Normal file
View file

@ -0,0 +1,15 @@
match p.type_.as_str() {
"IMG" => {
include!("./img.html");
},
_ => unimplemented!(),
}
//<. match p.type_ { .>
// <. "IMG" => { .>
// <. include!("./img.html") .>
// <. }, .>
// <. _ => log::error("Unable to find paragraph render class. Post ID: {}. Paragraph item {:?}", id, p),
// <. } .>
//

44
templates/p.html Normal file
View file

@ -0,0 +1,44 @@
<p>
<. if p.markups.is_empty() {.>
<.= p.text .>
<.} else {.>
<. let mut cur: usize = 0; .>
<. for markup in &p.markups {.>
<.= &p.text.substring(cur, (markup.start -1) as usize) .>
<. cur = (markup.end + 1) as usize; .>
<. let text = &p.text.substring(markup.start as usize, markup.end as usize); .>
<. if markup.type_ == "A" {.>
<. if let Some(anchor_type) = &markup.anchor_type {.>
<. if anchor_type == "LINK" {.>
<a rel="noreferrer" href="<.= markup.href.as_ref().unwrap() .>"><.= text .></a>
<.} else if anchor_type == "USER" {.>
<a
rel="noreferrer"
href="https://medium.com/u/<.= markup.user_id.as_ref().unwrap() .>"
>
<.= text .>
</a>
<.} else {.>
<. log::error!("unknown markup.anchor_type: {:?} post id {}", anchor_type, id); .>
<span><.= text .></span>
<.}.>
<.}.>
<.} else if markup.type_ == "EM" {.>
<em><.= text .></em>
<.} else if markup.type_ == "STRONG" {.>
<strong><.= text .></strong>
<.} else if markup.type_ == "CODE" {.>
<code><.= text .></code>
<.} else {.>
<. log::error!("unknown markup.type_: {:?} post id {}", markup.type_, id); .>
<span><.= text .></span>
<.}.>
<. if cur < p.text.len() {.>
<.= p.text.slice(cur..) .>
<.}.>
<.}.>
<.}.>
</p>
<!-- markup.type_ = [ "CODE", "A", "EM", "STRONG" -->