Inline mvp and custom override CSS when rendering RSS posts

This commit is contained in:
Bill Thiede 2024-08-05 15:47:31 -07:00
parent 359e798cfa
commit 530bd8e350
5 changed files with 315 additions and 274 deletions

8
server/src/custom.css Normal file
View File

@ -0,0 +1,8 @@
pre {
background-color: var(--color-bg);
color: var(--color-text);
}
code {
background-color: var(--color-bg-secondary);
}

View File

@ -13,10 +13,12 @@ use url::Url;
// TODO: figure out how to use Cow // TODO: figure out how to use Cow
trait Transformer { trait Transformer {
fn should_run(&self, input: &str) -> bool; fn should_run(&self, _html: &str) -> bool {
// TODO: should input be something like `html_escape` uses: true
}
// TODO: should html be something like `html_escape` uses:
// <S: ?Sized + AsRef<str>>(text: &S) -> Cow<str> // <S: ?Sized + AsRef<str>>(text: &S) -> Cow<str>
fn transform(&self, input: &str) -> Result<String, TransformError>; fn transform(&self, html: &str) -> Result<String, TransformError>;
} }
// TODO: how would we make this more generic to allow good implementations of Transformer outside // TODO: how would we make this more generic to allow good implementations of Transformer outside
@ -35,22 +37,50 @@ struct SanitizeHtml<'a> {
} }
impl<'a> Transformer for SanitizeHtml<'a> { impl<'a> Transformer for SanitizeHtml<'a> {
fn should_run(&self, _input: &str) -> bool { fn transform(&self, html: &str) -> Result<String, TransformError> {
true Ok(sanitize_html(html, self.cid_prefix, self.base_url)?)
}
fn transform(&self, input: &str) -> Result<String, TransformError> {
Ok(sanitize_html(input, self.cid_prefix, self.base_url)?)
} }
} }
struct EscapeHtml; struct EscapeHtml;
impl Transformer for EscapeHtml { impl Transformer for EscapeHtml {
fn should_run(&self, input: &str) -> bool { fn should_run(&self, html: &str) -> bool {
input.starts_with("&lt") html.starts_with("&lt")
} }
fn transform(&self, input: &str) -> Result<String, TransformError> { fn transform(&self, html: &str) -> Result<String, TransformError> {
Ok(html_escape::decode_html_entities(input).to_string()) Ok(html_escape::decode_html_entities(html).to_string())
}
}
struct InlineStyle;
impl Transformer for InlineStyle {
fn transform(&self, html: &str) -> Result<String, TransformError> {
let css = concat!(
"/* mvp.css */\n",
include_str!("mvp.css"),
"/* Xinu Specific overrides */\n",
include_str!("custom.css"),
);
let inline_opts = InlineOptions {
inline_style_tags: false,
keep_style_tags: false,
keep_link_tags: false,
base_url: None,
load_remote_stylesheets: false,
extra_css: Some(css.into()),
preallocate_node_capacity: 32,
..InlineOptions::default()
};
Ok(match CSSInliner::new(inline_opts).inline(&html) {
Ok(inlined_html) => inlined_html,
Err(err) => {
error!("failed to inline CSS: {err}");
html.to_string()
}
})
} }
} }

View File

