@@ -7,7 +7,7 @@ use crate::filters::{
77 Filter , FilterReturn :: FilterResult , FilterReturn :: Unchanged , topdown_traverse,
88} ;
99use crate :: pandoc:: attr:: { Attr , is_empty_attr} ;
10- use crate :: pandoc:: block:: { Block , Figure , Plain } ;
10+ use crate :: pandoc:: block:: { Block , DefinitionList , Div , Figure , Plain } ;
1111use crate :: pandoc:: caption:: Caption ;
1212use crate :: pandoc:: inline:: { Inline , Inlines , Space , Span , Str , Superscript } ;
1313use crate :: pandoc:: location:: { Range , SourceInfo , empty_range, empty_source_info} ;
@@ -166,6 +166,99 @@ pub fn coalesce_abbreviations(inlines: Vec<Inline>) -> (Vec<Inline>, bool) {
166166 ( result, did_coalesce)
167167}
168168
169+ /// Validate that a div has the structure required for a definition list.
170+ ///
171+ /// Valid structure:
172+ /// - Div must have "definition-list" class
173+ /// - Div must contain exactly one block, which must be a BulletList
174+ /// - Each item in the BulletList must have:
175+ /// - Exactly two blocks
176+ /// - First block must be Plain or Paragraph (contains the term)
177+ /// - Second block must be a BulletList (contains the definitions)
178+ ///
179+ /// Returns true if valid, false otherwise.
180+ fn is_valid_definition_list_div ( div : & Div ) -> bool {
181+ // Check if div has "definition-list" class
182+ if !div. attr . 1 . contains ( & "definition-list" . to_string ( ) ) {
183+ return false ;
184+ }
185+
186+ // Must contain exactly one block
187+ if div. content . len ( ) != 1 {
188+ // FUTURE: issue linter warning: "definition-list div must contain exactly one bullet list"
189+ return false ;
190+ }
191+
192+ // That block must be a BulletList
193+ let Block :: BulletList ( bullet_list) = & div. content [ 0 ] else {
194+ // FUTURE: issue linter warning: "definition-list div must contain a bullet list"
195+ return false ;
196+ } ;
197+
198+ // Check each item in the bullet list
199+ for item_blocks in & bullet_list. content {
200+ // Each item must have exactly 2 blocks
201+ if item_blocks. len ( ) != 2 {
202+ // FUTURE: issue linter warning: "each definition list item must have a term and a nested bullet list"
203+ return false ;
204+ }
205+
206+ // First block must be Plain or Paragraph
207+ match & item_blocks[ 0 ] {
208+ Block :: Plain ( _) | Block :: Paragraph ( _) => { }
209+ _ => {
210+ // FUTURE: issue linter warning: "definition list term must be Plain or Paragraph"
211+ return false ;
212+ }
213+ }
214+
215+ // Second block must be BulletList
216+ if !matches ! ( & item_blocks[ 1 ] , Block :: BulletList ( _) ) {
217+ // FUTURE: issue linter warning: "definitions must be in a nested bullet list"
218+ return false ;
219+ }
220+ }
221+
222+ true
223+ }
224+
225+ /// Transform a valid definition-list div into a DefinitionList block.
226+ ///
227+ /// PRECONDITION: div must pass is_valid_definition_list_div() check.
228+ /// This function uses unwrap() liberally since the structure has been pre-validated.
229+ fn transform_definition_list_div ( div : Div ) -> Block {
230+ // Extract the bullet list (validated to exist)
231+ let Block :: BulletList ( bullet_list) = div. content . into_iter ( ) . next ( ) . unwrap ( ) else {
232+ panic ! ( "BulletList expected after validation" ) ;
233+ } ;
234+
235+ // Transform each item into (term, definitions) tuple
236+ let mut definition_items: Vec < ( Inlines , Vec < crate :: pandoc:: block:: Blocks > ) > = Vec :: new ( ) ;
237+
238+ for mut item_blocks in bullet_list. content {
239+ // Extract term from first block (Plain or Paragraph)
240+ let term_inlines = match item_blocks. remove ( 0 ) {
241+ Block :: Plain ( plain) => plain. content ,
242+ Block :: Paragraph ( para) => para. content ,
243+ _ => panic ! ( "Plain or Paragraph expected after validation" ) ,
244+ } ;
245+
246+ // Extract definitions from second block (BulletList)
247+ let Block :: BulletList ( definitions_list) = item_blocks. remove ( 0 ) else {
248+ panic ! ( "BulletList expected after validation" ) ;
249+ } ;
250+
251+ // Each item in the definitions bullet list is a definition (Vec<Block>)
252+ definition_items. push ( ( term_inlines, definitions_list. content ) ) ;
253+ }
254+
255+ // Preserve source location from the original div
256+ Block :: DefinitionList ( DefinitionList {
257+ content : definition_items,
258+ source_info : div. source_info ,
259+ } )
260+ }
261+
169262/// Apply post-processing transformations to the Pandoc AST
170263pub fn postprocess ( doc : Pandoc ) -> Result < Pandoc , Vec < String > > {
171264 let mut errors = Vec :: new ( ) ;
@@ -273,6 +366,14 @@ pub fn postprocess(doc: Pandoc) -> Result<Pandoc, Vec<String>> {
273366 true ,
274367 )
275368 } )
369+ // Convert definition-list divs to DefinitionList blocks
370+ . with_div ( |div| {
371+ if is_valid_definition_list_div ( & div) {
372+ FilterResult ( vec ! [ transform_definition_list_div( div) ] , false )
373+ } else {
374+ Unchanged ( div)
375+ }
376+ } )
276377 . with_shortcode ( |shortcode| {
277378 FilterResult ( vec ! [ Inline :: Span ( shortcode_to_span( shortcode) ) ] , false )
278379 } )
0 commit comments