Hide cw pictures behind a summary/details (#483)
* Hide cw pictures behind a summary/details * refactor md_to_html a bit and add cw support * use random id for cw checkbox
This commit is contained in:
parent
eabe73ddc0
commit
12c2078c89
@ -56,16 +56,7 @@ fn to_inline(tag: Tag) -> Tag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns (HTML, mentions, hashtags)
|
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
|
||||||
pub fn md_to_html(
|
|
||||||
md: &str,
|
|
||||||
base_url: &str,
|
|
||||||
inline: bool,
|
|
||||||
) -> (String, HashSet<String>, HashSet<String>) {
|
|
||||||
let parser = Parser::new_ext(md, Options::all());
|
|
||||||
|
|
||||||
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
|
||||||
.scan(None, |state: &mut Option<String>, evt| {
|
|
||||||
let (s, res) = match evt {
|
let (s, res) = match evt {
|
||||||
Event::Text(txt) => match state.take() {
|
Event::Text(txt) => match state.take() {
|
||||||
Some(mut prev_txt) => {
|
Some(mut prev_txt) => {
|
||||||
@ -81,11 +72,13 @@ pub fn md_to_html(
|
|||||||
};
|
};
|
||||||
*state = s;
|
*state = s;
|
||||||
Some(res)
|
Some(res)
|
||||||
})
|
}
|
||||||
.flat_map(IntoIterator::into_iter)
|
|
||||||
// Ignore headings, images, and tables if inline = true
|
fn inline_tags<'a>(
|
||||||
.scan(vec![], |state: &mut Vec<Tag>, evt| {
|
(state, inline): &mut (Vec<Tag<'a>>, bool),
|
||||||
if inline {
|
evt: Event<'a>,
|
||||||
|
) -> Option<Event<'a>> {
|
||||||
|
if *inline {
|
||||||
let new_evt = match evt {
|
let new_evt = match evt {
|
||||||
Event::Start(t) => {
|
Event::Start(t) => {
|
||||||
let tag = to_inline(t);
|
let tag = to_inline(t);
|
||||||
@ -102,7 +95,78 @@ pub fn md_to_html(
|
|||||||
} else {
|
} else {
|
||||||
Some(evt)
|
Some(evt)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
pub type MediaProcessor<'a> = Box<'a + Fn(i32) -> Option<(String, Option<String>)>>;
|
||||||
|
|
||||||
|
fn process_image<'a, 'b>(
|
||||||
|
evt: Event<'a>,
|
||||||
|
inline: bool,
|
||||||
|
processor: &Option<MediaProcessor<'b>>,
|
||||||
|
) -> Event<'a> {
|
||||||
|
if let Some(ref processor) = *processor {
|
||||||
|
match evt {
|
||||||
|
Event::Start(Tag::Image(id, title)) => {
|
||||||
|
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||||
|
if inline || cw.is_none() {
|
||||||
|
Event::Start(Tag::Image(Cow::Owned(url), title))
|
||||||
|
} else {
|
||||||
|
// there is a cw, and where are not inline
|
||||||
|
Event::Html(Cow::Owned(format!(
|
||||||
|
r#"<label for="postcontent-cw-{id}">
|
||||||
|
<input type="checkbox" id="postcontent-cw-{id}" checked="checked" class="cw-checkbox">
|
||||||
|
<span class="cw-container">
|
||||||
|
<span class="cw-text">
|
||||||
|
{cw}
|
||||||
|
</span>
|
||||||
|
<img src="{url}" alt=""#,
|
||||||
|
id = random_hex(),
|
||||||
|
cw = cw.unwrap(),
|
||||||
|
url = url
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Event::Start(Tag::Image(id, title))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::End(Tag::Image(id, title)) => {
|
||||||
|
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||||
|
if inline || cw.is_none() {
|
||||||
|
Event::End(Tag::Image(Cow::Owned(url), title))
|
||||||
|
} else {
|
||||||
|
Event::Html(Cow::Borrowed(
|
||||||
|
r#""/>
|
||||||
|
</span>
|
||||||
|
</label>"#,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Event::End(Tag::Image(id, title))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => e,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
evt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns (HTML, mentions, hashtags)
|
||||||
|
pub fn md_to_html<'a>(
|
||||||
|
md: &str,
|
||||||
|
base_url: &str,
|
||||||
|
inline: bool,
|
||||||
|
media_processor: Option<MediaProcessor<'a>>,
|
||||||
|
) -> (String, HashSet<String>, HashSet<String>) {
|
||||||
|
let parser = Parser::new_ext(md, Options::all());
|
||||||
|
|
||||||
|
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
||||||
|
// Flatten text because pulldown_cmark break #hashtag in two individual text elements
|
||||||
|
.scan(None, flatten_text)
|
||||||
|
.flat_map(IntoIterator::into_iter)
|
||||||
|
.map(|evt| process_image(evt, inline, &media_processor))
|
||||||
|
// Ignore headings, images, and tables if inline = true
|
||||||
|
.scan((vec![], inline), inline_tags)
|
||||||
.map(|evt| match evt {
|
.map(|evt| match evt {
|
||||||
Event::Text(txt) => {
|
Event::Text(txt) => {
|
||||||
let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold(
|
let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold(
|
||||||
@ -273,7 +337,7 @@ mod tests {
|
|||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html(md, "", false).1,
|
md_to_html(md, "", false, None).1,
|
||||||
mentions
|
mentions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@ -298,7 +362,7 @@ mod tests {
|
|||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html(md, "", false).2,
|
md_to_html(md, "", false, None).2,
|
||||||
mentions
|
mentions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@ -310,11 +374,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_inline() {
|
fn test_inline() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html("# Hello", "", false).0,
|
md_to_html("# Hello", "", false, None).0,
|
||||||
String::from("<h1>Hello</h1>\n")
|
String::from("<h1>Hello</h1>\n")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html("# Hello", "", true).0,
|
md_to_html("# Hello", "", true, None).0,
|
||||||
String::from("<p>Hello</p>\n")
|
String::from("<p>Hello</p>\n")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use comment_seers::{CommentSeers, NewCommentSeers};
|
use comment_seers::{CommentSeers, NewCommentSeers};
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
|
use medias::Media;
|
||||||
use mentions::Mention;
|
use mentions::Mention;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
@ -102,14 +103,16 @@ impl Comment {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
pub fn to_activity<'b>(&self, conn: &'b Connection) -> Result<Note> {
|
||||||
|
let author = User::get(conn, self.author_id)?;
|
||||||
|
|
||||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||||
self.content.get().as_ref(),
|
self.content.get().as_ref(),
|
||||||
&Instance::get_local(conn)?.public_domain,
|
&Instance::get_local(conn)?.public_domain,
|
||||||
true,
|
true,
|
||||||
|
Some(Media::get_media_processor(conn, vec![&author])),
|
||||||
);
|
);
|
||||||
|
|
||||||
let author = User::get(conn, self.author_id)?;
|
|
||||||
let mut note = Note::default();
|
let mut note = Note::default();
|
||||||
let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())];
|
let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())];
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
|||||||
use std::iter::Iterator;
|
use std::iter::Iterator;
|
||||||
|
|
||||||
use ap_url;
|
use ap_url;
|
||||||
|
use medias::Media;
|
||||||
use plume_common::utils::md_to_html;
|
use plume_common::utils::md_to_html;
|
||||||
use safe_string::SafeString;
|
use safe_string::SafeString;
|
||||||
use schema::{instances, users};
|
use schema::{instances, users};
|
||||||
@ -128,8 +129,18 @@ impl Instance {
|
|||||||
short_description: SafeString,
|
short_description: SafeString,
|
||||||
long_description: SafeString,
|
long_description: SafeString,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain, true);
|
let (sd, _, _) = md_to_html(
|
||||||
let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain, false);
|
short_description.as_ref(),
|
||||||
|
&self.public_domain,
|
||||||
|
true,
|
||||||
|
Some(Media::get_media_processor(conn, vec![])),
|
||||||
|
);
|
||||||
|
let (ld, _, _) = md_to_html(
|
||||||
|
long_description.as_ref(),
|
||||||
|
&self.public_domain,
|
||||||
|
false,
|
||||||
|
Some(Media::get_media_processor(conn, vec![])),
|
||||||
|
);
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set((
|
.set((
|
||||||
instances::name.eq(name),
|
instances::name.eq(name),
|
||||||
|
@ -5,7 +5,7 @@ use guid_create::GUID;
|
|||||||
use reqwest;
|
use reqwest;
|
||||||
use std::{fs, path::Path};
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
use plume_common::activity_pub::Id;
|
use plume_common::{activity_pub::Id, utils::MediaProcessor};
|
||||||
|
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use safe_string::SafeString;
|
use safe_string::SafeString;
|
||||||
@ -124,10 +124,9 @@ impl Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
|
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
|
||||||
let url = self.url(conn)?;
|
|
||||||
Ok(match self.category() {
|
Ok(match self.category() {
|
||||||
MediaCategory::Image => {
|
MediaCategory::Image => {
|
||||||
SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url))
|
SafeString::new(&format!("![{}]({})", escape(&self.alt_text), self.id))
|
||||||
}
|
}
|
||||||
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
|
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
|
||||||
MediaCategory::Unknown => SafeString::new(""),
|
MediaCategory::Unknown => SafeString::new(""),
|
||||||
@ -225,6 +224,19 @@ impl Media {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
|
||||||
|
let uid = user.iter().map(|u| u.id).collect::<Vec<_>>();
|
||||||
|
Box::new(move |id| {
|
||||||
|
let media = Media::get(conn, id).ok()?;
|
||||||
|
// if owner is user or check is disabled
|
||||||
|
if uid.contains(&media.owner_id) || uid.is_empty() {
|
||||||
|
Some((media.url(conn).ok()?, media.content_warning))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -207,17 +207,19 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
|||||||
let domain = &Instance::get_local(&conn)
|
let domain = &Instance::get_local(&conn)
|
||||||
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
|
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
|
||||||
.public_domain;
|
.public_domain;
|
||||||
let (content, mentions, hashtags) = md_to_html(
|
|
||||||
query.source.clone().unwrap_or_default().clone().as_ref(),
|
|
||||||
domain,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
let author = User::get(
|
let author = User::get(
|
||||||
conn,
|
conn,
|
||||||
user_id.expect("<Post as Provider>::create: no user_id error"),
|
user_id.expect("<Post as Provider>::create: no user_id error"),
|
||||||
)
|
)
|
||||||
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
|
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
|
||||||
|
|
||||||
|
let (content, mentions, hashtags) = md_to_html(
|
||||||
|
query.source.clone().unwrap_or_default().clone().as_ref(),
|
||||||
|
domain,
|
||||||
|
false,
|
||||||
|
Some(Media::get_media_processor(conn, vec![&author])),
|
||||||
|
);
|
||||||
|
|
||||||
let blog = match query.blog_id {
|
let blog = match query.blog_id {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => {
|
None => {
|
||||||
@ -757,7 +759,7 @@ impl Post {
|
|||||||
post.license = license;
|
post.license = license;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut txt_hashtags = md_to_html(&post.source, "", false)
|
let mut txt_hashtags = md_to_html(&post.source, "", false, None)
|
||||||
.2
|
.2
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_camel_case())
|
.map(|s| s.to_camel_case())
|
||||||
@ -995,7 +997,7 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save mentions and tags
|
// save mentions and tags
|
||||||
let mut hashtags = md_to_html(&post.source, "", false)
|
let mut hashtags = md_to_html(&post.source, "", false, None)
|
||||||
.2
|
.2
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_camel_case())
|
.map(|s| s.to_camel_case())
|
||||||
|
@ -19,7 +19,7 @@ lazy_static! {
|
|||||||
static ref CLEAN: Builder<'static> = {
|
static ref CLEAN: Builder<'static> = {
|
||||||
let mut b = Builder::new();
|
let mut b = Builder::new();
|
||||||
b.add_generic_attributes(iter::once("id"))
|
b.add_generic_attributes(iter::once("id"))
|
||||||
.add_tags(&["iframe", "video", "audio"])
|
.add_tags(&["iframe", "video", "audio", "label", "input"])
|
||||||
.id_prefix(Some("postcontent-"))
|
.id_prefix(Some("postcontent-"))
|
||||||
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
||||||
.add_tag_attributes(
|
.add_tag_attributes(
|
||||||
@ -27,7 +27,23 @@ lazy_static! {
|
|||||||
["width", "height", "src", "frameborder"].iter().cloned(),
|
["width", "height", "src", "frameborder"].iter().cloned(),
|
||||||
)
|
)
|
||||||
.add_tag_attributes("video", ["src", "title", "controls"].iter())
|
.add_tag_attributes("video", ["src", "title", "controls"].iter())
|
||||||
.add_tag_attributes("audio", ["src", "title", "controls"].iter());
|
.add_tag_attributes("audio", ["src", "title", "controls"].iter())
|
||||||
|
.add_tag_attributes("label", ["for"].iter())
|
||||||
|
.add_tag_attributes("input", ["type", "checked"].iter())
|
||||||
|
.add_allowed_classes("input", ["cw-checkbox"].iter())
|
||||||
|
.add_allowed_classes("span", ["cw-container", "cw-text"].iter())
|
||||||
|
.attribute_filter(|elem, att, val| match (elem, att) {
|
||||||
|
("input", "type") => Some("checkbox".into()),
|
||||||
|
("input", "checked") => Some("checked".into()),
|
||||||
|
("label", "for") => {
|
||||||
|
if val.starts_with("postcontent-cw-") {
|
||||||
|
Some(val.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Some(val.into()),
|
||||||
|
});
|
||||||
b
|
b
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -209,7 +209,13 @@ impl User {
|
|||||||
.set((
|
.set((
|
||||||
users::display_name.eq(name),
|
users::display_name.eq(name),
|
||||||
users::email.eq(email),
|
users::email.eq(email),
|
||||||
users::summary_html.eq(utils::md_to_html(&summary, "", false).0),
|
users::summary_html.eq(utils::md_to_html(
|
||||||
|
&summary,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
Some(Media::get_media_processor(conn, vec![self])),
|
||||||
|
)
|
||||||
|
.0),
|
||||||
users::summary.eq(summary),
|
users::summary.eq(summary),
|
||||||
))
|
))
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
@ -868,7 +874,7 @@ impl NewUser {
|
|||||||
display_name,
|
display_name,
|
||||||
is_admin,
|
is_admin,
|
||||||
summary: summary.to_owned(),
|
summary: summary.to_owned(),
|
||||||
summary_html: SafeString::new(&utils::md_to_html(&summary, "", false).0),
|
summary_html: SafeString::new(&utils::md_to_html(&summary, "", false, None).0),
|
||||||
email: Some(email),
|
email: Some(email),
|
||||||
hashed_password: Some(password),
|
hashed_password: Some(password),
|
||||||
instance_id: Instance::get_local(conn)?.id,
|
instance_id: Instance::get_local(conn)?.id,
|
||||||
|
@ -280,7 +280,21 @@ pub fn update(
|
|||||||
|
|
||||||
blog.title = form.title.clone();
|
blog.title = form.title.clone();
|
||||||
blog.summary = form.summary.clone();
|
blog.summary = form.summary.clone();
|
||||||
blog.summary_html = SafeString::new(&utils::md_to_html(&form.summary, "", true).0);
|
blog.summary_html = SafeString::new(
|
||||||
|
&utils::md_to_html(
|
||||||
|
&form.summary,
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
Some(Media::get_media_processor(
|
||||||
|
&conn,
|
||||||
|
blog.list_authors(&conn)
|
||||||
|
.expect("Couldn't get list of authors")
|
||||||
|
.iter()
|
||||||
|
.collect(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
);
|
||||||
blog.icon_id = form.icon;
|
blog.icon_id = form.icon;
|
||||||
blog.banner_id = form.banner;
|
blog.banner_id = form.banner;
|
||||||
blog.save_changes::<Blog>(&*conn)
|
blog.save_changes::<Blog>(&*conn)
|
||||||
|
@ -15,8 +15,8 @@ use plume_common::{
|
|||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
blogs::Blog, comments::*, db_conn::DbConn, instance::Instance, mentions::Mention, posts::Post,
|
blogs::Blog, comments::*, db_conn::DbConn, instance::Instance, medias::Media,
|
||||||
safe_string::SafeString, tags::Tag, users::User,
|
mentions::Mention, posts::Post, safe_string::SafeString, tags::Tag, users::User,
|
||||||
};
|
};
|
||||||
use routes::errors::ErrorPage;
|
use routes::errors::ErrorPage;
|
||||||
use Worker;
|
use Worker;
|
||||||
@ -49,6 +49,7 @@ pub fn create(
|
|||||||
.expect("comments::create: local instance error")
|
.expect("comments::create: local instance error")
|
||||||
.public_domain,
|
.public_domain,
|
||||||
true,
|
true,
|
||||||
|
Some(Media::get_media_processor(&conn, vec![&user])),
|
||||||
);
|
);
|
||||||
let comm = Comment::insert(
|
let comm = Comment::insert(
|
||||||
&*conn,
|
&*conn,
|
||||||
|
@ -264,6 +264,13 @@ pub fn update(
|
|||||||
.expect("posts::update: Error getting local instance")
|
.expect("posts::update: Error getting local instance")
|
||||||
.public_domain,
|
.public_domain,
|
||||||
false,
|
false,
|
||||||
|
Some(Media::get_media_processor(
|
||||||
|
&conn,
|
||||||
|
b.list_authors(&conn)
|
||||||
|
.expect("Could not get author list")
|
||||||
|
.iter()
|
||||||
|
.collect(),
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// update publication date if when this article is no longer a draft
|
// update publication date if when this article is no longer a draft
|
||||||
@ -424,6 +431,13 @@ pub fn create(
|
|||||||
.expect("post::create: local instance error")
|
.expect("post::create: local instance error")
|
||||||
.public_domain,
|
.public_domain,
|
||||||
false,
|
false,
|
||||||
|
Some(Media::get_media_processor(
|
||||||
|
&conn,
|
||||||
|
blog.list_authors(&conn)
|
||||||
|
.expect("Could not get author list")
|
||||||
|
.iter()
|
||||||
|
.collect(),
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let searcher = rockets.searcher;
|
let searcher = rockets.searcher;
|
||||||
|
@ -322,3 +322,36 @@ main .article-meta {
|
|||||||
right: 0px;
|
right: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// content warning
|
||||||
|
.cw-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"].cw-checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ .cw-container:before {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ .cw-container > .cw-text {
|
||||||
|
display: inline;
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user