start refactoring
This commit is contained in:
parent
71c5915628
commit
4895160309
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -1276,16 +1276,23 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.9.6"
|
version = "0.13.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"getopts",
|
"getopts",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
"pulldown-cmark-escape",
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark-escape"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.4"
|
version = "0.38.4"
|
||||||
|
|||||||
@ -13,7 +13,7 @@ strip = true
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pulldown-cmark = "0.9"
|
pulldown-cmark = "0.13.4"
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
4
TODO.md
4
TODO.md
@ -1,4 +1,4 @@
|
|||||||
# TODO <!-- [NOGREP] -->
|
# TODO <!-- [NOGREP] -->
|
||||||
|
|
||||||
* [X] Сделать кнопку скопировать путь
|
* [X] ~~Сделать кнопку скопировать путь~~
|
||||||
* [X] Сделать кнопку скопировать путь вместе с командой (`note edit ...`)
|
* [X] ~~Сделать кнопку скопировать путь вместе с командой (`note edit ...`)~~
|
||||||
|
|||||||
227
src/markdown.rs
227
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::highlighting::ThemeSet;
|
||||||
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
|
|
||||||
use syntect::parsing::SyntaxSet;
|
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 {
|
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();
|
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_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 iterator = TextMergeStream::new(Parser::new_ext(markdown, options));
|
||||||
let mut in_code_block = false;
|
|
||||||
let mut current_lang: Option<String> = None;
|
|
||||||
let mut current_code = String::new();
|
|
||||||
|
|
||||||
for event in parser {
|
for event in iterator {
|
||||||
match event {
|
match event {
|
||||||
Event::Rule => {
|
Event::Start(tag) => {
|
||||||
continue;
|
html_output.push_str(render_start_tag(tag).as_str());
|
||||||
}
|
},
|
||||||
|
Event::End(tag) => {
|
||||||
Event::Start(Tag::CodeBlock(kind)) => {
|
html_output.push_str(render_end_tag(tag).as_str());
|
||||||
in_code_block = true;
|
},
|
||||||
current_code.clear();
|
Event::Text(text) => {
|
||||||
current_lang = if let CodeBlockKind::Fenced(l) = kind {
|
html_output.push_str(text.to_string().as_str());
|
||||||
Some(l.to_string())
|
},
|
||||||
|
Event::TaskListMarker(checked) => {
|
||||||
|
if checked {
|
||||||
|
html_output.push_str("<input type=\"checkbox\" checked></input>");
|
||||||
} else {
|
} else {
|
||||||
None
|
html_output.push_str("<input type=\"checkbox\"></input>");
|
||||||
};
|
|
||||||
}
|
|
||||||
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#"<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) = ¤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#"<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 => {
|
Event::Code(code) => {
|
||||||
current_code.push_str(&text);
|
html_output.push_str(format!("<code>{}</code>", code.to_string()).as_str());
|
||||||
}
|
},
|
||||||
_ => {
|
other => println!("[*] Event {:?}", other),
|
||||||
if !in_code_block {
|
|
||||||
processed_events.push(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut body_html = String::new();
|
println!("{}", html_output);
|
||||||
|
html_output
|
||||||
html::push_html(&mut body_html, processed_events.into_iter());
|
|
||||||
body_html
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_html(text: &str) -> String {
|
fn render_start_tag(tag: Tag<'_>) -> String {
|
||||||
text.replace('&', "&")
|
let mut output = String::new();
|
||||||
.replace('<', "<")
|
match tag {
|
||||||
.replace('>', ">")
|
Tag::Heading{level, ..} => {
|
||||||
.replace('"', """)
|
output.push_str(format!("<{level}>").as_str());
|
||||||
.replace('\'', "'")
|
},
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,4 +29,19 @@ function connect() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
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 %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user