| 1 | use markodown::{transform, ComponentImports, OutputFormat}; |
| 2 | use std::fs; |
| 3 | use std::path::Path; |
| 4 | |
| 5 | /// Load a fixture pair and test the transform. |
| 6 | /// |
| 7 | /// Each fixture has an `.mdo` input and either an `.html` expected output |
| 8 | /// (static mode) or a `.marko` expected output (marko mode), or both. |
| 9 | fn run_fixture(name: &str) { |
| 10 | run_fixture_with_options(name, None, None, None); |
| 11 | } |
| 12 | |
| 13 | /// Run fixture with optional layout, component_imports, and self_path arguments. |
| 14 | fn run_fixture_with_options( |
| 15 | name: &str, |
| 16 | layout: Option<&str>, |
| 17 | component_imports: Option<ComponentImports>, |
| 18 | self_path: Option<&str>, |
| 19 | ) { |
| 20 | let base = Path::new("tests/fixtures").join(name); |
| 21 | let input_path = base.with_extension("mdo"); |
| 22 | let html_path = base.with_extension("html"); |
| 23 | let marko_path = base.with_extension("marko"); |
| 24 | |
| 25 | let source = fs::read_to_string(&input_path) |
| 26 | .unwrap_or_else(|e| panic!("failed to read {}: {e}", input_path.display())); |
| 27 | |
| 28 | // Test HTML output if expected file exists |
| 29 | if html_path.exists() { |
| 30 | let expected = fs::read_to_string(&html_path) |
| 31 | .unwrap_or_else(|e| panic!("failed to read {}: {e}", html_path.display())); |
| 32 | |
| 33 | let output = transform( |
| 34 | &source, |
| 35 | Some(OutputFormat::Html), |
| 36 | layout.map(|s| s.to_string()), |
| 37 | component_imports.clone(), |
| 38 | self_path.map(|s| s.to_string()), |
| 39 | false, |
| 40 | None, |
| 41 | ) |
| 42 | .unwrap(); |
| 43 | |
| 44 | assert_eq!( |
| 45 | output.format, |
| 46 | OutputFormat::Html, |
| 47 | "[{name}] expected Html output but got Marko" |
| 48 | ); |
| 49 | |
| 50 | assert_eq!( |
| 51 | output.text.trim(), |
| 52 | expected.trim(), |
| 53 | "\n\n[{name}] HTML output mismatch\n\n--- expected ---\n{expected}\n--- actual ---\n{}\n", |
| 54 | output.text |
| 55 | ); |
| 56 | } |
| 57 | // Test Marko output if expected file exists |
| 58 | else if marko_path.exists() { |
| 59 | let expected = fs::read_to_string(&marko_path) |
| 60 | .unwrap_or_else(|e| panic!("failed to read {}: {e}", marko_path.display())); |
| 61 | |
| 62 | let output = transform( |
| 63 | &source, |
| 64 | Some(OutputFormat::Marko), |
| 65 | layout.map(|s| s.to_string()), |
| 66 | component_imports.clone(), |
| 67 | self_path.map(|s| s.to_string()), |
| 68 | false, |
| 69 | None, |
| 70 | ) |
| 71 | .unwrap(); |
| 72 | |
| 73 | assert_eq!( |
| 74 | output.format, |
| 75 | OutputFormat::Marko, |
| 76 | "[{name}] expected Marko output but got Html" |
| 77 | ); |
| 78 | |
| 79 | assert_eq!( |
| 80 | output.text.trim(), |
| 81 | expected.trim(), |
| 82 | "\n\n[{name}] Marko output mismatch\n\n--- expected ---\n{expected}\n--- actual ---\n{}\n", |
| 83 | output.text |
| 84 | ); |
| 85 | } else { |
| 86 | panic!("no test defined") |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | #[test] |
| 91 | fn fixture_01_static_markdown() { |
| 92 | run_fixture("01-static-markdown"); |
| 93 | } |
| 94 | |
| 95 | // The tests below have .marko expected outputs. They will be validated |
| 96 | // once the marko renderer and custom parse rules are implemented. |
| 97 | // For now they just verify the transform doesn't panic. |
| 98 | |
| 99 | #[test] |
| 100 | fn fixture_02_imports_and_components() { |
| 101 | run_fixture("02-imports-and-components"); |
| 102 | } |
| 103 | |
| 104 | #[test] |
| 105 | fn fixture_03_static_and_expressions() { |
| 106 | run_fixture("03-static-and-expressions"); |
| 107 | } |
| 108 | |
| 109 | #[test] |
| 110 | fn fixture_04_control_flow() { |
| 111 | run_fixture("04-control-flow"); |
| 112 | } |
| 113 | |
| 114 | #[test] |
| 115 | fn fixture_05_nested_tags_and_loops() { |
| 116 | run_fixture("05-nested-tags-and-loops"); |
| 117 | } |
| 118 | |
| 119 | #[test] |
| 120 | fn fixture_06_comments() { |
| 121 | run_fixture("06-comments"); |
| 122 | } |
| 123 | |
| 124 | #[test] |
| 125 | fn fixture_07_server_client() { |
| 126 | run_fixture("07-server-client"); |
| 127 | } |
| 128 | |
| 129 | #[test] |
| 130 | fn fixture_08_inline_tags() { |
| 131 | run_fixture("08-inline-tags"); |
| 132 | } |
| 133 | |
| 134 | #[test] |
| 135 | fn fixture_09_marko_shorthands() { |
| 136 | run_fixture("09-marko-shorthands"); |
| 137 | } |
| 138 | |
| 139 | #[test] |
| 140 | fn fixture_10_attribute_tags() { |
| 141 | run_fixture("10-attribute-tags"); |
| 142 | } |
| 143 | |
| 144 | #[test] |
| 145 | fn fixture_11_autolinks() { |
| 146 | run_fixture("11-autolinks"); |
| 147 | } |
| 148 | |
| 149 | #[test] |
| 150 | fn fixture_12_frontmatter() { |
| 151 | run_fixture("12-frontmatter"); |
| 152 | } |
| 153 | |
| 154 | #[test] |
| 155 | fn fixture_13_kitchen_sink() { |
| 156 | run_fixture("13-kitchen-sink"); |
| 157 | } |
| 158 | |
| 159 | #[test] |
| 160 | fn fixture_14_evil_braces_and_typescript() { |
| 161 | run_fixture("14-evil-braces-and-typescript"); |
| 162 | } |
| 163 | |
| 164 | #[test] |
| 165 | fn fixture_15_multiline_tag() { |
| 166 | run_fixture("15-multiline-tag"); |
| 167 | } |
| 168 | |
| 169 | #[test] |
| 170 | fn fixture_16_same_line_text() { |
| 171 | run_fixture("16-same-line-text"); |
| 172 | } |
| 173 | |
| 174 | #[test] |
| 175 | fn fixture_17_consecutive_statements() { |
| 176 | run_fixture("17-consecutive-statements"); |
| 177 | } |
| 178 | |
| 179 | #[test] |
| 180 | fn fixture_18_multiline_tag_with_value() { |
| 181 | run_fixture("18-multiline-tag-with-value"); |
| 182 | } |
| 183 | |
| 184 | #[test] |
| 185 | fn fixture_19_extra_cases() { |
| 186 | run_fixture("19-extra-cases"); |
| 187 | } |
| 188 | |
| 189 | #[test] |
| 190 | fn fixture_20_frontmatter_preamble() { |
| 191 | run_fixture("20-frontmatter-preamble"); |
| 192 | } |
| 193 | |
| 194 | #[test] |
| 195 | fn fixture_21_frontmatter_blank_lines() { |
| 196 | run_fixture("21-frontmatter-blank-lines"); |
| 197 | } |
| 198 | |
| 199 | #[test] |
| 200 | fn fixture_22_frontmatter_mixed_preamble() { |
| 201 | run_fixture("22-frontmatter-mixed-preamble"); |
| 202 | } |
| 203 | |
| 204 | #[test] |
| 205 | fn fixture_23_outline_extracting() { |
| 206 | run_fixture_with_options("23-outline-extracting", Some("./layout.marko"), None, None); |
| 207 | } |
| 208 | |
| 209 | #[test] |
| 210 | fn fixture_24_heading_import() { |
| 211 | run_fixture_with_options( |
| 212 | "24-heading-import", |
| 213 | None, |
| 214 | Some(ComponentImports { |
| 215 | heading: Some("./heading.marko".to_string()), |
| 216 | ..Default::default() |
| 217 | }), |
| 218 | None, |
| 219 | ); |
| 220 | } |
| 221 | |
| 222 | #[test] |
| 223 | fn fixture_25_code_block_import() { |
| 224 | run_fixture_with_options( |
| 225 | "25-code-block-import", |
| 226 | None, |
| 227 | Some(ComponentImports { |
| 228 | code_block: Some("./code-block.marko".to_string()), |
| 229 | ..Default::default() |
| 230 | }), |
| 231 | None, |
| 232 | ); |
| 233 | } |
| 234 | |
| 235 | #[test] |
| 236 | fn fixture_26_code_block_import() { |
| 237 | run_fixture_with_options("26-code-block-import", None, None, None); |
| 238 | } |
| 239 | |
| 240 | #[test] |
| 241 | fn fixture_27_frontmatter_layout() { |
| 242 | // layout key in frontmatter activates layout wrapping without a layoutImport option |
| 243 | run_fixture("27-frontmatter-layout"); |
| 244 | } |
| 245 | |
| 246 | #[test] |
| 247 | fn fixture_28_outline_self_import() { |
| 248 | // layout + selfImport produces module=self__markodown__ and the extra import |
| 249 | run_fixture_with_options( |
| 250 | "28-outline-self-import", |
| 251 | Some("./layout.marko"), |
| 252 | None, |
| 253 | Some("./self.marko"), |
| 254 | ); |
| 255 | } |
| 256 | |
| 257 | #[test] |
| 258 | fn fixture_29_layout_all_components() { |
| 259 | // layout emits boilerplate for all element types present in the document |
| 260 | run_fixture_with_options( |
| 261 | "29-layout-all-components", |
| 262 | Some("./layout.marko"), |
| 263 | None, |
| 264 | None, |
| 265 | ); |
| 266 | } |
| 267 | |
| 268 | #[test] |
| 269 | fn fixture_30_layout_selective_components() { |
| 270 | // only element types actually used in the document get boilerplate — others are omitted |
| 271 | run_fixture_with_options( |
| 272 | "30-layout-selective-components", |
| 273 | Some("./layout.marko"), |
| 274 | None, |
| 275 | None, |
| 276 | ); |
| 277 | } |
| 278 | |
| 279 | #[test] |
| 280 | fn fixture_31_import_with_markdown_content() { |
| 281 | // import statement followed (after blank line) by Marko tags with Markdown inline content |
| 282 | // regression: statement rule was passing unbounded source to OXC which tried to parse |
| 283 | // markdown link syntax [text](url) as a TypeScript array expression |
| 284 | run_fixture("31-import-with-markdown-content"); |
| 285 | } |
| 286 | |
| 287 | #[test] |
| 288 | fn fixture_32_inline_tag_in_paragraph() { |
| 289 | // regression: a self-contained inline tag (<b>text</b>) on its own line within a paragraph |
| 290 | // was being consumed by the block tag rule, splitting one paragraph into three |
| 291 | run_fixture("32-inline-tag-in-paragraph"); |
| 292 | } |