Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bb69bac34 | |||
| a8e009b957 | |||
| 348dc8819d | |||
| 027b9517e1 | |||
| df4abd2ee6 | |||
| 78c4d51769 | |||
| 4432d98281 | |||
| cb6c64d5ab | |||
| c9dee232ce | |||
| 705fc670c7 | |||
| 5963c2dfd4 | |||
| 1411dd0570 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
notes/
|
||||
target/
|
||||
assets/
|
||||
templates/*.min.html
|
||||
|
||||
@ -27,7 +27,7 @@ repos:
|
||||
- id: clippy
|
||||
name: clippy
|
||||
language: system
|
||||
entry: cargo clippy -- -D clippy::all -D clippy::pedantic
|
||||
entry: cargo clippy -- -D clippy::all -D clippy::pedantic -W clippy::struct-excessive-bools
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@ -6,7 +6,58 @@ Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/lang/en/).
|
||||
|
||||
|
||||
## v0.5.0 - 2026-07-26
|
||||
## `v0.6.3` - 20.07.2026
|
||||
### Fixed
|
||||
- Fixed minify script and templates.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## `v0.6.2` - 20.07.2026
|
||||
### Changed
|
||||
- Links are now opens in new tab.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## `v0.6.1` - 17.07.2026
|
||||
### Added
|
||||
- Add compile-time templates minificattion.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## `v0.6.0` - 17.07.2026
|
||||
### Added
|
||||
- Add text emoji support (`:smile:` for example).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## `v0.5.3` - 16.07.2026
|
||||
### Fixed
|
||||
- Fixed contend width
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## `v0.5.2` - 16.07.2026
|
||||
### Fixed
|
||||
- Fixed tables rendering
|
||||
|
||||
|
||||
---
|
||||
|
||||
## `v0.5.1` - 2026-07-14
|
||||
### Fixed
|
||||
- Fixed tables rendering
|
||||
|
||||
|
||||
---
|
||||
|
||||
## `v0.5.0` - 2026-07-14
|
||||
### Changed
|
||||
- Migrate to embed js/css deps
|
||||
|
||||
@ -14,7 +65,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
|
||||
---
|
||||
|
||||
|
||||
## v0.4.5 - 2026-07-26
|
||||
## `v0.4.5` - 2026-07-14
|
||||
### Changed
|
||||
- Refactoring
|
||||
|
||||
|
||||
1261
Cargo.lock
generated
1261
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdpreview"
|
||||
version = "0.5.0"
|
||||
version = "0.6.3"
|
||||
edition = "2024"
|
||||
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
||||
build = "build.rs"
|
||||
@ -14,9 +14,10 @@ strip = true
|
||||
|
||||
[build-dependencies]
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
minify-html = "0.18"
|
||||
|
||||
[dependencies]
|
||||
pulldown-cmark = "0.13.4"
|
||||
pulldown-cmark = { version = "0.13.4", features = ["simd"] }
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@ -3,7 +3,7 @@ FROM rust:1.94 AS chef
|
||||
RUN cargo install --locked cargo-chef
|
||||
WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock .
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
RUN mkdir src && touch src/main.rs
|
||||
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
2
TODO.md
2
TODO.md
@ -5,4 +5,4 @@
|
||||
* [X] ~~Сделать отображение типа кода и кнопку скопировать код~~
|
||||
* [X] ~~Вынести зависимости, типа скриптов mermaid и highlight.js в
|
||||
include_bytes~~
|
||||
* [ ] Починить заголовки таблицы
|
||||
* [X] ~~Починить заголовки таблицы~~
|
||||
|
||||
34
build.rs
34
build.rs
@ -1,5 +1,7 @@
|
||||
use minify_html::{Cfg, minify};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
const HIGHLIGHT_STYLE_URL_TEMPLATE: &str =
|
||||
@ -14,6 +16,11 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed=assets/");
|
||||
println!("cargo:rerun-if-env-changed=FORCE_DEPS_DOWNLOAD");
|
||||
|
||||
println!("cargo:rerun-if-changed=templates/");
|
||||
minify_template_file("templates/base.html", "templates/.base.min.html").unwrap();
|
||||
minify_template_file("templates/dir.html", "templates/.dir.min.html").unwrap();
|
||||
minify_template_file("templates/file.html", "templates/.file.min.html").unwrap();
|
||||
|
||||
let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let assets_path = Path::new(&out_dir).join("assets");
|
||||
fs::create_dir_all(&assets_path).expect("Fail to create directory");
|
||||
@ -79,3 +86,30 @@ fn should_download(path: &Path) -> bool {
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn minify_template_file<P: AsRef<str>>(input_path: P, output_path: P) -> io::Result<()> {
|
||||
let html_content = fs::read_to_string(input_path.as_ref())?;
|
||||
|
||||
let minified = minify_template_content(&html_content)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
fs::write(output_path.as_ref(), minified)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
pub fn minify_template_content(template: &str) -> Result<String, String> {
|
||||
let mut cfg = Cfg::new();
|
||||
cfg.allow_removing_spaces_between_attributes = true;
|
||||
cfg.keep_comments = false;
|
||||
cfg.minify_css = true;
|
||||
cfg.minify_js = true;
|
||||
cfg.keep_html_and_head_opening_tags = true;
|
||||
cfg.keep_closing_tags = true;
|
||||
cfg.remove_processing_instructions = true;
|
||||
|
||||
let minified_bytes = minify(template.as_bytes(), &cfg);
|
||||
|
||||
String::from_utf8(minified_bytes).map_err(|e| format!("Invalid UTF-8 in output: {e}"))
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ pub struct FileEntry {
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "dir.html")]
|
||||
#[template(path = ".dir.min.html")]
|
||||
pub struct DirectoryTemplate {
|
||||
pub title_path: String,
|
||||
pub files: Vec<FileEntry>,
|
||||
@ -65,7 +65,7 @@ pub struct DirectoryTemplate {
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "file.html")]
|
||||
#[template(path = ".file.min.html")]
|
||||
pub struct NoteTemplate {
|
||||
pub filename: String,
|
||||
pub back_link: String,
|
||||
|
||||
237
src/markdown.rs
237
src/markdown.rs
@ -1,237 +0,0 @@
|
||||
use pulldown_cmark::{
|
||||
BlockQuoteKind, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd,
|
||||
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);
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
options.insert(Options::ENABLE_TASKLISTS);
|
||||
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
||||
options.insert(Options::ENABLE_GFM);
|
||||
|
||||
let mut in_code_block = false;
|
||||
let mut code_content = String::new();
|
||||
|
||||
let mut html_output = String::new();
|
||||
|
||||
let iterator = TextMergeStream::new(Parser::new_ext(markdown, options));
|
||||
|
||||
for event in iterator {
|
||||
match event {
|
||||
Event::Start(tag) => {
|
||||
if let Tag::CodeBlock(_) = tag {
|
||||
in_code_block = true;
|
||||
code_content.clear();
|
||||
}
|
||||
html_output.push_str(render_start_tag(tag).as_str());
|
||||
}
|
||||
Event::End(tag) => {
|
||||
if let TagEnd::CodeBlock = tag {
|
||||
in_code_block = false;
|
||||
html_output.push_str(escape_html(&code_content).as_str());
|
||||
}
|
||||
html_output.push_str(render_end_tag(tag).as_str());
|
||||
}
|
||||
Event::Text(text) => {
|
||||
if in_code_block {
|
||||
code_content.push_str(&text);
|
||||
} else {
|
||||
html_output.push_str(&escape_html(&text));
|
||||
}
|
||||
}
|
||||
Event::TaskListMarker(true) => {
|
||||
html_output.push_str("<input type=\"checkbox\" checked/>");
|
||||
}
|
||||
Event::TaskListMarker(false) => {
|
||||
html_output.push_str("<input type=\"checkbox\"/>");
|
||||
}
|
||||
Event::Code(code) => {
|
||||
html_output.push_str(format!("<code>{}</code>", escape_html(&code)).as_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) => {
|
||||
let _ = write!(
|
||||
html_output,
|
||||
"<sup><a href=\"#{fref}\" id=\"{fref}\"></a></sup>"
|
||||
);
|
||||
}
|
||||
|
||||
Event::InlineHtml(_) | Event::InlineMath(_) | Event::DisplayMath(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
html_output
|
||||
}
|
||||
|
||||
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>"),
|
||||
Tag::Paragraph => output.push_str("<p>"),
|
||||
Tag::CodeBlock(kind) => output.push_str(render_codeblock(kind).as_str()),
|
||||
Tag::Emphasis => output.push_str("<em>"),
|
||||
Tag::Strong => output.push_str("<strong>"),
|
||||
Tag::BlockQuote(kind) => output.push_str(render_blockquote(kind).as_str()),
|
||||
Tag::Link {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
..
|
||||
} => 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()),
|
||||
|
||||
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::FootnoteDefinition(fref) => {
|
||||
let _ = write!(output, "<sup><a href=\"#{fref}\" id=\"{fref}\">");
|
||||
}
|
||||
|
||||
Tag::HtmlBlock
|
||||
| Tag::DefinitionList
|
||||
| Tag::DefinitionListTitle
|
||||
| Tag::DefinitionListDefinition
|
||||
| Tag::Superscript
|
||||
| Tag::Subscript
|
||||
| Tag::MetadataBlock(_) => (),
|
||||
}
|
||||
|
||||
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>"),
|
||||
TagEnd::Paragraph => output.push_str("</p>"),
|
||||
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>"),
|
||||
TagEnd::Link => output.push_str("</a>"),
|
||||
TagEnd::Image => output.push_str("</img>"),
|
||||
|
||||
TagEnd::Table => output.push_str("</table>"),
|
||||
TagEnd::TableHead => output.push_str("</thead>"),
|
||||
TagEnd::TableCell => output.push_str("</td>"),
|
||||
TagEnd::TableRow => output.push_str("</tr>"),
|
||||
|
||||
TagEnd::FootnoteDefinition => output.push_str("</a></sup>"),
|
||||
|
||||
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 => {
|
||||
output.push_str(&header.replace("%CODE%", "code"));
|
||||
output.push_str("<code>");
|
||||
}
|
||||
|
||||
CodeBlockKind::Fenced(CowStr::Borrowed("mermaid")) => {
|
||||
output.push_str(&header.replace("%CODE%", "mermaid"));
|
||||
output.push_str("<code class=\"language-text mermaid\">");
|
||||
}
|
||||
|
||||
CodeBlockKind::Fenced(CowStr::Borrowed("rawmermaid")) => {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn render_blockquote(quote_kind: Option<BlockQuoteKind>) -> 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 {
|
||||
match link_type {
|
||||
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 {
|
||||
format!("<img src=\"{dest_url}\" alt=\"{title}\">")
|
||||
}
|
||||
|
||||
fn escape_html(text: &str) -> String {
|
||||
text.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
910
src/markdown/emoji.rs
Normal file
910
src/markdown/emoji.rs
Normal file
@ -0,0 +1,910 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static EMOJIS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
("bowtie", "🙇"),
|
||||
("smile", "😄"),
|
||||
("laughing", "😆"),
|
||||
("blush", "😊"),
|
||||
("smiley", "😃"),
|
||||
("relaxed", "☺️"),
|
||||
("smirk", "😏"),
|
||||
("heart_eyes", "😍"),
|
||||
("kissing_heart", "😘"),
|
||||
("kissing_closed_eyes", "😚"),
|
||||
("flushed", "😳"),
|
||||
("relieved", "😌"),
|
||||
("satisfied", "😌"),
|
||||
("grin", "😁"),
|
||||
("wink", "😉"),
|
||||
("stuck_out_tongue_winking_eye", "😜"),
|
||||
("stuck_out_tongue_closed_eyes", "😝"),
|
||||
("grinning", "😀"),
|
||||
("kissing", "😗"),
|
||||
("kissing_smiling_eyes", "😙"),
|
||||
("stuck_out_tongue", "😛"),
|
||||
("sleeping", "😴"),
|
||||
("worried", "😟"),
|
||||
("frowning", "😦"),
|
||||
("anguished", "😧"),
|
||||
("open_mouth", "😮"),
|
||||
("grimacing", "😬"),
|
||||
("confused", "😕"),
|
||||
("hushed", "😯"),
|
||||
("expressionless", "😑"),
|
||||
("unamused", "😒"),
|
||||
("sweat_smile", "😅"),
|
||||
("sweat", "😓"),
|
||||
("disappointed_relieved", "😥"),
|
||||
("weary", "😩"),
|
||||
("pensive", "😔"),
|
||||
("disappointed", "😞"),
|
||||
("confounded", "😖"),
|
||||
("fearful", "😨"),
|
||||
("cold_sweat", "😰"),
|
||||
("persevere", "😣"),
|
||||
("cry", "😢"),
|
||||
("sob", "😭"),
|
||||
("joy", "😂"),
|
||||
("astonished", "😲"),
|
||||
("scream", "😱"),
|
||||
("neckbeard", "🧔"),
|
||||
("tired_face", "😫"),
|
||||
("angry", "😠"),
|
||||
("rage", "😡"),
|
||||
("triumph", "😤"),
|
||||
("sleepy", "😪"),
|
||||
("yum", "😋"),
|
||||
("mask", "😷"),
|
||||
("sunglasses", "😎"),
|
||||
("dizzy_face", "😵"),
|
||||
("imp", "👿"),
|
||||
("smiling_imp", "😈"),
|
||||
("neutral_face", "😐"),
|
||||
("no_mouth", "😶"),
|
||||
("innocent", "😇"),
|
||||
("alien", "👽"),
|
||||
("yellow_heart", "💛"),
|
||||
("blue_heart", "💙"),
|
||||
("purple_heart", "💜"),
|
||||
("heart", "❤️"),
|
||||
("green_heart", "💚"),
|
||||
("broken_heart", "💔"),
|
||||
("heartbeat", "💓"),
|
||||
("heartpulse", "💗"),
|
||||
("two_hearts", "💕"),
|
||||
("revolving_hearts", "💞"),
|
||||
("cupid", "💘"),
|
||||
("sparkling_heart", "💖"),
|
||||
("sparkles", "✨"),
|
||||
("star", "⭐"),
|
||||
("star2", "🌟"),
|
||||
("dizzy", "💫"),
|
||||
("boom", "💥"),
|
||||
("collision", "💥"),
|
||||
("anger", "💢"),
|
||||
("exclamation", "❗"),
|
||||
("question", "❓"),
|
||||
("grey_exclamation", "❕"),
|
||||
("grey_question", "❔"),
|
||||
("zzz", "💤"),
|
||||
("dash", "💨"),
|
||||
("sweat_drops", "💦"),
|
||||
("notes", "🎶"),
|
||||
("musical_note", "🎵"),
|
||||
("fire", "🔥"),
|
||||
("hankey", "💩"),
|
||||
("poop", "💩"),
|
||||
("shit", "💩"),
|
||||
("+1", "👍"),
|
||||
("thumbsup", "👍"),
|
||||
("-1", "👎"),
|
||||
("thumbsdown", "👎"),
|
||||
("ok_hand", "👌"),
|
||||
("punch", "👊"),
|
||||
("facepunch", "👊"),
|
||||
("fist", "✊"),
|
||||
("v", "✌️"),
|
||||
("wave", "👋"),
|
||||
("hand", "✋"),
|
||||
("raised_hand", "✋"),
|
||||
("open_hands", "👐"),
|
||||
("point_up", "☝️"),
|
||||
("point_down", "👇"),
|
||||
("point_left", "👈"),
|
||||
("point_right", "👉"),
|
||||
("raised_hands", "🙌"),
|
||||
("pray", "🙏"),
|
||||
("point_up_2", "👆"),
|
||||
("clap", "👏"),
|
||||
("muscle", "💪"),
|
||||
("metal", "🤘"),
|
||||
("fu", "🖕"),
|
||||
("walking", "🚶"),
|
||||
("runner", "🏃"),
|
||||
("running", "🏃"),
|
||||
("couple", "👫"),
|
||||
("family", "👨👩👦"),
|
||||
("two_men_holding_hands", "👬"),
|
||||
("two_women_holding_hands", "👭"),
|
||||
("dancer", "💃"),
|
||||
("dancers", "👯"),
|
||||
("ok_woman", "🙆"),
|
||||
("no_good", "🙅"),
|
||||
("information_desk_person", "💁"),
|
||||
("raising_hand", "🙋"),
|
||||
("bride_with_veil", "👰"),
|
||||
("person_with_pouting_face", "🙎"),
|
||||
("person_frowning", "🙍"),
|
||||
("bow", "🙇"),
|
||||
("couplekiss", "💏"),
|
||||
("couple_with_heart", "💑"),
|
||||
("massage", "💆"),
|
||||
("haircut", "💇"),
|
||||
("nail_care", "💅"),
|
||||
("boy", "👦"),
|
||||
("girl", "👧"),
|
||||
("woman", "👩"),
|
||||
("man", "👨"),
|
||||
("baby", "👶"),
|
||||
("older_woman", "👵"),
|
||||
("older_man", "👴"),
|
||||
("person_with_blond_hair", "👱"),
|
||||
("man_with_gua_pi_mao", "👲"),
|
||||
("man_with_turban", "👳"),
|
||||
("construction_worker", "👷"),
|
||||
("cop", "👮"),
|
||||
("angel", "👼"),
|
||||
("princess", "👸"),
|
||||
("smiley_cat", "😺"),
|
||||
("smile_cat", "😸"),
|
||||
("heart_eyes_cat", "😻"),
|
||||
("kissing_cat", "😽"),
|
||||
("smirk_cat", "😼"),
|
||||
("scream_cat", "🙀"),
|
||||
("crying_cat_face", "😿"),
|
||||
("joy_cat", "😹"),
|
||||
("pouting_cat", "😾"),
|
||||
("japanese_ogre", "👹"),
|
||||
("japanese_goblin", "👺"),
|
||||
("see_no_evil", "🙈"),
|
||||
("hear_no_evil", "🙉"),
|
||||
("speak_no_evil", "🙊"),
|
||||
("guardsman", "💂"),
|
||||
("skull", "💀"),
|
||||
("feet", "🐾"),
|
||||
("lips", "👄"),
|
||||
("kiss", "💋"),
|
||||
("droplet", "💧"),
|
||||
("ear", "👂"),
|
||||
("eyes", "👀"),
|
||||
("nose", "👃"),
|
||||
("tongue", "👅"),
|
||||
("love_letter", "💌"),
|
||||
("bust_in_silhouette", "👤"),
|
||||
("busts_in_silhouette", "👥"),
|
||||
("speech_balloon", "💬"),
|
||||
("thought_balloon", "💭"),
|
||||
("feelsgood", "😎"),
|
||||
("finnadie", "😵"),
|
||||
("goberserk", "😈"),
|
||||
("godmode", "😇"),
|
||||
("hurtrealbad", "😰"),
|
||||
("rage1", "😡"),
|
||||
("rage2", "😡"),
|
||||
("rage3", "😡"),
|
||||
("rage4", "😡"),
|
||||
("suspect", "😕"),
|
||||
("trollface", "😂"),
|
||||
// Nature
|
||||
("sunny", "☀️"),
|
||||
("umbrella", "☂️"),
|
||||
("cloud", "☁️"),
|
||||
("snowflake", "❄️"),
|
||||
("snowman", "⛄"),
|
||||
("zap", "⚡"),
|
||||
("cyclone", "🌀"),
|
||||
("foggy", "🌁"),
|
||||
("ocean", "🌊"),
|
||||
("cat", "🐱"),
|
||||
("dog", "🐶"),
|
||||
("mouse", "🐭"),
|
||||
("hamster", "🐹"),
|
||||
("rabbit", "🐰"),
|
||||
("wolf", "🐺"),
|
||||
("frog", "🐸"),
|
||||
("tiger", "🐯"),
|
||||
("koala", "🐨"),
|
||||
("bear", "🐻"),
|
||||
("pig", "🐷"),
|
||||
("pig_nose", "🐽"),
|
||||
("cow", "🐮"),
|
||||
("boar", "🐗"),
|
||||
("monkey_face", "🐵"),
|
||||
("monkey", "🐒"),
|
||||
("horse", "🐴"),
|
||||
("racehorse", "🐎"),
|
||||
("camel", "🐫"),
|
||||
("sheep", "🐑"),
|
||||
("elephant", "🐘"),
|
||||
("panda_face", "🐼"),
|
||||
("snake", "🐍"),
|
||||
("bird", "🐦"),
|
||||
("baby_chick", "🐤"),
|
||||
("hatched_chick", "🐥"),
|
||||
("hatching_chick", "🐣"),
|
||||
("chicken", "🐔"),
|
||||
("penguin", "🐧"),
|
||||
("turtle", "🐢"),
|
||||
("bug", "🐛"),
|
||||
("honeybee", "🐝"),
|
||||
("ant", "🐜"),
|
||||
("beetle", "🐞"),
|
||||
("snail", "🐌"),
|
||||
("octopus", "🐙"),
|
||||
("tropical_fish", "🐠"),
|
||||
("fish", "🐟"),
|
||||
("whale", "🐳"),
|
||||
("whale2", "🐋"),
|
||||
("dolphin", "🐬"),
|
||||
("cow2", "🐄"),
|
||||
("ram", "🐏"),
|
||||
("rat", "🐀"),
|
||||
("water_buffalo", "🐃"),
|
||||
("tiger2", "🐅"),
|
||||
("rabbit2", "🐇"),
|
||||
("dragon", "🐉"),
|
||||
("goat", "🐐"),
|
||||
("rooster", "🐓"),
|
||||
("dog2", "🐕"),
|
||||
("pig2", "🐖"),
|
||||
("mouse2", "🐁"),
|
||||
("ox", "🐂"),
|
||||
("dragon_face", "🐲"),
|
||||
("blowfish", "🐡"),
|
||||
("crocodile", "🐊"),
|
||||
("dromedary_camel", "🐪"),
|
||||
("leopard", "🐆"),
|
||||
("cat2", "🐈"),
|
||||
("poodle", "🐩"),
|
||||
("paw_prints", "🐾"),
|
||||
("bouquet", "💐"),
|
||||
("cherry_blossom", "🌸"),
|
||||
("tulip", "🌷"),
|
||||
("four_leaf_clover", "🍀"),
|
||||
("rose", "🌹"),
|
||||
("sunflower", "🌻"),
|
||||
("hibiscus", "🌺"),
|
||||
("maple_leaf", "🍁"),
|
||||
("leaves", "🍃"),
|
||||
("fallen_leaf", "🍂"),
|
||||
("herb", "🌿"),
|
||||
("mushroom", "🍄"),
|
||||
("cactus", "🌵"),
|
||||
("palm_tree", "🌴"),
|
||||
("evergreen_tree", "🌲"),
|
||||
("deciduous_tree", "🌳"),
|
||||
("chestnut", "🌰"),
|
||||
("seedling", "🌱"),
|
||||
("blossom", "🌼"),
|
||||
("ear_of_rice", "🌾"),
|
||||
("shell", "🐚"),
|
||||
("globe_with_meridians", "🌐"),
|
||||
("sun_with_face", "🌞"),
|
||||
("full_moon_with_face", "🌝"),
|
||||
("new_moon_with_face", "🌚"),
|
||||
("new_moon", "🌑"),
|
||||
("waxing_crescent_moon", "🌒"),
|
||||
("first_quarter_moon", "🌓"),
|
||||
("waxing_gibbous_moon", "🌔"),
|
||||
("full_moon", "🌕"),
|
||||
("waning_gibbous_moon", "🌖"),
|
||||
("last_quarter_moon", "🌗"),
|
||||
("waning_crescent_moon", "🌘"),
|
||||
("last_quarter_moon_with_face", "🌜"),
|
||||
("first_quarter_moon_with_face", "🌛"),
|
||||
("moon", "🌙"),
|
||||
("earth_africa", "🌍"),
|
||||
("earth_americas", "🌎"),
|
||||
("earth_asia", "🌏"),
|
||||
("volcano", "🌋"),
|
||||
("milky_way", "🌌"),
|
||||
("partly_sunny", "⛅"),
|
||||
("octocat", "🐙"),
|
||||
("squirrel", "🐿️"),
|
||||
// Objects
|
||||
("bamboo", "🎍"),
|
||||
("gift_heart", "💝"),
|
||||
("dolls", "🎎"),
|
||||
("school_satchel", "🎒"),
|
||||
("mortar_board", "🎓"),
|
||||
("flags", "🎏"),
|
||||
("fireworks", "🎆"),
|
||||
("sparkler", "🎇"),
|
||||
("wind_chime", "🎐"),
|
||||
("rice_scene", "🎑"),
|
||||
("jack_o_lantern", "🎃"),
|
||||
("ghost", "👻"),
|
||||
("santa", "🎅"),
|
||||
("christmas_tree", "🎄"),
|
||||
("gift", "🎁"),
|
||||
("bell", "🔔"),
|
||||
("no_bell", "🔕"),
|
||||
("tanabata_tree", "🎋"),
|
||||
("tada", "🎉"),
|
||||
("confetti_ball", "🎊"),
|
||||
("balloon", "🎈"),
|
||||
("crystal_ball", "🔮"),
|
||||
("cd", "💿"),
|
||||
("dvd", "📀"),
|
||||
("floppy_disk", "💾"),
|
||||
("camera", "📷"),
|
||||
("video_camera", "📹"),
|
||||
("movie_camera", "🎥"),
|
||||
("computer", "💻"),
|
||||
("tv", "📺"),
|
||||
("iphone", "📱"),
|
||||
("phone", "📞"),
|
||||
("telephone", "☎️"),
|
||||
("telephone_receiver", "📞"),
|
||||
("pager", "📟"),
|
||||
("fax", "📠"),
|
||||
("minidisc", "💽"),
|
||||
("vhs", "📼"),
|
||||
("sound", "🔊"),
|
||||
("speaker", "🔈"),
|
||||
("mute", "🔇"),
|
||||
("loudspeaker", "📢"),
|
||||
("mega", "📣"),
|
||||
("hourglass", "⌛"),
|
||||
("hourglass_flowing_sand", "⏳"),
|
||||
("alarm_clock", "⏰"),
|
||||
("watch", "⌚"),
|
||||
("radio", "📻"),
|
||||
("satellite", "📡"),
|
||||
("loop", "➰"),
|
||||
("mag", "🔍"),
|
||||
("mag_right", "🔎"),
|
||||
("unlock", "🔓"),
|
||||
("lock", "🔒"),
|
||||
("lock_with_ink_pen", "🔏"),
|
||||
("closed_lock_with_key", "🔐"),
|
||||
("key", "🔑"),
|
||||
("bulb", "💡"),
|
||||
("flashlight", "🔦"),
|
||||
("high_brightness", "🔆"),
|
||||
("low_brightness", "🔅"),
|
||||
("electric_plug", "🔌"),
|
||||
("battery", "🔋"),
|
||||
("calling", "📲"),
|
||||
("email", "📧"),
|
||||
("mailbox", "📫"),
|
||||
("postbox", "📮"),
|
||||
("bath", "🛀"),
|
||||
("bathtub", "🛁"),
|
||||
("shower", "🚿"),
|
||||
("toilet", "🚽"),
|
||||
("wrench", "🔧"),
|
||||
("nut_and_bolt", "🔩"),
|
||||
("hammer", "🔨"),
|
||||
("seat", "💺"),
|
||||
("moneybag", "💰"),
|
||||
("yen", "💴"),
|
||||
("dollar", "💵"),
|
||||
("pound", "💷"),
|
||||
("euro", "💶"),
|
||||
("credit_card", "💳"),
|
||||
("money_with_wings", "💸"),
|
||||
("e-mail", "📧"),
|
||||
("inbox_tray", "📥"),
|
||||
("outbox_tray", "📤"),
|
||||
("envelope", "✉️"),
|
||||
("incoming_envelope", "📨"),
|
||||
("postal_horn", "📯"),
|
||||
("mailbox_closed", "📪"),
|
||||
("mailbox_with_mail", "📬"),
|
||||
("mailbox_with_no_mail", "📭"),
|
||||
("door", "🚪"),
|
||||
("smoking", "🚬"),
|
||||
("bomb", "💣"),
|
||||
("gun", "🔫"),
|
||||
("hocho", "🔪"),
|
||||
("pill", "💊"),
|
||||
("syringe", "💉"),
|
||||
("page_facing_up", "📄"),
|
||||
("page_with_curl", "📃"),
|
||||
("bookmark_tabs", "📑"),
|
||||
("bar_chart", "📊"),
|
||||
("chart_with_upwards_trend", "📈"),
|
||||
("chart_with_downwards_trend", "📉"),
|
||||
("scroll", "📜"),
|
||||
("clipboard", "📋"),
|
||||
("calendar", "📅"),
|
||||
("date", "📆"),
|
||||
("card_index", "📇"),
|
||||
("file_folder", "📁"),
|
||||
("open_file_folder", "📂"),
|
||||
("scissors", "✂️"),
|
||||
("pushpin", "📌"),
|
||||
("paperclip", "📎"),
|
||||
("black_nib", "✒️"),
|
||||
("pencil2", "✏️"),
|
||||
("straight_ruler", "📏"),
|
||||
("triangular_ruler", "📐"),
|
||||
("closed_book", "📕"),
|
||||
("green_book", "📗"),
|
||||
("blue_book", "📘"),
|
||||
("orange_book", "📙"),
|
||||
("notebook", "📓"),
|
||||
("notebook_with_decorative_cover", "📔"),
|
||||
("ledger", "📒"),
|
||||
("books", "📚"),
|
||||
("bookmark", "🔖"),
|
||||
("name_badge", "📛"),
|
||||
("microscope", "🔬"),
|
||||
("telescope", "🔭"),
|
||||
("newspaper", "📰"),
|
||||
("football", "🏈"),
|
||||
("basketball", "🏀"),
|
||||
("soccer", "⚽"),
|
||||
("baseball", "⚾"),
|
||||
("tennis", "🎾"),
|
||||
("8ball", "🎱"),
|
||||
("rugby_football", "🏉"),
|
||||
("bowling", "🎳"),
|
||||
("golf", "⛳"),
|
||||
("mountain_bicyclist", "🚵"),
|
||||
("bicyclist", "🚴"),
|
||||
("horse_racing", "🏇"),
|
||||
("snowboarder", "🏂"),
|
||||
("swimmer", "🏊"),
|
||||
("surfer", "🏄"),
|
||||
("ski", "⛷️"),
|
||||
("spades", "♠️"),
|
||||
("hearts", "♥️"),
|
||||
("clubs", "♣️"),
|
||||
("diamonds", "♦️"),
|
||||
("gem", "💎"),
|
||||
("ring", "💍"),
|
||||
("trophy", "🏆"),
|
||||
("musical_score", "🎼"),
|
||||
("musical_keyboard", "🎹"),
|
||||
("violin", "🎻"),
|
||||
("space_invader", "👾"),
|
||||
("video_game", "🎮"),
|
||||
("black_joker", "🃏"),
|
||||
("flower_playing_cards", "🎴"),
|
||||
("game_die", "🎲"),
|
||||
("dart", "🎯"),
|
||||
("mahjong", "🀄"),
|
||||
("clapper", "🎬"),
|
||||
("memo", "📝"),
|
||||
("pencil", "📝"),
|
||||
("book", "📖"),
|
||||
("art", "🎨"),
|
||||
("microphone", "🎤"),
|
||||
("headphones", "🎧"),
|
||||
("trumpet", "🎺"),
|
||||
("saxophone", "🎷"),
|
||||
("guitar", "🎸"),
|
||||
("shoe", "👞"),
|
||||
("sandal", "👡"),
|
||||
("high_heel", "👠"),
|
||||
("lipstick", "💄"),
|
||||
("boot", "👢"),
|
||||
("shirt", "👕"),
|
||||
("tshirt", "👕"),
|
||||
("necktie", "👔"),
|
||||
("womans_clothes", "👚"),
|
||||
("dress", "👗"),
|
||||
("running_shirt_with_sash", "🎽"),
|
||||
("jeans", "👖"),
|
||||
("kimono", "👘"),
|
||||
("bikini", "👙"),
|
||||
("ribbon", "🎀"),
|
||||
("tophat", "🎩"),
|
||||
("crown", "👑"),
|
||||
("womans_hat", "👒"),
|
||||
("mans_shoe", "👞"),
|
||||
("closed_umbrella", "🌂"),
|
||||
("briefcase", "💼"),
|
||||
("handbag", "👜"),
|
||||
("pouch", "👝"),
|
||||
("purse", "👛"),
|
||||
("eyeglasses", "👓"),
|
||||
("fishing_pole_and_fish", "🎣"),
|
||||
("coffee", "☕"),
|
||||
("tea", "🍵"),
|
||||
("sake", "🍶"),
|
||||
("baby_bottle", "🍼"),
|
||||
("beer", "🍺"),
|
||||
("beers", "🍻"),
|
||||
("cocktail", "🍸"),
|
||||
("tropical_drink", "🍹"),
|
||||
("wine_glass", "🍷"),
|
||||
("fork_and_knife", "🍴"),
|
||||
("pizza", "🍕"),
|
||||
("hamburger", "🍔"),
|
||||
("fries", "🍟"),
|
||||
("poultry_leg", "🍗"),
|
||||
("meat_on_bone", "🍖"),
|
||||
("spaghetti", "🍝"),
|
||||
("curry", "🍛"),
|
||||
("fried_shrimp", "🍤"),
|
||||
("bento", "🍱"),
|
||||
("sushi", "🍣"),
|
||||
("fish_cake", "🍥"),
|
||||
("rice_ball", "🍙"),
|
||||
("rice_cracker", "🍘"),
|
||||
("rice", "🍚"),
|
||||
("ramen", "🍜"),
|
||||
("stew", "🍲"),
|
||||
("oden", "🍢"),
|
||||
("dango", "🍡"),
|
||||
("egg", "🍳"),
|
||||
("bread", "🍞"),
|
||||
("doughnut", "🍩"),
|
||||
("custard", "🍮"),
|
||||
("icecream", "🍦"),
|
||||
("ice_cream", "🍨"),
|
||||
("shaved_ice", "🍧"),
|
||||
("birthday", "🎂"),
|
||||
("cake", "🍰"),
|
||||
("cookie", "🍪"),
|
||||
("chocolate_bar", "🍫"),
|
||||
("candy", "🍬"),
|
||||
("lollipop", "🍭"),
|
||||
("honey_pot", "🍯"),
|
||||
("apple", "🍎"),
|
||||
("green_apple", "🍏"),
|
||||
("tangerine", "🍊"),
|
||||
("lemon", "🍋"),
|
||||
("cherries", "🍒"),
|
||||
("grapes", "🍇"),
|
||||
("watermelon", "🍉"),
|
||||
("strawberry", "🍓"),
|
||||
("peach", "🍑"),
|
||||
("melon", "🍈"),
|
||||
("banana", "🍌"),
|
||||
("pear", "🍐"),
|
||||
("pineapple", "🍍"),
|
||||
("sweet_potato", "🍠"),
|
||||
("eggplant", "🍆"),
|
||||
("tomato", "🍅"),
|
||||
("corn", "🌽"),
|
||||
// Places
|
||||
("house", "🏠"),
|
||||
("house_with_garden", "🏡"),
|
||||
("school", "🏫"),
|
||||
("office", "🏢"),
|
||||
("post_office", "🏣"),
|
||||
("hospital", "🏥"),
|
||||
("bank", "🏦"),
|
||||
("convenience_store", "🏪"),
|
||||
("love_hotel", "🏩"),
|
||||
("hotel", "🏨"),
|
||||
("wedding", "💒"),
|
||||
("church", "⛪"),
|
||||
("department_store", "🏬"),
|
||||
("european_post_office", "🏤"),
|
||||
("city_sunrise", "🌇"),
|
||||
("city_sunset", "🌆"),
|
||||
("japanese_castle", "🏯"),
|
||||
("european_castle", "🏰"),
|
||||
("tent", "⛺"),
|
||||
("factory", "🏭"),
|
||||
("tokyo_tower", "🗼"),
|
||||
("japan", "🗾"),
|
||||
("mount_fuji", "🗻"),
|
||||
("sunrise_over_mountains", "🌄"),
|
||||
("sunrise", "🌅"),
|
||||
("stars", "🌠"),
|
||||
("statue_of_liberty", "🗽"),
|
||||
("bridge_at_night", "🌉"),
|
||||
("carousel_horse", "🎠"),
|
||||
("rainbow", "🌈"),
|
||||
("ferris_wheel", "🎡"),
|
||||
("fountain", "⛲"),
|
||||
("roller_coaster", "🎢"),
|
||||
("ship", "🚢"),
|
||||
("speedboat", "🚤"),
|
||||
("boat", "⛵"),
|
||||
("sailboat", "⛵"),
|
||||
("rowboat", "🚣"),
|
||||
("anchor", "⚓"),
|
||||
("rocket", "🚀"),
|
||||
("airplane", "✈️"),
|
||||
("helicopter", "🚁"),
|
||||
("steam_locomotive", "🚂"),
|
||||
("tram", "🚋"),
|
||||
("mountain_railway", "🚞"),
|
||||
("bike", "🚲"),
|
||||
("aerial_tramway", "🚡"),
|
||||
("suspension_railway", "🚟"),
|
||||
("mountain_cableway", "🚠"),
|
||||
("tractor", "🚜"),
|
||||
("blue_car", "🚙"),
|
||||
("oncoming_automobile", "🚘"),
|
||||
("car", "🚗"),
|
||||
("red_car", "🚗"),
|
||||
("taxi", "🚕"),
|
||||
("oncoming_taxi", "🚖"),
|
||||
("articulated_lorry", "🚛"),
|
||||
("bus", "🚌"),
|
||||
("oncoming_bus", "🚍"),
|
||||
("rotating_light", "🚨"),
|
||||
("police_car", "🚓"),
|
||||
("oncoming_police_car", "🚔"),
|
||||
("fire_engine", "🚒"),
|
||||
("ambulance", "🚑"),
|
||||
("minibus", "🚐"),
|
||||
("truck", "🚚"),
|
||||
("train", "🚆"),
|
||||
("station", "🚉"),
|
||||
("train2", "🚄"),
|
||||
("bullettrain_front", "🚅"),
|
||||
("bullettrain_side", "🚄"),
|
||||
("light_rail", "🚈"),
|
||||
("monorail", "🚝"),
|
||||
("railway_car", "🚃"),
|
||||
("trolleybus", "🚎"),
|
||||
("ticket", "🎫"),
|
||||
("fuelpump", "⛽"),
|
||||
("vertical_traffic_light", "🚦"),
|
||||
("traffic_light", "🚥"),
|
||||
("warning", "⚠️"),
|
||||
("construction", "🚧"),
|
||||
("beginner", "🔰"),
|
||||
("atm", "🏧"),
|
||||
("slot_machine", "🎰"),
|
||||
("busstop", "🚏"),
|
||||
("barber", "💈"),
|
||||
("hotsprings", "♨️"),
|
||||
("checkered_flag", "🏁"),
|
||||
("crossed_flags", "🎌"),
|
||||
("izakaya_lantern", "🏮"),
|
||||
("moyai", "🗿"),
|
||||
("circus_tent", "🎪"),
|
||||
("performing_arts", "🎭"),
|
||||
("round_pushpin", "📍"),
|
||||
("triangular_flag_on_post", "🚩"),
|
||||
("jp", "🇯🇵"),
|
||||
("kr", "🇰🇷"),
|
||||
("cn", "🇨🇳"),
|
||||
("us", "🇺🇸"),
|
||||
("fr", "🇫🇷"),
|
||||
("es", "🇪🇸"),
|
||||
("it", "🇮🇹"),
|
||||
("ru", "🇷🇺"),
|
||||
("gb", "🇬🇧"),
|
||||
("uk", "🇬🇧"),
|
||||
("de", "🇩🇪"),
|
||||
// Symbols
|
||||
("one", "1️⃣"),
|
||||
("two", "2️⃣"),
|
||||
("three", "3️⃣"),
|
||||
("four", "4️⃣"),
|
||||
("five", "5️⃣"),
|
||||
("six", "6️⃣"),
|
||||
("seven", "7️⃣"),
|
||||
("eight", "8️⃣"),
|
||||
("nine", "9️⃣"),
|
||||
("keycap_ten", "🔟"),
|
||||
("1234", "🔢"),
|
||||
("zero", "0️⃣"),
|
||||
("hash", "#️⃣"),
|
||||
("symbols", "🔣"),
|
||||
("arrow_backward", "◀️"),
|
||||
("arrow_down", "⬇️"),
|
||||
("arrow_forward", "▶️"),
|
||||
("arrow_left", "⬅️"),
|
||||
("capital_abcd", "🔠"),
|
||||
("abcd", "🔡"),
|
||||
("abc", "🔤"),
|
||||
("arrow_lower_left", "↙️"),
|
||||
("arrow_lower_right", "↘️"),
|
||||
("arrow_right", "➡️"),
|
||||
("arrow_up", "⬆️"),
|
||||
("arrow_upper_left", "↖️"),
|
||||
("arrow_upper_right", "↗️"),
|
||||
("arrow_double_down", "⏬"),
|
||||
("arrow_double_up", "⏫"),
|
||||
("arrow_down_small", "🔽"),
|
||||
("arrow_heading_down", "↘️"),
|
||||
("arrow_heading_up", "↗️"),
|
||||
("leftwards_arrow_with_hook", "↩️"),
|
||||
("arrow_right_hook", "↪️"),
|
||||
("left_right_arrow", "↔️"),
|
||||
("arrow_up_down", "↕️"),
|
||||
("arrow_up_small", "🔼"),
|
||||
("arrows_clockwise", "🔃"),
|
||||
("arrows_counterclockwise", "🔄"),
|
||||
("rewind", "⏪"),
|
||||
("fast_forward", "⏩"),
|
||||
("information_source", "ℹ️"),
|
||||
("ok", "🆗"),
|
||||
("twisted_rightwards_arrows", "🔀"),
|
||||
("repeat", "🔁"),
|
||||
("repeat_one", "🔂"),
|
||||
("new", "🆕"),
|
||||
("top", "🔝"),
|
||||
("up", "🆙"),
|
||||
("cool", "🆒"),
|
||||
("free", "🆓"),
|
||||
("ng", "🆖"),
|
||||
("cinema", "🎦"),
|
||||
("koko", "🈁"),
|
||||
("signal_strength", "📶"),
|
||||
("u5272", "🈹"),
|
||||
("u5408", "🈴"),
|
||||
("u55b6", "🈺"),
|
||||
("u6307", "🈯"),
|
||||
("u6708", "🈷️"),
|
||||
("u6709", "🈶"),
|
||||
("u6e80", "🈵"),
|
||||
("u7121", "🈚"),
|
||||
("u7533", "🈸"),
|
||||
("u7a7a", "🈳"),
|
||||
("u7981", "🈲"),
|
||||
("sa", "🈂️"),
|
||||
("restroom", "🚻"),
|
||||
("mens", "🚹"),
|
||||
("womens", "🚺"),
|
||||
("baby_symbol", "🚼"),
|
||||
("no_smoking", "🚭"),
|
||||
("parking", "🅿️"),
|
||||
("wheelchair", "♿"),
|
||||
("metro", "🚇"),
|
||||
("baggage_claim", "🛄"),
|
||||
("accept", "🉑"),
|
||||
("wc", "🚾"),
|
||||
("potable_water", "🚰"),
|
||||
("put_litter_in_its_place", "🚮"),
|
||||
("secret", "㊙️"),
|
||||
("congratulations", "㊗️"),
|
||||
("m", "Ⓜ️"),
|
||||
("passport_control", "🛂"),
|
||||
("left_luggage", "🛅"),
|
||||
("customs", "🛃"),
|
||||
("ideograph_advantage", "🉐"),
|
||||
("cl", "🆑"),
|
||||
("sos", "🆘"),
|
||||
("id", "🆔"),
|
||||
("no_entry_sign", "🚫"),
|
||||
("underage", "🔞"),
|
||||
("no_mobile_phones", "📵"),
|
||||
("do_not_litter", "🚯"),
|
||||
("non-potable_water", "🚱"),
|
||||
("no_bicycles", "🚳"),
|
||||
("no_pedestrians", "🚷"),
|
||||
("children_crossing", "🚸"),
|
||||
("no_entry", "⛔"),
|
||||
("eight_spoked_asterisk", "✳️"),
|
||||
("eight_pointed_black_star", "✴️"),
|
||||
("heart_decoration", "💟"),
|
||||
("vs", "🆚"),
|
||||
("vibration_mode", "📳"),
|
||||
("mobile_phone_off", "📴"),
|
||||
("chart", "💹"),
|
||||
("currency_exchange", "💱"),
|
||||
("aries", "♈"),
|
||||
("taurus", "♉"),
|
||||
("gemini", "♊"),
|
||||
("cancer", "♋"),
|
||||
("leo", "♌"),
|
||||
("virgo", "♍"),
|
||||
("libra", "♎"),
|
||||
("scorpius", "♏"),
|
||||
("sagittarius", "♐"),
|
||||
("capricorn", "♑"),
|
||||
("aquarius", "♒"),
|
||||
("pisces", "♓"),
|
||||
("ophiuchus", "⛎"),
|
||||
("six_pointed_star", "🔯"),
|
||||
("negative_squared_cross_mark", "❎"),
|
||||
("a", "🅰️"),
|
||||
("b", "🅱️"),
|
||||
("ab", "🆎"),
|
||||
("o2", "🅾️"),
|
||||
("diamond_shape_with_a_dot_inside", "💠"),
|
||||
("recycle", "♻️"),
|
||||
("end", "🔚"),
|
||||
("on", "🔛"),
|
||||
("soon", "🔜"),
|
||||
("clock1", "🕐"),
|
||||
("clock130", "🕜"),
|
||||
("clock10", "🕙"),
|
||||
("clock1030", "🕥"),
|
||||
("clock11", "🕚"),
|
||||
("clock1130", "🕦"),
|
||||
("clock12", "🕛"),
|
||||
("clock1230", "🕧"),
|
||||
("clock2", "🕑"),
|
||||
("clock230", "🕝"),
|
||||
("clock3", "🕒"),
|
||||
("clock330", "🕞"),
|
||||
("clock4", "🕓"),
|
||||
("clock430", "🕟"),
|
||||
("clock5", "🕔"),
|
||||
("clock530", "🕠"),
|
||||
("clock6", "🕕"),
|
||||
("clock630", "🕡"),
|
||||
("clock7", "🕖"),
|
||||
("clock730", "🕢"),
|
||||
("clock8", "🕗"),
|
||||
("clock830", "🕣"),
|
||||
("clock9", "🕘"),
|
||||
("clock930", "🕤"),
|
||||
("heavy_dollar_sign", "💲"),
|
||||
("copyright", "©️"),
|
||||
("registered", "®️"),
|
||||
("tm", "™️"),
|
||||
("x", "❌"),
|
||||
("heavy_exclamation_mark", "❗"),
|
||||
("bangbang", "‼️"),
|
||||
("interrobang", "⁉️"),
|
||||
("o", "⭕"),
|
||||
("heavy_multiplication_x", "✖️"),
|
||||
("heavy_plus_sign", "➕"),
|
||||
("heavy_minus_sign", "➖"),
|
||||
("heavy_division_sign", "➗"),
|
||||
("white_flower", "💮"),
|
||||
("100", "💯"),
|
||||
("heavy_check_mark", "✔️"),
|
||||
("ballot_box_with_check", "☑️"),
|
||||
("radio_button", "🔘"),
|
||||
("link", "🔗"),
|
||||
("curly_loop", "➰"),
|
||||
("wavy_dash", "〰️"),
|
||||
("part_alternation_mark", "〽️"),
|
||||
("trident", "🔱"),
|
||||
("black_square", "⬛"),
|
||||
("white_square", "⬜"),
|
||||
("white_check_mark", "✅"),
|
||||
("black_square_button", "🔲"),
|
||||
("white_square_button", "🔳"),
|
||||
("black_circle", "⚫"),
|
||||
("white_circle", "⚪"),
|
||||
("red_circle", "🔴"),
|
||||
("large_blue_circle", "🔵"),
|
||||
("large_blue_diamond", "🔷"),
|
||||
("large_orange_diamond", "🔶"),
|
||||
("small_blue_diamond", "🔹"),
|
||||
("small_orange_diamond", "🔸"),
|
||||
("small_red_triangle", "🔺"),
|
||||
("small_red_triangle_down", "🔻"),
|
||||
("shipit", "🚢"),
|
||||
])
|
||||
});
|
||||
|
||||
pub fn replace_emoji(text: &str) -> String {
|
||||
let mut result = String::with_capacity(text.len());
|
||||
let mut current = text;
|
||||
|
||||
while let Some((before, after)) = current.split_once(':') {
|
||||
result.push_str(before);
|
||||
|
||||
if let Some((name, rest)) = after.split_once(':') {
|
||||
if let Some(emoji) = EMOJIS.get(name) {
|
||||
result.push_str(emoji);
|
||||
} else {
|
||||
result.push(':');
|
||||
result.push_str(name);
|
||||
result.push(':');
|
||||
}
|
||||
current = rest;
|
||||
} else {
|
||||
result.push(':');
|
||||
result.push_str(after);
|
||||
current = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !current.is_empty() {
|
||||
result.push_str(current);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
530
src/markdown/mod.rs
Normal file
530
src/markdown/mod.rs
Normal file
@ -0,0 +1,530 @@
|
||||
use pulldown_cmark::{
|
||||
Alignment, BlockQuoteKind, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag,
|
||||
TagEnd, TextMergeStream,
|
||||
};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
|
||||
mod emoji;
|
||||
use emoji::replace_emoji;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum AlignmentState {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<Alignment> for AlignmentState {
|
||||
fn from(alignment: Alignment) -> Self {
|
||||
match alignment {
|
||||
Alignment::Left => AlignmentState::Left,
|
||||
Alignment::Center => AlignmentState::Center,
|
||||
Alignment::Right => AlignmentState::Right,
|
||||
Alignment::None => AlignmentState::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AlignmentState {
|
||||
pub fn to_css_class(&self) -> &'static str {
|
||||
match self {
|
||||
AlignmentState::Left => "align-left",
|
||||
AlignmentState::Center => "align-center",
|
||||
AlignmentState::Right => "align-right",
|
||||
AlignmentState::None => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TableState {
|
||||
pub alignments: Vec<AlignmentState>,
|
||||
pub column_count: usize,
|
||||
pub is_inside_table: bool,
|
||||
pub is_inside_head: bool,
|
||||
pub is_inside_body: bool,
|
||||
pub current_row: usize,
|
||||
pub current_column: usize,
|
||||
pub cell_data: HashMap<(usize, usize), String>,
|
||||
pub has_header: bool,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn start_table(&mut self, alignments: Vec<Alignment>) {
|
||||
self.alignments = alignments.into_iter().map(AlignmentState::from).collect();
|
||||
self.column_count = self.alignments.len();
|
||||
self.is_inside_table = true;
|
||||
self.is_inside_head = false;
|
||||
self.is_inside_body = false;
|
||||
self.current_row = 0;
|
||||
self.current_column = 0;
|
||||
self.cell_data.clear();
|
||||
self.has_header = false;
|
||||
}
|
||||
|
||||
pub fn end_table(&mut self) {
|
||||
self.is_inside_table = false;
|
||||
self.is_inside_head = false;
|
||||
self.is_inside_body = false;
|
||||
}
|
||||
|
||||
pub fn start_head(&mut self) {
|
||||
if self.is_inside_table {
|
||||
self.is_inside_head = true;
|
||||
self.is_inside_body = false;
|
||||
self.current_row = 0;
|
||||
self.current_column = 0;
|
||||
self.has_header = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_head(&mut self) {
|
||||
self.is_inside_head = false;
|
||||
}
|
||||
|
||||
pub fn start_row(&mut self) {
|
||||
if self.is_inside_table {
|
||||
if self.is_inside_body {
|
||||
self.current_row += 1;
|
||||
}
|
||||
self.current_column = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_row(&mut self) {
|
||||
if self.current_column != self.column_count && self.is_inside_table && self.column_count > 0
|
||||
{
|
||||
// Исправляем неполные строки, добавляя недостающие ячейки
|
||||
while self.current_column < self.column_count {
|
||||
let row = if self.is_inside_head {
|
||||
0
|
||||
} else {
|
||||
self.current_row
|
||||
};
|
||||
let col = self.current_column;
|
||||
self.cell_data.insert((row, col), String::new());
|
||||
self.current_column += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_cell(&mut self) {
|
||||
if self.is_inside_table {
|
||||
self.current_column += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_cell(&mut self, data: Option<String>) {
|
||||
if let Some(text) = data {
|
||||
let row = if self.is_inside_head {
|
||||
0
|
||||
} else {
|
||||
self.current_row
|
||||
};
|
||||
let col = self.current_column - 1;
|
||||
self.cell_data.insert((row, col), text);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_alignment(&self) -> &AlignmentState {
|
||||
if self.current_column == 0 || self.current_column > self.alignments.len() {
|
||||
return &AlignmentState::None;
|
||||
}
|
||||
let col_index = self.current_column - 1;
|
||||
&self.alignments[col_index]
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Основной рендерер ==========
|
||||
pub struct MarkdownRenderer {
|
||||
table_state: TableState,
|
||||
in_code_block: bool,
|
||||
code_content: String,
|
||||
}
|
||||
|
||||
impl MarkdownRenderer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
table_state: TableState::new(),
|
||||
in_code_block: false,
|
||||
code_content: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_start_tag(&mut self, 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>"),
|
||||
Tag::Paragraph => output.push_str("<p>"),
|
||||
Tag::CodeBlock(kind) => {
|
||||
output.push_str(render_codeblock(kind).as_str());
|
||||
}
|
||||
Tag::Emphasis => output.push_str("<em>"),
|
||||
Tag::Strong => output.push_str("<strong>"),
|
||||
Tag::BlockQuote(kind) => output.push_str(render_blockquote(kind).as_str()),
|
||||
Tag::Link {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
..
|
||||
} => 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()),
|
||||
|
||||
Tag::Table(alignments) => {
|
||||
self.table_state.start_table(alignments.clone());
|
||||
output.push_str("<table>");
|
||||
}
|
||||
|
||||
Tag::TableHead => {
|
||||
self.table_state.start_head();
|
||||
output.push_str("<thead>");
|
||||
}
|
||||
|
||||
Tag::TableCell => {
|
||||
self.table_state.start_cell();
|
||||
let alignment = self.table_state.get_current_alignment();
|
||||
let css_class = alignment.to_css_class();
|
||||
|
||||
if css_class.is_empty() {
|
||||
output.push_str("<td>");
|
||||
} else {
|
||||
let _ = write!(output, "<td class=\"{css_class}\">");
|
||||
}
|
||||
}
|
||||
|
||||
Tag::TableRow => {
|
||||
self.table_state.start_row();
|
||||
output.push_str("<tr>");
|
||||
}
|
||||
|
||||
Tag::FootnoteDefinition(fref) => {
|
||||
let _ = write!(output, "<sup><a href=\"#{fref}\" id=\"{fref}\">");
|
||||
}
|
||||
|
||||
Tag::Superscript => {
|
||||
output.push_str("<sup>");
|
||||
}
|
||||
|
||||
Tag::Subscript => {
|
||||
output.push_str("<sub>");
|
||||
}
|
||||
|
||||
Tag::HtmlBlock
|
||||
| Tag::DefinitionList
|
||||
| Tag::DefinitionListTitle
|
||||
| Tag::DefinitionListDefinition
|
||||
| Tag::MetadataBlock(_) => (),
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn render_end_tag(&mut self, 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>"),
|
||||
TagEnd::Paragraph => output.push_str("</p>"),
|
||||
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>"),
|
||||
TagEnd::Link => output.push_str("</a>"),
|
||||
TagEnd::Image => output.push_str("</img>"),
|
||||
|
||||
TagEnd::Table => {
|
||||
self.table_state.end_table();
|
||||
output.push_str("</table>");
|
||||
}
|
||||
|
||||
TagEnd::TableHead => {
|
||||
self.table_state.end_head();
|
||||
output.push_str("</thead>");
|
||||
if self.table_state.is_inside_table {
|
||||
output.push_str("<tbody>");
|
||||
self.table_state.is_inside_body = true;
|
||||
}
|
||||
}
|
||||
|
||||
TagEnd::TableCell => {
|
||||
self.table_state.end_cell(None);
|
||||
output.push_str("</td>");
|
||||
}
|
||||
|
||||
TagEnd::TableRow => {
|
||||
self.table_state.end_row();
|
||||
output.push_str("</tr>");
|
||||
}
|
||||
|
||||
TagEnd::FootnoteDefinition => {
|
||||
output.push_str("</a></sup>");
|
||||
}
|
||||
|
||||
TagEnd::Superscript => {
|
||||
output.push_str("</sup>");
|
||||
}
|
||||
|
||||
TagEnd::Subscript => {
|
||||
output.push_str("</sub>");
|
||||
}
|
||||
|
||||
TagEnd::HtmlBlock
|
||||
| TagEnd::DefinitionList
|
||||
| TagEnd::DefinitionListTitle
|
||||
| TagEnd::DefinitionListDefinition
|
||||
| TagEnd::MetadataBlock(_) => (),
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Основная функция ==========
|
||||
pub fn markdown_to_html(markdown: &str, _file_path: &str) -> String {
|
||||
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_HEADING_ATTRIBUTES);
|
||||
options.insert(Options::ENABLE_GFM);
|
||||
options.insert(Options::ENABLE_SUBSCRIPT);
|
||||
options.insert(Options::ENABLE_SUPERSCRIPT);
|
||||
|
||||
let mut renderer = MarkdownRenderer::new();
|
||||
let mut html_output = String::new();
|
||||
|
||||
let iterator = TextMergeStream::new(Parser::new_ext(markdown, options));
|
||||
|
||||
for event in iterator {
|
||||
match event {
|
||||
Event::Start(tag) => {
|
||||
if let Tag::CodeBlock(_) = tag {
|
||||
renderer.in_code_block = true;
|
||||
renderer.code_content.clear();
|
||||
}
|
||||
html_output.push_str(renderer.render_start_tag(tag).as_str());
|
||||
}
|
||||
Event::End(tag) => {
|
||||
if let TagEnd::CodeBlock = tag {
|
||||
renderer.in_code_block = false;
|
||||
html_output.push_str(escape_html(&renderer.code_content).as_str());
|
||||
}
|
||||
html_output.push_str(renderer.render_end_tag(tag).as_str());
|
||||
}
|
||||
Event::Text(text) => {
|
||||
if renderer.in_code_block {
|
||||
renderer.code_content.push_str(&text);
|
||||
} else {
|
||||
html_output.push_str(&escape_html(&replace_emoji(&text)));
|
||||
}
|
||||
}
|
||||
Event::TaskListMarker(true) => {
|
||||
html_output.push_str("<input type=\"checkbox\" checked/>");
|
||||
}
|
||||
Event::TaskListMarker(false) => {
|
||||
html_output.push_str("<input type=\"checkbox\"/>");
|
||||
}
|
||||
Event::Code(code) => {
|
||||
html_output.push_str(format!("<code>{}</code>", escape_html(&code)).as_str());
|
||||
}
|
||||
Event::SoftBreak => html_output.push(' '),
|
||||
Event::HardBreak => html_output.push_str("<br>"),
|
||||
|
||||
Event::Html(html) | Event::InlineHtml(html) => html_output.push_str(&html),
|
||||
|
||||
Event::Rule => html_output.push_str("<hr>"),
|
||||
|
||||
Event::FootnoteReference(fref) => {
|
||||
let _ = write!(
|
||||
html_output,
|
||||
"<sup><a href=\"#{fref}\" id=\"{fref}\">{fref}</a></sup>"
|
||||
);
|
||||
}
|
||||
|
||||
Event::InlineMath(_) | Event::DisplayMath(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
html_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 => {
|
||||
output.push_str(&header.replace("%CODE%", "code"));
|
||||
output.push_str("<code>");
|
||||
}
|
||||
|
||||
CodeBlockKind::Fenced(CowStr::Borrowed("mermaid")) => {
|
||||
output.push_str(&header.replace("%CODE%", "mermaid"));
|
||||
output.push_str("<code class=\"language-text mermaid\">");
|
||||
}
|
||||
|
||||
CodeBlockKind::Fenced(CowStr::Borrowed("rawmermaid")) => {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn render_blockquote(quote_kind: Option<BlockQuoteKind>) -> 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 {
|
||||
match link_type {
|
||||
LinkType::Email => format!("<a href=\"mailto:{dest_url}\" title=\"{title}\">"),
|
||||
_ => format!(
|
||||
"<a href=\"{dest_url}\" title=\"{title}\" target=\"_blank\" rel=\"noopener noreferrer\">"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
text.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
// ========== Тесты ==========
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_table_with_alignments() {
|
||||
let markdown = r#"
|
||||
| Header 1 | Header 2 | Header 3 |
|
||||
|----------|:--------:|---------:|
|
||||
| Left | Center | Right |
|
||||
| Data 1 | Data 2 | Data 3 |
|
||||
"#;
|
||||
|
||||
let html = markdown_to_html(markdown, "");
|
||||
|
||||
// Проверяем, что HTML содержит правильные классы и атрибуты
|
||||
assert!(html.contains("<table>"));
|
||||
assert!(html.contains("<thead>"));
|
||||
assert!(html.contains("<tbody>"));
|
||||
assert!(html.contains("text-left"));
|
||||
assert!(html.contains("text-center"));
|
||||
assert!(html.contains("text-right"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_state_tracking() {
|
||||
let mut renderer = MarkdownRenderer::new();
|
||||
let alignments = vec![Alignment::Left, Alignment::Center, Alignment::Right];
|
||||
|
||||
renderer.render_start_tag(Tag::Table(alignments.clone()));
|
||||
assert!(renderer.table_state.is_inside_table);
|
||||
assert_eq!(renderer.table_state.column_count, 3);
|
||||
|
||||
renderer.render_start_tag(Tag::TableHead);
|
||||
assert!(renderer.table_state.is_inside_head);
|
||||
|
||||
renderer.render_start_tag(Tag::TableRow);
|
||||
renderer.render_start_tag(Tag::TableCell);
|
||||
assert_eq!(
|
||||
renderer.table_state.get_current_alignment(),
|
||||
&AlignmentState::Left
|
||||
);
|
||||
|
||||
renderer.render_end_tag(TagEnd::TableCell);
|
||||
renderer.render_start_tag(Tag::TableCell);
|
||||
assert_eq!(
|
||||
renderer.table_state.get_current_alignment(),
|
||||
&AlignmentState::Center
|
||||
);
|
||||
|
||||
renderer.render_end_tag(TagEnd::TableCell);
|
||||
renderer.render_start_tag(Tag::TableCell);
|
||||
assert_eq!(
|
||||
renderer.table_state.get_current_alignment(),
|
||||
&AlignmentState::Right
|
||||
);
|
||||
renderer.render_end_tag(TagEnd::TableCell);
|
||||
renderer.render_end_tag(TagEnd::TableRow);
|
||||
renderer.render_end_tag(TagEnd::TableHead);
|
||||
|
||||
// Проверяем, что тело было автоматически открыто
|
||||
renderer.render_start_tag(Tag::TableRow);
|
||||
renderer.render_start_tag(Tag::TableCell);
|
||||
assert_eq!(
|
||||
renderer.table_state.get_current_alignment(),
|
||||
&AlignmentState::Left
|
||||
);
|
||||
renderer.render_end_tag(TagEnd::TableCell);
|
||||
renderer.render_end_tag(TagEnd::TableRow);
|
||||
renderer.render_end_tag(TagEnd::Table);
|
||||
|
||||
assert!(!renderer.table_state.is_inside_table);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
<title>{% block title %}Note{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ highlight_style_url }}">
|
||||
<style>
|
||||
@ -16,20 +16,68 @@
|
||||
justify-content: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
background-color: #1e1e1e;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
a { color: #bb86fc; text-decoration: none !important; }
|
||||
h1, h2, h3, h4 { color: #ffffff; margin-top: 1.5em; margin-bottom: 0.5em; }
|
||||
h1 { border-bottom: 1px solid #333; padding-bottom: 10px; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
||||
th, td { border: 1px solid #444; padding: 8px; text-align: left; }
|
||||
th { background-color: #2c2c2c; }
|
||||
|
||||
a {
|
||||
color: #bb86fc;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
color: #ffffff;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #444;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
th, thead td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-none {
|
||||
text-align: default;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #bb86fc;
|
||||
margin: 1em 0;
|
||||
@ -38,14 +86,24 @@
|
||||
background: #252525;
|
||||
padding: 10px;
|
||||
}
|
||||
code { font-family: 'Consolas', 'Monaco', monospace; }
|
||||
|
||||
code {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
p > code, li > code {
|
||||
background-color: #2c2c2c;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: #ff79c6;
|
||||
}
|
||||
.header a { padding: 1em; color: #757575; font-size: 0.95em; }
|
||||
|
||||
.header a {
|
||||
padding: 1em;
|
||||
color: #757575;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.code-block-wrapper {
|
||||
margin: 1em 0;
|
||||
border: 1px solid #444;
|
||||
@ -64,7 +122,9 @@
|
||||
font-size: 0.85em;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.code-lang { font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
|
||||
.copy-btn {
|
||||
background: transparent;
|
||||
border: 1px solid #555;
|
||||
@ -75,7 +135,9 @@
|
||||
font-size: 0.8em;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.copy-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
||||
|
||||
.copy-btn:active { transform: scale(0.95); }
|
||||
|
||||
.copy-path-btn {
|
||||
@ -86,16 +148,22 @@
|
||||
font-size: 0.8em;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.copy-path-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
||||
|
||||
.copy-path-btn:active { transform: scale(0.95); }
|
||||
|
||||
pre { padding: 15px; overflow-x: auto; margin: 0; }
|
||||
|
||||
pre code { background: transparent; padding: 0; color: inherit; }
|
||||
|
||||
.mermaid-wrapper .mermaid {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 0 0 6px 6px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
color: #666;
|
||||
@ -103,7 +171,9 @@
|
||||
border-top: 1px solid #333;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.footer a { padding: 1em; color: #757575; }
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -112,41 +182,32 @@
|
||||
font-size: 0.95em;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.back-link span { margin-right: 8px; font-size: 1.2em; }
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1.5em auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1.5em auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
ol {
|
||||
padding-left: 1.5em;
|
||||
list-style-type: decimal;
|
||||
}
|
||||
li {
|
||||
padding: .5em 0;
|
||||
}
|
||||
ul > li::marker, ol > li::marker { display: inline-block; }
|
||||
|
||||
.file-link {
|
||||
color: #64b5f6;
|
||||
font-size: 1.1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-link:hover {
|
||||
color: #90caf9;
|
||||
color: #90caf9;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
min-width: 24px;
|
||||
margin-right: 10px;
|
||||
min-width: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@ -175,8 +236,6 @@
|
||||
<script src="{{ mermaid_script_url }}"></script>
|
||||
<script src="{{ highlight_script_url }}"></script>
|
||||
<script>
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
function copyCode(button) {
|
||||
const wrapper = button.closest('.code-block-wrapper');
|
||||
if (!wrapper) return;
|
||||
@ -215,6 +274,6 @@
|
||||
|
||||
hljs.highlightAll();
|
||||
</script>
|
||||
|
||||
<script>{% block scripts %}{% endblock %}</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends ".base.min.html" %}
|
||||
|
||||
{% block title %}Directory: /{{ title_path }}{% endblock %}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends ".base.min.html" %}
|
||||
|
||||
{% block title %}{{ filename }}{% endblock %}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user