Compare commits

...

3 Commits

1
.gitignore vendored

@ -1,6 +1,7 @@
test-archive.tar.gz
out/
dist/
temp/
# Added by cargo
/target

7
Cargo.lock generated

@ -10,6 +10,7 @@ dependencies = [
"clap",
"console_error_panic_hook",
"flate2",
"fs_extra",
"futures",
"serde",
"serde_json",
@ -273,6 +274,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
version = "0.3.26"

@ -16,3 +16,4 @@ tar = "0.4.38"
yew = { version = "0.20.0", features = ["ssr", "csr"] }
console_error_panic_hook = { version ="0.1" }
wasm-bindgen = {version = "0.2" }
fs_extra = "1.3.0"

@ -3,7 +3,24 @@ release = false
dist = "dist"
public_url = "/"
[watch]
ignore = ["temp"]
[serve]
address = "0.0.0.0"
port = 3000
[[hooks]]
stage = "pre_build"
command = "sh"
command_arguments = ["-c", "mkdir -p ./temp && tar -xzf ./test-archive.tar.gz -C ./temp"]
[[hooks]]
stage = "pre_build"
command = "cp"
command_arguments = ["-r", "./temp/", "./dist/.stage/media" ]
[[hooks]]
stage = "pre_build"
command = "cp"
command_arguments = ["-r", "./resources/", "./dist/.stage/resources" ]

@ -3,7 +3,8 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Trunk | Yew | YBC</title>
<title>Mastodon Export</title>
<link rel="stylesheet" href="/resources/style.css" />
<base data-trunk-public-url/>
</head>
@ -11,3 +12,4 @@
<link data-trunk rel="rust" href="Cargo.toml" />
</body>
</html>

@ -0,0 +1,126 @@
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./roboto/KFOmCnqEu92Fr1Mu72xKOzY.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./roboto/KFOmCnqEu92Fr1Mu5mxKOzY.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./roboto/KFOmCnqEu92Fr1Mu7mxKOzY.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./roboto/KFOmCnqEu92Fr1Mu4WxKOzY.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./roboto/KFOmCnqEu92Fr1Mu7WxKOzY.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./roboto/KFOmCnqEu92Fr1Mu7GxKOzY.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./roboto/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(./roboto/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(./roboto/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(./roboto/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(./roboto/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(./roboto/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(./roboto/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(./roboto/KFOlCnqEu92Fr1MmEU9fBBc4.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@ -0,0 +1,99 @@
@import url('./fonts/roboto.css');
:root {
--page-background: lightgray;
--body-background: white;
--body-width: 600px;
--body-padding: 15px;
--profile-size: 120px;
--cover-size: 250px;
}
html, body {
margin: 0;
padding: 0;
background: var(--page-background);
font-family: 'Roboto';
}
body {
max-width: var(--body-width);
min-height: 100vh;
margin: 0 auto;
background: var(--body-background);
box-sizing: border-box;
padding: var(--body-padding);
}
img.banner {
width: calc(100% + 2 * var(--body-padding));
object-fit: cover;
object-position: center;
max-height: var(--cover-size);
margin: calc(-1 * var(--body-padding));
}
img.avatar {
width: var(--profile-size);
height: var(--profile-size);
border: solid 2px white;
border-radius: var(--body-padding);
margin-top: calc(var(--profile-size) / -2 + var(--body-padding));
display: inline-block;
}
h1.name {
font-size: 16px;
line-height: 24px;
font-weight: 500;
margin-bottom: 0;
}
.username, .archived_on {
font-size: 14px;
line-height: 24px;
font-weight: 400px;
margin: 0;
display: block;
color: inherit;
text-decoration: none;
}
.archived_on {
font-style: italic;
opacity: .8;
margin-top: -4px;
}
.profile-grid {
display: grid;
grid-template-columns: 120px 1fr;
margin: var(--body-padding) calc(var(--body-padding) * -1);
font-size: 14px;
}
.profile_property {
display: contents;
border-top: solid 1px gray;
}
.profile_property .property_name {
background: lightgray;
}
.profile_property > span {
border-top: solid 1px gray;
padding: var(--body-padding);
}
.profile_property:last-child > span {
border-bottom: solid 1px gray;
}
.profile_property .invisible {
display: none;
}
.bio {
font-size: 14px;
}

@ -1,3 +1,5 @@
use std::str::FromStr;
use serde::Deserialize;
#[derive(Deserialize, Debug, PartialEq, Clone)]
@ -20,6 +22,33 @@ pub struct Person {
pub attachments: Option<Vec<Attachment>>,
}
impl Person {
pub fn full_username(&self) -> Option<String> {
let url = &self.url;
let domain_separator = url.find("//")?;
if domain_separator + 2 >= url.len() {
return None;
}
let mut parts = url.get((domain_separator + 2)..)?.split("/");
let domain = parts.next()?;
let user = parts.next()?;
if parts.count() > 0 {
return None;
}
let mut username = String::new();
username.push_str(user);
username.push('@');
username.push_str(domain);
Some(username)
}
}
#[derive(Deserialize, Debug, PartialEq, Clone)]
#[serde(tag = "type")]
pub enum Item {

@ -1,6 +1,7 @@
use chrono::{DateTime, NaiveDateTime};
use clap::Parser;
use flate2::read::GzDecoder;
use fs_extra::copy_items;
use serde_json;
use std::{fs, fs::File, io::Read, path::Path, str::FromStr};
use tar::Archive;
@ -61,7 +62,13 @@ fn generate() -> Result<(), Ærror> {
{
let output_str = render_ssr(outbox, author, archive_time)?;
println!("{}", output_str);
fs::write("out/index.html", output_str);
copy_items(
&mut vec!["resources/"],
"out",
&fs_extra::dir::CopyOptions::new(),
);
}
Ok(())
@ -69,10 +76,12 @@ fn generate() -> Result<(), Ærror> {
fn setup_dirs(args: &GeneratorOptions) -> Result<(), crate::error::Ærror> {
let output_dir = Path::new(&args.output_dir);
let media_dir = output_dir.join("media");
// Create output dir
if !output_dir.exists() {
fs::create_dir(output_dir)?;
fs::create_dir(media_dir)?;
} else if !output_dir.read_dir()?.next().is_none() && !args.overwrite {
return Err(Ærror::new(format!(
"Output directory {} exists but --overwrite not given",
@ -81,6 +90,7 @@ fn setup_dirs(args: &GeneratorOptions) -> Result<(), crate::error::Ærror> {
} else {
fs::remove_dir_all(output_dir)?;
fs::create_dir(output_dir)?;
fs::create_dir(media_dir)?;
}
Ok(())
@ -91,6 +101,8 @@ fn read_archive(
) -> Result<(String, String, DateTime<chrono::Utc>), crate::error::Ærror> {
#[cfg(not(debug_assertions))]
let path = &args.archive_file;
let output_dir = Path::new(&args.output_dir);
let media_dir = output_dir.join("media");
let tar;
@ -144,11 +156,11 @@ fn read_archive(
"avatar.png" => {
#[cfg(not(debug_assertions))]
file.unpack_in(&args.output_dir)?;
file.unpack_in(&media_dir)?;
}
"header.png" => {
#[cfg(not(debug_assertions))]
file.unpack_in(&args.output_dir)?;
file.unpack_in(&media_dir)?;
}
_ => (),
};

@ -1,14 +1,14 @@
use chrono::DateTime;
use futures::executor;
use yew::{function_component, html, Html, Renderer, ServerRenderer};
use yew::{function_component, html, AttrValue, Html, Renderer, ServerRenderer};
#[cfg(debug_assertions)]
use console_error_panic_hook::set_once as set_panic_hook;
#[cfg(debug_assertions)]
use wasm_bindgen::prelude::*;
use yew::prelude::*;
use crate::{
data::{Outbox, Person},
data::{Attachment, Outbox, Person},
error::Ærror,
};
@ -25,9 +25,13 @@ pub fn render_ssr(
author: Person,
archive_time: DateTime<chrono::Utc>,
) -> Result<String, Ærror> {
let output_template = include_str!("../index.html");
let output_string = executor::block_on(render_async(outbox, author, archive_time));
Ok(output_string)
Ok(output_template.replace(
"<link data-trunk rel=\"rust\" href=\"Cargo.toml\" />",
&output_string,
))
}
#[cfg(debug_assertions)]
@ -76,10 +80,60 @@ fn Layout(props: &Props) -> Html {
#[function_component]
fn ProfileHeader(props: &Props) -> Html {
html! {
<p>
{"Generated on "}
{props.archive_time.to_rfc2822()}
</p>
<div class="header">
<img class="banner" src="/media/header.png"/>
<img class="avatar" src="/media/avatar.png"/>
<h1 class="name">{props.author.name.as_str()}</h1>
<a href={props.author.url.clone()} class="username" target="_blank">
{props.author.full_username().unwrap().as_str()}
</a>
{ if props.author.attachments.is_some() {
html! {
<div class="profile-grid">
{
props.author.clone().attachments.unwrap().into_iter().filter_map(|attach| match attach {
Attachment::Property {name, value} => Some((name, value)),
_ => None,
}).map(|(name, value)| {
html! {
<div key={name.clone()} class="profile_property">
<span class="property_name">{name}</span>
<span class="property_value">{
if value.find("<a").is_some() {
Html::from_html_unchecked(AttrValue::from(value))
} else {
Html::from(value)
}
}</span>
</div>
}
}).collect::<Html>()
}
</div>
}
} else {
html! {
<></>
}
}}
<div class="bio">
{Html::from_html_unchecked(
AttrValue::from(
props.author.summary.replace("class=\"u-url mention\"", "class=\"mention\" target=\"_blank\"")
)
)}
</div>
<span class="archived_on">
{"Archived "}
{props.archive_time.format("%b %e, %Y")}
</span>
</div>
}
}
#[function_component]
fn Posts(props: &Props) -> Html {
html! {
<p>{"Posts"}</p>
}
}

Loading…
Cancel
Save