@@ -935,4 +935,387 @@ mod tests {
935935 assert_eq ! ( table_styles. len( ) , 1 ) ;
936936 assert_eq ! ( table_styles[ 0 ] . id, "Table1" ) ;
937937 }
938+
939+ // ==================== Sprint 15: StyleMap Validation & Edge Cases ====================
940+
941+ #[ test]
942+ fn test_style_map_validate_all_present ( ) {
943+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
944+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
945+ <w:style w:type="paragraph" w:styleId="Heading1">
946+ <w:name w:val="Heading 1"/>
947+ </w:style>
948+ <w:style w:type="paragraph" w:styleId="Normal">
949+ <w:name w:val="Normal"/>
950+ </w:style>
951+ </w:styles>"# ;
952+
953+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
954+ let mut map = StyleMap :: new ( ) ;
955+ map. set ( ElementType :: Heading ( 1 ) , "Heading1" ) ;
956+ map. set ( ElementType :: Paragraph , "Normal" ) ;
957+
958+ let missing = map. validate ( & styles) ;
959+ assert ! ( missing. is_empty( ) , "All mapped styles exist in stylesheet" ) ;
960+ }
961+
962+ #[ test]
963+ fn test_style_map_validate_missing_styles ( ) {
964+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
965+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
966+ <w:style w:type="paragraph" w:styleId="Normal">
967+ <w:name w:val="Normal"/>
968+ </w:style>
969+ </w:styles>"# ;
970+
971+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
972+ let mut map = StyleMap :: new ( ) ;
973+ map. set ( ElementType :: Heading ( 1 ) , "MissingH1" ) ;
974+ map. set ( ElementType :: Paragraph , "Normal" ) ;
975+ map. set ( ElementType :: CodeBlock , "MissingCode" ) ;
976+
977+ let missing = map. validate ( & styles) ;
978+ assert_eq ! ( missing. len( ) , 2 ) ;
979+ assert ! ( missing. contains( & "MissingH1" . to_string( ) ) ) ;
980+ assert ! ( missing. contains( & "MissingCode" . to_string( ) ) ) ;
981+ }
982+
983+ #[ test]
984+ fn test_style_map_validate_default_map ( ) {
985+ // Validate default StyleMap against a minimal stylesheet
986+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
987+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
988+ <w:style w:type="paragraph" w:styleId="Normal">
989+ <w:name w:val="Normal"/>
990+ </w:style>
991+ </w:styles>"# ;
992+
993+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
994+ let map = StyleMap :: default ( ) ;
995+
996+ let missing = map. validate ( & styles) ;
997+ // Default map has many styles that won't exist in a minimal stylesheet
998+ assert ! ( !missing. is_empty( ) ) ;
999+ // But Normal should not be in missing list
1000+ assert ! ( !missing. contains( & "Normal" . to_string( ) ) ) ;
1001+ }
1002+
1003+ #[ test]
1004+ fn test_style_map_from_stylesheet_localized_headings ( ) {
1005+ // Italian localized template with "Titolo" instead of "Heading"
1006+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1007+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1008+ <w:style w:type="paragraph" w:styleId="Titolo1">
1009+ <w:name w:val="Titolo 1"/>
1010+ <w:pPr><w:outlineLvl w:val="0"/></w:pPr>
1011+ </w:style>
1012+ <w:style w:type="paragraph" w:styleId="Titolo2">
1013+ <w:name w:val="Titolo 2"/>
1014+ <w:pPr><w:outlineLvl w:val="1"/></w:pPr>
1015+ </w:style>
1016+ <w:style w:type="paragraph" w:styleId="Normale" w:default="1">
1017+ <w:name w:val="Normale"/>
1018+ </w:style>
1019+ </w:styles>"# ;
1020+
1021+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1022+ let map = StyleMap :: from_stylesheet ( & styles) ;
1023+
1024+ // Should detect headings by outline level, not by name
1025+ assert_eq ! ( map. heading( 1 ) , "Titolo1" ) ;
1026+ assert_eq ! ( map. heading( 2 ) , "Titolo2" ) ;
1027+ // Default paragraph style detected
1028+ assert_eq ! ( map. paragraph( ) , "Normale" ) ;
1029+ }
1030+
1031+ #[ test]
1032+ fn test_style_map_from_stylesheet_code_style_detection ( ) {
1033+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1034+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1035+ <w:style w:type="paragraph" w:styleId="Normal" w:default="1">
1036+ <w:name w:val="Normal"/>
1037+ </w:style>
1038+ <w:style w:type="paragraph" w:styleId="HTMLPreformatted">
1039+ <w:name w:val="HTML Preformatted"/>
1040+ </w:style>
1041+ </w:styles>"# ;
1042+
1043+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1044+ let map = StyleMap :: from_stylesheet ( & styles) ;
1045+
1046+ // Should detect HTML Preformatted as code style
1047+ assert_eq ! ( map. code_block( ) , "HTMLPreformatted" ) ;
1048+ }
1049+
1050+ #[ test]
1051+ fn test_style_map_from_stylesheet_list_detection ( ) {
1052+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1053+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1054+ <w:style w:type="paragraph" w:styleId="Normal" w:default="1">
1055+ <w:name w:val="Normal"/>
1056+ </w:style>
1057+ <w:style w:type="paragraph" w:styleId="ListParagraph">
1058+ <w:name w:val="List Paragraph"/>
1059+ </w:style>
1060+ </w:styles>"# ;
1061+
1062+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1063+ let map = StyleMap :: from_stylesheet ( & styles) ;
1064+
1065+ // Should detect list style by name
1066+ assert_eq ! ( map. list( false ) , "ListParagraph" ) ;
1067+ }
1068+
1069+ #[ test]
1070+ fn test_style_map_from_stylesheet_table_detection ( ) {
1071+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1072+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1073+ <w:style w:type="paragraph" w:styleId="Normal" w:default="1">
1074+ <w:name w:val="Normal"/>
1075+ </w:style>
1076+ <w:style w:type="table" w:styleId="GridTable1Light">
1077+ <w:name w:val="Grid Table 1 Light"/>
1078+ </w:style>
1079+ </w:styles>"# ;
1080+
1081+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1082+ let map = StyleMap :: from_stylesheet ( & styles) ;
1083+
1084+ // Should detect table style by name
1085+ assert_eq ! ( map. table( ) , "GridTable1Light" ) ;
1086+ }
1087+
1088+ #[ test]
1089+ fn test_style_map_from_stylesheet_empty ( ) {
1090+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1091+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1092+ </w:styles>"# ;
1093+
1094+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1095+ let map = StyleMap :: from_stylesheet ( & styles) ;
1096+
1097+ // Should fallback to defaults for unmapped elements
1098+ assert_eq ! ( map. heading( 1 ) , "Heading1" ) ; // Fallback
1099+ assert_eq ! ( map. paragraph( ) , "Normal" ) ; // Fallback
1100+ }
1101+
1102+ #[ test]
1103+ fn test_stylesheet_parse_character_style ( ) {
1104+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1105+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1106+ <w:style w:type="character" w:styleId="Strong" w:default="1">
1107+ <w:name w:val="Strong"/>
1108+ <w:uiPriority w:val="22"/>
1109+ </w:style>
1110+ <w:style w:type="character" w:styleId="Emphasis">
1111+ <w:name w:val="Emphasis"/>
1112+ </w:style>
1113+ </w:styles>"# ;
1114+
1115+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1116+
1117+ // Check character style parsed correctly
1118+ let strong = styles. get ( "Strong" ) . unwrap ( ) ;
1119+ assert_eq ! ( strong. style_type, StyleType :: Character ) ;
1120+ assert_eq ! ( strong. ui_priority, Some ( 22 ) ) ;
1121+
1122+ // Default character style should be tracked
1123+ assert_eq ! ( styles. default_character, Some ( "Strong" . to_string( ) ) ) ;
1124+ }
1125+
1126+ #[ test]
1127+ fn test_stylesheet_parse_numbering_style ( ) {
1128+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1129+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1130+ <w:style w:type="numbering" w:styleId="ListBullet">
1131+ <w:name w:val="List Bullet"/>
1132+ </w:style>
1133+ </w:styles>"# ;
1134+
1135+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1136+
1137+ let list = styles. get ( "ListBullet" ) . unwrap ( ) ;
1138+ assert_eq ! ( list. style_type, StyleType :: Numbering ) ;
1139+ }
1140+
1141+ #[ test]
1142+ fn test_stylesheet_all_iterator ( ) {
1143+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1144+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1145+ <w:style w:type="paragraph" w:styleId="Normal">
1146+ <w:name w:val="Normal"/>
1147+ </w:style>
1148+ <w:style w:type="character" w:styleId="Strong">
1149+ <w:name w:val="Strong"/>
1150+ </w:style>
1151+ <w:style w:type="table" w:styleId="TableGrid">
1152+ <w:name w:val="Table Grid"/>
1153+ </w:style>
1154+ </w:styles>"# ;
1155+
1156+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1157+
1158+ // all() should return all styles
1159+ let all: Vec < _ > = styles. all ( ) . collect ( ) ;
1160+ assert_eq ! ( all. len( ) , 3 ) ;
1161+
1162+ // Verify different types exist
1163+ let types: Vec < StyleType > = all. iter ( ) . map ( |s| s. style_type ) . collect ( ) ;
1164+ assert ! ( types. contains( & StyleType :: Paragraph ) ) ;
1165+ assert ! ( types. contains( & StyleType :: Character ) ) ;
1166+ assert ! ( types. contains( & StyleType :: Table ) ) ;
1167+ }
1168+
1169+ #[ test]
1170+ fn test_stylesheet_heading_styles_iterator ( ) {
1171+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1172+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1173+ <w:style w:type="paragraph" w:styleId="Heading1">
1174+ <w:name w:val="Heading 1"/>
1175+ <w:pPr><w:outlineLvl w:val="0"/></w:pPr>
1176+ </w:style>
1177+ <w:style w:type="paragraph" w:styleId="Heading2">
1178+ <w:name w:val="Heading 2"/>
1179+ <w:pPr><w:outlineLvl w:val="1"/></w:pPr>
1180+ </w:style>
1181+ <w:style w:type="paragraph" w:styleId="Normal">
1182+ <w:name w:val="Normal"/>
1183+ </w:style>
1184+ </w:styles>"# ;
1185+
1186+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1187+
1188+ let headings: Vec < _ > = styles. heading_styles ( ) . collect ( ) ;
1189+ assert_eq ! ( headings. len( ) , 2 ) ;
1190+
1191+ // Verify all have outline levels
1192+ for h in & headings {
1193+ assert ! ( h. outline_level. is_some( ) ) ;
1194+ }
1195+ }
1196+
1197+ #[ test]
1198+ fn test_stylesheet_default_paragraph_detection ( ) {
1199+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1200+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1201+ <w:style w:type="paragraph" w:styleId="BodyText" w:default="1">
1202+ <w:name w:val="Body Text"/>
1203+ </w:style>
1204+ <w:style w:type="paragraph" w:styleId="Heading1">
1205+ <w:name w:val="Heading 1"/>
1206+ </w:style>
1207+ </w:styles>"# ;
1208+
1209+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1210+
1211+ // Should detect non-Normal default paragraph style
1212+ assert_eq ! ( styles. default_paragraph, Some ( "BodyText" . to_string( ) ) ) ;
1213+ }
1214+
1215+ #[ test]
1216+ fn test_style_next_attribute ( ) {
1217+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1218+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1219+ <w:style w:type="paragraph" w:styleId="Heading1">
1220+ <w:name w:val="Heading 1"/>
1221+ <w:next w:val="Normal"/>
1222+ </w:style>
1223+ <w:style w:type="paragraph" w:styleId="Normal">
1224+ <w:name w:val="Normal"/>
1225+ </w:style>
1226+ </w:styles>"# ;
1227+
1228+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1229+
1230+ let h1 = styles. get ( "Heading1" ) . unwrap ( ) ;
1231+ assert_eq ! ( h1. next, Some ( "Normal" . to_string( ) ) ) ;
1232+ }
1233+
1234+ #[ test]
1235+ fn test_style_map_element_type_coverage ( ) {
1236+ let map = StyleMap :: default ( ) ;
1237+
1238+ // Test all admonition types have defaults
1239+ assert_eq ! ( map. get( ElementType :: AdmonitionNote ) , "Note" ) ;
1240+ assert_eq ! ( map. get( ElementType :: AdmonitionTip ) , "Tip" ) ;
1241+ assert_eq ! ( map. get( ElementType :: AdmonitionImportant ) , "Important" ) ;
1242+ assert_eq ! ( map. get( ElementType :: AdmonitionWarning ) , "Warning" ) ;
1243+ assert_eq ! ( map. get( ElementType :: AdmonitionCaution ) , "Caution" ) ;
1244+
1245+ // Test list types
1246+ assert_eq ! ( map. get( ElementType :: ListDescription ) , "ListParagraph" ) ;
1247+ assert_eq ! ( map. get( ElementType :: TableHeader ) , "TableGrid" ) ;
1248+ }
1249+
1250+ #[ test]
1251+ fn test_style_map_fallback_styles ( ) {
1252+ let map = StyleMap :: new ( ) ; // Empty map
1253+
1254+ // All element types should have fallback styles
1255+ assert_eq ! ( map. get( ElementType :: Heading ( 5 ) ) , "Heading1" ) ;
1256+ assert_eq ! ( map. get( ElementType :: Paragraph ) , "Normal" ) ;
1257+ assert_eq ! ( map. get( ElementType :: CodeBlock ) , "Normal" ) ;
1258+ assert_eq ! ( map. get( ElementType :: ListBullet ) , "ListBullet" ) ;
1259+ assert_eq ! ( map. get( ElementType :: ListNumber ) , "ListNumber" ) ;
1260+ assert_eq ! ( map. get( ElementType :: ListDescription ) , "Normal" ) ;
1261+ assert_eq ! ( map. get( ElementType :: Table ) , "TableGrid" ) ;
1262+ assert_eq ! ( map. get( ElementType :: TableHeader ) , "TableGrid" ) ;
1263+ assert_eq ! ( map. get( ElementType :: AdmonitionNote ) , "Normal" ) ;
1264+ }
1265+
1266+ #[ test]
1267+ fn test_stylesheet_unknown_type_defaults_to_paragraph ( ) {
1268+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1269+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1270+ <w:style w:type="unknown" w:styleId="Mystery">
1271+ <w:name w:val="Mystery Style"/>
1272+ </w:style>
1273+ </w:styles>"# ;
1274+
1275+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1276+
1277+ let mystery = styles. get ( "Mystery" ) . unwrap ( ) ;
1278+ // Unknown types default to Paragraph
1279+ assert_eq ! ( mystery. style_type, StyleType :: Paragraph ) ;
1280+ }
1281+
1282+ #[ test]
1283+ fn test_stylesheet_style_without_name ( ) {
1284+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1285+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1286+ <w:style w:type="paragraph" w:styleId="NoName">
1287+ </w:style>
1288+ </w:styles>"# ;
1289+
1290+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1291+
1292+ let no_name = styles. get ( "NoName" ) . unwrap ( ) ;
1293+ // Name should default to ID when not specified
1294+ assert_eq ! ( no_name. name, "NoName" ) ;
1295+ }
1296+
1297+ #[ test]
1298+ fn test_style_map_from_stylesheet_fallback_by_id ( ) {
1299+ // Test that from_stylesheet falls back to checking by styleId
1300+ let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1301+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1302+ <w:style w:type="paragraph" w:styleId="Heading1">
1303+ <w:name w:val="Custom Name"/>
1304+ </w:style>
1305+ <w:style w:type="paragraph" w:styleId="Normal">
1306+ <w:name w:val="Another Name"/>
1307+ </w:style>
1308+ <w:style w:type="paragraph" w:styleId="CodeBlock">
1309+ <w:name w:val="My Code"/>
1310+ </w:style>
1311+ </w:styles>"# ;
1312+
1313+ let styles = StyleSheet :: parse ( xml) . unwrap ( ) ;
1314+ let map = StyleMap :: from_stylesheet ( & styles) ;
1315+
1316+ // Should find by styleId fallback
1317+ assert_eq ! ( map. heading( 1 ) , "Heading1" ) ;
1318+ assert_eq ! ( map. paragraph( ) , "Normal" ) ;
1319+ assert_eq ! ( map. code_block( ) , "CodeBlock" ) ;
1320+ }
9381321}
0 commit comments