cmark-writer 0.7.4

A CommonMark writer implementation in Rust for serializing AST nodes to CommonMark format
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
use cmark_writer::ast::HeadingType;
use cmark_writer::coded_error;
use cmark_writer::structure_error;
use cmark_writer::CommonMarkWriter;
use cmark_writer::Node;
use cmark_writer::WriteError;
use cmark_writer::WriteResult;
use cmark_writer::WriterOptions;
use std::fmt::{self, Display};

#[test]
fn test_invalid_heading_level() {
    let mut writer = CommonMarkWriter::new();

    // Test heading level 0 (invalid)
    let invalid_heading_0 = Node::Heading {
        level: 0,
        content: vec![Node::Text("Invalid Heading".to_string())],
        heading_type: HeadingType::Atx,
    };
    let result = writer.write(&invalid_heading_0);
    assert!(result.is_err());

    if let Err(WriteError::InvalidHeadingLevel(level)) = result {
        assert_eq!(level, 0);
    } else {
        panic!("Expected InvalidHeadingLevel error");
    }

    // Test heading level 7 (invalid)
    let mut writer = CommonMarkWriter::new();
    let invalid_heading_7 = Node::Heading {
        level: 7,
        content: vec![Node::Text("Invalid Heading".to_string())],
        heading_type: HeadingType::Atx,
    };
    let result = writer.write(&invalid_heading_7);
    assert!(result.is_err());

    if let Err(WriteError::InvalidHeadingLevel(level)) = result {
        assert_eq!(level, 7);
    } else {
        panic!("Expected InvalidHeadingLevel error");
    }

    // Test heading level 6 (valid) - should not error
    let mut writer = CommonMarkWriter::new();
    let valid_heading = Node::Heading {
        level: 6,
        content: vec![Node::Text("Valid Heading".to_string())],
        heading_type: HeadingType::Atx,
    };
    assert!(writer.write(&valid_heading).is_ok());
}

#[test]
fn test_newline_in_inline_element() {
    let mut writer = CommonMarkWriter::new();

    // Test newline in text
    let text_with_newline = Node::Text("Line 1\nLine 2".to_string());
    let result = writer.write(&text_with_newline);
    assert!(result.is_err());

    match result {
        Err(WriteError::NewlineInInlineElement(context)) => {
            assert_eq!(context, "Text");
        }
        _ => panic!("Expected NewlineInInlineElement error"),
    }

    // Test newline in emphasis
    let mut writer = CommonMarkWriter::new();
    let emphasis_with_newline = Node::Emphasis(vec![Node::Text("Line 1\nLine 2".to_string())]);
    let result = writer.write(&emphasis_with_newline);
    assert!(result.is_err());

    // Test newline in strong
    let mut writer = CommonMarkWriter::new();
    let strong_with_newline = Node::Strong(vec![Node::Text("Line 1\nLine 2".to_string())]);
    let result = writer.write(&strong_with_newline);
    assert!(result.is_err());

    // Test newline in inline code
    let mut writer = CommonMarkWriter::new();
    let code_with_newline = Node::InlineCode("Line 1\nLine 2".to_string());
    let result = writer.write(&code_with_newline);
    assert!(result.is_err());
}

#[test]
fn test_unsupported_node_type() {
    // Create a mock unsupported node type by creating a custom struct
    // that implements Display like Node but is not a valid Node variant
    struct MockUnsupportedNode;

    impl Display for MockUnsupportedNode {
        fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
            Ok(())
        }
    }

    // Create an instance to use for testing
    let _mock_node = MockUnsupportedNode;

    // Test that the error message matches what we expect
    let err = WriteError::UnsupportedNodeType;
    let error_message = format!("{}", err);
    assert_eq!(
        error_message,
        "Unsupported node type encountered during writing."
    );
}

#[test]
fn test_fmt_error_conversion() {
    // Test conversion from fmt::Error to WriteError
    let fmt_error = fmt::Error;
    let write_error = WriteError::from(fmt_error);

    match write_error {
        WriteError::FmtError(_) => (), // Success
        _ => panic!("Expected FmtError variant"),
    }
}

#[test]
fn test_error_display() {
    // Test that all error types can be displayed correctly
    let errors = vec![
        WriteError::InvalidHeadingLevel(0),
        WriteError::NewlineInInlineElement("Text".to_string()),
        WriteError::FmtError("test error".to_string()),
        WriteError::UnsupportedNodeType,
    ];

    for err in errors {
        let display_str = format!("{}", err);
        assert!(
            !display_str.is_empty(),
            "Error should have non-empty display: {:?}",
            err
        );
    }
}