@ -1,224 +1,185 @@
/* MVP.css v1.15 - https://github.com/andybrewer/mvp */ /* MVP.css v1.15 - https://github.com/andybrewer/mvp */
:root { /* :root content stored in client side index.html */
--active-brightness: 0.85;
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color-accent: #118bee15;
--color-bg: #fff;
--color-bg-secondary: #e9e9e9;
--color-link: #118bee;
--color-secondary: #920de9;
--color-secondary-accent: #920de90b;
--color-shadow: #f4f4f4;
--color-table: #118bee;
--color-text: #000;
--color-text-secondary: #999;
--color-scrollbar: #cacae8;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--hover-brightness: 1.2;
--justify-important: center;
--justify-normal: left;
--line-height: 1.5;
--width-card: 285px;
--width-card-medium: 460px;
--width-card-wide: 800px;
--width-content: 1080px;
}
@media (prefers-color-scheme: dark) {
:root[color-mode="user"] {
--color-accent: #0097fc4f;
--color-bg: #333;
--color-bg-secondary: #555;
--color-link: #0097fc;
--color-secondary: #e20de9;
--color-secondary-accent: #e20de94f;
--color-shadow: #bbbbbb20;
--color-table: #0097fc;
--color-text: #f7f7f7;
--color-text-secondary: #aaa;
}
}
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
html { html {
scroll-behavior: auto; scroll-behavior: auto;
} }
} }
/* Layout */ /* Layout */
article aside { article aside {
background: var(--color-secondary-accent); background: var(--color-secondary-accent);
border-left: 4px solid var(--color-secondary); border-left: 4px solid var(--color-secondary);
padding: 0.01rem 0.8rem; padding: 0.01rem 0.8rem;
} }
body { body {
background: var(--color-bg); background: var(--color-bg);
color: var(--color-text); color: var(--color-text);
font-family: var(--font-family); font-family: var(--font-family);
line-height: var(--line-height); line-height: var(--line-height);
margin: 0; margin: 0;
overflow-x: hidden; overflow-x: hidden;
padding: 0; padding: 0;
} }
footer, footer,
header, header,
main { main {
margin: 0 auto; margin: 0 auto;
max-width: var(--width-content); max-width: var(--width-content);
padding: 3rem 1rem; padding: 3rem 1rem;
} }
hr { hr {
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
border: none; border: none;
height: 1px; height: 1px;
margin: 4rem 0; margin: 4rem 0;
width: 100%; width: 100%;
} }
section { section {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: var(--justify-important); justify-content: var(--justify-important);
} }
section img, section img,
article img { article img {
max-width: 100%; max-width: 100%;
} }
section pre { section pre {
overflow: auto; overflow: auto;
} }
section aside { section aside {
border: 1px solid var(--color-bg-secondary); border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow); box-shadow: var(--box-shadow) var(--color-shadow);
margin: 1rem; margin: 1rem;
padding: 1.25rem; padding: 1.25rem;
width: var(--width-card); width: var(--width-card);
} }
section aside:hover { section aside:hover {
box-shadow: var(--box-shadow) var(--color-bg-secondary); box-shadow: var(--box-shadow) var(--color-bg-secondary);
} }
[hidden] { [hidden] {
display: none; display: none;
} }
/* Headers */ /* Headers */
article header, article header,
div header, div header,
main header { main header {
padding-top: 0; padding-top: 0;
} }
header { header {
text-align: var(--justify-important); text-align: var(--justify-important);
} }
header a b, header a b,
header a em, header a em,
header a i, header a i,
header a strong { header a strong {
margin-left: 0.5rem; margin-left: 0.5rem;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
header nav img { header nav img {
margin: 1rem 0; margin: 1rem 0;
} }
section header { section header {
padding-top: 0; padding-top: 0;
width: 100%; width: 100%;
} }
/* Nav */ /* Nav */
nav { nav {
align-items: center; align-items: center;
display: flex; display: flex;
font-weight: bold; font-weight: bold;
justify-content: space-between; justify-content: space-between;
margin-bottom: 7rem; margin-bottom: 7rem;
} }
nav ul { nav ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
} }
nav ul li { nav ul li {
display: inline-block; display: inline-block;
margin: 0 0.5rem; margin: 0 0.5rem;
position: relative; position: relative;
text-align: left; text-align: left;
} }
/* Nav Dropdown */ /* Nav Dropdown */
nav ul li:hover ul { nav ul li:hover ul {
display: block; display: block;
} }
nav ul li ul { nav ul li ul {
background: var(--color-bg); background: var(--color-bg);
border: 1px solid var(--color-bg-secondary); border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow); box-shadow: var(--box-shadow) var(--color-shadow);
display: none; display: none;
height: auto; height: auto;
left: -2px; left: -2px;
padding: .5rem 1rem; padding: .5rem 1rem;
position: absolute; position: absolute;
top: 1.7rem; top: 1.7rem;
white-space: nowrap; white-space: nowrap;
width: auto; width: auto;
z-index: 1; z-index: 1;
} }
nav ul li ul::before { nav ul li ul::before {
/* fill gap above to make mousing over them easier */ /* fill gap above to make mousing over them easier */
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
top: -0.5rem; top: -0.5rem;
height: 0.5rem; height: 0.5rem;
} }
nav ul li ul li, nav ul li ul li,
nav ul li ul li a { nav ul li ul li a {
display: block; display: block;
} }
/* Typography */ /* Typography */
code, code,
samp { samp {
background-color: var(--color-accent); background-color: var(--color-accent);
border-radius: var(--border-radius); border-radius: var(--border-radius);
color: var(--color-text); color: var(--color-text);
display: inline-block; display: inline-block;
margin: 0 0.1rem; margin: 0 0.1rem;
padding: 0 0.5rem; padding: 0 0.5rem;
} }
details { details {
margin: 1.3rem 0; margin: 1.3rem 0;
} }
details summary { details summary {
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
} }
h1, h1,
@ -227,69 +188,69 @@ h3,
h4, h4,
h5, h5,
h6 { h6 {
line-height: var(--line-height); line-height: var(--line-height);
text-wrap: balance; text-wrap: balance;
} }
mark { mark {
padding: 0.1rem; padding: 0.1rem;
} }
ol li, ol li,
ul li { ul li {
padding: 0.2rem 0; padding: 0.2rem 0;
} }
p { p {
margin: 0.75rem 0; margin: 0.75rem 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
} }
pre { pre {
margin: 1rem 0; margin: 1rem 0;
max-width: var(--width-card-wide); max-width: var(--width-card-wide);
padding: 1rem 0; padding: 1rem 0;
} }
pre code, pre code,
pre samp { pre samp {
display: block; display: block;
max-width: var(--width-card-wide); max-width: var(--width-card-wide);
padding: 0.5rem 2rem; padding: 0.5rem 2rem;
white-space: pre-wrap; white-space: pre-wrap;
} }
small { small {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
sup { sup {
background-color: var(--color-secondary); background-color: var(--color-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
color: var(--color-bg); color: var(--color-bg);
font-size: xx-small; font-size: xx-small;
font-weight: bold; font-weight: bold;
margin: 0.2rem; margin: 0.2rem;
padding: 0.2rem 0.3rem; padding: 0.2rem 0.3rem;
position: relative; position: relative;
top: -2px; top: -2px;
} }
/* Links */ /* Links */
a { a {
color: var(--color-link); color: var(--color-link);
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
text-decoration: underline; text-decoration: underline;
} }
a:hover { a:hover {
filter: brightness(var(--hover-brightness)); filter: brightness(var(--hover-brightness));
} }
a:active { a:active {
filter: brightness(var(--active-brightness)); filter: brightness(var(--active-brightness));
} }
a b, a b,
@ -298,241 +259,240 @@ a i,
a strong, a strong,
button, button,
input[type="submit"] { input[type="submit"] {
border-radius: var(--border-radius); border-radius: var(--border-radius);
display: inline-block; display: inline-block;
font-size: medium; font-size: medium;
font-weight: bold; font-weight: bold;
line-height: var(--line-height); line-height: var(--line-height);
margin: 0.5rem 0; margin: 0.5rem 0;
padding: 1rem 2rem; padding: 1rem 2rem;
} }
button, button,
input[type="submit"] { input[type="submit"] {
font-family: var(--font-family); font-family: var(--font-family);
} }
button:hover, button:hover,
input[type="submit"]:hover { input[type="submit"]:hover {
cursor: pointer; cursor: pointer;
filter: brightness(var(--hover-brightness)); filter: brightness(var(--hover-brightness));
} }
button:active, button:active,
input[type="submit"]:active { input[type="submit"]:active {
filter: brightness(var(--active-brightness)); filter: brightness(var(--active-brightness));
} }
a b, a b,
a strong, a strong,
button, button,
input[type="submit"] { input[type="submit"] {
background-color: var(--color-link); background-color: var(--color-link);
border: 2px solid var(--color-link); border: 2px solid var(--color-link);
color: var(--color-bg); color: var(--color-bg);
} }
a em, a em,
a i { a i {
border: 2px solid var(--color-link); border: 2px solid var(--color-link);
border-radius: var(--border-radius); border-radius: var(--border-radius);
color: var(--color-link); color: var(--color-link);
display: inline-block; display: inline-block;
padding: 1rem 2rem; padding: 1rem 2rem;
} }
article aside a { article aside a {
color: var(--color-secondary); color: var(--color-secondary);
} }
/* Images */ /* Images */
figure { figure {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
figure img { figure img {
max-width: 100%; max-width: 100%;
} }
figure figcaption { figure figcaption {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
/* Forms */ /* Forms */
button:disabled, button:disabled,
input:disabled { input:disabled {
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
border-color: var(--color-bg-secondary); border-color: var(--color-bg-secondary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: not-allowed; cursor: not-allowed;
} }
button[disabled]:hover, button[disabled]:hover,
input[type="submit"][disabled]:hover { input[type="submit"][disabled]:hover {
filter: none; filter: none;
} }
form { form {
border: 1px solid var(--color-bg-secondary); border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow); box-shadow: var(--box-shadow) var(--color-shadow);
display: block; display: block;
max-width: var(--width-card-wide); max-width: var(--width-card-wide);
min-width: var(--width-card); min-width: var(--width-card);
padding: 1.5rem; padding: 1.5rem;
text-align: var(--justify-normal); text-align: var(--justify-normal);
} }
form header { form header {
margin: 1.5rem 0; margin: 1.5rem 0;
padding: 1.5rem 0; padding: 1.5rem 0;
} }
input, input,
label, label,
select, select,
textarea { textarea {
display: block; display: block;
font-size: inherit; font-size: inherit;
max-width: var(--width-card-wide); max-width: var(--width-card-wide);
} }
input[type="checkbox"], input[type="checkbox"],
input[type="radio"] { input[type="radio"] {
display: inline-block; display: inline-block;
} }
input[type="checkbox"]+label, input[type="checkbox"]+label,
input[type="radio"]+label { input[type="radio"]+label {
display: inline-block; display: inline-block;
font-weight: normal; font-weight: normal;
position: relative; position: relative;
top: 1px; top: 1px;
} }
input[type="range"] { input[type="range"] {
padding: 0.4rem 0; padding: 0.4rem 0;
} }
input, input,
select, select,
textarea { textarea {
border: 1px solid var(--color-bg-secondary); border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 0.4rem 0.8rem; padding: 0.4rem 0.8rem;
} }
input[type="text"], input[type="text"],
input[type="password"] input[type="password"] textarea {
textarea { width: calc(100% - 1.6rem);
width: calc(100% - 1.6rem);
} }
input[readonly], input[readonly],
textarea[readonly] { textarea[readonly] {
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
} }
label { label {
font-weight: bold; font-weight: bold;
margin-bottom: 0.2rem; margin-bottom: 0.2rem;
} }
/* Popups */ /* Popups */
dialog { dialog {
border: 1px solid var(--color-bg-secondary); border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow); box-shadow: var(--box-shadow) var(--color-shadow);
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 50%; width: 50%;
z-index: 999; z-index: 999;
} }
/* Tables */ /* Tables */
table { table {
border: 1px solid var(--color-bg-secondary); border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
border-spacing: 0; border-spacing: 0;
display: inline-block; display: inline-block;
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
padding: 0; padding: 0;
white-space: nowrap; white-space: nowrap;
} }
table td, table td,
table th, table th,
table tr { table tr {
padding: 0.4rem 0.8rem; padding: 0.4rem 0.8rem;
text-align: var(--justify-important); text-align: var(--justify-important);
} }
table thead { table thead {
background-color: var(--color-table); background-color: var(--color-table);
border-collapse: collapse; border-collapse: collapse;
border-radius: var(--border-radius); border-radius: var(--border-radius);
color: var(--color-bg); color: var(--color-bg);
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
table thead tr:first-child th:first-child { table thead tr:first-child th:first-child {
border-top-left-radius: var(--border-radius); border-top-left-radius: var(--border-radius);
} }
table thead tr:first-child th:last-child { table thead tr:first-child th:last-child {
border-top-right-radius: var(--border-radius); border-top-right-radius: var(--border-radius);
} }
table thead th:first-child, table thead th:first-child,
table tr td:first-child { table tr td:first-child {
text-align: var(--justify-normal); text-align: var(--justify-normal);
} }
table tr:nth-child(even) { table tr:nth-child(even) {
background-color: var(--color-accent); background-color: var(--color-accent);
} }
/* Quotes */ /* Quotes */
blockquote { blockquote {
display: block; display: block;
font-size: x-large; font-size: x-large;
line-height: var(--line-height); line-height: var(--line-height);
margin: 1rem auto; margin: 1rem auto;
max-width: var(--width-card-medium); max-width: var(--width-card-medium);
padding: 1.5rem 1rem; padding: 1.5rem 1rem;
text-align: var(--justify-important); text-align: var(--justify-important);
} }
blockquote footer { blockquote footer {
color: var(--color-text-secondary); color: var(--color-text-secondary);
display: block; display: block;
font-size: small; font-size: small;
line-height: var(--line-height); line-height: var(--line-height);
padding: 1.5rem 0; padding: 1.5rem 0;
} }
/* Scrollbars */ /* Scrollbars */
* { * {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--color-scrollbar) transparent; scrollbar-color: var(--color-scrollbar) transparent;
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 5px; width: 5px;
height: 5px; height: 5px;
} }
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar); background-color: var(--color-scrollbar);
border-radius: 10px; border-radius: 10px;
} }

View File

@ -14,7 +14,7 @@ const THREAD_PREFIX: &'static str = "news:";
use crate::{ use crate::{
error::ServerError, error::ServerError,
graphql::{Body, Email, Html, Message, Tag, Thread, ThreadSummary}, graphql::{Body, Email, Html, Message, Tag, Thread, ThreadSummary},
EscapeHtml, SanitizeHtml, Transformer, EscapeHtml, InlineStyle, SanitizeHtml, Transformer,
}; };
pub fn is_newsreader_search(query: &str) -> bool { pub fn is_newsreader_search(query: &str) -> bool {
@ -213,6 +213,7 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
// * Some sites appear to be HTML encoded, unencode them, i.e. imperialviolent // * Some sites appear to be HTML encoded, unencode them, i.e. imperialviolent
let tranformers: Vec<Box<dyn Transformer>> = vec![ let tranformers: Vec<Box<dyn Transformer>> = vec![
Box::new(EscapeHtml), Box::new(EscapeHtml),
Box::new(InlineStyle),
Box::new(SanitizeHtml { Box::new(SanitizeHtml {
cid_prefix: "", cid_prefix: "",
base_url: &link, base_url: &link,
@ -224,7 +225,7 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
} }
} }
let body = Body::Html(Html { let body = Body::Html(Html {
html: html.to_string(), html,
content_tree: "".to_string(), content_tree: "".to_string(),
}); });
let title = r.title.unwrap_or("NO TITLE".to_string()); let title = r.title.unwrap_or("NO TITLE".to_string());

View File

@ -22,6 +22,48 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet">
<style>
:root {
--active-brightness: 0.85;
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color-accent: #118bee15;
--color-bg: #fff;
--color-bg-secondary: #e9e9e9;
--color-link: #118bee;
--color-secondary: #920de9;
--color-secondary-accent: #920de90b;
--color-shadow: #f4f4f4;
--color-table: #118bee;
--color-text: #000;
--color-text-secondary: #999;
--color-scrollbar: #cacae8;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--hover-brightness: 1.2;
--justify-important: center;
--justify-normal: left;
--line-height: 1.5;
--width-card: 285px;
--width-card-medium: 460px;
--width-card-wide: 800px;
--width-content: 1080px;
}
@media (prefers-color-scheme: dark) {
:root[color-mode="user"] {
--color-accent: #0097fc4f;
--color-bg: #333;
--color-bg-secondary: #555;
--color-link: #0097fc;
--color-secondary: #e20de9;
--color-secondary-accent: #e20de94f;
--color-shadow: #bbbbbb20;
--color-table: #0097fc;
--color-text: #f7f7f7;
--color-text-secondary: #aaa;
}
}
</style>
</head> </head>
<body> <body>