fix(copy-button): fix copy code button

This commit is contained in:
thek4n 2026-07-13 19:44:44 +03:00
parent b1114217e0
commit f15e8166d1
6 changed files with 118 additions and 135 deletions

View File

@ -27,7 +27,7 @@ repos:
- id: clippy
name: clippy
language: system
entry: cargo clippy -- -W clippy::all -W clippy::pedantic
entry: cargo clippy -- -D clippy::all -D clippy::pedantic
pass_filenames: false
always_run: true

View File

@ -2,3 +2,5 @@
* [X] ~~Сделать кнопку скопировать путь~~
* [X] ~~Сделать кнопку скопировать путь вместе с командой (`note edit ...`)~~
* [X] ~~Сделать отображение типа кода и кнопку скопировать код~~
* [ ] Починить заголовки таблицы

View File

@ -28,7 +28,6 @@ use syntect::parsing::SyntaxSet;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use webbrowser;
use clap::Parser;
use std::io;
@ -152,10 +151,8 @@ async fn main() {
if args.browser {
let mut url = format!("http://{actual_addr}");
if args.random {
if !state.is_root_file {
url = format!("{url}/random")
}
if args.random && !state.is_root_file {
url = format!("{url}/random");
}
let _ = webbrowser::open(url.as_str());
}
@ -185,12 +182,9 @@ async fn root_handler(State(state): State<AppState>) -> impl IntoResponse {
}
}
async fn render_single_file(
file_path: &StdPath,
) -> Result<NoteTemplate, StatusCode> {
let metadata = match fs::metadata(file_path).await {
Ok(m) => m,
Err(_) => return Err(StatusCode::NOT_FOUND),
async fn render_single_file(file_path: &StdPath) -> Result<NoteTemplate, StatusCode> {
let Ok(metadata) = fs::metadata(file_path).await else {
return Err(StatusCode::NOT_FOUND);
};
if metadata.is_dir() {
@ -203,9 +197,8 @@ async fn render_single_file(
.unwrap_or("")
.to_lowercase();
let content = match fs::read_to_string(file_path).await {
Ok(c) => c,
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
let Ok(content) = fs::read_to_string(file_path).await else {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
};
let display_path = "";
@ -216,12 +209,7 @@ async fn render_single_file(
let html_content = if extension == "md" {
markdown_to_html(&content, display_path)
} else {
code_to_html(
&content,
extension.as_str(),
&ss,
&ts,
)
code_to_html(&content, extension.as_str(), &ss, &ts)
};
let filename = file_path
@ -275,9 +263,8 @@ async fn serve_file(
return Err(StatusCode::FORBIDDEN);
}
let metadata = match fs::metadata(&safe_path).await {
Ok(m) => m,
Err(_) => return Err(StatusCode::NOT_FOUND),
let Ok(metadata) = fs::metadata(&safe_path).await else {
return Err(StatusCode::NOT_FOUND);
};
if metadata.is_dir() {
@ -299,17 +286,15 @@ async fn serve_file(
);
if is_image {
let file_content = match fs::read(&safe_path).await {
Ok(content) => content,
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
let Ok(file_content) = fs::read(&safe_path).await else {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
};
let mime_type = from_path(&safe_path).first_or_octet_stream();
return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response());
}
let content = match fs::read_to_string(&safe_path).await {
Ok(c) => c,
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
let Ok(content) = fs::read_to_string(&safe_path).await else {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
};
let ss = SyntaxSet::load_defaults_newlines();
@ -318,12 +303,7 @@ async fn serve_file(
let html_content = if extension == "md" {
markdown_to_html(&content, &full_path)
} else {
code_to_html(
&content,
extension.as_str(),
&ss,
&ts,
)
code_to_html(&content, extension.as_str(), &ss, &ts)
};
let filename = safe_path
@ -365,9 +345,8 @@ async fn render_directory_index(
dir_path: &StdPath,
request_path: &str,
) -> Result<DirectoryTemplate, StatusCode> {
let mut entries = match fs::read_dir(dir_path).await {
Ok(list) => list,
Err(_) => return Err(StatusCode::FORBIDDEN),
let Ok(mut entries) = fs::read_dir(dir_path).await else {
return Err(StatusCode::FORBIDDEN);
};
let mut files: Vec<FileEntry> = Vec::new();
@ -388,7 +367,7 @@ async fn render_directory_index(
let link = if request_path.is_empty() {
file_name.clone()
} else {
format!("{}/{}", request_path, file_name)
format!("{request_path}/{file_name}")
};
files.push(FileEntry {
@ -513,9 +492,8 @@ async fn random_file(State(state): State<AppState>) -> impl IntoResponse {
let mut rng = rand::thread_rng();
let random_path = files.choose(&mut rng).unwrap();
let relative_path = match random_path.strip_prefix(&state.root) {
Ok(p) => p,
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
let Ok(relative_path) = random_path.strip_prefix(&state.root) else {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
};
let url_path = relative_path.to_string_lossy().replace('\\', "/");

View File

@ -3,6 +3,8 @@ use pulldown_cmark::{
TextMergeStream,
};
use std::fmt::Write;
pub fn markdown_to_html(markdown: &str, _file_path: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
@ -51,21 +53,21 @@ pub fn markdown_to_html(markdown: &str, _file_path: &str) -> String {
Event::Code(code) => {
html_output.push_str(format!("<code>{}</code>", escape_html(&code)).as_str());
}
Event::SoftBreak => html_output.push_str(" "),
Event::SoftBreak => html_output.push(' '),
Event::HardBreak => html_output.push_str("<br>"),
Event::Html(html) => html_output.push_str(&html),
Event::Rule => html_output.push_str("<hr>"),
Event::FootnoteReference(fref) => html_output.push_str(&format!(
"<sup><a href=\"#{}\" id=\"{}\"></a></sup>",
fref, fref
)),
Event::FootnoteReference(fref) => {
let _ = write!(
html_output,
"<sup><a href=\"#{fref}\" id=\"{fref}\"></a></sup>"
);
}
Event::InlineHtml(_) => (),
Event::InlineMath(_) => (),
Event::DisplayMath(_) => (),
Event::InlineHtml(_) | Event::InlineMath(_) | Event::DisplayMath(_) => (),
}
}
@ -94,31 +96,30 @@ fn render_start_tag(tag: Tag<'_>) -> String {
dest_url,
title,
..
} => output.push_str(render_link(link_type, dest_url, title).as_str()),
} => output.push_str(render_link(link_type, &dest_url, &title).as_str()),
Tag::Image {
link_type,
dest_url,
title,
..
} => output.push_str(render_image(link_type, dest_url, title).as_str()),
} => output.push_str(render_image(link_type, &dest_url, &title).as_str()),
Tag::Table(_) => output.push_str("<table>"), // TODO: fix table
Tag::Table(_) => output.push_str("<table>"),
Tag::TableHead => output.push_str("<thead>"),
Tag::TableCell => output.push_str("<td>"),
Tag::TableRow => output.push_str("<tr>"),
Tag::HtmlBlock => (), // TODO: implement
Tag::FootnoteDefinition(fref) => {
output.push_str(&format!("<sup><a href=\"#{}\" id=\"{}\">", fref, fref))
let _ = write!(output, "<sup><a href=\"#{fref}\" id=\"{fref}\">");
}
Tag::DefinitionList => (),
Tag::DefinitionListTitle => (),
Tag::DefinitionListDefinition => (),
Tag::Superscript => (),
Tag::Subscript => (),
Tag::MetadataBlock(_) => (),
Tag::HtmlBlock
| Tag::DefinitionList
| Tag::DefinitionListTitle
| Tag::DefinitionListDefinition
| Tag::Superscript
| Tag::Subscript
| Tag::MetadataBlock(_) => (),
}
output
@ -137,7 +138,7 @@ fn render_end_tag(tag: TagEnd) -> String {
TagEnd::List(false) => output.push_str("</ul>"),
TagEnd::Item => output.push_str("</li>"),
TagEnd::Paragraph => output.push_str("</p>"),
TagEnd::CodeBlock => output.push_str("</code></pre>"),
TagEnd::CodeBlock => output.push_str("</code></pre></div>"),
TagEnd::Emphasis => output.push_str("</em>"),
TagEnd::Strong => output.push_str("</strong>"),
TagEnd::BlockQuote(_) => output.push_str("</blockquote>"),
@ -150,78 +151,81 @@ fn render_end_tag(tag: TagEnd) -> String {
TagEnd::TableRow => output.push_str("</tr>"),
TagEnd::FootnoteDefinition => output.push_str("</a></sup>"),
TagEnd::HtmlBlock => (), // TODO: implement
TagEnd::DefinitionList => (),
TagEnd::DefinitionListTitle => (),
TagEnd::DefinitionListDefinition => (),
TagEnd::Superscript => (),
TagEnd::Subscript => (),
TagEnd::MetadataBlock(_) => (),
TagEnd::HtmlBlock
| TagEnd::DefinitionList
| TagEnd::DefinitionListTitle
| TagEnd::DefinitionListDefinition
| TagEnd::Superscript
| TagEnd::Subscript
| TagEnd::MetadataBlock(_) => (),
}
output
}
fn render_codeblock(block_kind: CodeBlockKind<'_>) -> String {
let header = String::from(
r#"
<div class="code-block-wrapper">
<div class="code-header">
<span class="code-lang">%CODE%</span> <button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre>"#,
);
let mut output = String::new();
match block_kind {
CodeBlockKind::Indented => "<pre><code>".to_string(),
CodeBlockKind::Indented => {
output.push_str(&header.replace("%CODE%", "code"));
output.push_str("<code>");
}
CodeBlockKind::Fenced(CowStr::Borrowed("mermaid")) => {
"<pre><code class=\"language-text mermaid\">".to_string()
output.push_str(&header.replace("%CODE%", "mermaid"));
output.push_str("<code class=\"language-text mermaid\">");
}
CodeBlockKind::Fenced(CowStr::Borrowed("rawmermaid")) => {
"<pre><code class=\"language-text\">".to_string()
output.push_str(&header.replace("%CODE%", "mermaid"));
output.push_str("<code class=\"language-text\">");
}
CodeBlockKind::Fenced(code_type) => format!("<pre><code class=\"language-{}\">", code_type),
CodeBlockKind::Fenced(code_type) => {
output.push_str(&header.replace("%CODE%", &code_type));
output.push_str(format!("<code class=\"language-{code_type}\">").as_str());
}
}
output
}
fn render_blockquote(quote_kind: Option<BlockQuoteKind>) -> String {
match quote_kind {
None => {
"<blockquote>".to_string()
},
Some(BlockQuoteKind::Note) => "<blockquote><div class=\"markdown-alert markdown-alert-note\"><p class=\"markdown-alert-title\">Note</p>".to_string(),
Some(BlockQuoteKind::Tip) => "<blockquote><div class=\"markdown-alert markdown-alert-tip\"><p class=\"markdown-alert-title\">Tip</p>".to_string(),
Some(BlockQuoteKind::Important) =>"<blockquote><div class=\"markdown-alert markdown-alert-important\"><p class=\"markdown-alert-title\">Important</p>".to_string(),
Some(BlockQuoteKind::Warning) => "<blockquote><div class=\"markdown-alert markdown-alert-warning\"><p class=\"markdown-alert-title\">Warning</p>".to_string(),
Some(BlockQuoteKind::Caution) => "<blockquote><div class=\"markdown-alert markdown-alert-caution\"><p class=\"markdown-alert-title\">Caution</p>".to_string(),
}
let alert_class_and_title = match quote_kind {
None => return "<blockquote>".to_string(),
Some(BlockQuoteKind::Note) => ("note", "Note"),
Some(BlockQuoteKind::Tip) => ("tip", "Tip"),
Some(BlockQuoteKind::Important) => ("important", "Important"),
Some(BlockQuoteKind::Warning) => ("warning", "Warning"),
Some(BlockQuoteKind::Caution) => ("caution", "Caution"),
};
format!(
"<blockquote><div class=\"markdown-alert markdown-alert-{}\"><p class=\"markdown-alert-title\">{}</p>",
alert_class_and_title.0, alert_class_and_title.1
)
}
fn render_link(link_type: LinkType, dest_url: CowStr<'_>, title: CowStr<'_>) -> String {
fn render_link(link_type: LinkType, dest_url: &CowStr<'_>, title: &CowStr<'_>) -> String {
match link_type {
LinkType::Inline => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::Reference => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::ReferenceUnknown => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::Collapsed => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::CollapsedUnknown => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::Shortcut => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::ShortcutUnknown => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::Autolink => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::Email => format!("<a href=\"{}\" title=\"{}\">", dest_url, title),
LinkType::WikiLink { has_pothole: _ } => {
// TODO: refactor
format!("<a href=\"{}\" title=\"{}\">", dest_url, title)
}
LinkType::Email => format!("<a href=\"mailto:{dest_url}\" title=\"{title}\">"),
_ => format!("<a href=\"{dest_url}\" title=\"{title}\">"),
}
}
fn render_image(link_type: LinkType, dest_url: CowStr<'_>, title: CowStr<'_>) -> String {
match link_type {
LinkType::Inline => format!("<img src=\"{}\" alt=\"{}\">", dest_url, title),
LinkType::Reference => format!("<img src=\"{}\" title=\"{}\">", dest_url, title),
LinkType::ReferenceUnknown => format!("<img src=\"{}\" alt=\"{}\">", dest_url, title),
LinkType::Collapsed => format!("<img src=\"{}\" alt=\"{}\">", dest_url, title),
LinkType::CollapsedUnknown => format!("<img src=\"{}\" alt=\"{}\">", dest_url, title),
LinkType::Shortcut => format!("<img src=\"{}\" alt=\"{}\">", dest_url, title),
LinkType::ShortcutUnknown => format!("<img src=\"{}\" alt=\"{}\">", dest_url, title),
LinkType::Autolink => format!("<img src=\"{}\" alt=\"{}\">", dest_url, title),
LinkType::Email => format!("<img src=\"{}\" alt=\"{}\">", dest_url, title),
LinkType::WikiLink { has_pothole: _ } => {
// TODO: refactor
format!("<img src=\"{}\" alt=\"{}\">", dest_url, title)
}
}
fn render_image(_link_type: LinkType, dest_url: &CowStr<'_>, title: &CowStr<'_>) -> String {
format!("<img src=\"{dest_url}\" alt=\"{title}\">")
}
fn escape_html(text: &str) -> String {

View File

@ -8,8 +8,8 @@ use syntect::parsing::SyntaxSet;
/// # Аргументы
/// * `code` - Исходный код как строка.
/// * `lang` - Идентификатор языка (например, "rust", "python", "mermaid").
/// * `ss` - Набор синтаксисов (SyntaxSet).
/// * `ts` - Набор тем (ThemeSet).
/// * `ss` - Набор синтаксисов (`SyntaxSet`).
/// * `ts` - Набор тем (`ThemeSet`).
///
/// # Возвращает
/// Строку HTML, содержащую обертку блока кода с заголовком и кнопкой копирования.
@ -32,8 +32,9 @@ pub fn code_to_html(code: &str, lang: &str, ss: &SyntaxSet, ts: &ThemeSet) -> St
let lang_display = if lang.is_empty() { "text" } else { lang };
let lang_escaped = escape_html(lang_display);
let highlighted_html = if !lang.is_empty() {
if let Some(syntax) = ss.find_syntax_by_token(lang) {
let highlighted_html = if lang.is_empty() {
escape_html(code)
} else if let Some(syntax) = ss.find_syntax_by_token(lang) {
let mut h = HighlightLines::new(syntax, theme);
let mut result_html = String::new();
@ -55,9 +56,6 @@ pub fn code_to_html(code: &str, lang: &str, ss: &SyntaxSet, ts: &ThemeSet) -> St
result_html
} else {
escape_html(code)
}
} else {
escape_html(code)
};
format!(

View File

@ -55,6 +55,7 @@
overflow: hidden;
background-color: #2b303b;
}
.code-header {
display: flex;
justify-content: space-between;