@@ -1161,4 +1161,328 @@ mod tests {
11611161 panic ! ( "Expected Table block" ) ;
11621162 }
11631163 }
1164+
1165+ // ==================== Sprint 17: Conversion Edge Cases ====================
1166+
1167+ #[ test]
1168+ fn test_block_section_break_conversion ( ) {
1169+ let ctx = ConversionContext :: default ( ) ;
1170+
1171+ let section_break = Block :: SectionBreak ;
1172+ let ast_block = section_break. to_ast ( & ctx) ;
1173+
1174+ // Section break should convert to a Break block
1175+ assert ! ( matches!( ast_block, AstBlock :: Break ( _) ) ) ;
1176+ }
1177+
1178+ #[ test]
1179+ fn test_document_with_section_breaks ( ) {
1180+ let doc = Document {
1181+ blocks : vec ! [
1182+ Block :: Paragraph ( Paragraph {
1183+ style_id: None ,
1184+ children: vec![ ParagraphChild :: Run ( Run {
1185+ text: "Before" . to_string( ) ,
1186+ bold: false ,
1187+ italic: false ,
1188+ monospace: false ,
1189+ } ) ] ,
1190+ numbering: None ,
1191+ } ) ,
1192+ Block :: SectionBreak ,
1193+ Block :: Paragraph ( Paragraph {
1194+ style_id: None ,
1195+ children: vec![ ParagraphChild :: Run ( Run {
1196+ text: "After" . to_string( ) ,
1197+ bold: false ,
1198+ italic: false ,
1199+ monospace: false ,
1200+ } ) ] ,
1201+ numbering: None ,
1202+ } ) ,
1203+ ] ,
1204+ } ;
1205+
1206+ let ast_doc = convert_document ( & doc) ;
1207+ assert_eq ! ( ast_doc. blocks. len( ) , 3 ) ;
1208+ assert ! ( matches!( & ast_doc. blocks[ 0 ] , AstBlock :: Paragraph ( _) ) ) ;
1209+ assert ! ( matches!( & ast_doc. blocks[ 1 ] , AstBlock :: Break ( _) ) ) ;
1210+ assert ! ( matches!( & ast_doc. blocks[ 2 ] , AstBlock :: Paragraph ( _) ) ) ;
1211+ }
1212+
1213+ #[ test]
1214+ fn test_table_row_header_flag ( ) {
1215+ let ctx = ConversionContext :: default ( ) ;
1216+
1217+ let table = Table {
1218+ style_id : None ,
1219+ rows : vec ! [
1220+ TableRow {
1221+ is_header: true ,
1222+ cells: vec![ TableCell {
1223+ paragraphs: vec![ Paragraph {
1224+ style_id: None ,
1225+ children: vec![ ParagraphChild :: Run ( Run {
1226+ text: "Header" . to_string( ) ,
1227+ bold: false ,
1228+ italic: false ,
1229+ monospace: false ,
1230+ } ) ] ,
1231+ numbering: None ,
1232+ } ] ,
1233+ } ] ,
1234+ } ,
1235+ TableRow {
1236+ is_header: false ,
1237+ cells: vec![ TableCell {
1238+ paragraphs: vec![ Paragraph {
1239+ style_id: None ,
1240+ children: vec![ ParagraphChild :: Run ( Run {
1241+ text: "Data" . to_string( ) ,
1242+ bold: false ,
1243+ italic: false ,
1244+ monospace: false ,
1245+ } ) ] ,
1246+ numbering: None ,
1247+ } ] ,
1248+ } ] ,
1249+ } ,
1250+ ] ,
1251+ } ;
1252+
1253+ let block = table. to_ast ( & ctx) ;
1254+
1255+ if let AstBlock :: Table ( ast_table) = block {
1256+ assert_eq ! ( ast_table. rows. len( ) , 2 ) ;
1257+ // Header flag should be preserved
1258+ assert ! ( ast_table. rows[ 0 ] . is_header) ;
1259+ assert ! ( !ast_table. rows[ 1 ] . is_header) ;
1260+ } else {
1261+ panic ! ( "Expected Table block" ) ;
1262+ }
1263+ }
1264+
1265+ #[ test]
1266+ fn test_table_cell_multiple_paragraphs ( ) {
1267+ let ctx = ConversionContext :: default ( ) ;
1268+
1269+ let table = Table {
1270+ style_id : None ,
1271+ rows : vec ! [ TableRow {
1272+ is_header: false ,
1273+ cells: vec![ TableCell {
1274+ paragraphs: vec![
1275+ Paragraph {
1276+ style_id: None ,
1277+ children: vec![ ParagraphChild :: Run ( Run {
1278+ text: "Line 1" . to_string( ) ,
1279+ bold: false ,
1280+ italic: false ,
1281+ monospace: false ,
1282+ } ) ] ,
1283+ numbering: None ,
1284+ } ,
1285+ Paragraph {
1286+ style_id: None ,
1287+ children: vec![ ParagraphChild :: Run ( Run {
1288+ text: "Line 2" . to_string( ) ,
1289+ bold: false ,
1290+ italic: false ,
1291+ monospace: false ,
1292+ } ) ] ,
1293+ numbering: None ,
1294+ } ,
1295+ ] ,
1296+ } ] ,
1297+ } ] ,
1298+ } ;
1299+
1300+ let block = table. to_ast ( & ctx) ;
1301+
1302+ if let AstBlock :: Table ( ast_table) = block {
1303+ // Cell should have content for each paragraph
1304+ assert ! ( !ast_table. rows[ 0 ] . cells[ 0 ] . content. is_empty( ) ) ;
1305+ } else {
1306+ panic ! ( "Expected Table block" ) ;
1307+ }
1308+ }
1309+
1310+ #[ test]
1311+ fn test_run_with_only_monospace ( ) {
1312+ let ctx = ConversionContext :: default ( ) ;
1313+
1314+ let run = Run {
1315+ text : "code" . to_string ( ) ,
1316+ bold : false ,
1317+ italic : false ,
1318+ monospace : true ,
1319+ } ;
1320+
1321+ let inline = run. to_ast ( & ctx) ;
1322+
1323+ // Should be wrapped in Monospace format
1324+ if let Inline :: Format ( FormatType :: Monospace , inner) = inline {
1325+ if let Inline :: Text ( text) = * inner {
1326+ assert_eq ! ( text, "code" ) ;
1327+ } else {
1328+ panic ! ( "Expected inner Text" ) ;
1329+ }
1330+ } else {
1331+ panic ! ( "Expected Monospace format" ) ;
1332+ }
1333+ }
1334+
1335+ #[ test]
1336+ fn test_run_with_all_formatting_nested ( ) {
1337+ let ctx = ConversionContext :: default ( ) ;
1338+
1339+ let run = Run {
1340+ text : "formatted" . to_string ( ) ,
1341+ bold : true ,
1342+ italic : true ,
1343+ monospace : true ,
1344+ } ;
1345+
1346+ let inline = run. to_ast ( & ctx) ;
1347+
1348+ // Should have nested formatting (order: bold > italic > monospace)
1349+ fn count_nesting ( inline : & Inline ) -> u32 {
1350+ match inline {
1351+ Inline :: Format ( _, inner) => 1 + count_nesting ( inner) ,
1352+ _ => 0 ,
1353+ }
1354+ }
1355+
1356+ assert_eq ! ( count_nesting( & inline) , 3 ) ;
1357+ }
1358+
1359+ #[ test]
1360+ fn test_hyperlink_internal_anchor ( ) {
1361+ let ctx = ConversionContext :: default ( ) ;
1362+
1363+ let hyperlink = Hyperlink {
1364+ id : None ,
1365+ anchor : Some ( "section-1" . to_string ( ) ) ,
1366+ runs : vec ! [ Run {
1367+ text: "Go to section" . to_string( ) ,
1368+ bold: false ,
1369+ italic: false ,
1370+ monospace: false ,
1371+ } ] ,
1372+ } ;
1373+
1374+ let inline = hyperlink. to_ast ( & ctx) ;
1375+
1376+ if let Inline :: Link ( link) = inline {
1377+ assert ! ( link. url. starts_with( '#' ) ) ;
1378+ assert ! ( link. url. contains( "section-1" ) ) ;
1379+ } else {
1380+ panic ! ( "Expected Link inline" ) ;
1381+ }
1382+ }
1383+
1384+ #[ test]
1385+ fn test_paragraph_child_image_conversion ( ) {
1386+ let ctx = ConversionContext :: default ( ) ;
1387+
1388+ let para = Paragraph {
1389+ style_id : None ,
1390+ children : vec ! [ ParagraphChild :: Image ( crate :: image:: Image :: new_inline(
1391+ 1 ,
1392+ "rId1" . to_string( ) ,
1393+ "media/image.png" . to_string( ) ,
1394+ ) ) ] ,
1395+ numbering : None ,
1396+ } ;
1397+
1398+ let block = para. to_ast ( & ctx) ;
1399+
1400+ // Image in paragraph should create some inline content
1401+ if let AstBlock :: Paragraph ( ast_para) = block {
1402+ assert ! ( !ast_para. inlines. is_empty( ) ) ;
1403+ } else {
1404+ panic ! ( "Expected Paragraph block" ) ;
1405+ }
1406+ }
1407+
1408+ #[ test]
1409+ fn test_paragraph_child_bookmark_ignored ( ) {
1410+ let ctx = ConversionContext :: default ( ) ;
1411+
1412+ let para = Paragraph {
1413+ style_id : None ,
1414+ children : vec ! [
1415+ ParagraphChild :: Bookmark ( crate :: document:: Bookmark {
1416+ name: "_Toc123" . to_string( ) ,
1417+ } ) ,
1418+ ParagraphChild :: Run ( Run {
1419+ text: "Visible" . to_string( ) ,
1420+ bold: false ,
1421+ italic: false ,
1422+ monospace: false ,
1423+ } ) ,
1424+ ] ,
1425+ numbering : None ,
1426+ } ;
1427+
1428+ let block = para. to_ast ( & ctx) ;
1429+
1430+ if let AstBlock :: Paragraph ( ast_para) = block {
1431+ // Bookmark should not create visible inline, only the run text
1432+ let text_count = ast_para
1433+ . inlines
1434+ . iter ( )
1435+ . filter ( |i| matches ! ( i, Inline :: Text ( _) ) )
1436+ . count ( ) ;
1437+ assert_eq ! ( text_count, 1 ) ;
1438+ } else {
1439+ panic ! ( "Expected Paragraph block" ) ;
1440+ }
1441+ }
1442+
1443+ #[ test]
1444+ fn test_convert_document_with_styles_preserves_headings ( ) {
1445+ use crate :: styles:: StyleSheet ;
1446+
1447+ let style_xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1448+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1449+ <w:style w:type="paragraph" w:styleId="Heading1">
1450+ <w:name w:val="Heading 1"/>
1451+ <w:pPr><w:outlineLvl w:val="0"/></w:pPr>
1452+ </w:style>
1453+ </w:styles>"# ;
1454+
1455+ let styles = StyleSheet :: parse ( style_xml) . unwrap ( ) ;
1456+
1457+ let doc = Document {
1458+ blocks : vec ! [ Block :: Paragraph ( Paragraph {
1459+ style_id: Some ( "Heading1" . to_string( ) ) ,
1460+ children: vec![ ParagraphChild :: Run ( Run {
1461+ text: "Title" . to_string( ) ,
1462+ bold: false ,
1463+ italic: false ,
1464+ monospace: false ,
1465+ } ) ] ,
1466+ numbering: None ,
1467+ } ) ] ,
1468+ } ;
1469+
1470+ let ast_doc = convert_document_with_styles ( & doc, & styles) ;
1471+ assert_eq ! ( ast_doc. blocks. len( ) , 1 ) ;
1472+
1473+ // Should be converted to a heading
1474+ if let AstBlock :: Heading ( heading) = & ast_doc. blocks [ 0 ] {
1475+ assert_eq ! ( heading. level, 1 ) ;
1476+ } else {
1477+ panic ! ( "Expected Heading block for heading style" ) ;
1478+ }
1479+ }
1480+
1481+ #[ test]
1482+ fn test_context_heading_level_unknown_style ( ) {
1483+ let ctx = ConversionContext :: default ( ) ;
1484+
1485+ // Unknown style should return None
1486+ assert ! ( ctx. heading_level( "NonExistentStyle" ) . is_none( ) ) ;
1487+ }
11641488}
0 commit comments