From 6e09d4881ffcda39f7d3c6d6d712cd83048093f6 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 15 Feb 2024 12:30:51 +0100 Subject: [PATCH] Add markdown syntax for article links --- README.md | 2 +- src/frontend/app.rs | 11 +--- src/frontend/markdown.rs | 80 ++++++++++++++++++++++++++++++ src/frontend/mod.rs | 14 ++++++ src/frontend/pages/article/read.rs | 10 +--- 5 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 src/frontend/markdown.rs diff --git a/README.md b/README.md index bf2851d..493b0b7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ You can start by reading the main page which is rendered from Markdown. In the " To continue, register a new account and you are logged in immediately without further confirmation. If you are admin of a newly created instance, login to the automatically created admin account instead. Login details are specified in the config file (by default user `ibis` and password also `ibis`). -On a new instance only the default Main Page will be shown. Use "Create Article" to create a new one. You have to enter the title, text and a summary of the edit. Afterwards press the submit button, and you are redirected to the new article. You can also make changes to existing articles with the "Edit" button at the top. If multiple users attempt to edit an article at the same time, Ibis will attempt to merge the changes automatically. If this is unsuccessful, the user has to perform a manual merge (again like in git). For remote articles, there is additionally a "Fork" option under the "Actions" tab. This allows copying a remote article including the full change history to the local instance. It can be useful if the original instance is dead, or if there are disagreements how the article should be written. +On a new instance only the default Main Page will be shown. Use "Create Article" to create a new one. You have to enter the title, text and a summary of the edit. Afterwards press the submit button, and you are redirected to the new article. You can also make changes to existing articles with the "Edit" button at the top. The article text uses standard markdown. Additionally you can link to other articles with `[[Title@example.com]]`. If multiple users attempt to edit an article at the same time, Ibis will attempt to merge the changes automatically. If this is unsuccessful, the user has to perform a manual merge (again like in git). For remote articles, there is additionally a "Fork" option under the "Actions" tab. This allows copying a remote article including the full change history to the local instance. It can be useful if the original instance is dead, or if there are disagreements how the article should be written. To kickstart federation, paste the domain of a remote instance into the search field, eg `https://example.com`. This will fetch the instance data over Activitypub, and also fetch all articles to make them available locally. The search page will show a link to the instance details page. Here you can follow the instance, so that new articles and edits are automatically federated to your local instance. You can also fetch individual articles from remote instances by pasting the URL into the search field. diff --git a/src/frontend/app.rs b/src/frontend/app.rs index 8736bfe..f42da6a 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -1,5 +1,6 @@ use crate::common::LocalUserView; use crate::frontend::api::ApiClient; +use crate::frontend::backend_hostname; use crate::frontend::components::nav::Nav; use crate::frontend::pages::article::actions::ArticleActions; use crate::frontend::pages::article::create::CreateArticle; @@ -65,15 +66,7 @@ impl GlobalState { #[component] pub fn App() -> impl IntoView { - let backend_hostname; - #[cfg(not(feature = "ssr"))] - { - backend_hostname = web_sys::window().unwrap().location().host().unwrap(); - } - #[cfg(feature = "ssr")] - { - backend_hostname = crate::backend::config::IbisConfig::read().bind.to_string(); - } + let backend_hostname = backend_hostname(); provide_meta_context(); let backend_hostname = GlobalState { diff --git a/src/frontend/markdown.rs b/src/frontend/markdown.rs new file mode 100644 index 0000000..1af830b --- /dev/null +++ b/src/frontend/markdown.rs @@ -0,0 +1,80 @@ +use crate::frontend::backend_hostname; +use markdown_it::parser::inline::{InlineRule, InlineState}; +use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; + +pub fn markdown_parser() -> MarkdownIt { + let mut parser = MarkdownIt::new(); + markdown_it::plugins::cmark::add(&mut parser); + markdown_it::plugins::extra::add(&mut parser); + parser.inline.add_rule::(); + parser +} + +#[derive(Debug)] +pub struct ArticleLink { + title: String, + domain: String, +} + +// This defines how your custom node should be rendered. +impl NodeValue for ArticleLink { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let mut attrs = node.attrs.clone(); + + let local = backend_hostname() == self.domain; + let link = if local { + format!("/article/{}", self.title) + } else { + format!("/article/{}@{}", self.title, self.domain) + }; + attrs.push(("href", link)); + + fmt.open("a", &attrs); + fmt.text(&self.title); + fmt.close("a"); + } +} + +struct ArticleLinkScanner; + +impl InlineRule for ArticleLinkScanner { + const MARKER: char = '['; + + /// Find `[[Title@example.com]], return the position and split title/domain. + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let input = &state.src[state.pos..state.pos_max]; + if !input.starts_with("[[") { + return None; + } + const SEPARATOR_LENGTH: usize = 2; + + input.find("]]").and_then(|i| { + let start = state.pos + SEPARATOR_LENGTH; + let content = &state.src[start..i]; + content.split_once('@').map(|(title, domain)| { + let node = Node::new(ArticleLink { + title: title.to_string(), + domain: domain.to_string(), + }); + (node, i + SEPARATOR_LENGTH) + }) + }) + } +} + +#[test] +fn test_markdown_local_article_link() { + let parser = markdown_parser(); + let rendered = parser.parse("[[Title@127.0.0.1:8081]]").render(); + assert_eq!("

Title

\n", rendered); +} + +#[test] +fn test_markdown_remote_article_link() { + let parser = markdown_parser(); + let rendered = parser.parse("[[Title@example.com]]").render(); + assert_eq!( + "

Title

\n", + rendered + ); +} diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index c2db8a9..129782a 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -6,6 +6,7 @@ pub mod api; pub mod app; mod components; pub mod error; +pub mod markdown; pub mod pages; #[cfg(feature = "hydrate")] @@ -47,3 +48,16 @@ fn user_link(person: &DbPerson) -> impl IntoView { {user_title(person)} } } + +fn backend_hostname() -> String { + let backend_hostname; + #[cfg(not(feature = "ssr"))] + { + backend_hostname = web_sys::window().unwrap().location().host().unwrap(); + } + #[cfg(feature = "ssr")] + { + backend_hostname = crate::backend::config::IbisConfig::read().bind.to_string(); + } + backend_hostname +} diff --git a/src/frontend/pages/article/read.rs b/src/frontend/pages/article/read.rs index 9b808ce..53f47cf 100644 --- a/src/frontend/pages/article/read.rs +++ b/src/frontend/pages/article/read.rs @@ -1,10 +1,9 @@ use crate::frontend::article_title; use crate::frontend::components::article_nav::ArticleNav; +use crate::frontend::markdown::markdown_parser; use crate::frontend::pages::article_resource; use leptos::*; -use markdown_it::MarkdownIt; - #[component] pub fn ReadArticle() -> impl IntoView { let article = article_resource(); @@ -24,10 +23,3 @@ pub fn ReadArticle() -> impl IntoView { } } - -fn markdown_parser() -> MarkdownIt { - let mut parser = MarkdownIt::new(); - markdown_it::plugins::cmark::add(&mut parser); - markdown_it::plugins::extra::add(&mut parser); - parser -}