fix: tables
This commit is contained in:
parent
1411dd0570
commit
b5b104a28e
@ -5,6 +5,12 @@ The format is based on [Keep a
|
|||||||
Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
|
Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
|
||||||
[Semantic Versioning](https://semver.org/lang/en/).
|
[Semantic Versioning](https://semver.org/lang/en/).
|
||||||
|
|
||||||
|
## v0.5.1 - 2026-07-27
|
||||||
|
### Fixed
|
||||||
|
- Fixed tables rendering
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.5.0 - 2026-07-26
|
## v0.5.0 - 2026-07-26
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1188,7 +1188,7 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdpreview"
|
name = "mdpreview"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_axum",
|
"askama_axum",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mdpreview"
|
name = "mdpreview"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|||||||
2
TODO.md
2
TODO.md
@ -5,4 +5,4 @@
|
|||||||
* [X] ~~Сделать отображение типа кода и кнопку скопировать код~~
|
* [X] ~~Сделать отображение типа кода и кнопку скопировать код~~
|
||||||
* [X] ~~Вынести зависимости, типа скриптов mermaid и highlight.js в
|
* [X] ~~Вынести зависимости, типа скриптов mermaid и highlight.js в
|
||||||
include_bytes~~
|
include_bytes~~
|
||||||
* [ ] Починить заголовки таблицы
|
* [X] ~~Починить заголовки таблицы~~
|
||||||
|
|||||||
481
src/markdown.rs
481
src/markdown.rs
@ -1,10 +1,300 @@
|
|||||||
use pulldown_cmark::{
|
use pulldown_cmark::{
|
||||||
BlockQuoteKind, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd,
|
Alignment, BlockQuoteKind, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag,
|
||||||
TextMergeStream,
|
TagEnd, TextMergeStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
// ========== Класс состояния таблицы ==========
|
||||||
|
#[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::HtmlBlock
|
||||||
|
| Tag::DefinitionList
|
||||||
|
| Tag::DefinitionListTitle
|
||||||
|
| Tag::DefinitionListDefinition
|
||||||
|
| Tag::Superscript
|
||||||
|
| Tag::Subscript
|
||||||
|
| 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::HtmlBlock
|
||||||
|
| TagEnd::DefinitionList
|
||||||
|
| TagEnd::DefinitionListTitle
|
||||||
|
| TagEnd::DefinitionListDefinition
|
||||||
|
| TagEnd::Superscript
|
||||||
|
| TagEnd::Subscript
|
||||||
|
| TagEnd::MetadataBlock(_) => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Основная функция ==========
|
||||||
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);
|
||||||
@ -14,9 +304,7 @@ pub fn markdown_to_html(markdown: &str, _file_path: &str) -> String {
|
|||||||
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
||||||
options.insert(Options::ENABLE_GFM);
|
options.insert(Options::ENABLE_GFM);
|
||||||
|
|
||||||
let mut in_code_block = false;
|
let mut renderer = MarkdownRenderer::new();
|
||||||
let mut code_content = String::new();
|
|
||||||
|
|
||||||
let mut html_output = String::new();
|
let mut html_output = String::new();
|
||||||
|
|
||||||
let iterator = TextMergeStream::new(Parser::new_ext(markdown, options));
|
let iterator = TextMergeStream::new(Parser::new_ext(markdown, options));
|
||||||
@ -25,21 +313,21 @@ pub fn markdown_to_html(markdown: &str, _file_path: &str) -> String {
|
|||||||
match event {
|
match event {
|
||||||
Event::Start(tag) => {
|
Event::Start(tag) => {
|
||||||
if let Tag::CodeBlock(_) = tag {
|
if let Tag::CodeBlock(_) = tag {
|
||||||
in_code_block = true;
|
renderer.in_code_block = true;
|
||||||
code_content.clear();
|
renderer.code_content.clear();
|
||||||
}
|
}
|
||||||
html_output.push_str(render_start_tag(tag).as_str());
|
html_output.push_str(renderer.render_start_tag(tag).as_str());
|
||||||
}
|
}
|
||||||
Event::End(tag) => {
|
Event::End(tag) => {
|
||||||
if let TagEnd::CodeBlock = tag {
|
if let TagEnd::CodeBlock = tag {
|
||||||
in_code_block = false;
|
renderer.in_code_block = false;
|
||||||
html_output.push_str(escape_html(&code_content).as_str());
|
html_output.push_str(escape_html(&renderer.code_content).as_str());
|
||||||
}
|
}
|
||||||
html_output.push_str(render_end_tag(tag).as_str());
|
html_output.push_str(renderer.render_end_tag(tag).as_str());
|
||||||
}
|
}
|
||||||
Event::Text(text) => {
|
Event::Text(text) => {
|
||||||
if in_code_block {
|
if renderer.in_code_block {
|
||||||
code_content.push_str(&text);
|
renderer.code_content.push_str(&text);
|
||||||
} else {
|
} else {
|
||||||
html_output.push_str(&escape_html(&text));
|
html_output.push_str(&escape_html(&text));
|
||||||
}
|
}
|
||||||
@ -74,96 +362,7 @@ pub fn markdown_to_html(markdown: &str, _file_path: &str) -> String {
|
|||||||
html_output
|
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 {
|
fn render_codeblock(block_kind: CodeBlockKind<'_>) -> String {
|
||||||
let header = String::from(
|
let header = String::from(
|
||||||
r#"
|
r#"
|
||||||
@ -235,3 +434,79 @@ fn escape_html(text: &str) -> String {
|
|||||||
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -24,12 +25,54 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
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; }
|
a {
|
||||||
h1 { border-bottom: 1px solid #333; padding-bottom: 10px; }
|
color: #bb86fc;
|
||||||
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
text-decoration: none !important;
|
||||||
th, td { border: 1px solid #444; padding: 8px; text-align: left; }
|
}
|
||||||
th { background-color: #2c2c2c; }
|
|
||||||
|
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 {
|
blockquote {
|
||||||
border-left: 4px solid #bb86fc;
|
border-left: 4px solid #bb86fc;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
@ -38,14 +81,24 @@
|
|||||||
background: #252525;
|
background: #252525;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
code { font-family: 'Consolas', 'Monaco', monospace; }
|
|
||||||
|
code {
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
p > code, li > code {
|
p > code, li > code {
|
||||||
background-color: #2c2c2c;
|
background-color: #2c2c2c;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #ff79c6;
|
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 {
|
.code-block-wrapper {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
@ -64,7 +117,9 @@
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #a0a0a0;
|
color: #a0a0a0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-lang { font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; }
|
.code-lang { font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||||
|
|
||||||
.copy-btn {
|
.copy-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
@ -75,7 +130,9 @@
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
.copy-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
||||||
|
|
||||||
.copy-btn:active { transform: scale(0.95); }
|
.copy-btn:active { transform: scale(0.95); }
|
||||||
|
|
||||||
.copy-path-btn {
|
.copy-path-btn {
|
||||||
@ -86,16 +143,22 @@
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-path-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
.copy-path-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
||||||
|
|
||||||
.copy-path-btn:active { transform: scale(0.95); }
|
.copy-path-btn:active { transform: scale(0.95); }
|
||||||
|
|
||||||
pre { padding: 15px; overflow-x: auto; margin: 0; }
|
pre { padding: 15px; overflow-x: auto; margin: 0; }
|
||||||
|
|
||||||
pre code { background: transparent; padding: 0; color: inherit; }
|
pre code { background: transparent; padding: 0; color: inherit; }
|
||||||
|
|
||||||
.mermaid-wrapper .mermaid {
|
.mermaid-wrapper .mermaid {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
border-radius: 0 0 6px 6px;
|
border-radius: 0 0 6px 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@ -103,7 +166,9 @@
|
|||||||
border-top: 1px solid #333;
|
border-top: 1px solid #333;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer a { padding: 1em; color: #757575; }
|
.footer a { padding: 1em; color: #757575; }
|
||||||
|
|
||||||
.back-link {
|
.back-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -112,41 +177,32 @@
|
|||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-link span { margin-right: 8px; font-size: 1.2em; }
|
.back-link span { margin-right: 8px; font-size: 1.2em; }
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 1.5em auto;
|
margin: 1.5em auto;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
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 {
|
.file-link {
|
||||||
color: #64b5f6;
|
color: #64b5f6;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-link:hover {
|
.file-link:hover {
|
||||||
color: #90caf9;
|
color: #90caf9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user