Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::filters::{
Filter, FilterReturn::FilterResult, FilterReturn::Unchanged, topdown_traverse,
};
use crate::pandoc::attr::{Attr, is_empty_attr};
use crate::pandoc::block::{Block, Figure, Plain};
use crate::pandoc::block::{Block, DefinitionList, Div, Figure, Plain};
use crate::pandoc::caption::Caption;
use crate::pandoc::inline::{Inline, Inlines, Space, Span, Str, Superscript};
use crate::pandoc::location::{Range, SourceInfo, empty_range, empty_source_info};
Expand Down Expand Up @@ -166,6 +166,99 @@ pub fn coalesce_abbreviations(inlines: Vec<Inline>) -> (Vec<Inline>, bool) {
(result, did_coalesce)
}

/// Validate that a div has the structure required for a definition list.
///
/// Valid structure:
/// - Div must have "definition-list" class
/// - Div must contain exactly one block, which must be a BulletList
/// - Each item in the BulletList must have:
/// - Exactly two blocks
/// - First block must be Plain or Paragraph (contains the term)
/// - Second block must be a BulletList (contains the definitions)
///
/// Returns true if valid, false otherwise.
fn is_valid_definition_list_div(div: &Div) -> bool {
// Check if div has "definition-list" class
if !div.attr.1.contains(&"definition-list".to_string()) {
return false;
}

// Must contain exactly one block
if div.content.len() != 1 {
// FUTURE: issue linter warning: "definition-list div must contain exactly one bullet list"
return false;
}

// That block must be a BulletList
let Block::BulletList(bullet_list) = &div.content[0] else {
// FUTURE: issue linter warning: "definition-list div must contain a bullet list"
return false;
};

// Check each item in the bullet list
for item_blocks in &bullet_list.content {
// Each item must have exactly 2 blocks
if item_blocks.len() != 2 {
// FUTURE: issue linter warning: "each definition list item must have a term and a nested bullet list"
return false;
}

// First block must be Plain or Paragraph
match &item_blocks[0] {
Block::Plain(_) | Block::Paragraph(_) => {}
_ => {
// FUTURE: issue linter warning: "definition list term must be Plain or Paragraph"
return false;
}
}

// Second block must be BulletList
if !matches!(&item_blocks[1], Block::BulletList(_)) {
// FUTURE: issue linter warning: "definitions must be in a nested bullet list"
return false;
}
}

true
}

/// Transform a valid definition-list div into a DefinitionList block.
///
/// PRECONDITION: div must pass is_valid_definition_list_div() check.
/// This function uses unwrap() liberally since the structure has been pre-validated.
fn transform_definition_list_div(div: Div) -> Block {
// Extract the bullet list (validated to exist)
let Block::BulletList(bullet_list) = div.content.into_iter().next().unwrap() else {
panic!("BulletList expected after validation");
};

// Transform each item into (term, definitions) tuple
let mut definition_items: Vec<(Inlines, Vec<crate::pandoc::block::Blocks>)> = Vec::new();

for mut item_blocks in bullet_list.content {
// Extract term from first block (Plain or Paragraph)
let term_inlines = match item_blocks.remove(0) {
Block::Plain(plain) => plain.content,
Block::Paragraph(para) => para.content,
_ => panic!("Plain or Paragraph expected after validation"),
};

// Extract definitions from second block (BulletList)
let Block::BulletList(definitions_list) = item_blocks.remove(0) else {
panic!("BulletList expected after validation");
};

// Each item in the definitions bullet list is a definition (Vec<Block>)
definition_items.push((term_inlines, definitions_list.content));
}

// Preserve source location from the original div
Block::DefinitionList(DefinitionList {
content: definition_items,
source_info: div.source_info,
})
}

