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 - id: clippy
name: clippy name: clippy
language: system language: system
entry: cargo clippy -- -W clippy::all -W clippy::pedantic entry: cargo clippy -- -D clippy::all -D clippy::pedantic
pass_filenames: false pass_filenames: false
always_run: true always_run: true

View File

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

View File

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

View File

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

View File

@ -8,8 +8,8 @@ use syntect::parsing::SyntaxSet;
/// # Аргументы /// # Аргументы
/// * `code` - Исходный код как строка. /// * `code` - Исходный код как строка.
/// * `lang` - Идентификатор языка (например, "rust", "python", "mermaid"). /// * `lang` - Идентификатор языка (например, "rust", "python", "mermaid").
/// * `ss` - Набор синтаксисов (SyntaxSet). /// * `ss` - Набор синтаксисов (`SyntaxSet`).
/// * `ts` - Набор тем (ThemeSet). /// * `ts` - Набор тем (`ThemeSet`).
/// ///
/// # Возвращает /// # Возвращает
/// Строку HTML, содержащую обертку блока кода с заголовком и кнопкой копирования. /// Строку HTML, содержащую обертку блока кода с заголовком и кнопкой копирования.
@ -32,30 +32,28 @@ 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_display = if lang.is_empty() { "text" } else { lang };
let lang_escaped = escape_html(lang_display); let lang_escaped = escape_html(lang_display);
let highlighted_html = if !lang.is_empty() { let highlighted_html = if lang.is_empty() {
if let Some(syntax) = ss.find_syntax_by_token(lang) { escape_html(code)
let mut h = HighlightLines::new(syntax, theme); } else if let Some(syntax) = ss.find_syntax_by_token(lang) {
let mut result_html = String::new(); let mut h = HighlightLines::new(syntax, theme);
let mut result_html = String::new();
for line in code.lines() { for line in code.lines() {
let line_with_newline = format!("{line}\n"); let line_with_newline = format!("{line}\n");
match h.highlight_line(&line_with_newline, ss) { match h.highlight_line(&line_with_newline, ss) {
Ok(regions) => { Ok(regions) => {
match styled_line_to_highlighted_html(&regions[..], IncludeBackground::No) { match styled_line_to_highlighted_html(&regions[..], IncludeBackground::No) {
Ok(html_line) => result_html.push_str(&html_line), Ok(html_line) => result_html.push_str(&html_line),
Err(_) => result_html.push_str(&escape_html(&line_with_newline)), Err(_) => result_html.push_str(&escape_html(&line_with_newline)),
}
}
Err(_) => {
result_html.push_str(&escape_html(&line_with_newline));
} }
} }
Err(_) => {
result_html.push_str(&escape_html(&line_with_newline));
}
} }
result_html
} else {
escape_html(code)
} }
result_html
} else { } else {
escape_html(code) escape_html(code)
}; };

View File

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