From 02aa38053096fcfdcfa3c21d9434872979a53c6a Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Mon, 6 Jan 2025 11:54:26 +0100 Subject: [PATCH 1/2] Make links clickable --- Cargo.lock | 1 + Cargo.toml | 1 + src/highlight.rs | 32 +++++++++++++++++++++++++++++--- src/themes/style.css | 10 ++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d08f358d..d743104e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,6 +2158,7 @@ dependencies = [ "parking_lot", "qrcodegen", "rand", + "regex", "reqwest", "rusqlite", "rusqlite_migration", diff --git a/Cargo.toml b/Cargo.toml index 6af5ecd6..14e7cbb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ url = "2" zstd = "0.13" parking_lot = "0.12.1" http = "1.1.0" +regex = { version = "1.11.1", default-features = false, features = ["std"] } [dev-dependencies] reqwest = { version = "0", default-features = false, features = ["cookies", "json"] } diff --git a/src/highlight.rs b/src/highlight.rs index 99fe0a2b..a07e9372 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -1,7 +1,9 @@ use crate::db::read::Entry; use crate::errors::Error; +use regex::{Captures, Regex}; use sha2::{Digest, Sha256}; use std::cmp::Ordering; +use std::collections::HashMap; use std::io::Cursor; use std::sync::LazyLock; use syntect::highlighting::ThemeSet; @@ -44,6 +46,14 @@ pub static DATA: LazyLock = LazyLock::new(|| { } }); +static LINK_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r#"https?://[^ "><'\t\n`]+"#).unwrap() +}); + +static LINK_REGEX_2: LazyLock = LazyLock::new(|| { + Regex::new(r#"LINKWASTEBIN\d+END"#).unwrap() +}); + /// Combines CSS content with a filename containing the hash of the content. pub struct Css<'a> { pub name: String, @@ -86,12 +96,21 @@ fn highlight(source: &str, ext: &str) -> Result { let mut scope_stack = ScopeStack::new(); for (mut line_number, line) in LinesWithEndings::from(source).enumerate() { - let (formatted, delta) = if line.len() > HIGHLIGHT_LINE_LENGTH_CUTOFF { + let mut links = HashMap::new(); + let (mut formatted, delta) = if line.len() > HIGHLIGHT_LINE_LENGTH_CUTOFF { (line.to_string(), 0) } else { - let parsed = parse_state.parse_line(line, &DATA.syntax_set)?; + // Add placeholder for link elements. + let line = LINK_REGEX.replace_all(&line, |x: &Captures| { + let num = links.len(); + let placeholder = format!("LINKWASTEBIN{num}END"); + links.insert(placeholder.clone(), x.get(0).unwrap().as_str().to_owned()); + placeholder + }).to_string(); + + let parsed = parse_state.parse_line(&line, &DATA.syntax_set)?; line_tokens_to_classed_spans( - line, + &line, parsed.as_slice(), ClassStyle::Spaced, &mut scope_stack, @@ -109,6 +128,13 @@ fn highlight(source: &str, ext: &str) -> Result { html.push_str(&"".repeat(delta.abs().try_into()?)); } + // Process link element placeholders. + formatted = LINK_REGEX_2.replace_all(&formatted, |x: &Captures| { + let id = x.get(0).unwrap().as_str(); + let link = links.get(id).map(|x| &**x).unwrap_or(id); + format!("{link}") + }).to_string(); + // Strip stray newlines that cause vertically stretched lines. for c in formatted.chars().filter(|c| *c != '\n') { html.push(c); diff --git a/src/themes/style.css b/src/themes/style.css index d7c18538..fd910f7f 100644 --- a/src/themes/style.css +++ b/src/themes/style.css @@ -239,6 +239,16 @@ td.line-number { user-select: text; } +.line a { + color: rgba(57, 186, 230, .7); +} +.line a:hover { + text-decoration: underline; +} +.line a:visited { + color: rgba(225, 57, 230, 0.7); +} + .center { position: absolute; left: 50%; From 79717c2a9efb0b642cd498222484d911278860ee Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Thu, 9 Jan 2025 23:54:37 +0100 Subject: [PATCH 2/2] Use syntect to properly process links --- Cargo.lock | 17 ++++++++++- Cargo.toml | 3 +- assets/LinkHighlight.sublime-syntax | 33 +++++++++++++++++++++ src/highlight.rs | 45 +++++++++-------------------- templates/base.html | 1 + templates/paste.html | 23 +++++++++++++++ 6 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 assets/LinkHighlight.sublime-syntax diff --git a/Cargo.lock b/Cargo.lock index d743104e..3fca9747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1062,6 +1062,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "litemap" version = "0.7.4" @@ -1730,6 +1736,7 @@ dependencies = [ "serde_json", "thiserror 1.0.69", "walkdir", + "yaml-rust", ] [[package]] @@ -2158,7 +2165,6 @@ dependencies = [ "parking_lot", "qrcodegen", "rand", - "regex", "reqwest", "rusqlite", "rusqlite_migration", @@ -2343,6 +2349,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 14e7cbb6..2da651fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ rusqlite_migration = { version = "1", default-features = false } rust-argon2 = "2.0.0" sha2 = "0" serde = { version = "1", features = ["derive"] } -syntect = { version = "5", default-features = false, features = ["html", "plist-load", "regex-fancy"] } +syntect = { version = "5", default-features = false, features = ["html", "plist-load", "regex-fancy", "yaml-load"] } thiserror = "2" time = { version = "0.3", features = ["macros", "serde"] } tokio = { version = "1", features = ["full"] } @@ -34,7 +34,6 @@ url = "2" zstd = "0.13" parking_lot = "0.12.1" http = "1.1.0" -regex = { version = "1.11.1", default-features = false, features = ["std"] } [dev-dependencies] reqwest = { version = "0", default-features = false, features = ["cookies", "json"] } diff --git a/assets/LinkHighlight.sublime-syntax b/assets/LinkHighlight.sublime-syntax new file mode 100644 index 00000000..44a6004f --- /dev/null +++ b/assets/LinkHighlight.sublime-syntax @@ -0,0 +1,33 @@ +%YAML 1.2 +--- +# http://www.sublimetext.com/docs/3/syntax.html +name: LinkHighlight +file_extensions: + - link_highlight +scope: text.highlighting +contexts: + main: + - match: (<)((?:https?|ftp)://.*?)(>) + scope: meta.link.inet.markdown + captures: + 1: punctuation.definition.link.begin.markdown + 2: markup.underline.link.markdown + 3: punctuation.definition.link.end.markdown + - match: (((https|http|ftp)://)|www\.)[\w-]+(\.[\w-]+)+ + scope: markup.underline.link.markdown + push: # After a valid domain, zero or more non-space non-< characters may follow + - match: (?=[?!.,:*_~]*[\s<]) # Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not be considered part of the autolink, though they may be included in the interior of the link + pop: true + - match: (?={{html_entity}}[?!.,:*_~]*[\s<]) # If an autolink ends in a semicolon (;), we check to see if it appears to resemble an entity reference; if the preceding text is & followed by one or more alphanumeric characters. If so, it is excluded from the autolink + pop: true + - match: \( # When an autolink ends in ), we scan the entire autolink for the total number of parentheses. If there is a greater number of closing parentheses than opening ones, we don’t consider the last character part of the autolink, in order to facilitate including an autolink inside a parenthesis + push: + - meta_scope: markup.underline.link.markdown + - match: (?=[?!.,:*_~]*[\s<]) + pop: true + - match: \) + pop: true + - match: (?=\)[?!.,:*_~]*[\s<]) + pop: true + - match: '[^?!.,:*_~\s<&()]+|\S' + scope: markup.underline.link.markdown diff --git a/src/highlight.rs b/src/highlight.rs index a07e9372..d22f4f69 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -1,14 +1,12 @@ use crate::db::read::Entry; use crate::errors::Error; -use regex::{Captures, Regex}; use sha2::{Digest, Sha256}; use std::cmp::Ordering; -use std::collections::HashMap; use std::io::Cursor; use std::sync::LazyLock; use syntect::highlighting::ThemeSet; use syntect::html::{css_for_theme_with_class_style, line_tokens_to_classed_spans, ClassStyle}; -use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}; +use syntect::parsing::{ParseState, ScopeStack, SyntaxDefinition, SyntaxReference, SyntaxSet}; use syntect::util::LinesWithEndings; const HIGHLIGHT_LINE_LENGTH_CUTOFF: usize = 2048; @@ -34,6 +32,11 @@ pub static DATA: LazyLock = LazyLock::new(|| { let dark = Css::new("dark", &DARK_CSS); let syntax_set: SyntaxSet = syntect::dumps::from_binary(include_bytes!("../assets/newlines.packdump")); + let link_highlighting = SyntaxDefinition::load_from_str( + include_str!("../assets/LinkHighlight.sublime-syntax"), false, None).expect("loading link style"); + let mut builder = syntax_set.into_builder(); + builder.add(link_highlighting); + let syntax_set = builder.build(); let mut syntaxes = syntax_set.syntaxes().to_vec(); syntaxes.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap_or(Ordering::Less)); @@ -46,14 +49,6 @@ pub static DATA: LazyLock = LazyLock::new(|| { } }); -static LINK_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r#"https?://[^ "><'\t\n`]+"#).unwrap() -}); - -static LINK_REGEX_2: LazyLock = LazyLock::new(|| { - Regex::new(r#"LINKWASTEBIN\d+END"#).unwrap() -}); - /// Combines CSS content with a filename containing the hash of the content. pub struct Css<'a> { pub name: String, @@ -82,12 +77,13 @@ impl<'a> Css<'a> { } fn highlight(source: &str, ext: &str) -> Result { - let syntax_ref = DATA - .syntax_set - .find_syntax_by_extension(ext) + let syntax_ref = (ext != "txt").then(|| + DATA + .syntax_set + .find_syntax_by_extension(ext)).flatten() .unwrap_or_else(|| { DATA.syntax_set - .find_syntax_by_extension("txt") + .find_syntax_by_extension("link_highlight") .expect("finding txt syntax") }); @@ -96,18 +92,10 @@ fn highlight(source: &str, ext: &str) -> Result { let mut scope_stack = ScopeStack::new(); for (mut line_number, line) in LinesWithEndings::from(source).enumerate() { - let mut links = HashMap::new(); - let (mut formatted, delta) = if line.len() > HIGHLIGHT_LINE_LENGTH_CUTOFF { + // let mut links = HashMap::new(); + let (formatted, delta) = if line.len() > HIGHLIGHT_LINE_LENGTH_CUTOFF { (line.to_string(), 0) } else { - // Add placeholder for link elements. - let line = LINK_REGEX.replace_all(&line, |x: &Captures| { - let num = links.len(); - let placeholder = format!("LINKWASTEBIN{num}END"); - links.insert(placeholder.clone(), x.get(0).unwrap().as_str().to_owned()); - placeholder - }).to_string(); - let parsed = parse_state.parse_line(&line, &DATA.syntax_set)?; line_tokens_to_classed_spans( &line, @@ -128,13 +116,6 @@ fn highlight(source: &str, ext: &str) -> Result { html.push_str(&"".repeat(delta.abs().try_into()?)); } - // Process link element placeholders. - formatted = LINK_REGEX_2.replace_all(&formatted, |x: &Captures| { - let id = x.get(0).unwrap().as_str(); - let link = links.get(id).map(|x| &**x).unwrap_or(id); - format!("{link}") - }).to_string(); - // Strip stray newlines that cause vertically stretched lines. for c in formatted.chars().filter(|c| *c != '\n') { html.push(c); diff --git a/templates/base.html b/templates/base.html index ff4ac602..0e1b1af6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -23,6 +23,7 @@
{% block content %}{% endblock %} + {% block content_post %}{% endblock %}
diff --git a/templates/paste.html b/templates/paste.html index 763dd213..c5e0f92f 100644 --- a/templates/paste.html +++ b/templates/paste.html @@ -53,3 +53,26 @@
  • {% endblock %} + +{% block content_post %} + +{% endblock %}