/// Apply post-processing transformations to the Pandoc AST
pub fn postprocess(doc: Pandoc) -> Result<Pandoc, Vec<String>> {
let mut errors = Vec::new();
Expand Down Expand Up @@ -273,6 +366,14 @@ pub fn postprocess(doc: Pandoc) -> Result<Pandoc, Vec<String>> {
true,
)
})
// Convert definition-list divs to DefinitionList blocks
.with_div(|div| {
if is_valid_definition_list_div(&div) {
FilterResult(vec![transform_definition_list_div(div)], false)
} else {
Unchanged(div)
}
})
.with_shortcode(|shortcode| {
FilterResult(vec![Inline::Span(shortcode_to_span(shortcode))], false)
})
Expand Down Expand Up @@ -412,6 +513,64 @@ pub fn postprocess(doc: Pandoc) -> Result<Pandoc, Vec<String>> {
if let Some(mut cite) = pending_cite.take() {
// Add span content to the citation's suffix
cite.citations[0].suffix = span.content.clone();

// Update the content field to include the rendered suffix with brackets
// Pandoc breaks up the bracketed suffix text by spaces, with the opening
// bracket attached to the first word and closing bracket to the last word
// e.g., "@knuth [p. 33]" becomes: Str("@knuth"), Space, Str("[p."), Space, Str("33]")
cite.content.push(Inline::Space(Space {
source_info: SourceInfo::with_range(empty_range()),
}));

// The span content may have been merged into a single string, so we need to
// intelligently break it up to match Pandoc's behavior
let mut bracketed_content: Vec<Inline> = vec![];
for inline in &span.content {
if let Inline::Str(s) = inline {
// Split the string by spaces and create Str/Space inlines
let words: Vec<&str> = s.text.split(' ').collect();
for (i, word) in words.iter().enumerate() {
if i > 0 {
bracketed_content.push(Inline::Space(
Space {
source_info: SourceInfo::with_range(
empty_range(),
),
},
));
}
if !word.is_empty() {
bracketed_content.push(Inline::Str(Str {
text: word.to_string(),
source_info: s.source_info.clone(),
}));
}
}
} else {
bracketed_content.push(inline.clone());
}
}

// Now add brackets to the first and last Str elements
if !bracketed_content.is_empty() {
// Prepend "[" to the first Str element
if let Some(Inline::Str(first_str)) =
bracketed_content.first_mut()
{
first_str.text = format!("[{}", first_str.text);
}
// Append "]" to the last Str element (search from the end)
for i in (0..bracketed_content.len()).rev() {
if let Inline::Str(last_str) =
&mut bracketed_content[i]
{
last_str.text = format!("{}]", last_str.text);
break;
}
}
}

cite.content.extend(bracketed_content);
result.push(Inline::Cite(cite));
}
state = 0;
Expand Down
26 changes: 26 additions & 0 deletions crates/quarto-markdown-pandoc/src/writers/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,32 @@ fn write_block<T: std::io::Write>(block: &Block, buf: &mut T) -> std::io::Result
write!(buf, "] ")?;
write_native_table_foot(foot, buf)?;
}
Block::DefinitionList(crate::pandoc::DefinitionList { content, .. }) => {
write!(buf, "DefinitionList [")?;
for (i, (term, definitions)) in content.iter().enumerate() {
if i > 0 {
write!(buf, ", ")?;
}
write!(buf, "(")?;
write_inlines(term, buf)?;
write!(buf, ", [")?;
for (j, def_blocks) in definitions.iter().enumerate() {
if j > 0 {
write!(buf, ", ")?;
}
write!(buf, "[")?;
for (k, block) in def_blocks.iter().enumerate() {
if k > 0 {
write!(buf, ", ")?;
}
write_block(block, buf)?;
}
write!(buf, "]")?;
}
write!(buf, "])")?;
}
write!(buf, "]")?;
}
_ => panic!("Unsupported block type in native writer: {:?}", block),
}
Ok(())
Expand Down
10 changes: 10 additions & 0 deletions crates/quarto-markdown-pandoc/src/writers/qmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,16 @@ fn write_cite(cite: &crate::pandoc::Cite, buf: &mut dyn std::io::Write) -> std::
write!(buf, "; ")?;
}
write!(buf, "@{}", citation.id)?;

// Write suffix if it exists
// For AuthorInText mode, suffix appears as: @citation [suffix]
if !citation.suffix.is_empty() {
write!(buf, " [")?;
for inline in &citation.suffix {
write_inline(inline, buf)?;
}
write!(buf, "]")?;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@knuth [p. 33]
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[ Para [Cite [Citation { citationId = "smith04", citationPrefix = [], citationSuffix = [Str "p. 33"], citationMode = AuthorInText, citationNoteNum = 1, citationHash = 0 }] [Str "@smith04"]] ]
[ Para [Cite [Citation { citationId = "smith04", citationPrefix = [], citationSuffix = [Str "p. 33"], citationMode = AuthorInText, citationNoteNum = 1, citationHash = 0 }] [Str "@smith04", Space, Str "[p. 33]"]] ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
::: {.definition-list}
* term 1
- definition 1
* term 2
- definition 2
:::
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ DefinitionList [([Str "term", Space, Str "1"], [[Plain [Str "definition", Space, Str "1"]]]), ([Str "term", Space, Str "2"], [[Plain [Str "definition", Space, Str "2"]]])] ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
::: {.definition-list}
* **term with emphasis**
- definition for emphasized term
* `code term`
- definition for code term
:::
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ DefinitionList [([Strong [Str "term", Space, Str "with", Space, Str "emphasis"]], [[Plain [Str "definition", Space, Str "for", Space, Str "emphasized", Space, Str "term"]]]), ([Code ( "" , [] , [] ) "code term"], [[Plain [Str "definition", Space, Str "for", Space, Str "code", Space, Str "term"]]])] ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
::: {.definition-list}
:::
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ Div ( "" , ["definition-list"] , [] ) [] ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
::: {.definition-list}
* term

extra paragraph

- definition
:::
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ Div ( "" , ["definition-list"] , [] ) [BulletList [[Para [Str "term"], Para [Str "extra", Space, Str "paragraph"], BulletList [[Plain [Str "definition"]]]]]] ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
::: {.definition-list}
* term without nested list
* another term
:::
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ Div ( "" , ["definition-list"] , [] ) [BulletList [[Plain [Str "term", Space, Str "without", Space, Str "nested", Space, Str "list"]], [Plain [Str "another", Space, Str "term"]]]] ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
::: {.definition-list}
* term 1
- definition 1a
- definition 1b
* term 2
- definition 2a
- definition 2b
- definition 2c
:::
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ DefinitionList [([Str "term", Space, Str "1"], [[Plain [Str "definition", Space, Str "1a"]], [Plain [Str "definition", Space, Str "1b"]]]), ([Str "term", Space, Str "2"], [[Plain [Str "definition", Space, Str "2a"]], [Plain [Str "definition", Space, Str "2b"]], [Plain [Str "definition", Space, Str "2c"]]])] ]