implement rendering on medium-formatted data
This commit is contained in:
parent
63bd10688c
commit
f0853a4a64
9 changed files with 333 additions and 68 deletions
112
Cargo.lock
generated
112
Cargo.lock
generated
|
@ -381,6 +381,19 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "combine"
|
||||
version = "3.8.1"
|
||||
|
@ -545,6 +558,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "firestorm"
|
||||
version = "0.4.6"
|
||||
|
@ -764,6 +789,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.5"
|
||||
|
@ -886,6 +920,12 @@ version = "0.4.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "itoap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.24"
|
||||
|
@ -943,6 +983,7 @@ dependencies = [
|
|||
"actix-rt",
|
||||
"actix-web",
|
||||
"actix-web-codegen 0.5.0-beta.4",
|
||||
"chrono",
|
||||
"config",
|
||||
"derive_more",
|
||||
"graphql_client",
|
||||
|
@ -951,6 +992,7 @@ dependencies = [
|
|||
"num_cpus",
|
||||
"pretty_env_logger",
|
||||
"reqwest",
|
||||
"sailfish",
|
||||
"serde 1.0.130",
|
||||
"serde_json",
|
||||
"url",
|
||||
|
@ -1086,6 +1128,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
|
@ -1276,9 +1328,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.32"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
|
||||
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
@ -1444,6 +1496,43 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "schannel"
|
||||
version = "0.1.19"
|
||||
|
@ -1693,9 +1782,9 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.81"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
|
||||
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1737,6 +1826,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "time"
|
||||
version = "0.2.27"
|
||||
|
@ -1973,9 +2073,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
|
|
|
@ -31,7 +31,10 @@ lazy_static = "1.4"
|
|||
url = "2.2"
|
||||
|
||||
derive_more = "0.99"
|
||||
sailfish = "0.3.2"
|
||||
|
||||
num_cpus = "1.13"
|
||||
reqwest = { version = "0.11.6", features = ["json"] }
|
||||
graphql_client = { version = "0.10.0", features = ["reqwest"]}
|
||||
|
||||
chrono = "0.4.19"
|
||||
|
|
1
sailfish.yml
Normal file
1
sailfish.yml
Normal file
|
@ -0,0 +1 @@
|
|||
delimiter: "."
|
153
src/proxy.rs
153
src/proxy.rs
|
@ -14,11 +14,13 @@
|
|||
* 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/>.
|
||||
*/
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use graphql_client::{reqwest::post_graphql, GraphQLQuery};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sailfish::TemplateOnce;
|
||||
|
||||
use crate::SETTINGS;
|
||||
use crate::AppData;
|
||||
|
||||
pub mod routes {
|
||||
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)]
|
||||
#[graphql(
|
||||
schema_path = "schemas/schema.graphql",
|
||||
|
@ -49,44 +103,39 @@ pub mod routes {
|
|||
)]
|
||||
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")]
|
||||
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();
|
||||
if post_id.is_none() {
|
||||
return HttpResponse::BadRequest();
|
||||
return HttpResponse::BadRequest().finish();
|
||||
}
|
||||
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";
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = post_graphql::<GetPost, _>(&client, URL, vars)
|
||||
let res = post_graphql::<GetPost, _>(&data.client, URL, vars)
|
||||
.await
|
||||
.unwrap();
|
||||
println!("{:?}", res);
|
||||
|
||||
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()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(page)
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
|
@ -97,42 +146,22 @@ pub fn services(cfg: &mut web::ServiceConfig) {
|
|||
mod tests {
|
||||
use actix_web::{http::StatusCode, test, App};
|
||||
|
||||
use crate::services;
|
||||
use crate::*;
|
||||
use crate::{services, Data};
|
||||
|
||||
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]
|
||||
// async fn deploy_update_works() {
|
||||
// let app = test::init_service(App::new().configure(services)).await;
|
||||
//
|
||||
// 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);
|
||||
// }
|
||||
for uri in urls.iter() {
|
||||
let resp =
|
||||
test::call_service(&app, test::TestRequest::get().uri(uri).to_request()).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
9
templates/img.html
Normal file
9
templates/img.html
Normal 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
37
templates/index.html
Normal 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
27
templates/main.css
Normal 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
15
templates/matcher.rs
Normal 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
44
templates/p.html
Normal 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" -->
|
Loading…
Reference in a new issue