@@ -3716,4 +3716,397 @@ paragraph = "Normal"
37163716 assert ! ( archive. contains( "utf8dok/source.adoc" ) ) ;
37173717 assert ! ( archive. contains( "utf8dok/utf8dok.toml" ) ) ;
37183718 }
3719+
3720+ // ==================== Sprint 21: Comments and Content Types Tests ====================
3721+
3722+ #[ test]
3723+ fn test_generate_comments_xml_empty ( ) {
3724+ let writer = DocxWriter :: new ( ) ;
3725+ assert ! ( writer. generate_comments_xml( ) . is_none( ) ) ;
3726+ }
3727+
3728+ #[ test]
3729+ fn test_generate_comments_xml_with_comments ( ) {
3730+ let mut writer = DocxWriter :: new ( ) ;
3731+ writer. comments . push ( Comment {
3732+ id : 1 ,
3733+ text : "rust" . to_string ( ) ,
3734+ author : "utf8dok" . to_string ( ) ,
3735+ } ) ;
3736+
3737+ let xml = writer. generate_comments_xml ( ) ;
3738+ assert ! ( xml. is_some( ) ) ;
3739+
3740+ let content = xml. unwrap ( ) ;
3741+ assert ! ( content. contains( "w:comments" ) ) ;
3742+ assert ! ( content. contains( "w:id=\" 1\" " ) ) ;
3743+ assert ! ( content. contains( "w:author=\" utf8dok\" " ) ) ;
3744+ assert ! ( content. contains( "rust" ) ) ;
3745+ }
3746+
3747+ #[ test]
3748+ fn test_generate_comments_xml_escapes_special_chars ( ) {
3749+ let mut writer = DocxWriter :: new ( ) ;
3750+ writer. comments . push ( Comment {
3751+ id : 1 ,
3752+ text : "code with <tags> & \" quotes\" " . to_string ( ) ,
3753+ author : "Test <Author>" . to_string ( ) ,
3754+ } ) ;
3755+
3756+ let xml = writer. generate_comments_xml ( ) . unwrap ( ) ;
3757+ assert ! ( xml. contains( "<tags>" ) ) ;
3758+ assert ! ( xml. contains( "&" ) ) ;
3759+ assert ! ( xml. contains( ""quotes"" ) ) ;
3760+ assert ! ( xml. contains( "Test <Author>" ) ) ;
3761+ }
3762+
3763+ #[ test]
3764+ fn test_generate_comments_xml_multiple_comments ( ) {
3765+ let mut writer = DocxWriter :: new ( ) ;
3766+ writer. comments . push ( Comment {
3767+ id : 1 ,
3768+ text : "First" . to_string ( ) ,
3769+ author : "Author1" . to_string ( ) ,
3770+ } ) ;
3771+ writer. comments . push ( Comment {
3772+ id : 2 ,
3773+ text : "Second" . to_string ( ) ,
3774+ author : "Author2" . to_string ( ) ,
3775+ } ) ;
3776+
3777+ let xml = writer. generate_comments_xml ( ) . unwrap ( ) ;
3778+ assert ! ( xml. contains( "w:id=\" 1\" " ) ) ;
3779+ assert ! ( xml. contains( "w:id=\" 2\" " ) ) ;
3780+ assert ! ( xml. contains( "First" ) ) ;
3781+ assert ! ( xml. contains( "Second" ) ) ;
3782+ }
3783+
3784+ #[ test]
3785+ fn test_write_comments_adds_to_archive ( ) {
3786+ use crate :: archive:: OoxmlArchive ;
3787+ use crate :: test_utils:: create_minimal_template;
3788+
3789+ let template = create_minimal_template ( ) ;
3790+ let cursor = Cursor :: new ( & template) ;
3791+ let mut archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
3792+
3793+ let mut writer = DocxWriter :: new ( ) ;
3794+ writer. comments . push ( Comment {
3795+ id : 1 ,
3796+ text : "test language" . to_string ( ) ,
3797+ author : "utf8dok" . to_string ( ) ,
3798+ } ) ;
3799+
3800+ let result = writer. write_comments ( & mut archive) ;
3801+ assert ! ( result. is_ok( ) ) ;
3802+
3803+ // Check comments.xml was created
3804+ let comments = archive. get_string ( "word/comments.xml" ) . unwrap ( ) ;
3805+ assert ! ( comments. is_some( ) ) ;
3806+ assert ! ( comments. unwrap( ) . contains( "test language" ) ) ;
3807+ }
3808+
3809+ #[ test]
3810+ fn test_write_comments_updates_relationships ( ) {
3811+ use crate :: archive:: OoxmlArchive ;
3812+ use crate :: test_utils:: create_minimal_template;
3813+
3814+ let template = create_minimal_template ( ) ;
3815+ let cursor = Cursor :: new ( & template) ;
3816+ let mut archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
3817+
3818+ let mut writer = DocxWriter :: new ( ) ;
3819+ writer. comments . push ( Comment {
3820+ id : 1 ,
3821+ text : "test" . to_string ( ) ,
3822+ author : "utf8dok" . to_string ( ) ,
3823+ } ) ;
3824+
3825+ writer. write_comments ( & mut archive) . unwrap ( ) ;
3826+
3827+ // Check relationships were updated
3828+ let rels = archive
3829+ . get_string ( "word/_rels/document.xml.rels" )
3830+ . unwrap ( )
3831+ . unwrap ( ) ;
3832+ assert ! ( rels. contains( "comments.xml" ) ) ;
3833+ assert ! ( rels. contains( "relationships/comments" ) ) ;
3834+ }
3835+
3836+ #[ test]
3837+ fn test_write_comments_updates_content_types ( ) {
3838+ use crate :: archive:: OoxmlArchive ;
3839+ use crate :: test_utils:: create_minimal_template;
3840+
3841+ let template = create_minimal_template ( ) ;
3842+ let cursor = Cursor :: new ( & template) ;
3843+ let mut archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
3844+
3845+ let mut writer = DocxWriter :: new ( ) ;
3846+ writer. comments . push ( Comment {
3847+ id : 1 ,
3848+ text : "test" . to_string ( ) ,
3849+ author : "utf8dok" . to_string ( ) ,
3850+ } ) ;
3851+
3852+ writer. write_comments ( & mut archive) . unwrap ( ) ;
3853+
3854+ // Check content types were updated
3855+ let content_types = archive. get_string ( "[Content_Types].xml" ) . unwrap ( ) . unwrap ( ) ;
3856+ assert ! ( content_types. contains( "/word/comments.xml" ) ) ;
3857+ }
3858+
3859+ #[ test]
3860+ fn test_write_comments_empty_does_nothing ( ) {
3861+ use crate :: archive:: OoxmlArchive ;
3862+ use crate :: test_utils:: create_minimal_template;
3863+
3864+ let template = create_minimal_template ( ) ;
3865+ let cursor = Cursor :: new ( & template) ;
3866+ let mut archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
3867+
3868+ let writer = DocxWriter :: new ( ) ;
3869+ let result = writer. write_comments ( & mut archive) ;
3870+ assert ! ( result. is_ok( ) ) ;
3871+
3872+ // Should not have created comments.xml
3873+ assert ! ( archive. get( "word/comments.xml" ) . is_none( ) ) ;
3874+ }
3875+
3876+ #[ test]
3877+ fn test_update_content_types_adds_png ( ) {
3878+ use crate :: archive:: OoxmlArchive ;
3879+ use crate :: test_utils:: create_minimal_template;
3880+
3881+ let template = create_minimal_template ( ) ;
3882+ let cursor = Cursor :: new ( & template) ;
3883+ let mut archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
3884+
3885+ // Simulate having media files
3886+ let mut writer = DocxWriter :: new ( ) ;
3887+ writer. media_files . push ( ( "word/media/image1.png" . to_string ( ) , vec ! [ 0x89 , 0x50 ] ) ) ;
3888+
3889+ let result = writer. update_content_types ( & mut archive) ;
3890+ assert ! ( result. is_ok( ) ) ;
3891+
3892+ let content_types = archive. get_string ( "[Content_Types].xml" ) . unwrap ( ) . unwrap ( ) ;
3893+ assert ! ( content_types. contains( "Extension=\" png\" " ) ) ;
3894+ assert ! ( content_types. contains( "image/png" ) ) ;
3895+ }
3896+
3897+ #[ test]
3898+ fn test_update_content_types_does_not_duplicate ( ) {
3899+ use crate :: archive:: OoxmlArchive ;
3900+ use crate :: test_utils:: create_minimal_template;
3901+
3902+ let template = create_minimal_template ( ) ;
3903+ let cursor = Cursor :: new ( & template) ;
3904+ let mut archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
3905+
3906+ // Pre-add PNG extension
3907+ let existing = archive. get_string ( "[Content_Types].xml" ) . unwrap ( ) . unwrap ( ) ;
3908+ let with_png = existing. replace (
3909+ "</Types>" ,
3910+ "<Default Extension=\" png\" ContentType=\" image/png\" /></Types>" ,
3911+ ) ;
3912+ archive. set_string ( "[Content_Types].xml" , with_png) ;
3913+
3914+ let mut writer = DocxWriter :: new ( ) ;
3915+ writer. media_files . push ( ( "word/media/image1.png" . to_string ( ) , vec ! [ ] ) ) ;
3916+
3917+ writer. update_content_types ( & mut archive) . unwrap ( ) ;
3918+
3919+ // Should not have duplicated
3920+ let content = archive. get_string ( "[Content_Types].xml" ) . unwrap ( ) . unwrap ( ) ;
3921+ let count = content. matches ( "Extension=\" png\" " ) . count ( ) ;
3922+ assert_eq ! ( count, 1 ) ;
3923+ }
3924+
3925+ #[ test]
3926+ fn test_literal_block_with_language_creates_comment ( ) {
3927+ use crate :: test_utils:: create_minimal_template;
3928+ use utf8dok_ast:: LiteralBlock ;
3929+
3930+ let doc = Document {
3931+ metadata : Default :: default ( ) ,
3932+ intent : None ,
3933+ blocks : vec ! [ Block :: Literal ( LiteralBlock {
3934+ content: "fn main() {}" . to_string( ) ,
3935+ language: Some ( "rust" . to_string( ) ) ,
3936+ title: None ,
3937+ style_id: None ,
3938+ } ) ] ,
3939+ } ;
3940+
3941+ let template = create_minimal_template ( ) ;
3942+ let result = DocxWriter :: generate ( & doc, & template) . unwrap ( ) ;
3943+
3944+ // Extract and check comments
3945+ let cursor = Cursor :: new ( & result) ;
3946+ let archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
3947+
3948+ let comments = archive. get_string ( "word/comments.xml" ) . unwrap ( ) ;
3949+ assert ! ( comments. is_some( ) ) ;
3950+ assert ! ( comments. unwrap( ) . contains( "rust" ) ) ;
3951+ }
3952+
3953+ #[ test]
3954+ fn test_literal_block_without_language_no_comment ( ) {
3955+ use crate :: test_utils:: create_minimal_template;
3956+ use utf8dok_ast:: LiteralBlock ;
3957+
3958+ let doc = Document {
3959+ metadata : Default :: default ( ) ,
3960+ intent : None ,
3961+ blocks : vec ! [ Block :: Literal ( LiteralBlock {
3962+ content: "plain text" . to_string( ) ,
3963+ language: None ,
3964+ title: None ,
3965+ style_id: None ,
3966+ } ) ] ,
3967+ } ;
3968+
3969+ let template = create_minimal_template ( ) ;
3970+ let result = DocxWriter :: generate ( & doc, & template) . unwrap ( ) ;
3971+
3972+ let cursor = Cursor :: new ( & result) ;
3973+ let archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
3974+
3975+ // Should not have comments.xml
3976+ assert ! ( archive. get( "word/comments.xml" ) . is_none( ) ) ;
3977+ }
3978+
3979+ #[ test]
3980+ fn test_cover_page_with_full_metadata ( ) {
3981+ use crate :: test_utils:: create_template_with_styles;
3982+
3983+ let mut meta = utf8dok_ast:: DocumentMeta :: default ( ) ;
3984+ meta. title = Some ( "Corporate Report" . to_string ( ) ) ;
3985+ meta. authors = vec ! [ "John Doe" . to_string( ) ] ;
3986+ meta. revision = Some ( "1.0" . to_string ( ) ) ;
3987+ meta. attributes
3988+ . insert ( "revdate" . to_string ( ) , "2025-01-15" . to_string ( ) ) ;
3989+
3990+ let doc = Document {
3991+ metadata : meta,
3992+ intent : None ,
3993+ blocks : vec ! [ Block :: Paragraph ( Paragraph {
3994+ inlines: vec![ Inline :: Text ( "Content" . to_string( ) ) ] ,
3995+ ..Default :: default ( )
3996+ } ) ] ,
3997+ } ;
3998+
3999+ let mut writer = DocxWriter :: new ( ) ;
4000+ // Create a minimal cover image (1x1 PNG)
4001+ let cover_png = vec ! [
4002+ 0x89 , 0x50 , 0x4E , 0x47 , 0x0D , 0x0A , 0x1A , 0x0A , 0x00 , 0x00 , 0x00 , 0x0D , 0x49 , 0x48 ,
4003+ 0x44 , 0x52 , 0x00 , 0x00 , 0x00 , 0x01 , 0x00 , 0x00 , 0x00 , 0x01 , 0x08 , 0x02 , 0x00 , 0x00 ,
4004+ 0x00 , 0x90 , 0x77 , 0x53 , 0xDE , 0x00 , 0x00 , 0x00 , 0x0C , 0x49 , 0x44 , 0x41 , 0x54 , 0x08 ,
4005+ 0xD7 , 0x63 , 0xF8 , 0xFF , 0xFF , 0x3F , 0x00 , 0x05 , 0xFE , 0x02 , 0xFE , 0xDC , 0xCC , 0x59 ,
4006+ 0xE7 , 0x00 , 0x00 , 0x00 , 0x00 , 0x49 , 0x45 , 0x4E , 0x44 , 0xAE , 0x42 , 0x60 , 0x82 ,
4007+ ] ;
4008+ writer. set_cover_image ( "cover.png" , cover_png) ;
4009+
4010+ let template = create_template_with_styles ( ) ;
4011+ let template_obj = Template :: from_bytes ( & template) . unwrap ( ) ;
4012+ let result = writer. generate_with_template ( & doc, template_obj) . unwrap ( ) ;
4013+
4014+ let cursor = Cursor :: new ( & result) ;
4015+ let archive = OoxmlArchive :: from_reader ( cursor) . unwrap ( ) ;
4016+
4017+ // Should have cover image in media
4018+ assert ! ( archive. contains( "word/media/cover_cover.png" ) ) ;
4019+
4020+ // Document should contain the metadata text
4021+ let doc_xml = crate :: test_utils:: extract_document_xml ( & result) ;
4022+ assert ! ( doc_xml. contains( "Corporate Report" ) ) ;
4023+ assert ! ( doc_xml. contains( "John Doe" ) ) ;
4024+ }
4025+
4026+ #[ test]
4027+ fn test_cover_page_generates_page_break ( ) {
4028+ use crate :: test_utils:: create_template_with_styles;
4029+
4030+ let mut meta = utf8dok_ast:: DocumentMeta :: default ( ) ;
4031+ meta. title = Some ( "Title" . to_string ( ) ) ;
4032+
4033+ let doc = Document {
4034+ metadata : meta,
4035+ intent : None ,
4036+ blocks : vec ! [ Block :: Paragraph ( Paragraph {
4037+ inlines: vec![ Inline :: Text ( "Body content" . to_string( ) ) ] ,
4038+ ..Default :: default ( )
4039+ } ) ] ,
4040+ } ;
4041+
4042+ let mut writer = DocxWriter :: new ( ) ;
4043+ writer. set_cover_image ( "cover.png" , vec ! [ 0x89 , 0x50 , 0x4E , 0x47 ] ) ;
4044+
4045+ let template = create_template_with_styles ( ) ;
4046+ let template_obj = Template :: from_bytes ( & template) . unwrap ( ) ;
4047+ let result = writer. generate_with_template ( & doc, template_obj) . unwrap ( ) ;
4048+
4049+ let doc_xml = crate :: test_utils:: extract_document_xml ( & result) ;
4050+ // After cover page, should have page break before content
4051+ assert ! ( doc_xml. contains( "<w:br w:type=\" page\" />" ) ) ;
4052+ }
4053+
4054+ #[ test]
4055+ fn test_next_comment_id_increments ( ) {
4056+ let mut writer = DocxWriter :: new ( ) ;
4057+
4058+ assert_eq ! ( writer. next_comment_id, 1 ) ;
4059+ writer. next_comment_id += 1 ;
4060+ assert_eq ! ( writer. next_comment_id, 2 ) ;
4061+ writer. next_comment_id += 1 ;
4062+ assert_eq ! ( writer. next_comment_id, 3 ) ;
4063+ }
4064+
4065+ #[ test]
4066+ fn test_next_image_id_increments ( ) {
4067+ let mut writer = DocxWriter :: new ( ) ;
4068+
4069+ assert_eq ! ( writer. next_image_id, 1 ) ;
4070+ writer. next_image_id += 1 ;
4071+ assert_eq ! ( writer. next_image_id, 2 ) ;
4072+ }
4073+
4074+ #[ test]
4075+ fn test_next_drawing_id_increments ( ) {
4076+ let mut writer = DocxWriter :: new ( ) ;
4077+
4078+ assert_eq ! ( writer. next_drawing_id, 1 ) ;
4079+ writer. next_drawing_id += 1 ;
4080+ assert_eq ! ( writer. next_drawing_id, 2 ) ;
4081+ }
4082+
4083+ #[ test]
4084+ fn test_media_files_collection ( ) {
4085+ let mut writer = DocxWriter :: new ( ) ;
4086+ assert ! ( writer. media_files. is_empty( ) ) ;
4087+
4088+ writer
4089+ . media_files
4090+ . push ( ( "word/media/image1.png" . to_string ( ) , vec ! [ 1 , 2 , 3 ] ) ) ;
4091+ writer
4092+ . media_files
4093+ . push ( ( "word/media/image2.png" . to_string ( ) , vec ! [ 4 , 5 , 6 ] ) ) ;
4094+
4095+ assert_eq ! ( writer. media_files. len( ) , 2 ) ;
4096+ assert_eq ! ( writer. media_files[ 0 ] . 0 , "word/media/image1.png" ) ;
4097+ assert_eq ! ( writer. media_files[ 1 ] . 0 , "word/media/image2.png" ) ;
4098+ }
4099+
4100+ #[ test]
4101+ fn test_diagram_sources_collection ( ) {
4102+ let mut writer = DocxWriter :: new ( ) ;
4103+ assert ! ( writer. diagram_sources. is_empty( ) ) ;
4104+
4105+ writer
4106+ . diagram_sources
4107+ . push ( ( "utf8dok/diagrams/d1.mmd" . to_string ( ) , "graph TD" . to_string ( ) ) ) ;
4108+
4109+ assert_eq ! ( writer. diagram_sources. len( ) , 1 ) ;
4110+ assert ! ( writer. diagram_sources[ 0 ] . 1 . contains( "graph TD" ) ) ;
4111+ }
37194112}
0 commit comments