#[test]
fn test_error_debug() {
    // Test that all error types can be debug formatted
    let errors = vec![
        WriteError::InvalidHeadingLevel(0),
        WriteError::NewlineInInlineElement("Text".to_string()),
        WriteError::FmtError("test error".to_string()),
        WriteError::UnsupportedNodeType,
    ];

    for err in errors {
        let debug_str = format!("{:?}", err);
        assert!(!debug_str.is_empty());
    }
}

#[test]
fn test_write_result_alias() {
    // Test that WriteResult alias works correctly
    let ok_result: WriteResult<()> = Ok(());
    assert!(ok_result.is_ok());

    let err_result: WriteResult<()> = Err(WriteError::UnsupportedNodeType);
    assert!(err_result.is_err());
}

#[test]
fn test_custom_errors() {
    use cmark_writer::error::WriteError;
    use std::error::Error;

    let custom_err = WriteError::custom("这是一个自定义错误");
    assert_eq!(custom_err.to_string(), "Custom error: 这是一个自定义错误");

    let coded_err =
        WriteError::custom_with_code("表格行单元格数与表头数不匹配", "TABLE_STRUCTURE_ERROR");
    assert_eq!(
        coded_err.to_string(),
        "Custom error [TABLE_STRUCTURE_ERROR]: 表格行单元格数与表头数不匹配"
    );

    let structure_err = WriteError::InvalidStructure("表格结构无效".to_string());
    assert_eq!(structure_err.to_string(), "Invalid structure: 表格结构无效");

    fn takes_error(_: &dyn Error) {}
    takes_error(&custom_err);
    takes_error(&coded_err);
    takes_error(&structure_err);
}

