start refactoring

This commit is contained in:
thek4n 2026-07-13 15:32:28 +03:00
parent 71c5915628
commit 4895160309
5 changed files with 90 additions and 169 deletions

11
Cargo.lock generated
View File

@ -1276,16 +1276,23 @@ dependencies = [
[[package]]
name = "pulldown-cmark"
version = "0.9.6"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
dependencies = [
"bitflags 2.11.0",
"getopts",
"memchr",
"pulldown-cmark-escape",
"unicase",
]
[[package]]
name = "pulldown-cmark-escape"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quick-xml"
version = "0.38.4"

View File

@ -13,7 +13,7 @@ strip = true
[dependencies]
pulldown-cmark = "0.9"
pulldown-cmark = "0.13.4"
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }

View File

@ -1,4 +1,4 @@
# TODO <!-- [NOGREP] -->
* [X] Сделать кнопку скопировать путь
* [X] Сделать кнопку скопировать путь вместе с командой (`note edit ...`)
* [X] ~~Сделать кнопку скопировать путь~~
* [X] ~~Сделать кнопку скопировать путь вместе с командой (`note edit ...`)~~

View File

@ -1,183 +1,82 @@
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html};
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
use syntect::parsing::SyntaxSet;
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd, TextMergeStream};
fn normalize_lists(input: &str) -> String {
let mut result = String::with_capacity(input.len() + input.len() / 10);
for line in input.lines() {
if let Some(first_non_ws) = line.find(|c: char| !c.is_whitespace()) {
let marker_char = line.chars().nth(first_non_ws);
if let Some(marker) = marker_char {
if marker == '*' || marker == '-' || marker == '+' {
let rest_start = first_non_ws + 1;
if rest_start < line.len() {
let next_char = line.chars().nth(rest_start).unwrap();
if !next_char.is_whitespace() {
let mut marker_count = 1;
let mut is_only_markers = true;
for c in line.chars().skip(rest_start) {
if c == marker {
marker_count += 1;
} else if !c.is_whitespace() {
is_only_markers = false;
break;
}
}
if !(is_only_markers && marker_count >= 2) {
result.push_str(&line[..rest_start]);
result.push(' ');
result.push_str(&line[rest_start..]);
result.push('\n');
continue;
}
}
}
}
}
}
result.push_str(line);
result.push('\n');
}
if result.ends_with('\n') && !input.ends_with('\n') {
result.pop();
}
result
}
pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &str) -> String {
let normalized_markdown = normalize_lists(markdown);
let theme = ts
.themes
.get("base16-ocean.dark")
.unwrap_or_else(|| ts.themes.values().next().expect("No themes available"));
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_SMART_PUNCTUATION);
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_DEFINITION_LIST);
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
let parser = pulldown_cmark::Parser::new_ext(&normalized_markdown, options);
let mut html_output = String::new();
let mut processed_events: Vec<Event> = Vec::new();
let mut in_code_block = false;
let mut current_lang: Option<String> = None;
let mut current_code = String::new();
let iterator = TextMergeStream::new(Parser::new_ext(markdown, options));
for event in parser {
for event in iterator {
match event {
Event::Rule => {
continue;
}
Event::Start(Tag::CodeBlock(kind)) => {
in_code_block = true;
current_code.clear();
current_lang = if let CodeBlockKind::Fenced(l) = kind {
Some(l.to_string())
Event::Start(tag) => {
html_output.push_str(render_start_tag(tag).as_str());
},
Event::End(tag) => {
html_output.push_str(render_end_tag(tag).as_str());
},
Event::Text(text) => {
html_output.push_str(text.to_string().as_str());
},
Event::TaskListMarker(checked) => {
if checked {
html_output.push_str("<input type=\"checkbox\" checked></input>");
} else {
None
};
}
Event::End(Tag::CodeBlock(_)) => {
in_code_block = false;
let is_mermaid = current_lang.as_deref() == Some("mermaid");
if is_mermaid {
let escaped_code = escape_html(&current_code);
let mermaid_html = format!(
r#"<div class="code-block-wrapper mermaid-wrapper">
<div class="code-header">
<span class="code-lang">Mermaid Diagram</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<div class="mermaid" style="background: transparent; padding: 20px; text-align: center;">{escaped_code}</div>
</div>"#
);
processed_events.push(Event::Html(mermaid_html.into()));
} else {
let lang_display = current_lang.as_deref().unwrap_or("text");
let lang_escaped = escape_html(lang_display);
let highlighted_html = if let Some(lang) = &current_lang {
if let Some(syntax) = ss.find_syntax_by_token(lang) {
let mut h = HighlightLines::new(syntax, theme);
let mut result_html = String::new();
let lines: Vec<&str> = current_code.lines().collect();
for line in lines.iter() {
match h.highlight_line(&line, ss) {
Ok(regions) => {
let html_line = styled_line_to_highlighted_html(
&regions[..],
IncludeBackground::No,
)
.unwrap_or_else(|_| escape_html(&line));
result_html.push_str(&html_line);
result_html.push('\n');
}
Err(_) => {
result_html.push_str(&escape_html(&line));
result_html.push('\n');
}
}
}
result_html
} else {
escape_html(&current_code)
}
} else {
escape_html(&current_code)
};
let code_container = format!(
r#"<div class="code-block-wrapper">
<div class="code-header">
<span class="code-lang">{lang_escaped}</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre style="margin: 0; border-radius: 0 0 6px 6px;"><code>{highlighted_html}</code></pre>
</div>"#
);
processed_events.push(Event::Html(code_container.into()));
}
}
Event::Text(text) if in_code_block => {
current_code.push_str(&text);
}
_ => {
if !in_code_block {
processed_events.push(event);
}
html_output.push_str("<input type=\"checkbox\"></input>");
}
},
Event::Code(code) => {
html_output.push_str(format!("<code>{}</code>", code.to_string()).as_str());
},
other => println!("[*] Event {:?}", other),
}
}
let mut body_html = String::new();
html::push_html(&mut body_html, processed_events.into_iter());
body_html
println!("{}", html_output);
html_output
}
fn escape_html(text: &str) -> String {
text.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
fn render_start_tag(tag: Tag<'_>) -> String {
let mut output = String::new();
match tag {
Tag::Heading{level, ..} => {
output.push_str(format!("<{level}>").as_str());
},
Tag::Strikethrough => {
output.push_str("<strike>");
},
Tag::List(Some(_)) => output.push_str("<ol>"),
Tag::List(None) => output.push_str("<ul>"),
Tag::Item => output.push_str("<li>"),
_ => println!("* [Tag] {:?}", tag),
}
output
}
fn render_end_tag(tag: TagEnd) -> String {
let mut output = String::new();
match tag {
TagEnd::Heading(level) => {
output.push_str(format!("</{level}>").as_str());
},
TagEnd::Strikethrough => {
output.push_str("</strike>");
},
TagEnd::List(true) => output.push_str("</ol>"),
TagEnd::List(false) => output.push_str("</ul>"),
TagEnd::Item => output.push_str("</li>"),
_ => println!("* [TagEnd] {:?}", tag),
}
println!("* [TagEnd] {:?}", tag);
output
}

View File

@ -29,4 +29,19 @@ function connect() {
});
}
connect();
document.querySelectorAll('.task-list-item-checkbox').forEach(cb => {
cb.addEventListener('change', function() {
const taskText = this.parentElement.textContent.trim();
const items = JSON.parse(localStorage.getItem('taskStates') || '{}');
items[taskText] = this.checked;
localStorage.setItem('taskStates', JSON.stringify(items));
});
// Восстановление состояния
const taskText = this.parentElement.textContent.trim();
const items = JSON.parse(localStorage.getItem('taskStates') || '{}');
if (items[taskText] !== undefined) {
this.checked = items[taskText];
}
});
{% endblock %}