diff --git a/Cargo.lock b/Cargo.lock index c6d952e..d8b9aa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index a1e84d8..fd6f59a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/TODO.md b/TODO.md index 76c3e8f..fa56e1b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ # TODO -* [X] Сделать кнопку скопировать путь -* [X] Сделать кнопку скопировать путь вместе с командой (`note edit ...`) +* [X] ~~Сделать кнопку скопировать путь~~ +* [X] ~~Сделать кнопку скопировать путь вместе с командой (`note edit ...`)~~ diff --git a/src/markdown.rs b/src/markdown.rs index bf3ab24..8465413 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -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 = Vec::new(); - let mut in_code_block = false; - let mut current_lang: Option = 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(""); } 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(¤t_code); - let mermaid_html = format!( - r#"
-
- Mermaid Diagram - -
-
{escaped_code}
-
"# - ); - 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) = ¤t_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( - ®ions[..], - 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(¤t_code) - } - } else { - escape_html(¤t_code) - }; - - let code_container = format!( - r#"
-
- {lang_escaped} - -
-
{highlighted_html}
-
"# - ); - processed_events.push(Event::Html(code_container.into())); + html_output.push_str(""); } - } - Event::Text(text) if in_code_block => { - current_code.push_str(&text); - } - _ => { - if !in_code_block { - processed_events.push(event); - } - } + }, + Event::Code(code) => { + html_output.push_str(format!("{}", 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('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") +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(""); + }, + Tag::List(Some(_)) => output.push_str("
    "), + Tag::List(None) => output.push_str("
      "), + Tag::Item => output.push_str("
    • "), + _ => 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!("").as_str()); + }, + TagEnd::Strikethrough => { + output.push_str(""); + }, + TagEnd::List(true) => output.push_str("
"), + TagEnd::List(false) => output.push_str(""), + TagEnd::Item => output.push_str(""), + _ => println!("* [TagEnd] {:?}", tag), + } + + println!("* [TagEnd] {:?}", tag); + output } diff --git a/templates/file.html b/templates/file.html index 590678b..2378000 100644 --- a/templates/file.html +++ b/templates/file.html @@ -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 %}