mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 18:28:02 +00:00
fix(typlite): table with table.header
did not convert properly (#1812)
* chore: add TODO comment for table.header rendering in md-table function * feat: enhance table parsing to support thead/tbody structure and complex cells * fmt * feat: add InlineNode for handling flattened inline content in table cells * feat: update snapshot files and enhance DOCX/LaTeX writer to support InlineNode processing * refactor: custom node handling in DOCX and LaTeX writers * dev: md-grid should have similar signature to grid --------- Co-authored-by: Myriad-Dreamin <camiyoru@gmail.com>
This commit is contained in:
parent
9a44074629
commit
8fb118f3ff
12 changed files with 345 additions and 187 deletions
|
@ -188,6 +188,30 @@ impl CenterNode {
|
|||
}
|
||||
}
|
||||
|
||||
/// Inline node for flattened inline content (useful for table cells)
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[custom_node(block = false, html_impl = true)]
|
||||
pub struct InlineNode {
|
||||
/// The inline content nodes
|
||||
pub content: Vec<Node>,
|
||||
}
|
||||
|
||||
impl InlineNode {
|
||||
fn write_custom(&self, writer: &mut CommonMarkWriter) -> WriteResult<()> {
|
||||
for node in &self.content {
|
||||
writer.write(node)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_html_custom(&self, writer: &mut HtmlWriter) -> HtmlWriteResult<()> {
|
||||
for node in &self.content {
|
||||
writer.write_node(node)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Alert node for alert messages
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[custom_node(block = true, html_impl = false)]
|
||||
|
|
|
@ -43,6 +43,17 @@ In Figure 1 you can see a common representation of the Sun, which is a star tha
|
|||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim aeque doleamus animo, cum corpore dolemus, fieri tamen permagna accessio potest, si aliquod aeternum et infinitum impendere malum nobis opinemur. Quod idem licet transferre in voluptatem, ut postea variari voluptas distinguique possit, augeri amplificarique non possit. At etiam Athenis, ut e patre audiebam facete et urbane Stoicos irridente, statua est in quo a nobis philosophia defensa et collaudata est, cum id, quod maxime placeat, facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum.
|
||||
|
||||
| Planet | Distance (million km) |
|
||||
| --- | --- |
|
||||
| Mercury | 57.9 |
|
||||
| Venus | 108.2 |
|
||||
| Earth | 149.6 |
|
||||
| Mars | 227.9 |
|
||||
| Jupiter | 778.6 |
|
||||
| Saturn | 1,433.5 |
|
||||
| Uranus | 2,872.5 |
|
||||
| Neptune | 4,495.1 |
|
||||
|
||||
<p align="center"><figure class="figure"><p></p>
|
||||
</figure></p>
|
||||
|
||||
|
@ -54,6 +65,6 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
|
|||
|
||||
## Bibliography
|
||||
|
||||
| \[1\] | R. Astley | and | L. Morris | , | “At-scale impact of the Net Wok: A culinarically holistic investigation of distributed dumplings,” | | Armenian Journal of Proceedings | , vol. | 61 | , | pp. | | 192–219 | , | 2020 | . |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| \[2\] | L. Morris | and | R. Astley | , | “Net Wok++: Taking distributed dumplings to the cloud,” | | Armenian Journal of Proceedings | , vol. | 65 | , | pp. | | 101–118 | , | 2022 | . |
|
||||
| \[1\] | R. Astley and L. Morris, “At-scale impact of the Net Wok: A culinarically holistic investigation of distributed dumplings,” Armenian Journal of Proceedings, vol. 61, pp. 192–219, 2020. |
|
||||
| --- | --- |
|
||||
| \[2\] | L. Morris and R. Astley, “Net Wok++: Taking distributed dumplings to the cloud,” Armenian Journal of Proceedings, vol. 65, pp. 101–118, 2022. |
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
---
|
||||
source: crates/typlite/src/tests.rs
|
||||
expression: "conv(world, false)"
|
||||
expression: "conv(world, ConvKind::Md { for_docs: false })"
|
||||
input_file: crates/typlite/src/fixtures/integration/table.typ
|
||||
snapshot_kind: text
|
||||
---
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -20,6 +21,6 @@ input_file: crates/typlite/src/fixtures/integration/table.typ
|
|||
| --- | --- | --- | --- | --- | --- |
|
||||
| 6 | 7 | 8 | 9 | 10 | 11 |
|
||||
| 12 | 13 | 14 | 15 | 16 | 17 |
|
||||
| 18 | 19 |
|
||||
| 18 | 19 | | | | |
|
||||
|
||||
<table><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr><tr><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td></tr><tr><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td></tr><tr><td>18</td><td>19</td><td colspan="2">0</td><td colspan="2">1</td></tr><tr><td colspan="2">2</td><td colspan="2">3</td><td colspan="2">4</td></tr><tr><td colspan="2">5</td><td colspan="2">6</td><td colspan="2">7</td></tr><tr><td colspan="2">8</td><td colspan="2">9</td><td colspan="2">10</td></tr><tr><td colspan="2">11</td><td colspan="2">12</td><td colspan="2">13</td></tr><tr><td colspan="2">14</td><td colspan="2">15</td><td colspan="2">16</td></tr><tr><td colspan="2">17</td><td colspan="2">18</td><td colspan="2">19</td></tr></table>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
source: crates/typlite/src/tests.rs
|
||||
expression: "conv(world, ConvKind::LaTeX)"
|
||||
input_file: crates/typlite/src/fixtures/integration/ieee.typ
|
||||
snapshot_kind: text
|
||||
---
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -50,6 +51,24 @@ In Figure 1 you can see a common representation of the Sun, which is a star tha
|
|||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim aeque doleamus animo, cum corpore dolemus, fieri tamen permagna accessio potest, si aliquod aeternum et infinitum impendere malum nobis opinemur. Quod idem licet transferre in voluptatem, ut postea variari voluptas distinguique possit, augeri amplificarique non possit. At etiam Athenis, ut e patre audiebam facete et urbane Stoicos irridente, statua est in quo a nobis philosophia defensa et collaudata est, cum id, quod maxime placeat, facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum.
|
||||
|
||||
\begin{table}[htbp]
|
||||
\centering
|
||||
\begin{tabular}{cc}
|
||||
\hline
|
||||
Planet & Distance (million km) \\
|
||||
\hline
|
||||
Mercury & 57.9 \\
|
||||
Venus & 108.2 \\
|
||||
Earth & 149.6 \\
|
||||
Mars & 227.9 \\
|
||||
Jupiter & 778.6 \\
|
||||
Saturn & 1,433.5 \\
|
||||
Uranus & 2,872.5 \\
|
||||
Neptune & 4,495.1 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
\begin{center}
|
||||
\begin{figure}[htbp]
|
||||
\centering
|
||||
|
@ -68,11 +87,11 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
|
|||
|
||||
\begin{table}[htbp]
|
||||
\centering
|
||||
\begin{tabular}{ccccccccccccccccc}
|
||||
\begin{tabular}{cc}
|
||||
\hline
|
||||
[1] & R. Astley & and & L. Morris & , & “At-scale impact of the Net Wok: A culinarically holistic investigation of distributed dumplings,” & & Armenian Journal of Proceedings & , vol. & 61 & , & pp. & & 192–219 & , & 2020 & . \\
|
||||
[1] & R. Astley and L. Morris, “At-scale impact of the Net Wok: A culinarically holistic investigation of distributed dumplings,” Armenian Journal of Proceedings, vol. 61, pp. 192–219, 2020. \\
|
||||
\hline
|
||||
[2] & L. Morris & and & R. Astley & , & “Net Wok++: Taking distributed dumplings to the cloud,” & & Armenian Journal of Proceedings & , vol. & 65 & , & pp. & & 101–118 & , & 2022 & . \\
|
||||
[2] & L. Morris and R. Astley, “Net Wok++: Taking distributed dumplings to the cloud,” Armenian Journal of Proceedings, vol. 65, pp. 101–118, 2022. \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
source: crates/typlite/src/tests.rs
|
||||
expression: "conv(world, ConvKind::LaTeX)"
|
||||
input_file: crates/typlite/src/fixtures/integration/table.typ
|
||||
snapshot_kind: text
|
||||
---
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -33,7 +34,7 @@ input_file: crates/typlite/src/fixtures/integration/table.typ
|
|||
\hline
|
||||
6 & 7 & 8 & 9 & 10 & 11 \\
|
||||
12 & 13 & 14 & 15 & 16 & 17 \\
|
||||
18 & 19 \\
|
||||
18 & 19 & & & & \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
source: crates/typlite/src/tests.rs
|
||||
expression: hash
|
||||
input_file: crates/typlite/src/fixtures/integration/ieee.typ
|
||||
snapshot_kind: text
|
||||
---
|
||||
siphash128_13:75c6857b5ce54f3db89646afcd1f4f78
|
||||
siphash128_13:914f20b049c227a73ad350294a7d79b3
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
source: crates/typlite/src/tests.rs
|
||||
expression: hash
|
||||
input_file: crates/typlite/src/fixtures/integration/image.typ
|
||||
snapshot_kind: text
|
||||
---
|
||||
siphash128_13:89ee713812f00bde9ac174f72c81760
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
source: crates/typlite/src/tests.rs
|
||||
expression: hash
|
||||
input_file: crates/typlite/src/fixtures/integration/table.typ
|
||||
snapshot_kind: text
|
||||
---
|
||||
siphash128_13:ce1b6f668016a12edf304ab7f38aea42
|
||||
siphash128_13:593e825fdc657e578e557142b4e22211
|
||||
|
|
|
@ -76,7 +76,23 @@
|
|||
)
|
||||
#let md-grid(columns: auto, ..children) = html.elem(
|
||||
"m1grid",
|
||||
table(columns: columns, ..children.pos().map(it => table.cell(it))),
|
||||
{
|
||||
let children = children.pos()
|
||||
let header = if children.first().func() == grid.header {
|
||||
(table.header(..children.first().children.map(cell => table.cell(cell.body))),)
|
||||
children = children.slice(1)
|
||||
} else {
|
||||
()
|
||||
}
|
||||
let footer = if children.last().func() == grid.footer {
|
||||
(table.footer(..children.last().children.map(cell => table.cell(cell.body))),)
|
||||
children = children.slice(0, -1)
|
||||
} else {
|
||||
()
|
||||
}
|
||||
|
||||
table(columns: columns, ..header, ..children.map(it => table.cell(it)), ..footer)
|
||||
},
|
||||
)
|
||||
#let md-image(src: "", alt: none) = html.elem(
|
||||
"m1image",
|
||||
|
|
|
@ -5,6 +5,7 @@ use cmark_writer::gfm::TableAlignment;
|
|||
use typst::html::{tag, HtmlElement, HtmlNode};
|
||||
use typst::utils::PicoStr;
|
||||
|
||||
use crate::common::InlineNode;
|
||||
use crate::tags::md_tag;
|
||||
use crate::Result;
|
||||
|
||||
|
@ -88,21 +89,56 @@ impl TableParser {
|
|||
fn extract_table_content(
|
||||
parser: &mut HtmlToAstParser,
|
||||
table: &HtmlElement,
|
||||
headers: &mut Vec<Vec<Node>>,
|
||||
rows: &mut Vec<Vec<Vec<Node>>>,
|
||||
headers: &mut Vec<Node>,
|
||||
rows: &mut Vec<Vec<Node>>,
|
||||
is_header: &mut bool,
|
||||
) -> Result<()> {
|
||||
// Process rows in the table
|
||||
for row_node in &table.children {
|
||||
// Process table structure (direct rows or thead/tbody)
|
||||
for child_node in &table.children {
|
||||
if let HtmlNode::Element(element) = child_node {
|
||||
match element.tag {
|
||||
tag::thead => {
|
||||
// Process header rows
|
||||
Self::process_table_section(parser, element, headers, rows, true)?;
|
||||
*is_header = false;
|
||||
}
|
||||
tag::tbody => {
|
||||
// Process body rows
|
||||
Self::process_table_section(parser, element, headers, rows, false)?;
|
||||
}
|
||||
tag::tr => {
|
||||
// Direct row (no thead/tbody structure)
|
||||
let current_row =
|
||||
Self::process_table_row(parser, element, *is_header, headers)?;
|
||||
|
||||
// After the first row, treat remaining rows as data rows
|
||||
if *is_header {
|
||||
*is_header = false;
|
||||
} else if !current_row.is_empty() {
|
||||
rows.push(current_row);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_table_section(
|
||||
parser: &mut HtmlToAstParser,
|
||||
section: &HtmlElement,
|
||||
headers: &mut Vec<Node>,
|
||||
rows: &mut Vec<Vec<Node>>,
|
||||
is_header_section: bool,
|
||||
) -> Result<()> {
|
||||
for row_node in §ion.children {
|
||||
if let HtmlNode::Element(row_elem) = row_node {
|
||||
if row_elem.tag == tag::tr {
|
||||
let current_row =
|
||||
Self::process_table_row(parser, row_elem, *is_header, headers)?;
|
||||
Self::process_table_row(parser, row_elem, is_header_section, headers)?;
|
||||
|
||||
// After the first row, treat remaining rows as data rows
|
||||
if *is_header {
|
||||
*is_header = false;
|
||||
} else if !current_row.is_empty() {
|
||||
if !is_header_section && !current_row.is_empty() {
|
||||
rows.push(current_row);
|
||||
}
|
||||
}
|
||||
|
@ -115,22 +151,25 @@ impl TableParser {
|
|||
parser: &mut HtmlToAstParser,
|
||||
row_elem: &HtmlElement,
|
||||
is_header: bool,
|
||||
headers: &mut Vec<Vec<Node>>,
|
||||
) -> Result<Vec<Vec<Node>>> {
|
||||
headers: &mut Vec<Node>,
|
||||
) -> Result<Vec<Node>> {
|
||||
let mut current_row = Vec::new();
|
||||
|
||||
// Process cells in this row
|
||||
for cell_node in &row_elem.children {
|
||||
if let HtmlNode::Element(cell) = cell_node {
|
||||
if cell.tag == tag::td {
|
||||
if cell.tag == tag::td || cell.tag == tag::th {
|
||||
let mut cell_content = Vec::new();
|
||||
parser.convert_children_into(&mut cell_content, cell)?;
|
||||
|
||||
// Merge cell content into a single node
|
||||
let merged_cell = Self::merge_cell_content(cell_content);
|
||||
|
||||
// Add to appropriate section
|
||||
if is_header {
|
||||
headers.push(cell_content);
|
||||
if is_header || cell.tag == tag::th {
|
||||
headers.push(merged_cell);
|
||||
} else {
|
||||
current_row.push(cell_content);
|
||||
current_row.push(merged_cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,48 +178,75 @@ impl TableParser {
|
|||
Ok(current_row)
|
||||
}
|
||||
|
||||
/// Merge cell content nodes into a single node
|
||||
fn merge_cell_content(content: Vec<Node>) -> Node {
|
||||
match content.len() {
|
||||
0 => Node::Text("".to_string()),
|
||||
1 => content.into_iter().next().unwrap(),
|
||||
_ => Node::Custom(Box::new(InlineNode { content })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the table has complex cells (rowspan/colspan)
|
||||
fn table_has_complex_cells(table: &HtmlElement) -> bool {
|
||||
for row_node in &table.children {
|
||||
if let HtmlNode::Element(row_elem) = row_node {
|
||||
if row_elem.tag == tag::tr {
|
||||
for cell_node in &row_elem.children {
|
||||
if let HtmlNode::Element(cell) = cell_node {
|
||||
if (cell.tag == tag::td || cell.tag == tag::th)
|
||||
&& cell.attrs.0.iter().any(|(name, _)| {
|
||||
let name = name.into_inner();
|
||||
name == PicoStr::constant("colspan")
|
||||
|| name == PicoStr::constant("rowspan")
|
||||
})
|
||||
{
|
||||
return true;
|
||||
}
|
||||
for child_node in &table.children {
|
||||
if let HtmlNode::Element(element) = child_node {
|
||||
match element.tag {
|
||||
tag::thead | tag::tbody => {
|
||||
// Check rows within thead/tbody
|
||||
if Self::check_section_for_complex_cells(element) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
tag::tr => {
|
||||
// Direct row
|
||||
if Self::check_row_for_complex_cells(element) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn create_table_node(
|
||||
headers: Vec<Vec<Node>>,
|
||||
rows: Vec<Vec<Vec<Node>>>,
|
||||
) -> Result<Option<Node>> {
|
||||
fn check_section_for_complex_cells(section: &HtmlElement) -> bool {
|
||||
for row_node in §ion.children {
|
||||
if let HtmlNode::Element(row_elem) = row_node {
|
||||
if row_elem.tag == tag::tr && Self::check_row_for_complex_cells(row_elem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn check_row_for_complex_cells(row_elem: &HtmlElement) -> bool {
|
||||
for cell_node in &row_elem.children {
|
||||
if let HtmlNode::Element(cell) = cell_node {
|
||||
if (cell.tag == tag::td || cell.tag == tag::th)
|
||||
&& cell.attrs.0.iter().any(|(name, _)| {
|
||||
let name = name.into_inner();
|
||||
name == PicoStr::constant("colspan") || name == PicoStr::constant("rowspan")
|
||||
})
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn create_table_node(headers: Vec<Node>, rows: Vec<Vec<Node>>) -> Result<Option<Node>> {
|
||||
// Create alignment array (default to None for all columns)
|
||||
let alignments = vec![TableAlignment::None; headers.len().max(1)];
|
||||
|
||||
// If there is content, add the table to blocks
|
||||
if !headers.is_empty() || !rows.is_empty() {
|
||||
let flattened_headers = headers.into_iter().flatten().collect();
|
||||
let flattened_rows: Vec<_> = rows
|
||||
.into_iter()
|
||||
.map(|row| row.into_iter().flatten().collect())
|
||||
.collect();
|
||||
|
||||
return Ok(Some(Node::Table {
|
||||
headers: flattened_headers,
|
||||
rows: flattened_rows,
|
||||
headers,
|
||||
rows,
|
||||
alignments,
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use ecow::EcoString;
|
|||
use std::fs;
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::common::{CenterNode, FigureNode, FormatWriter, HighlightNode};
|
||||
use crate::common::{CenterNode, FigureNode, FormatWriter, HighlightNode, InlineNode};
|
||||
use crate::Result;
|
||||
|
||||
use super::image_processor::DocxImageProcessor;
|
||||
|
@ -235,15 +235,17 @@ impl DocxWriter {
|
|||
Node::SoftBreak => {
|
||||
run = run.add_text(" ");
|
||||
}
|
||||
Node::Custom(custom_node) => {
|
||||
if let Some(highlight_node) = custom_node.as_any().downcast_ref::<HighlightNode>() {
|
||||
run = run.highlight("yellow");
|
||||
for child in &highlight_node.content {
|
||||
run = self.process_inline_to_run(run, child)?;
|
||||
}
|
||||
} else {
|
||||
// Handle other custom inline nodes if needed
|
||||
println!("Unhandled custom inline node: {:?}", custom_node);
|
||||
node if node.is_custom_type::<HighlightNode>() => {
|
||||
let highlight_node = node.as_custom_type::<HighlightNode>().unwrap();
|
||||
run = run.highlight("yellow");
|
||||
for child in &highlight_node.content {
|
||||
run = self.process_inline_to_run(run, child)?;
|
||||
}
|
||||
}
|
||||
node if node.is_custom_type::<InlineNode>() => {
|
||||
let inline_node = node.as_custom_type::<InlineNode>().unwrap();
|
||||
for child in &inline_node.content {
|
||||
run = self.process_inline_to_run(run, child)?;
|
||||
}
|
||||
}
|
||||
// Other inline element types
|
||||
|
@ -423,65 +425,76 @@ impl DocxWriter {
|
|||
Node::Image { url, title: _, alt } => {
|
||||
docx = self.process_image(docx, url, alt)?;
|
||||
}
|
||||
Node::Custom(custom_node) => {
|
||||
if let Some(figure_node) = custom_node.as_any().downcast_ref::<FigureNode>() {
|
||||
docx = self.process_figure(docx, figure_node)?;
|
||||
} else if let Some(center_node) = custom_node.as_any().downcast_ref::<CenterNode>()
|
||||
{
|
||||
// Handle regular node but with center alignment
|
||||
match ¢er_node.node {
|
||||
Node::Paragraph(content) => {
|
||||
docx = self.process_paragraph(docx, content, None)?;
|
||||
// Get the last paragraph and center it
|
||||
if let Some(DocumentChild::Paragraph(para)) =
|
||||
docx.document.children.last_mut()
|
||||
{
|
||||
para.property = para.property.clone().align(AlignmentType::Center);
|
||||
}
|
||||
}
|
||||
other => {
|
||||
docx = self.process_node(docx, other)?;
|
||||
// Get the last element and center it if it's a paragraph
|
||||
if let Some(DocumentChild::Paragraph(para)) =
|
||||
docx.document.children.last_mut()
|
||||
{
|
||||
para.property = para.property.clone().align(AlignmentType::Center);
|
||||
}
|
||||
node if node.is_custom_type::<FigureNode>() => {
|
||||
let figure_node = node.as_custom_type::<FigureNode>().unwrap();
|
||||
docx = self.process_figure(docx, figure_node)?;
|
||||
}
|
||||
node if node.is_custom_type::<CenterNode>() => {
|
||||
let center_node = node.as_custom_type::<CenterNode>().unwrap();
|
||||
// Handle regular node but with center alignment
|
||||
match ¢er_node.node {
|
||||
Node::Paragraph(content) => {
|
||||
docx = self.process_paragraph(docx, content, None)?;
|
||||
// Get the last paragraph and center it
|
||||
if let Some(DocumentChild::Paragraph(para)) =
|
||||
docx.document.children.last_mut()
|
||||
{
|
||||
para.property = para.property.clone().align(AlignmentType::Center);
|
||||
}
|
||||
}
|
||||
} else if let Some(external_frame) = custom_node
|
||||
.as_any()
|
||||
.downcast_ref::<crate::common::ExternalFrameNode>(
|
||||
) {
|
||||
let data = base64::engine::general_purpose::STANDARD
|
||||
.decode(&external_frame.svg)
|
||||
.map_err(|e| format!("Failed to decode SVG data: {}", e))?;
|
||||
|
||||
docx = self.image_processor.process_image_data(
|
||||
docx,
|
||||
&data,
|
||||
Some(&external_frame.alt_text),
|
||||
None,
|
||||
);
|
||||
} else if let Some(highlight_node) =
|
||||
custom_node.as_any().downcast_ref::<HighlightNode>()
|
||||
{
|
||||
// Handle HighlightNode at block level (convert to paragraph)
|
||||
let mut para = Paragraph::new();
|
||||
let mut run = Run::new().highlight("yellow");
|
||||
|
||||
for child in &highlight_node.content {
|
||||
run = self.process_inline_to_run(run, child)?;
|
||||
other => {
|
||||
docx = self.process_node(docx, other)?;
|
||||
// Get the last element and center it if it's a paragraph
|
||||
if let Some(DocumentChild::Paragraph(para)) =
|
||||
docx.document.children.last_mut()
|
||||
{
|
||||
para.property = para.property.clone().align(AlignmentType::Center);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
node if node.is_custom_type::<crate::common::ExternalFrameNode>() => {
|
||||
let external_frame = node
|
||||
.as_custom_type::<crate::common::ExternalFrameNode>()
|
||||
.unwrap();
|
||||
let data = base64::engine::general_purpose::STANDARD
|
||||
.decode(&external_frame.svg)
|
||||
.map_err(|e| format!("Failed to decode SVG data: {}", e))?;
|
||||
|
||||
if !run.children.is_empty() {
|
||||
para = para.add_run(run);
|
||||
docx = docx.add_paragraph(para);
|
||||
}
|
||||
} else {
|
||||
// Fallback for unknown custom nodes - ignore or add placeholder
|
||||
let placeholder = "[Unknown custom content]";
|
||||
let para = Paragraph::new().add_run(Run::new().add_text(placeholder));
|
||||
docx = self.image_processor.process_image_data(
|
||||
docx,
|
||||
&data,
|
||||
Some(&external_frame.alt_text),
|
||||
None,
|
||||
);
|
||||
}
|
||||
node if node.is_custom_type::<HighlightNode>() => {
|
||||
let highlight_node = node.as_custom_type::<HighlightNode>().unwrap();
|
||||
// Handle HighlightNode at block level (convert to paragraph)
|
||||
let mut para = Paragraph::new();
|
||||
let mut run = Run::new().highlight("yellow");
|
||||
|
||||
for child in &highlight_node.content {
|
||||
run = self.process_inline_to_run(run, child)?;
|
||||
}
|
||||
|
||||
if !run.children.is_empty() {
|
||||
para = para.add_run(run);
|
||||
docx = docx.add_paragraph(para);
|
||||
}
|
||||
}
|
||||
node if node.is_custom_type::<InlineNode>() => {
|
||||
let inline_node = node.as_custom_type::<InlineNode>().unwrap();
|
||||
// Handle InlineNode at block level (convert to paragraph)
|
||||
let mut para = Paragraph::new();
|
||||
let mut run = Run::new();
|
||||
|
||||
for child in &inline_node.content {
|
||||
run = self.process_inline_to_run(run, child)?;
|
||||
}
|
||||
|
||||
if !run.children.is_empty() {
|
||||
para = para.add_run(run);
|
||||
docx = docx.add_paragraph(para);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use ecow::EcoString;
|
|||
use tinymist_std::path::unix_slash;
|
||||
|
||||
use crate::common::{
|
||||
CenterNode, ExternalFrameNode, FigureNode, FormatWriter, HighlightNode, ListState,
|
||||
CenterNode, ExternalFrameNode, FigureNode, FormatWriter, HighlightNode, InlineNode, ListState,
|
||||
};
|
||||
use crate::Result;
|
||||
|
||||
|
@ -231,83 +231,87 @@ impl LaTeXWriter {
|
|||
output.push_str("\\end{tabular}\n");
|
||||
output.push_str("\\end{table}\n\n");
|
||||
}
|
||||
Node::Custom(custom_node) => {
|
||||
if let Some(figure_node) = custom_node.as_any().downcast_ref::<FigureNode>() {
|
||||
// Start figure environment
|
||||
output.push_str("\\begin{figure}[htbp]\n\\centering\n");
|
||||
node if node.is_custom_type::<FigureNode>() => {
|
||||
let figure_node = node.as_custom_type::<FigureNode>().unwrap();
|
||||
// Start figure environment
|
||||
output.push_str("\\begin{figure}[htbp]\n\\centering\n");
|
||||
|
||||
// Handle the body content (typically an image)
|
||||
match &*figure_node.body {
|
||||
Node::Paragraph(content) => {
|
||||
for node in content {
|
||||
// Special handling for image nodes in figures
|
||||
if let Node::Image {
|
||||
url,
|
||||
title: _,
|
||||
alt: _,
|
||||
} = node
|
||||
{
|
||||
// Path to the image file
|
||||
let path = unix_slash(Path::new(url));
|
||||
// Handle the body content (typically an image)
|
||||
match &*figure_node.body {
|
||||
Node::Paragraph(content) => {
|
||||
for node in content {
|
||||
// Special handling for image nodes in figures
|
||||
if let Node::Image {
|
||||
url,
|
||||
title: _,
|
||||
alt: _,
|
||||
} = node
|
||||
{
|
||||
// Path to the image file
|
||||
let path = unix_slash(Path::new(url));
|
||||
|
||||
// Write includegraphics command
|
||||
output.push_str("\\includegraphics[width=0.8\\textwidth]{");
|
||||
output.push_str(&path);
|
||||
output.push_str("}\n");
|
||||
} else {
|
||||
// For non-image content, just render it normally
|
||||
self.write_node(node, output)?;
|
||||
}
|
||||
// Write includegraphics command
|
||||
output.push_str("\\includegraphics[width=0.8\\textwidth]{");
|
||||
output.push_str(&path);
|
||||
output.push_str("}\n");
|
||||
} else {
|
||||
// For non-image content, just render it normally
|
||||
self.write_node(node, output)?;
|
||||
}
|
||||
}
|
||||
// Directly handle the node if it's not in a paragraph
|
||||
node => self.write_node(node, output)?,
|
||||
}
|
||||
// Directly handle the node if it's not in a paragraph
|
||||
node => self.write_node(node, output)?,
|
||||
}
|
||||
|
||||
// Add caption if present
|
||||
if !figure_node.caption.is_empty() {
|
||||
output.push_str("\\caption{");
|
||||
output.push_str(&escape_latex(&figure_node.caption));
|
||||
output.push_str("}\n");
|
||||
}
|
||||
|
||||
// Close figure environment
|
||||
output.push_str("\\end{figure}\n\n");
|
||||
} else if let Some(external_frame) =
|
||||
custom_node.as_any().downcast_ref::<ExternalFrameNode>()
|
||||
{
|
||||
// Handle externally stored frames
|
||||
let path = unix_slash(&external_frame.file_path);
|
||||
|
||||
output.push_str("\\begin{figure}[htbp]\n");
|
||||
output.push_str("\\centering\n");
|
||||
output.push_str("\\includegraphics[width=0.8\\textwidth]{");
|
||||
output.push_str(&path);
|
||||
// Add caption if present
|
||||
if !figure_node.caption.is_empty() {
|
||||
output.push_str("\\caption{");
|
||||
output.push_str(&escape_latex(&figure_node.caption));
|
||||
output.push_str("}\n");
|
||||
}
|
||||
|
||||
if !external_frame.alt_text.is_empty() {
|
||||
output.push_str("\\caption{");
|
||||
output.push_str(&escape_latex(&external_frame.alt_text));
|
||||
output.push_str("}\n");
|
||||
}
|
||||
// Close figure environment
|
||||
output.push_str("\\end{figure}\n\n");
|
||||
}
|
||||
node if node.is_custom_type::<ExternalFrameNode>() => {
|
||||
let external_frame = node.as_custom_type::<ExternalFrameNode>().unwrap();
|
||||
// Handle externally stored frames
|
||||
let path = unix_slash(&external_frame.file_path);
|
||||
|
||||
output.push_str("\\end{figure}\n\n");
|
||||
} else if let Some(center_node) = custom_node.as_any().downcast_ref::<CenterNode>()
|
||||
{
|
||||
output.push_str("\\begin{center}\n");
|
||||
self.write_node(¢er_node.node, output)?;
|
||||
output.push_str("\\end{center}\n\n");
|
||||
} else if let Some(highlight_node) =
|
||||
custom_node.as_any().downcast_ref::<HighlightNode>()
|
||||
{
|
||||
output.push_str("\\colorbox{yellow}{");
|
||||
for child in &highlight_node.content {
|
||||
self.write_node(child, output)?;
|
||||
}
|
||||
output.push_str("}");
|
||||
} else {
|
||||
// Fallback for unknown custom nodes
|
||||
output.push_str("[Unknown custom node]");
|
||||
output.push_str("\\begin{figure}[htbp]\n");
|
||||
output.push_str("\\centering\n");
|
||||
output.push_str("\\includegraphics[width=0.8\\textwidth]{");
|
||||
output.push_str(&path);
|
||||
output.push_str("}\n");
|
||||
|
||||
if !external_frame.alt_text.is_empty() {
|
||||
output.push_str("\\caption{");
|
||||
output.push_str(&escape_latex(&external_frame.alt_text));
|
||||
output.push_str("}\n");
|
||||
}
|
||||
|
||||
output.push_str("\\end{figure}\n\n");
|
||||
}
|
||||
node if node.is_custom_type::<CenterNode>() => {
|
||||
let center_node = node.as_custom_type::<CenterNode>().unwrap();
|
||||
output.push_str("\\begin{center}\n");
|
||||
self.write_node(¢er_node.node, output)?;
|
||||
output.push_str("\\end{center}\n\n");
|
||||
}
|
||||
node if node.is_custom_type::<HighlightNode>() => {
|
||||
let highlight_node = node.as_custom_type::<HighlightNode>().unwrap();
|
||||
output.push_str("\\colorbox{yellow}{");
|
||||
for child in &highlight_node.content {
|
||||
self.write_node(child, output)?;
|
||||
}
|
||||
output.push_str("}");
|
||||
}
|
||||
node if node.is_custom_type::<InlineNode>() => {
|
||||
let inline_node = node.as_custom_type::<InlineNode>().unwrap();
|
||||
// Process all child nodes inline
|
||||
for child in &inline_node.content {
|
||||
self.write_node(child, output)?;
|
||||
}
|
||||
}
|
||||
Node::Text(text) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue