feat: add syntect code highlighting with catppuccin themes
- Add syntect dependency (server feature, fancy-regex backend) - Create highlight module with LazyLock globals for SyntaxSet + themes - Intercept CodeBlock events in markdown rendering for syntax highlighting - Update ammonia whitelist to allow span/pre/code class/style attributes - Add generate_highlight_css binary for CSS generation - Add highlight-css Makefile target (runs before tailwindcss) - Import generated highlight.css in input.css - Remove hardcoded code block colors, let catppuccin CSS take over
This commit is contained in:
parent
37d95e6a33
commit
11261836c7
135
Cargo.lock
generated
135
Cargo.lock
generated
@ -2,6 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@ -233,6 +239,30 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
@ -572,6 +602,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@ -1482,12 +1521,33 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@ -2276,6 +2336,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@ -2484,6 +2550,16 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
@ -2780,6 +2856,19 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"indexmap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postgres-protocol"
|
||||
version = "0.6.11"
|
||||
@ -2915,6 +3004,15 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@ -3437,6 +3535,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
@ -3617,6 +3721,27 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syntect"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"fancy-regex",
|
||||
"flate2",
|
||||
"fnv",
|
||||
"once_cell",
|
||||
"plist",
|
||||
"regex-syntax",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
@ -4868,6 +4993,15 @@ version = "0.8.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
|
||||
|
||||
[[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 = "yggdrasil"
|
||||
version = "0.1.0"
|
||||
@ -4885,6 +5019,7 @@ dependencies = [
|
||||
"rand 0.8.6",
|
||||
"regex",
|
||||
"serde",
|
||||
"syntect",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tower-http",
|
||||
|
||||
@ -22,6 +22,7 @@ rand = { version = "0.8", features = ["getrandom"] }
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
http = "1"
|
||||
ammonia = { version = "4", optional = true }
|
||||
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "default-fancy", "html", "parsing", "dump-load"], optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
web-sys = { version = "0.3", features = ["Document", "Window", "HtmlDocument", "Storage", "Element", "DomTokenList", "MediaQueryList", "HtmlScriptElement"] }
|
||||
@ -50,4 +51,5 @@ server = [
|
||||
"dep:tracing-subscriber",
|
||||
"dep:tower-http",
|
||||
"dep:ammonia",
|
||||
"dep:syntect",
|
||||
]
|
||||
|
||||
6
Makefile
6
Makefile
@ -1,10 +1,14 @@
|
||||
.PHONY: dev build css css-watch clean build-editor
|
||||
.PHONY: dev build css css-watch clean build-editor highlight-css
|
||||
|
||||
build:
|
||||
@$(MAKE) build-editor
|
||||
@$(MAKE) highlight-css
|
||||
@tailwindcss -i input.css -o public/style.css --minify
|
||||
@dx build --release
|
||||
|
||||
highlight-css:
|
||||
@cargo run --bin generate_highlight_css
|
||||
|
||||
build-editor:
|
||||
@echo "Building Tiptap editor..."
|
||||
@cd libs/tiptap-editor && npm install && npx vite build
|
||||
|
||||
1895
generated/highlight.css
Normal file
1895
generated/highlight.css
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@import "./generated/highlight.css";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@ -342,7 +343,6 @@
|
||||
.md-content pre {
|
||||
position: relative;
|
||||
margin-bottom: var(--content-gap-paper);
|
||||
background: var(--color-paper-code-block);
|
||||
border-radius: var(--radius-paper);
|
||||
overflow-x: auto;
|
||||
}
|
||||
@ -350,7 +350,6 @@
|
||||
.md-content pre code {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
color: rgb(213, 213, 214);
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
overflow-x: auto;
|
||||
|
||||
@ -172,10 +172,13 @@ async fn ensure_unique_slug(
|
||||
fn clean_html(input: &str) -> String {
|
||||
let mut builder = ammonia::Builder::default();
|
||||
builder
|
||||
.add_generic_attributes(&["class", "aria-hidden", "aria-label", "id", "role", "accesskey", "title"])
|
||||
.add_generic_attributes(&["class", "style", "aria-hidden", "aria-label", "id", "role", "accesskey", "title"])
|
||||
.add_tags(&["details", "summary"])
|
||||
.url_relative(ammonia::UrlRelative::PassThrough)
|
||||
.add_tag_attributes("a", &["class", "aria-hidden", "aria-label"])
|
||||
.add_tag_attributes("span", &["class", "style"])
|
||||
.add_tag_attributes("pre", &["class", "style"])
|
||||
.add_tag_attributes("code", &["class", "style"])
|
||||
.add_tag_attributes("h1", &["id", "class"])
|
||||
.add_tag_attributes("h2", &["id", "class"])
|
||||
.add_tag_attributes("h3", &["id", "class"])
|
||||
@ -243,12 +246,14 @@ fn render_markdown_enhanced(md: &str) -> RenderedContent {
|
||||
let mut html = String::new();
|
||||
let mut heading_idx = 0;
|
||||
let mut in_heading = false;
|
||||
let mut in_codeblock = false;
|
||||
let mut code_lang: Option<String> = None;
|
||||
let mut code_buffer = String::new();
|
||||
let mut non_heading_events: Vec<Event> = Vec::new();
|
||||
|
||||
for event in parser {
|
||||
match event {
|
||||
Event::Start(Tag::Heading { level, .. }) => {
|
||||
// Flush non-heading events first
|
||||
if !non_heading_events.is_empty() {
|
||||
pulldown_cmark::html::push_html(&mut html, non_heading_events.into_iter());
|
||||
non_heading_events = Vec::new();
|
||||
@ -286,9 +291,39 @@ fn render_markdown_enhanced(md: &str) -> RenderedContent {
|
||||
}
|
||||
in_heading = false;
|
||||
}
|
||||
Event::Start(Tag::CodeBlock(kind)) => {
|
||||
if !non_heading_events.is_empty() {
|
||||
pulldown_cmark::html::push_html(&mut html, non_heading_events.into_iter());
|
||||
non_heading_events = Vec::new();
|
||||
}
|
||||
in_codeblock = true;
|
||||
code_lang = match kind {
|
||||
pulldown_cmark::CodeBlockKind::Fenced(lang) => {
|
||||
if lang.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(lang.to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
code_buffer.clear();
|
||||
}
|
||||
Event::Text(text) if in_codeblock => {
|
||||
code_buffer.push_str(&text);
|
||||
}
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
let highlighted =
|
||||
crate::highlight::server::highlight_code(&code_buffer, code_lang.as_deref());
|
||||
html.push_str("<pre><code>");
|
||||
html.push_str(&highlighted);
|
||||
html.push_str("</code></pre>");
|
||||
in_codeblock = false;
|
||||
code_lang = None;
|
||||
code_buffer.clear();
|
||||
}
|
||||
_ => {
|
||||
if in_heading {
|
||||
// Manually render heading content
|
||||
match event {
|
||||
Event::Text(text) => html.push_str(&clean_html(&text)),
|
||||
Event::Code(code) => {
|
||||
@ -298,7 +333,7 @@ fn render_markdown_enhanced(md: &str) -> RenderedContent {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
} else if !in_codeblock {
|
||||
non_heading_events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
104
src/bin/generate_highlight_css.rs
Normal file
104
src/bin/generate_highlight_css.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
|
||||
|
||||
fn main() {
|
||||
let latte = ThemeSet::get_theme("themes/Catppuccin Latte.tmTheme")
|
||||
.expect("Failed to load Catppuccin Latte theme");
|
||||
let mocha = ThemeSet::get_theme("themes/Catppuccin Mocha.tmTheme")
|
||||
.expect("Failed to load Catppuccin Mocha theme");
|
||||
|
||||
let latte_css = css_for_theme_with_class_style(&latte, ClassStyle::Spaced)
|
||||
.expect("Failed to generate Latte CSS");
|
||||
let mocha_css = css_for_theme_with_class_style(&mocha, ClassStyle::Spaced)
|
||||
.expect("Failed to generate Mocha CSS");
|
||||
|
||||
let latte_clean = strip_comments(&latte_css);
|
||||
let mocha_clean = strip_comments(&mocha_css);
|
||||
|
||||
let latte_rewritten = rewrite_rules(&latte_clean, ".md-content pre code", "");
|
||||
let mocha_rewritten = rewrite_rules(&mocha_clean, ".md-content pre code", ".dark ");
|
||||
|
||||
let mut output = String::new();
|
||||
output.push_str("/* Auto-generated by generate_highlight_css — DO NOT EDIT */\n\n");
|
||||
output.push_str("/* Catppuccin Latte (light) */\n");
|
||||
output.push_str(&latte_rewritten);
|
||||
output.push_str("\n/* Catppuccin Mocha (dark) */\n");
|
||||
output.push_str(&mocha_rewritten);
|
||||
|
||||
std::fs::create_dir_all("generated").expect("Failed to create generated/");
|
||||
std::fs::write("generated/highlight.css", output)
|
||||
.expect("Failed to write generated/highlight.css");
|
||||
|
||||
println!("Generated generated/highlight.css");
|
||||
}
|
||||
|
||||
fn strip_comments(css: &str) -> String {
|
||||
let mut result = String::with_capacity(css.len());
|
||||
let chars: Vec<char> = css.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {
|
||||
while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {
|
||||
i += 1;
|
||||
}
|
||||
i += 2;
|
||||
} else {
|
||||
result.push(chars[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn rewrite_rules(css: &str, base: &str, prefix: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut pos = 0;
|
||||
let bytes = css.as_bytes();
|
||||
|
||||
while pos < bytes.len() {
|
||||
let rest = &css[pos..];
|
||||
|
||||
let open = match rest.find('{') {
|
||||
Some(i) => i,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let selector = rest[..open].trim();
|
||||
if selector.is_empty() {
|
||||
pos += open + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let after_open = pos + open + 1;
|
||||
let mut depth = 1usize;
|
||||
let mut close = after_open;
|
||||
while close < bytes.len() && depth > 0 {
|
||||
if bytes[close] == b'{' {
|
||||
depth += 1;
|
||||
} else if bytes[close] == b'}' {
|
||||
depth -= 1;
|
||||
}
|
||||
close += 1;
|
||||
}
|
||||
|
||||
let body = css[after_open..close - 1].trim();
|
||||
pos = close;
|
||||
|
||||
if body.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if selector == ".code" {
|
||||
out.push_str(&format!("{}{} {{\n{}\n}}\n\n", prefix, base, body));
|
||||
} else {
|
||||
let rewritten = selector
|
||||
.split(',')
|
||||
.map(|s| format!("{}{} {}", prefix, base, s.trim()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",\n");
|
||||
out.push_str(&format!("{} {{\n{}\n}}\n\n", rewritten, body));
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
68
src/highlight.rs
Normal file
68
src/highlight.rs
Normal file
@ -0,0 +1,68 @@
|
||||
#[cfg(feature = "server")]
|
||||
pub mod server {
|
||||
use std::sync::{LazyLock, OnceLock};
|
||||
|
||||
use syntect::highlighting::{Theme, ThemeSet};
|
||||
use syntect::html::{ClassedHTMLGenerator, ClassStyle};
|
||||
use syntect::parsing::{SyntaxReference, SyntaxSet};
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
static SYNTAX_SET: LazyLock<SyntaxSet> =
|
||||
LazyLock::new(|| SyntaxSet::load_defaults_newlines());
|
||||
|
||||
static LATTE_THEME: LazyLock<Theme> = LazyLock::new(|| {
|
||||
ThemeSet::get_theme("themes/Catppuccin Latte.tmTheme")
|
||||
.expect("Failed to load Catppuccin Latte theme")
|
||||
});
|
||||
|
||||
static MOCHA_THEME: LazyLock<Theme> = LazyLock::new(|| {
|
||||
ThemeSet::get_theme("themes/Catppuccin Mocha.tmTheme")
|
||||
.expect("Failed to load Catppuccin Mocha theme")
|
||||
});
|
||||
|
||||
static FALLBACK_SYNTAX: OnceLock<SyntaxReference> = OnceLock::new();
|
||||
|
||||
fn find_syntax(lang: Option<&str>) -> &'static SyntaxReference {
|
||||
let ss = &*SYNTAX_SET;
|
||||
if let Some(lang) = lang {
|
||||
if !lang.is_empty() {
|
||||
if let Some(s) = ss.find_syntax_by_extension(lang) {
|
||||
return s;
|
||||
}
|
||||
if let Some(s) = ss.find_syntax_by_name(lang) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
let plain = ss
|
||||
.find_syntax_by_extension("txt")
|
||||
.or_else(|| ss.find_syntax_by_name("Plain Text"))
|
||||
.expect("no plain text syntax");
|
||||
FALLBACK_SYNTAX.get_or_init(|| plain.clone())
|
||||
}
|
||||
|
||||
pub fn highlight_code(code: &str, lang: Option<&str>) -> String {
|
||||
let syntax = find_syntax(lang);
|
||||
let ss = &*SYNTAX_SET;
|
||||
let mut generator =
|
||||
ClassedHTMLGenerator::new_with_class_style(syntax, ss, ClassStyle::Spaced);
|
||||
|
||||
for line in LinesWithEndings::from(code) {
|
||||
let _ = generator.parse_html_for_line_which_includes_newline(line);
|
||||
}
|
||||
|
||||
generator.finalize()
|
||||
}
|
||||
|
||||
pub fn get_latte_theme() -> &'static Theme {
|
||||
&*LATTE_THEME
|
||||
}
|
||||
|
||||
pub fn get_mocha_theme() -> &'static Theme {
|
||||
&*MOCHA_THEME
|
||||
}
|
||||
|
||||
pub fn get_syntax_set() -> &'static SyntaxSet {
|
||||
&*SYNTAX_SET
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ mod auth;
|
||||
mod components;
|
||||
mod context;
|
||||
mod db;
|
||||
mod highlight;
|
||||
mod hooks;
|
||||
mod models;
|
||||
mod pages;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user