#[test]
fn test_custom_error_attribute() {
    // 使用属性宏定义自定义错误

    #[structure_error(format = "表格列数不匹配:{}")]
    struct TableColumnMismatchError(pub &'static str);

    #[structure_error(format = "表格空表头:{}")]
    struct TableEmptyHeaderError(pub &'static str);

    #[structure_error(format = "文档格式错误:{}")]
    struct DocumentFormatError(pub &'static str);

    #[coded_error]
    struct MarkdownSyntaxError(pub String, pub String);

    let err1 = TableColumnMismatchError("第 3 行有 4 列,但表头只有 3 列").into_error();
    assert_eq!(
        err1.to_string(),
        "Invalid structure: 表格列数不匹配:第 3 行有 4 列,但表头只有 3 列"
    );

    let err2 = TableEmptyHeaderError("表格必须包含至少一个表头").into_error();
    assert_eq!(
        err2.to_string(),
        "Invalid structure: 表格空表头:表格必须包含至少一个表头"
    );

    let err3 = MarkdownSyntaxError(
        "缺少闭合代码块标记".to_string(),
        "CODE_BLOCK_UNCLOSED".to_string(),
    )
    .into_error();
    assert_eq!(
        err3.to_string(),
        "Custom error [CODE_BLOCK_UNCLOSED]: 缺少闭合代码块标记"
    );

    let err4 = DocumentFormatError("文档超过最大嵌套深度").into_error();
    assert_eq!(
        err4.to_string(),
        "Invalid structure: 文档格式错误:文档超过最大嵌套深度"
    );

    let err5: WriteError = TableColumnMismatchError("错误示例").into();
    assert!(matches!(err5, WriteError::InvalidStructure(_)));
}

#[test]
fn test_mixed_order_custom_errors() {
    // 使用属性宏定义多个自定义错误,顺序混合

    #[coded_error]
    struct ValidationError(pub String, pub String);

    #[structure_error(format = "解析错误:{}")]
    struct ParseError(pub &'static str);

    #[coded_error]
    struct FormatError(pub String, pub String);

    #[structure_error(format = "渲染错误:{}")]
    struct RenderError(pub &'static str);

    let err1 = ValidationError(
        "数据验证失败".to_string(),
        "DATA_VALIDATION_FAILED".to_string(),
    )
    .into_error();
    assert_eq!(
        err1.to_string(),
        "Custom error [DATA_VALIDATION_FAILED]: 数据验证失败"
    );

    let err2 = ParseError("无法解析 Markdown").into_error();
    assert_eq!(
        err2.to_string(),
        "Invalid structure: 解析错误:无法解析 Markdown"
    );

    let err3 = FormatError("格式化失败".to_string(), "FORMAT_FAILED".to_string()).into_error();
    assert_eq!(err3.to_string(), "Custom error [FORMAT_FAILED]: 格式化失败");

    let err4 = RenderError("无法渲染表格").into_error();
    assert_eq!(
        err4.to_string(),
        "Invalid structure: 渲染错误:无法渲染表格"
    );
}

// Helper to initialize logger for tests.
// Call this at the beginning of each test or in a common setup function if needed.
fn init_logger() {
    // Using try_init() to avoid panic if logger is already initialized,
    // which can happen if tests are run in parallel or multiple times.
    let _ = env_logger::builder().is_test(true).try_init();
}

#[test]
fn test_invalid_heading_level_strict() {
    init_logger();
    let options = WriterOptions {
        strict: true,
        ..Default::default()
    };
    let mut writer = CommonMarkWriter::with_options(options);
    let node = Node::Heading {
        level: 0, // Invalid level
        content: vec![Node::Text("Test".to_string())],
        heading_type: HeadingType::Atx,
    };
    match writer.write(&node) {
        Err(WriteError::InvalidHeadingLevel(level)) => assert_eq!(level, 0),
        _ => panic!("Expected InvalidHeadingLevel error"),
    }
}

#[test]
fn test_invalid_heading_level_non_strict() {
    init_logger();
    let options = WriterOptions {
        strict: false,
        ..Default::default()
    };
    let mut writer = CommonMarkWriter::with_options(options);
    let node = Node::Heading {
        level: 0, // Invalid level
        content: vec![Node::Text("Test".to_string())],
        heading_type: HeadingType::Atx,
    };
    assert!(writer.write(&node).is_ok());
    // In non-strict, level 0 should be clamped to 1.
    assert_eq!(writer.into_string(), "# Test\n");
    // Manually check stderr for log: "Invalid heading level: 0. Corrected to 1..."
}

#[test]
fn test_invalid_heading_level_7_non_strict() {
    init_logger();
    let options = WriterOptions {
        strict: false,
        ..Default::default()
    };
    let mut writer = CommonMarkWriter::with_options(options);
    let node = Node::Heading {
        level: 7, // Invalid level
        content: vec![Node::Text("Test".to_string())],
        heading_type: HeadingType::Atx,
    };
    assert!(writer.write(&node).is_ok());
    // In non-strict, level 7 should be clamped to 6.
    assert_eq!(writer.into_string(), "###### Test\n");
    // Manually check stderr for log: "Invalid heading level: 7. Corrected to 6..."
}

#[test]
fn test_newline_in_link_text_strict() {
    init_logger();
    let options = WriterOptions {
        strict: true,
        ..Default::default()
    };
    let mut writer = CommonMarkWriter::with_options(options);
    let node = Node::Link {
        url: "http://example.com".to_string(),
        title: None,
        content: vec![Node::Text("Link\nText".to_string())], // Newline in link text
    };
    match writer.write(&node) {
        Err(WriteError::NewlineInInlineElement(context)) => assert_eq!(context, "Link content"),
        _ => panic!("Expected NewlineInInlineElement error for link text"),
    }
}

#[test]
fn test_newline_in_link_text_non_strict() {
    init_logger();
    let options = WriterOptions {
        strict: false,
        ..Default::default()
    };
    let mut writer = CommonMarkWriter::with_options(options);
    let node = Node::Link {
        url: "http://example.com".to_string(),
        title: None,
        content: vec![Node::Text("Link\nText".to_string())], // Newline in link text
    };
    assert!(writer.write(&node).is_ok());
    // Output will contain the newline as per current non-strict behavior
    assert_eq!(writer.into_string(), "[Link\nText](http://example.com)");
    // Manually check stderr for log: "Newline character found in inline element 'Link Text'..."
}

// TODO: Add test for UnsupportedNodeType if a stable way to construct/mock one exists.
// For example, if Node enum had a test-only variant:
// #[cfg(test)]
// TestOnlyUnsupported,
//
// Then you could write:
// #[test]
// fn test_unsupported_node_type_strict() {
//     init_logger();
//     let options = WriterOptions { strict: true, ..Default::default() };
//     let mut writer = CommonMarkWriter::with_options(options);
//     let node = Node::TestOnlyUnsupported; // Hypothetical
//     match writer.write(&node) {
//         Err(WriteError::UnsupportedNodeType) => { /* Expected */ }
//         _ => panic!("Expected UnsupportedNodeType error"),
//     }
// }
//
// #[test]
// fn test_unsupported_node_type_non_strict() {
//     init_logger();
//     let options = WriterOptions { strict: false, ..Default::default() };
//     let mut writer = CommonMarkWriter::with_options(options);
//     let node = Node::TestOnlyUnsupported; // Hypothetical
//     assert!(writer.write(&node).is_ok());
//     assert_eq!(writer.into_string(), ""); // Or placeholder if you decide to write one
//     // Manually check stderr for log: "Unsupported node type encountered and skipped..."
// }