1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
7use crate::utils::regex_cache::*;
8use std::collections::HashSet;
9
10mod md033_config;
11use md033_config::MD033Config;
12
13#[derive(Clone)]
14pub struct MD033NoInlineHtml {
15 config: MD033Config,
16 allowed: HashSet<String>,
17 disallowed: HashSet<String>,
18}
19
20impl Default for MD033NoInlineHtml {
21 fn default() -> Self {
22 let config = MD033Config::default();
23 let allowed = config.allowed_set();
24 let disallowed = config.disallowed_set();
25 Self {
26 config,
27 allowed,
28 disallowed,
29 }
30 }
31}
32
33impl MD033NoInlineHtml {
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn with_allowed(allowed_vec: Vec<String>) -> Self {
39 let config = MD033Config {
40 allowed: allowed_vec.clone(),
41 disallowed: Vec::new(),
42 fix: false,
43 br_style: md033_config::BrStyle::default(),
44 };
45 let allowed = config.allowed_set();
46 let disallowed = config.disallowed_set();
47 Self {
48 config,
49 allowed,
50 disallowed,
51 }
52 }
53
54 pub fn with_disallowed(disallowed_vec: Vec<String>) -> Self {
55 let config = MD033Config {
56 allowed: Vec::new(),
57 disallowed: disallowed_vec.clone(),
58 fix: false,
59 br_style: md033_config::BrStyle::default(),
60 };
61 let allowed = config.allowed_set();
62 let disallowed = config.disallowed_set();
63 Self {
64 config,
65 allowed,
66 disallowed,
67 }
68 }
69
70 pub fn with_fix(fix: bool) -> Self {
72 let config = MD033Config {
73 allowed: Vec::new(),
74 disallowed: Vec::new(),
75 fix,
76 br_style: md033_config::BrStyle::default(),
77 };
78 let allowed = config.allowed_set();
79 let disallowed = config.disallowed_set();
80 Self {
81 config,
82 allowed,
83 disallowed,
84 }
85 }
86
87 pub fn from_config_struct(config: MD033Config) -> Self {
88 let allowed = config.allowed_set();
89 let disallowed = config.disallowed_set();
90 Self {
91 config,
92 allowed,
93 disallowed,
94 }
95 }
96
97 #[inline]
99 fn is_tag_allowed(&self, tag: &str) -> bool {
100 if self.allowed.is_empty() {
101 return false;
102 }
103 let tag = tag.trim_start_matches('<').trim_start_matches('/');
105 let tag_name = tag
106 .split(|c: char| c.is_whitespace() || c == '>' || c == '/')
107 .next()
108 .unwrap_or("");
109 self.allowed.contains(&tag_name.to_lowercase())
110 }
111
112 #[inline]
114 fn is_tag_disallowed(&self, tag: &str) -> bool {
115 if self.disallowed.is_empty() {
116 return false;
117 }
118 let tag = tag.trim_start_matches('<').trim_start_matches('/');
120 let tag_name = tag
121 .split(|c: char| c.is_whitespace() || c == '>' || c == '/')
122 .next()
123 .unwrap_or("");
124 self.disallowed.contains(&tag_name.to_lowercase())
125 }
126
127 #[inline]
129 fn is_disallowed_mode(&self) -> bool {
130 self.config.is_disallowed_mode()
131 }
132
133 #[inline]
135 fn is_html_comment(&self, tag: &str) -> bool {
136 tag.starts_with("<!--") && tag.ends_with("-->")
137 }
138
139 #[inline]
144 fn is_html_element_or_custom(tag_name: &str) -> bool {
145 const HTML_ELEMENTS: &[&str] = &[
146 "html",
148 "head",
149 "body",
150 "title",
151 "base",
152 "link",
153 "meta",
154 "style",
155 "article",
157 "section",
158 "nav",
159 "aside",
160 "h1",
161 "h2",
162 "h3",
163 "h4",
164 "h5",
165 "h6",
166 "hgroup",
167 "header",
168 "footer",
169 "address",
170 "main",
171 "search",
172 "p",
174 "hr",
175 "pre",
176 "blockquote",
177 "ol",
178 "ul",
179 "menu",
180 "li",
181 "dl",
182 "dt",
183 "dd",
184 "figure",
185 "figcaption",
186 "div",
187 "a",
189 "em",
190 "strong",
191 "small",
192 "s",
193 "cite",
194 "q",
195 "dfn",
196 "abbr",
197 "ruby",
198 "rt",
199 "rp",
200 "data",
201 "time",
202 "code",
203 "var",
204 "samp",
205 "kbd",
206 "sub",
207 "sup",
208 "i",
209 "b",
210 "u",
211 "mark",
212 "bdi",
213 "bdo",
214 "span",
215 "br",
216 "wbr",
217 "ins",
219 "del",
220 "picture",
222 "source",
223 "img",
224 "iframe",
225 "embed",
226 "object",
227 "param",
228 "video",
229 "audio",
230 "track",
231 "map",
232 "area",
233 "svg",
234 "math",
235 "canvas",
236 "table",
238 "caption",
239 "colgroup",
240 "col",
241 "tbody",
242 "thead",
243 "tfoot",
244 "tr",
245 "td",
246 "th",
247 "form",
249 "label",
250 "input",
251 "button",
252 "select",
253 "datalist",
254 "optgroup",
255 "option",
256 "textarea",
257 "output",
258 "progress",
259 "meter",
260 "fieldset",
261 "legend",
262 "details",
264 "summary",
265 "dialog",
266 "script",
268 "noscript",
269 "template",
270 "slot",
271 "acronym",
273 "applet",
274 "basefont",
275 "big",
276 "center",
277 "dir",
278 "font",
279 "frame",
280 "frameset",
281 "isindex",
282 "marquee",
283 "noembed",
284 "noframes",
285 "plaintext",
286 "strike",
287 "tt",
288 "xmp",
289 ];
290
291 let lower = tag_name.to_ascii_lowercase();
292 if HTML_ELEMENTS.contains(&lower.as_str()) {
293 return true;
294 }
295 tag_name.contains('-')
297 }
298
299 #[inline]
301 fn is_likely_type_annotation(&self, tag: &str) -> bool {
302 const COMMON_TYPES: &[&str] = &[
304 "string",
305 "number",
306 "any",
307 "void",
308 "null",
309 "undefined",
310 "array",
311 "promise",
312 "function",
313 "error",
314 "date",
315 "regexp",
316 "symbol",
317 "bigint",
318 "map",
319 "set",
320 "weakmap",
321 "weakset",
322 "iterator",
323 "generator",
324 "t",
325 "u",
326 "v",
327 "k",
328 "e", "userdata",
330 "apiresponse",
331 "config",
332 "options",
333 "params",
334 "result",
335 "response",
336 "request",
337 "data",
338 "item",
339 "element",
340 "node",
341 ];
342
343 let tag_content = tag
344 .trim_start_matches('<')
345 .trim_end_matches('>')
346 .trim_start_matches('/');
347 let tag_name = tag_content
348 .split(|c: char| c.is_whitespace() || c == '>' || c == '/')
349 .next()
350 .unwrap_or("");
351
352 if !tag_content.contains(' ') && !tag_content.contains('=') {
354 COMMON_TYPES.contains(&tag_name.to_ascii_lowercase().as_str())
355 } else {
356 false
357 }
358 }
359
360 #[inline]
362 fn is_email_address(&self, tag: &str) -> bool {
363 let content = tag.trim_start_matches('<').trim_end_matches('>');
364 content.contains('@')
366 && content.chars().all(|c| c.is_alphanumeric() || "@.-_+".contains(c))
367 && content.split('@').count() == 2
368 && content.split('@').all(|part| !part.is_empty())
369 }
370
371 #[inline]
373 fn has_markdown_attribute(&self, tag: &str) -> bool {
374 tag.contains(" markdown>") || tag.contains(" markdown=") || tag.contains(" markdown ")
377 }
378
379 #[inline]
386 fn has_jsx_attributes(tag: &str) -> bool {
387 tag.contains("className")
389 || tag.contains("htmlFor")
390 || tag.contains("dangerouslySetInnerHTML")
391 || tag.contains("onClick")
393 || tag.contains("onChange")
394 || tag.contains("onSubmit")
395 || tag.contains("onFocus")
396 || tag.contains("onBlur")
397 || tag.contains("onKeyDown")
398 || tag.contains("onKeyUp")
399 || tag.contains("onKeyPress")
400 || tag.contains("onMouseDown")
401 || tag.contains("onMouseUp")
402 || tag.contains("onMouseEnter")
403 || tag.contains("onMouseLeave")
404 || tag.contains("={")
406 }
407
408 #[inline]
410 fn is_url_in_angle_brackets(&self, tag: &str) -> bool {
411 let content = tag.trim_start_matches('<').trim_end_matches('>');
412 content.starts_with("http://")
414 || content.starts_with("https://")
415 || content.starts_with("ftp://")
416 || content.starts_with("ftps://")
417 || content.starts_with("mailto:")
418 }
419
420 fn convert_to_markdown(tag_name: &str, inner_content: &str) -> Option<String> {
423 if inner_content.contains('<') {
425 return None;
426 }
427 if inner_content.contains('&') && inner_content.contains(';') {
430 let has_entity = inner_content
432 .split('&')
433 .skip(1)
434 .any(|part| part.split(';').next().is_some_and(|e| !e.is_empty() && e.len() < 10));
435 if has_entity {
436 return None;
437 }
438 }
439 match tag_name {
440 "em" | "i" => Some(format!("*{inner_content}*")),
441 "strong" | "b" => Some(format!("**{inner_content}**")),
442 "code" => {
443 if inner_content.contains('`') {
445 Some(format!("`` {inner_content} ``"))
446 } else {
447 Some(format!("`{inner_content}`"))
448 }
449 }
450 _ => None,
451 }
452 }
453
454 fn convert_self_closing_to_markdown(&self, tag_name: &str, opening_tag: &str) -> Option<String> {
456 match tag_name {
457 "br" => match self.config.br_style {
458 md033_config::BrStyle::TrailingSpaces => Some(" \n".to_string()),
459 md033_config::BrStyle::Backslash => Some("\\\n".to_string()),
460 },
461 "hr" => Some("\n---\n".to_string()),
462 "img" => Self::convert_img_to_markdown(opening_tag),
463 _ => None,
464 }
465 }
466
467 fn parse_attributes(tag: &str) -> Vec<(String, Option<String>)> {
470 let mut attrs = Vec::new();
471
472 let tag_content = tag.trim_start_matches('<').trim_end_matches('>').trim_end_matches('/');
474
475 let attr_start = tag_content
477 .find(|c: char| c.is_whitespace())
478 .map(|i| i + 1)
479 .unwrap_or(tag_content.len());
480
481 if attr_start >= tag_content.len() {
482 return attrs;
483 }
484
485 let attr_str = &tag_content[attr_start..];
486 let mut chars = attr_str.chars().peekable();
487
488 while chars.peek().is_some() {
489 while chars.peek().is_some_and(|c| c.is_whitespace()) {
491 chars.next();
492 }
493
494 if chars.peek().is_none() {
495 break;
496 }
497
498 let mut attr_name = String::new();
500 while let Some(&c) = chars.peek() {
501 if c.is_whitespace() || c == '=' || c == '>' || c == '/' {
502 break;
503 }
504 attr_name.push(c);
505 chars.next();
506 }
507
508 if attr_name.is_empty() {
509 break;
510 }
511
512 while chars.peek().is_some_and(|c| c.is_whitespace()) {
514 chars.next();
515 }
516
517 if chars.peek() == Some(&'=') {
519 chars.next(); while chars.peek().is_some_and(|c| c.is_whitespace()) {
523 chars.next();
524 }
525
526 let mut value = String::new();
528 if let Some("e) = chars.peek() {
529 if quote == '"' || quote == '\'' {
530 chars.next(); for c in chars.by_ref() {
532 if c == quote {
533 break;
534 }
535 value.push(c);
536 }
537 } else {
538 while let Some(&c) = chars.peek() {
540 if c.is_whitespace() || c == '>' || c == '/' {
541 break;
542 }
543 value.push(c);
544 chars.next();
545 }
546 }
547 }
548 attrs.push((attr_name.to_ascii_lowercase(), Some(value)));
549 } else {
550 attrs.push((attr_name.to_ascii_lowercase(), None));
552 }
553 }
554
555 attrs
556 }
557
558 fn extract_attribute(tag: &str, attr_name: &str) -> Option<String> {
562 let attrs = Self::parse_attributes(tag);
563 let attr_lower = attr_name.to_ascii_lowercase();
564
565 attrs
566 .into_iter()
567 .find(|(name, _)| name == &attr_lower)
568 .and_then(|(_, value)| value)
569 }
570
571 fn has_extra_attributes(tag: &str, allowed_attrs: &[&str]) -> bool {
574 let attrs = Self::parse_attributes(tag);
575
576 const DANGEROUS_ATTR_PREFIXES: &[&str] = &["on"]; const DANGEROUS_ATTRS: &[&str] = &[
580 "class",
581 "id",
582 "style",
583 "target",
584 "rel",
585 "download",
586 "referrerpolicy",
587 "crossorigin",
588 "loading",
589 "decoding",
590 "fetchpriority",
591 "sizes",
592 "srcset",
593 "usemap",
594 "ismap",
595 "width",
596 "height",
597 "name", "data-*", ];
600
601 for (attr_name, _) in attrs {
602 if allowed_attrs.iter().any(|a| a.to_ascii_lowercase() == attr_name) {
604 continue;
605 }
606
607 for prefix in DANGEROUS_ATTR_PREFIXES {
609 if attr_name.starts_with(prefix) && attr_name.len() > prefix.len() {
610 return true;
611 }
612 }
613
614 if attr_name.starts_with("data-") {
616 return true;
617 }
618
619 if DANGEROUS_ATTRS.contains(&attr_name.as_str()) {
621 return true;
622 }
623 }
624
625 false
626 }
627
628 fn convert_a_to_markdown(opening_tag: &str, inner_content: &str) -> Option<String> {
631 let href = Self::extract_attribute(opening_tag, "href")?;
633
634 if !MD033Config::is_safe_url(&href) {
636 return None;
637 }
638
639 if inner_content.contains('<') {
641 return None;
642 }
643
644 if inner_content.contains('&') && inner_content.contains(';') {
646 let has_entity = inner_content
647 .split('&')
648 .skip(1)
649 .any(|part| part.split(';').next().is_some_and(|e| !e.is_empty() && e.len() < 10));
650 if has_entity {
651 return None;
652 }
653 }
654
655 let title = Self::extract_attribute(opening_tag, "title");
657
658 if Self::has_extra_attributes(opening_tag, &["href", "title"]) {
660 return None;
661 }
662
663 let trimmed_inner = inner_content.trim();
668 let is_markdown_image =
669 trimmed_inner.starts_with(" && trimmed_inner.ends_with(')') && {
670 if let Some(bracket_close) = trimmed_inner.rfind("](") {
673 let after_paren = &trimmed_inner[bracket_close + 2..];
674 after_paren.ends_with(')')
676 && after_paren.chars().filter(|&c| c == ')').count()
677 >= after_paren.chars().filter(|&c| c == '(').count()
678 } else {
679 false
680 }
681 };
682 let escaped_text = if is_markdown_image {
683 trimmed_inner.to_string()
684 } else {
685 inner_content.replace('[', r"\[").replace(']', r"\]")
688 };
689
690 let escaped_url = href.replace('(', "%28").replace(')', "%29");
692
693 if let Some(title_text) = title {
695 let escaped_title = title_text.replace('"', r#"\""#);
697 Some(format!("[{escaped_text}]({escaped_url} \"{escaped_title}\")"))
698 } else {
699 Some(format!("[{escaped_text}]({escaped_url})"))
700 }
701 }
702
703 fn convert_img_to_markdown(tag: &str) -> Option<String> {
706 let src = Self::extract_attribute(tag, "src")?;
708
709 if !MD033Config::is_safe_url(&src) {
711 return None;
712 }
713
714 let alt = Self::extract_attribute(tag, "alt").unwrap_or_default();
716
717 let title = Self::extract_attribute(tag, "title");
719
720 if Self::has_extra_attributes(tag, &["src", "alt", "title"]) {
722 return None;
723 }
724
725 let escaped_alt = alt.replace('[', r"\[").replace(']', r"\]");
727
728 let escaped_url = src.replace('(', "%28").replace(')', "%29");
730
731 if let Some(title_text) = title {
733 let escaped_title = title_text.replace('"', r#"\""#);
735 Some(format!(""))
736 } else {
737 Some(format!(""))
738 }
739 }
740
741 fn has_significant_attributes(opening_tag: &str) -> bool {
743 let tag_content = opening_tag
745 .trim_start_matches('<')
746 .trim_end_matches('>')
747 .trim_end_matches('/');
748
749 let parts: Vec<&str> = tag_content.split_whitespace().collect();
751 parts.len() > 1
752 }
753
754 fn is_nested_in_html(content: &str, tag_byte_start: usize, tag_byte_end: usize) -> bool {
757 if tag_byte_start > 0 {
759 let before = &content[..tag_byte_start];
760 let before_trimmed = before.trim_end();
761 if before_trimmed.ends_with('>') && !before_trimmed.ends_with("->") {
762 if let Some(last_lt) = before_trimmed.rfind('<') {
764 let potential_tag = &before_trimmed[last_lt..];
765 if !potential_tag.starts_with("</") && !potential_tag.starts_with("<!--") {
767 return true;
768 }
769 }
770 }
771 }
772 if tag_byte_end < content.len() {
774 let after = &content[tag_byte_end..];
775 let after_trimmed = after.trim_start();
776 if after_trimmed.starts_with("</") {
777 return true;
778 }
779 }
780 false
781 }
782
783 fn calculate_fix(
794 &self,
795 content: &str,
796 opening_tag: &str,
797 tag_byte_start: usize,
798 in_html_block: bool,
799 ) -> Option<(std::ops::Range<usize>, String)> {
800 let tag_name = opening_tag
802 .trim_start_matches('<')
803 .split(|c: char| c.is_whitespace() || c == '>' || c == '/')
804 .next()?
805 .to_lowercase();
806
807 let is_self_closing =
809 opening_tag.ends_with("/>") || matches!(tag_name.as_str(), "br" | "hr" | "img" | "input" | "meta" | "link");
810
811 if is_self_closing {
812 if self.config.fix
815 && MD033Config::is_safe_fixable_tag(&tag_name)
816 && !in_html_block
817 && let Some(markdown) = self.convert_self_closing_to_markdown(&tag_name, opening_tag)
818 {
819 return Some((tag_byte_start..tag_byte_start + opening_tag.len(), markdown));
820 }
821 return None;
824 }
825
826 let search_start = tag_byte_start + opening_tag.len();
828 let search_slice = &content[search_start..];
829
830 let closing_tag_lower = format!("</{tag_name}>");
832 let closing_pos = search_slice.to_ascii_lowercase().find(&closing_tag_lower);
833
834 if let Some(closing_pos) = closing_pos {
835 let closing_tag_len = closing_tag_lower.len();
837 let closing_byte_start = search_start + closing_pos;
838 let closing_byte_end = closing_byte_start + closing_tag_len;
839
840 let inner_content = &content[search_start..closing_byte_start];
842
843 if in_html_block {
846 return None;
847 }
848
849 if Self::is_nested_in_html(content, tag_byte_start, closing_byte_end) {
852 return None;
853 }
854
855 if self.config.fix && MD033Config::is_safe_fixable_tag(&tag_name) {
857 if tag_name == "a" {
859 if let Some(markdown) = Self::convert_a_to_markdown(opening_tag, inner_content) {
860 return Some((tag_byte_start..closing_byte_end, markdown));
861 }
862 return None;
864 }
865
866 if Self::has_significant_attributes(opening_tag) {
868 return None;
871 }
872 if let Some(markdown) = Self::convert_to_markdown(&tag_name, inner_content) {
873 return Some((tag_byte_start..closing_byte_end, markdown));
874 }
875 return None;
878 }
879
880 return None;
883 }
884
885 None
887 }
888}
889
890impl Rule for MD033NoInlineHtml {
891 fn name(&self) -> &'static str {
892 "MD033"
893 }
894
895 fn description(&self) -> &'static str {
896 "Inline HTML is not allowed"
897 }
898
899 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
900 let content = ctx.content;
901
902 if content.is_empty() || !ctx.likely_has_html() {
904 return Ok(Vec::new());
905 }
906
907 if !HTML_TAG_QUICK_CHECK.is_match(content) {
909 return Ok(Vec::new());
910 }
911
912 let mut warnings = Vec::new();
913
914 let html_tags = ctx.html_tags();
916
917 for html_tag in html_tags.iter() {
918 if html_tag.is_closing {
920 continue;
921 }
922
923 let line_num = html_tag.line;
924 let tag_byte_start = html_tag.byte_offset;
925
926 let tag = &content[html_tag.byte_offset..html_tag.byte_end];
928
929 if ctx
931 .line_info(line_num)
932 .is_some_and(|info| info.in_code_block || info.in_pymdown_block || info.is_kramdown_block_ial)
933 {
934 continue;
935 }
936
937 if ctx.is_in_html_comment(tag_byte_start) {
939 continue;
940 }
941
942 if self.is_html_comment(tag) {
944 continue;
945 }
946
947 if ctx.is_in_link_title(tag_byte_start) {
950 continue;
951 }
952
953 if ctx.flavor.supports_jsx() && html_tag.tag_name.chars().next().is_some_and(|c| c.is_uppercase()) {
955 continue;
956 }
957
958 if ctx.flavor.supports_jsx() && (html_tag.tag_name.is_empty() || tag == "<>" || tag == "</>") {
960 continue;
961 }
962
963 if ctx.flavor.supports_jsx() && Self::has_jsx_attributes(tag) {
966 continue;
967 }
968
969 if !Self::is_html_element_or_custom(&html_tag.tag_name) {
971 continue;
972 }
973
974 if self.is_likely_type_annotation(tag) {
976 continue;
977 }
978
979 if self.is_email_address(tag) {
981 continue;
982 }
983
984 if self.is_url_in_angle_brackets(tag) {
986 continue;
987 }
988
989 if ctx.is_byte_offset_in_code_span(tag_byte_start) {
991 continue;
992 }
993
994 if self.is_disallowed_mode() {
998 if !self.is_tag_disallowed(tag) {
1000 continue;
1001 }
1002 } else {
1003 if self.is_tag_allowed(tag) {
1005 continue;
1006 }
1007 }
1008
1009 if ctx.flavor == crate::config::MarkdownFlavor::MkDocs && self.has_markdown_attribute(tag) {
1011 continue;
1012 }
1013
1014 let in_html_block = ctx.is_in_html_block(line_num);
1016
1017 let fix = self
1019 .calculate_fix(content, tag, tag_byte_start, in_html_block)
1020 .map(|(range, replacement)| Fix { range, replacement });
1021
1022 let (end_line, end_col) = if html_tag.byte_end > 0 {
1025 ctx.offset_to_line_col(html_tag.byte_end - 1)
1026 } else {
1027 (line_num, html_tag.end_col + 1)
1028 };
1029
1030 warnings.push(LintWarning {
1032 rule_name: Some(self.name().to_string()),
1033 line: line_num,
1034 column: html_tag.start_col + 1, end_line, end_column: end_col + 1, message: format!("Inline HTML found: {tag}"),
1038 severity: Severity::Warning,
1039 fix,
1040 });
1041 }
1042
1043 Ok(warnings)
1044 }
1045
1046 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
1047 if !self.config.fix {
1049 return Ok(ctx.content.to_string());
1050 }
1051
1052 let warnings = self.check(ctx)?;
1054
1055 if warnings.is_empty() || !warnings.iter().any(|w| w.fix.is_some()) {
1057 return Ok(ctx.content.to_string());
1058 }
1059
1060 let mut fixes: Vec<_> = warnings
1062 .iter()
1063 .filter_map(|w| w.fix.as_ref().map(|f| (f.range.start, f.range.end, &f.replacement)))
1064 .collect();
1065 fixes.sort_by(|a, b| b.0.cmp(&a.0));
1066
1067 let mut result = ctx.content.to_string();
1069 for (start, end, replacement) in fixes {
1070 if start < result.len() && end <= result.len() && start <= end {
1071 result.replace_range(start..end, replacement);
1072 }
1073 }
1074
1075 Ok(result)
1076 }
1077
1078 fn fix_capability(&self) -> crate::rule::FixCapability {
1079 if self.config.fix {
1080 crate::rule::FixCapability::FullyFixable
1081 } else {
1082 crate::rule::FixCapability::Unfixable
1083 }
1084 }
1085
1086 fn category(&self) -> RuleCategory {
1088 RuleCategory::Html
1089 }
1090
1091 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1093 ctx.content.is_empty() || !ctx.likely_has_html()
1094 }
1095
1096 fn as_any(&self) -> &dyn std::any::Any {
1097 self
1098 }
1099
1100 fn default_config_section(&self) -> Option<(String, toml::Value)> {
1101 let json_value = serde_json::to_value(&self.config).ok()?;
1102 Some((
1103 self.name().to_string(),
1104 crate::rule_config_serde::json_to_toml_value(&json_value)?,
1105 ))
1106 }
1107
1108 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1109 where
1110 Self: Sized,
1111 {
1112 let rule_config = crate::rule_config_serde::load_rule_config::<MD033Config>(config);
1113 Box::new(Self::from_config_struct(rule_config))
1114 }
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119 use super::*;
1120 use crate::lint_context::LintContext;
1121 use crate::rule::Rule;
1122
1123 #[test]
1124 fn test_md033_basic_html() {
1125 let rule = MD033NoInlineHtml::default();
1126 let content = "<div>Some content</div>";
1127 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1128 let result = rule.check(&ctx).unwrap();
1129 assert_eq!(result.len(), 1); assert!(result[0].message.starts_with("Inline HTML found: <div>"));
1132 }
1133
1134 #[test]
1135 fn test_md033_case_insensitive() {
1136 let rule = MD033NoInlineHtml::default();
1137 let content = "<DiV>Some <B>content</B></dIv>";
1138 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1139 let result = rule.check(&ctx).unwrap();
1140 assert_eq!(result.len(), 2); assert_eq!(result[0].message, "Inline HTML found: <DiV>");
1143 assert_eq!(result[1].message, "Inline HTML found: <B>");
1144 }
1145
1146 #[test]
1147 fn test_md033_allowed_tags() {
1148 let rule = MD033NoInlineHtml::with_allowed(vec!["div".to_string(), "br".to_string()]);
1149 let content = "<div>Allowed</div><p>Not allowed</p><br/>";
1150 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1151 let result = rule.check(&ctx).unwrap();
1152 assert_eq!(result.len(), 1);
1154 assert_eq!(result[0].message, "Inline HTML found: <p>");
1155
1156 let content2 = "<DIV>Allowed</DIV><P>Not allowed</P><BR/>";
1158 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
1159 let result2 = rule.check(&ctx2).unwrap();
1160 assert_eq!(result2.len(), 1); assert_eq!(result2[0].message, "Inline HTML found: <P>");
1162 }
1163
1164 #[test]
1165 fn test_md033_html_comments() {
1166 let rule = MD033NoInlineHtml::default();
1167 let content = "<!-- This is a comment --> <p>Not a comment</p>";
1168 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1169 let result = rule.check(&ctx).unwrap();
1170 assert_eq!(result.len(), 1); assert_eq!(result[0].message, "Inline HTML found: <p>");
1173 }
1174
1175 #[test]
1176 fn test_md033_tags_in_links() {
1177 let rule = MD033NoInlineHtml::default();
1178 let content = "[Link](http://example.com/<div>)";
1179 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1180 let result = rule.check(&ctx).unwrap();
1181 assert_eq!(result.len(), 1);
1183 assert_eq!(result[0].message, "Inline HTML found: <div>");
1184
1185 let content2 = "[Link <a>text</a>](url)";
1186 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
1187 let result2 = rule.check(&ctx2).unwrap();
1188 assert_eq!(result2.len(), 1); assert_eq!(result2[0].message, "Inline HTML found: <a>");
1191 }
1192
1193 #[test]
1194 fn test_md033_fix_escaping() {
1195 let rule = MD033NoInlineHtml::default();
1196 let content = "Text with <div> and <br/> tags.";
1197 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1198 let fixed_content = rule.fix(&ctx).unwrap();
1199 assert_eq!(fixed_content, content);
1201 }
1202
1203 #[test]
1204 fn test_md033_in_code_blocks() {
1205 let rule = MD033NoInlineHtml::default();
1206 let content = "```html\n<div>Code</div>\n```\n<div>Not code</div>";
1207 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1208 let result = rule.check(&ctx).unwrap();
1209 assert_eq!(result.len(), 1); assert_eq!(result[0].message, "Inline HTML found: <div>");
1212 }
1213
1214 #[test]
1215 fn test_md033_in_code_spans() {
1216 let rule = MD033NoInlineHtml::default();
1217 let content = "Text with `<p>in code</p>` span. <br/> Not in span.";
1218 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1219 let result = rule.check(&ctx).unwrap();
1220 assert_eq!(result.len(), 1);
1222 assert_eq!(result[0].message, "Inline HTML found: <br/>");
1223 }
1224
1225 #[test]
1226 fn test_md033_issue_90_code_span_with_diff_block() {
1227 let rule = MD033NoInlineHtml::default();
1229 let content = r#"# Heading
1230
1231`<env>`
1232
1233```diff
1234- this
1235+ that
1236```"#;
1237 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1238 let result = rule.check(&ctx).unwrap();
1239 assert_eq!(result.len(), 0, "Should not report HTML tags inside code spans");
1241 }
1242
1243 #[test]
1244 fn test_md033_multiple_code_spans_with_angle_brackets() {
1245 let rule = MD033NoInlineHtml::default();
1247 let content = "`<one>` and `<two>` and `<three>` are all code spans";
1248 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1249 let result = rule.check(&ctx).unwrap();
1250 assert_eq!(result.len(), 0, "Should not report HTML tags inside any code spans");
1251 }
1252
1253 #[test]
1254 fn test_md033_nested_angle_brackets_in_code_span() {
1255 let rule = MD033NoInlineHtml::default();
1257 let content = "Text with `<<nested>>` brackets";
1258 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1259 let result = rule.check(&ctx).unwrap();
1260 assert_eq!(result.len(), 0, "Should handle nested angle brackets in code spans");
1261 }
1262
1263 #[test]
1264 fn test_md033_code_span_at_end_before_code_block() {
1265 let rule = MD033NoInlineHtml::default();
1267 let content = "Testing `<test>`\n```\ncode here\n```";
1268 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1269 let result = rule.check(&ctx).unwrap();
1270 assert_eq!(result.len(), 0, "Should handle code span before code block");
1271 }
1272
1273 #[test]
1274 fn test_md033_quick_fix_inline_tag() {
1275 let rule = MD033NoInlineHtml::default();
1278 let content = "This has <span>inline text</span> that should keep content.";
1279 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1280 let result = rule.check(&ctx).unwrap();
1281
1282 assert_eq!(result.len(), 1, "Should find one HTML tag");
1283 assert!(
1285 result[0].fix.is_none(),
1286 "Non-fixable tags like <span> should not have a fix"
1287 );
1288 }
1289
1290 #[test]
1291 fn test_md033_quick_fix_multiline_tag() {
1292 let rule = MD033NoInlineHtml::default();
1295 let content = "<div>\nBlock content\n</div>";
1296 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1297 let result = rule.check(&ctx).unwrap();
1298
1299 assert_eq!(result.len(), 1, "Should find one HTML tag");
1300 assert!(result[0].fix.is_none(), "HTML block elements should NOT have auto-fix");
1302 }
1303
1304 #[test]
1305 fn test_md033_quick_fix_self_closing_tag() {
1306 let rule = MD033NoInlineHtml::default();
1308 let content = "Self-closing: <br/>";
1309 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1310 let result = rule.check(&ctx).unwrap();
1311
1312 assert_eq!(result.len(), 1, "Should find one HTML tag");
1313 assert!(
1315 result[0].fix.is_none(),
1316 "Self-closing tags should not have a fix when fix config is false"
1317 );
1318 }
1319
1320 #[test]
1321 fn test_md033_quick_fix_multiple_tags() {
1322 let rule = MD033NoInlineHtml::default();
1325 let content = "<span>first</span> and <strong>second</strong>";
1326 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1327 let result = rule.check(&ctx).unwrap();
1328
1329 assert_eq!(result.len(), 2, "Should find two HTML tags");
1330 assert!(result[0].fix.is_none(), "Non-fixable <span> should not have a fix");
1332 assert!(
1333 result[1].fix.is_none(),
1334 "<strong> should not have a fix when fix config is false"
1335 );
1336 }
1337
1338 #[test]
1339 fn test_md033_skip_angle_brackets_in_link_titles() {
1340 let rule = MD033NoInlineHtml::default();
1342 let content = r#"# Test
1343
1344[example]: <https://example.com> "Title with <Angle Brackets> inside"
1345
1346Regular text with <div>content</div> HTML tag.
1347"#;
1348 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1349 let result = rule.check(&ctx).unwrap();
1350
1351 assert_eq!(result.len(), 1, "Should find opening div tag");
1354 assert!(
1355 result[0].message.contains("<div>"),
1356 "Should flag <div>, got: {}",
1357 result[0].message
1358 );
1359 }
1360
1361 #[test]
1362 fn test_md033_skip_angle_brackets_in_link_title_single_quotes() {
1363 let rule = MD033NoInlineHtml::default();
1365 let content = r#"[ref]: url 'Title <Help Wanted> here'
1366
1367<span>text</span> here
1368"#;
1369 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1370 let result = rule.check(&ctx).unwrap();
1371
1372 assert_eq!(result.len(), 1, "Should find opening span tag");
1375 assert!(
1376 result[0].message.contains("<span>"),
1377 "Should flag <span>, got: {}",
1378 result[0].message
1379 );
1380 }
1381
1382 #[test]
1383 fn test_md033_multiline_tag_end_line_calculation() {
1384 let rule = MD033NoInlineHtml::default();
1386 let content = "<div\n class=\"test\"\n id=\"example\">";
1387 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1388 let result = rule.check(&ctx).unwrap();
1389
1390 assert_eq!(result.len(), 1, "Should find one HTML tag");
1391 assert_eq!(result[0].line, 1, "Start line should be 1");
1393 assert_eq!(result[0].end_line, 3, "End line should be 3");
1395 }
1396
1397 #[test]
1398 fn test_md033_single_line_tag_same_start_end_line() {
1399 let rule = MD033NoInlineHtml::default();
1401 let content = "Some text <div class=\"test\"> more text";
1402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1403 let result = rule.check(&ctx).unwrap();
1404
1405 assert_eq!(result.len(), 1, "Should find one HTML tag");
1406 assert_eq!(result[0].line, 1, "Start line should be 1");
1407 assert_eq!(result[0].end_line, 1, "End line should be 1 for single-line tag");
1408 }
1409
1410 #[test]
1411 fn test_md033_multiline_tag_with_many_attributes() {
1412 let rule = MD033NoInlineHtml::default();
1414 let content =
1415 "Text\n<div\n data-attr1=\"value1\"\n data-attr2=\"value2\"\n data-attr3=\"value3\">\nMore text";
1416 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1417 let result = rule.check(&ctx).unwrap();
1418
1419 assert_eq!(result.len(), 1, "Should find one HTML tag");
1420 assert_eq!(result[0].line, 2, "Start line should be 2");
1422 assert_eq!(result[0].end_line, 5, "End line should be 5");
1424 }
1425
1426 #[test]
1427 fn test_md033_disallowed_mode_basic() {
1428 let rule = MD033NoInlineHtml::with_disallowed(vec!["script".to_string(), "iframe".to_string()]);
1430 let content = "<div>Safe content</div><script>alert('xss')</script>";
1431 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1432 let result = rule.check(&ctx).unwrap();
1433
1434 assert_eq!(result.len(), 1, "Should only flag disallowed tags");
1436 assert!(result[0].message.contains("<script>"), "Should flag script tag");
1437 }
1438
1439 #[test]
1440 fn test_md033_disallowed_gfm_security_tags() {
1441 let rule = MD033NoInlineHtml::with_disallowed(vec!["gfm".to_string()]);
1443 let content = r#"
1444<div>Safe</div>
1445<title>Bad title</title>
1446<textarea>Bad textarea</textarea>
1447<style>.bad{}</style>
1448<iframe src="evil"></iframe>
1449<script>evil()</script>
1450<plaintext>old tag</plaintext>
1451<span>Safe span</span>
1452"#;
1453 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1454 let result = rule.check(&ctx).unwrap();
1455
1456 assert_eq!(result.len(), 6, "Should flag 6 GFM security tags");
1459
1460 let flagged_tags: Vec<&str> = result
1461 .iter()
1462 .filter_map(|w| w.message.split("<").nth(1))
1463 .filter_map(|s| s.split(">").next())
1464 .filter_map(|s| s.split_whitespace().next())
1465 .collect();
1466
1467 assert!(flagged_tags.contains(&"title"), "Should flag title");
1468 assert!(flagged_tags.contains(&"textarea"), "Should flag textarea");
1469 assert!(flagged_tags.contains(&"style"), "Should flag style");
1470 assert!(flagged_tags.contains(&"iframe"), "Should flag iframe");
1471 assert!(flagged_tags.contains(&"script"), "Should flag script");
1472 assert!(flagged_tags.contains(&"plaintext"), "Should flag plaintext");
1473 assert!(!flagged_tags.contains(&"div"), "Should NOT flag div");
1474 assert!(!flagged_tags.contains(&"span"), "Should NOT flag span");
1475 }
1476
1477 #[test]
1478 fn test_md033_disallowed_case_insensitive() {
1479 let rule = MD033NoInlineHtml::with_disallowed(vec!["script".to_string()]);
1481 let content = "<SCRIPT>alert('xss')</SCRIPT><Script>alert('xss')</Script>";
1482 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1483 let result = rule.check(&ctx).unwrap();
1484
1485 assert_eq!(result.len(), 2, "Should flag both case variants");
1487 }
1488
1489 #[test]
1490 fn test_md033_disallowed_with_attributes() {
1491 let rule = MD033NoInlineHtml::with_disallowed(vec!["iframe".to_string()]);
1493 let content = r#"<iframe src="https://evil.com" width="100" height="100"></iframe>"#;
1494 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1495 let result = rule.check(&ctx).unwrap();
1496
1497 assert_eq!(result.len(), 1, "Should flag iframe with attributes");
1498 assert!(result[0].message.contains("iframe"), "Should flag iframe");
1499 }
1500
1501 #[test]
1502 fn test_md033_disallowed_all_gfm_tags() {
1503 use md033_config::GFM_DISALLOWED_TAGS;
1505 let rule = MD033NoInlineHtml::with_disallowed(vec!["gfm".to_string()]);
1506
1507 for tag in GFM_DISALLOWED_TAGS {
1508 let content = format!("<{tag}>content</{tag}>");
1509 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
1510 let result = rule.check(&ctx).unwrap();
1511
1512 assert_eq!(result.len(), 1, "GFM tag <{tag}> should be flagged");
1513 }
1514 }
1515
1516 #[test]
1517 fn test_md033_disallowed_mixed_with_custom() {
1518 let rule = MD033NoInlineHtml::with_disallowed(vec![
1520 "gfm".to_string(),
1521 "marquee".to_string(), ]);
1523 let content = r#"<script>bad</script><marquee>annoying</marquee><div>ok</div>"#;
1524 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1525 let result = rule.check(&ctx).unwrap();
1526
1527 assert_eq!(result.len(), 2, "Should flag both gfm and custom tags");
1529 }
1530
1531 #[test]
1532 fn test_md033_disallowed_empty_means_default_mode() {
1533 let rule = MD033NoInlineHtml::with_disallowed(vec![]);
1535 let content = "<div>content</div>";
1536 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1537 let result = rule.check(&ctx).unwrap();
1538
1539 assert_eq!(result.len(), 1, "Empty disallowed = default mode");
1541 }
1542
1543 #[test]
1544 fn test_md033_jsx_fragments_in_mdx() {
1545 let rule = MD033NoInlineHtml::default();
1547 let content = r#"# MDX Document
1548
1549<>
1550 <Heading />
1551 <Content />
1552</>
1553
1554<div>Regular HTML should still be flagged</div>
1555"#;
1556 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDX, None);
1557 let result = rule.check(&ctx).unwrap();
1558
1559 assert_eq!(result.len(), 1, "Should only find one HTML tag (the div)");
1561 assert!(
1562 result[0].message.contains("<div>"),
1563 "Should flag <div>, not JSX fragments"
1564 );
1565 }
1566
1567 #[test]
1568 fn test_md033_jsx_components_in_mdx() {
1569 let rule = MD033NoInlineHtml::default();
1571 let content = r#"<CustomComponent prop="value">
1572 Content
1573</CustomComponent>
1574
1575<MyButton onClick={handler}>Click</MyButton>
1576"#;
1577 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDX, None);
1578 let result = rule.check(&ctx).unwrap();
1579
1580 assert_eq!(result.len(), 0, "Should not flag JSX components in MDX");
1582 }
1583
1584 #[test]
1585 fn test_md033_jsx_not_skipped_in_standard_markdown() {
1586 let rule = MD033NoInlineHtml::default();
1588 let content = "<Script>alert(1)</Script>";
1589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1590 let result = rule.check(&ctx).unwrap();
1591
1592 assert_eq!(result.len(), 1, "Should flag <Script> in standard markdown");
1594 }
1595
1596 #[test]
1597 fn test_md033_jsx_attributes_in_mdx() {
1598 let rule = MD033NoInlineHtml::default();
1600 let content = r#"# MDX with JSX Attributes
1601
1602<div className="card big">Content</div>
1603
1604<button onClick={handleClick}>Click me</button>
1605
1606<label htmlFor="input-id">Label</label>
1607
1608<input onChange={handleChange} />
1609
1610<div class="html-class">Regular HTML should be flagged</div>
1611"#;
1612 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDX, None);
1613 let result = rule.check(&ctx).unwrap();
1614
1615 assert_eq!(
1617 result.len(),
1618 1,
1619 "Should only flag HTML element without JSX attributes, got: {result:?}"
1620 );
1621 assert!(
1622 result[0].message.contains("<div class="),
1623 "Should flag the div with HTML class attribute"
1624 );
1625 }
1626
1627 #[test]
1628 fn test_md033_jsx_attributes_not_skipped_in_standard() {
1629 let rule = MD033NoInlineHtml::default();
1631 let content = r#"<div className="card">Content</div>"#;
1632 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1633 let result = rule.check(&ctx).unwrap();
1634
1635 assert_eq!(result.len(), 1, "Should flag JSX-style elements in standard markdown");
1637 }
1638
1639 #[test]
1642 fn test_md033_fix_disabled_by_default() {
1643 let rule = MD033NoInlineHtml::default();
1645 assert!(!rule.config.fix, "Fix should be disabled by default");
1646 assert_eq!(rule.fix_capability(), crate::rule::FixCapability::Unfixable);
1647 }
1648
1649 #[test]
1650 fn test_md033_fix_enabled_em_to_italic() {
1651 let rule = MD033NoInlineHtml::with_fix(true);
1653 let content = "This has <em>emphasized text</em> here.";
1654 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1655 let fixed = rule.fix(&ctx).unwrap();
1656 assert_eq!(fixed, "This has *emphasized text* here.");
1657 }
1658
1659 #[test]
1660 fn test_md033_fix_enabled_i_to_italic() {
1661 let rule = MD033NoInlineHtml::with_fix(true);
1663 let content = "This has <i>italic text</i> here.";
1664 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1665 let fixed = rule.fix(&ctx).unwrap();
1666 assert_eq!(fixed, "This has *italic text* here.");
1667 }
1668
1669 #[test]
1670 fn test_md033_fix_enabled_strong_to_bold() {
1671 let rule = MD033NoInlineHtml::with_fix(true);
1673 let content = "This has <strong>bold text</strong> here.";
1674 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1675 let fixed = rule.fix(&ctx).unwrap();
1676 assert_eq!(fixed, "This has **bold text** here.");
1677 }
1678
1679 #[test]
1680 fn test_md033_fix_enabled_b_to_bold() {
1681 let rule = MD033NoInlineHtml::with_fix(true);
1683 let content = "This has <b>bold text</b> here.";
1684 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1685 let fixed = rule.fix(&ctx).unwrap();
1686 assert_eq!(fixed, "This has **bold text** here.");
1687 }
1688
1689 #[test]
1690 fn test_md033_fix_enabled_code_to_backticks() {
1691 let rule = MD033NoInlineHtml::with_fix(true);
1693 let content = "This has <code>inline code</code> here.";
1694 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1695 let fixed = rule.fix(&ctx).unwrap();
1696 assert_eq!(fixed, "This has `inline code` here.");
1697 }
1698
1699 #[test]
1700 fn test_md033_fix_enabled_code_with_backticks() {
1701 let rule = MD033NoInlineHtml::with_fix(true);
1703 let content = "This has <code>text with `backticks`</code> here.";
1704 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1705 let fixed = rule.fix(&ctx).unwrap();
1706 assert_eq!(fixed, "This has `` text with `backticks` `` here.");
1707 }
1708
1709 #[test]
1710 fn test_md033_fix_enabled_br_trailing_spaces() {
1711 let rule = MD033NoInlineHtml::with_fix(true);
1713 let content = "First line<br>Second line";
1714 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1715 let fixed = rule.fix(&ctx).unwrap();
1716 assert_eq!(fixed, "First line \nSecond line");
1717 }
1718
1719 #[test]
1720 fn test_md033_fix_enabled_br_self_closing() {
1721 let rule = MD033NoInlineHtml::with_fix(true);
1723 let content = "First<br/>second<br />third";
1724 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1725 let fixed = rule.fix(&ctx).unwrap();
1726 assert_eq!(fixed, "First \nsecond \nthird");
1727 }
1728
1729 #[test]
1730 fn test_md033_fix_enabled_br_backslash_style() {
1731 let config = MD033Config {
1733 allowed: Vec::new(),
1734 disallowed: Vec::new(),
1735 fix: true,
1736 br_style: md033_config::BrStyle::Backslash,
1737 };
1738 let rule = MD033NoInlineHtml::from_config_struct(config);
1739 let content = "First line<br>Second line";
1740 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1741 let fixed = rule.fix(&ctx).unwrap();
1742 assert_eq!(fixed, "First line\\\nSecond line");
1743 }
1744
1745 #[test]
1746 fn test_md033_fix_enabled_hr() {
1747 let rule = MD033NoInlineHtml::with_fix(true);
1749 let content = "Above<hr>Below";
1750 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1751 let fixed = rule.fix(&ctx).unwrap();
1752 assert_eq!(fixed, "Above\n---\nBelow");
1753 }
1754
1755 #[test]
1756 fn test_md033_fix_enabled_hr_self_closing() {
1757 let rule = MD033NoInlineHtml::with_fix(true);
1759 let content = "Above<hr/>Below";
1760 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1761 let fixed = rule.fix(&ctx).unwrap();
1762 assert_eq!(fixed, "Above\n---\nBelow");
1763 }
1764
1765 #[test]
1766 fn test_md033_fix_skips_nested_tags() {
1767 let rule = MD033NoInlineHtml::with_fix(true);
1770 let content = "This has <em>text with <strong>nested</strong> tags</em> here.";
1771 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1772 let fixed = rule.fix(&ctx).unwrap();
1773 assert_eq!(fixed, "This has <em>text with **nested** tags</em> here.");
1776 }
1777
1778 #[test]
1779 fn test_md033_fix_skips_tags_with_attributes() {
1780 let rule = MD033NoInlineHtml::with_fix(true);
1783 let content = "This has <em class=\"highlight\">emphasized</em> text.";
1784 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1785 let fixed = rule.fix(&ctx).unwrap();
1786 assert_eq!(fixed, content);
1788 }
1789
1790 #[test]
1791 fn test_md033_fix_disabled_no_changes() {
1792 let rule = MD033NoInlineHtml::default(); let content = "This has <em>emphasized text</em> here.";
1795 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1796 let fixed = rule.fix(&ctx).unwrap();
1797 assert_eq!(fixed, content, "Should return original content when fix is disabled");
1798 }
1799
1800 #[test]
1801 fn test_md033_fix_capability_enabled() {
1802 let rule = MD033NoInlineHtml::with_fix(true);
1803 assert_eq!(rule.fix_capability(), crate::rule::FixCapability::FullyFixable);
1804 }
1805
1806 #[test]
1807 fn test_md033_fix_multiple_tags() {
1808 let rule = MD033NoInlineHtml::with_fix(true);
1810 let content = "Here is <em>italic</em> and <strong>bold</strong> text.";
1811 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1812 let fixed = rule.fix(&ctx).unwrap();
1813 assert_eq!(fixed, "Here is *italic* and **bold** text.");
1814 }
1815
1816 #[test]
1817 fn test_md033_fix_uppercase_tags() {
1818 let rule = MD033NoInlineHtml::with_fix(true);
1820 let content = "This has <EM>emphasized</EM> text.";
1821 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1822 let fixed = rule.fix(&ctx).unwrap();
1823 assert_eq!(fixed, "This has *emphasized* text.");
1824 }
1825
1826 #[test]
1827 fn test_md033_fix_unsafe_tags_not_modified() {
1828 let rule = MD033NoInlineHtml::with_fix(true);
1831 let content = "This has <div>a div</div> content.";
1832 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1833 let fixed = rule.fix(&ctx).unwrap();
1834 assert_eq!(fixed, "This has <div>a div</div> content.");
1836 }
1837
1838 #[test]
1839 fn test_md033_fix_img_tag_converted() {
1840 let rule = MD033NoInlineHtml::with_fix(true);
1842 let content = "Image: <img src=\"photo.jpg\" alt=\"My Photo\">";
1843 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1844 let fixed = rule.fix(&ctx).unwrap();
1845 assert_eq!(fixed, "Image: ");
1847 }
1848
1849 #[test]
1850 fn test_md033_fix_img_tag_with_extra_attrs_not_converted() {
1851 let rule = MD033NoInlineHtml::with_fix(true);
1853 let content = "Image: <img src=\"photo.jpg\" alt=\"My Photo\" width=\"100\">";
1854 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1855 let fixed = rule.fix(&ctx).unwrap();
1856 assert_eq!(fixed, "Image: <img src=\"photo.jpg\" alt=\"My Photo\" width=\"100\">");
1858 }
1859
1860 #[test]
1861 fn test_md033_fix_mixed_safe_tags() {
1862 let rule = MD033NoInlineHtml::with_fix(true);
1864 let content = "<em>italic</em> and <img src=\"x.jpg\"> and <strong>bold</strong>";
1865 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1866 let fixed = rule.fix(&ctx).unwrap();
1867 assert_eq!(fixed, "*italic* and  and **bold**");
1869 }
1870
1871 #[test]
1872 fn test_md033_fix_multiple_tags_same_line() {
1873 let rule = MD033NoInlineHtml::with_fix(true);
1875 let content = "Regular text <i>italic</i> and <b>bold</b> here.";
1876 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1877 let fixed = rule.fix(&ctx).unwrap();
1878 assert_eq!(fixed, "Regular text *italic* and **bold** here.");
1879 }
1880
1881 #[test]
1882 fn test_md033_fix_multiple_em_tags_same_line() {
1883 let rule = MD033NoInlineHtml::with_fix(true);
1885 let content = "<em>first</em> and <strong>second</strong> and <code>third</code>";
1886 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1887 let fixed = rule.fix(&ctx).unwrap();
1888 assert_eq!(fixed, "*first* and **second** and `third`");
1889 }
1890
1891 #[test]
1892 fn test_md033_fix_skips_tags_inside_pre() {
1893 let rule = MD033NoInlineHtml::with_fix(true);
1895 let content = "<pre><code><em>VALUE</em></code></pre>";
1896 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1897 let fixed = rule.fix(&ctx).unwrap();
1898 assert!(
1901 !fixed.contains("*VALUE*"),
1902 "Tags inside <pre> should not be converted to markdown. Got: {fixed}"
1903 );
1904 }
1905
1906 #[test]
1907 fn test_md033_fix_skips_tags_inside_div() {
1908 let rule = MD033NoInlineHtml::with_fix(true);
1910 let content = "<div>\n<em>emphasized</em>\n</div>";
1911 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1912 let fixed = rule.fix(&ctx).unwrap();
1913 assert!(
1915 !fixed.contains("*emphasized*"),
1916 "Tags inside HTML blocks should not be converted. Got: {fixed}"
1917 );
1918 }
1919
1920 #[test]
1921 fn test_md033_fix_outside_html_block() {
1922 let rule = MD033NoInlineHtml::with_fix(true);
1924 let content = "<div>\ncontent\n</div>\n\nOutside <em>emphasized</em> text.";
1925 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1926 let fixed = rule.fix(&ctx).unwrap();
1927 assert!(
1929 fixed.contains("*emphasized*"),
1930 "Tags outside HTML blocks should be converted. Got: {fixed}"
1931 );
1932 }
1933
1934 #[test]
1935 fn test_md033_fix_with_id_attribute() {
1936 let rule = MD033NoInlineHtml::with_fix(true);
1938 let content = "See <em id=\"important\">this note</em> for details.";
1939 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1940 let fixed = rule.fix(&ctx).unwrap();
1941 assert_eq!(fixed, content);
1943 }
1944
1945 #[test]
1946 fn test_md033_fix_with_style_attribute() {
1947 let rule = MD033NoInlineHtml::with_fix(true);
1949 let content = "This is <strong style=\"color: red\">important</strong> text.";
1950 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1951 let fixed = rule.fix(&ctx).unwrap();
1952 assert_eq!(fixed, content);
1954 }
1955
1956 #[test]
1957 fn test_md033_fix_mixed_with_and_without_attributes() {
1958 let rule = MD033NoInlineHtml::with_fix(true);
1960 let content = "<em>normal</em> and <em class=\"special\">styled</em> text.";
1961 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1962 let fixed = rule.fix(&ctx).unwrap();
1963 assert_eq!(fixed, "*normal* and <em class=\"special\">styled</em> text.");
1965 }
1966
1967 #[test]
1968 fn test_md033_quick_fix_tag_with_attributes_no_fix() {
1969 let rule = MD033NoInlineHtml::with_fix(true);
1971 let content = "<em class=\"test\">emphasized</em>";
1972 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1973 let result = rule.check(&ctx).unwrap();
1974
1975 assert_eq!(result.len(), 1, "Should find one HTML tag");
1976 assert!(
1978 result[0].fix.is_none(),
1979 "Should NOT have a fix for tags with attributes"
1980 );
1981 }
1982
1983 #[test]
1984 fn test_md033_fix_skips_html_entities() {
1985 let rule = MD033NoInlineHtml::with_fix(true);
1988 let content = "<code>|</code>";
1989 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1990 let fixed = rule.fix(&ctx).unwrap();
1991 assert_eq!(fixed, content);
1993 }
1994
1995 #[test]
1996 fn test_md033_fix_skips_multiple_html_entities() {
1997 let rule = MD033NoInlineHtml::with_fix(true);
1999 let content = "<code><T></code>";
2000 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2001 let fixed = rule.fix(&ctx).unwrap();
2002 assert_eq!(fixed, content);
2004 }
2005
2006 #[test]
2007 fn test_md033_fix_allows_ampersand_without_entity() {
2008 let rule = MD033NoInlineHtml::with_fix(true);
2010 let content = "<code>a & b</code>";
2011 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2012 let fixed = rule.fix(&ctx).unwrap();
2013 assert_eq!(fixed, "`a & b`");
2015 }
2016
2017 #[test]
2018 fn test_md033_fix_em_with_entities_skipped() {
2019 let rule = MD033NoInlineHtml::with_fix(true);
2021 let content = "<em> text</em>";
2022 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2023 let fixed = rule.fix(&ctx).unwrap();
2024 assert_eq!(fixed, content);
2026 }
2027
2028 #[test]
2029 fn test_md033_fix_skips_nested_em_in_code() {
2030 let rule = MD033NoInlineHtml::with_fix(true);
2033 let content = "<code><em>n</em></code>";
2034 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2035 let fixed = rule.fix(&ctx).unwrap();
2036 assert!(
2039 !fixed.contains("*n*"),
2040 "Nested <em> should not be converted to markdown. Got: {fixed}"
2041 );
2042 }
2043
2044 #[test]
2045 fn test_md033_fix_skips_nested_in_table() {
2046 let rule = MD033NoInlineHtml::with_fix(true);
2048 let content = "| <code>><em>n</em></code> | description |";
2049 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2050 let fixed = rule.fix(&ctx).unwrap();
2051 assert!(
2053 !fixed.contains("*n*"),
2054 "Nested tags in table should not be converted. Got: {fixed}"
2055 );
2056 }
2057
2058 #[test]
2059 fn test_md033_fix_standalone_em_still_converted() {
2060 let rule = MD033NoInlineHtml::with_fix(true);
2062 let content = "This is <em>emphasized</em> text.";
2063 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2064 let fixed = rule.fix(&ctx).unwrap();
2065 assert_eq!(fixed, "This is *emphasized* text.");
2066 }
2067
2068 #[test]
2080 fn test_md033_templater_basic_interpolation_not_flagged() {
2081 let rule = MD033NoInlineHtml::default();
2084 let content = "Today is <% tp.date.now() %> which is nice.";
2085 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2086 let result = rule.check(&ctx).unwrap();
2087 assert!(
2088 result.is_empty(),
2089 "Templater basic interpolation should not be flagged as HTML. Got: {result:?}"
2090 );
2091 }
2092
2093 #[test]
2094 fn test_md033_templater_file_functions_not_flagged() {
2095 let rule = MD033NoInlineHtml::default();
2097 let content = "File: <% tp.file.title %>\nCreated: <% tp.file.creation_date() %>";
2098 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2099 let result = rule.check(&ctx).unwrap();
2100 assert!(
2101 result.is_empty(),
2102 "Templater file functions should not be flagged. Got: {result:?}"
2103 );
2104 }
2105
2106 #[test]
2107 fn test_md033_templater_with_arguments_not_flagged() {
2108 let rule = MD033NoInlineHtml::default();
2110 let content = r#"Date: <% tp.date.now("YYYY-MM-DD") %>"#;
2111 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2112 let result = rule.check(&ctx).unwrap();
2113 assert!(
2114 result.is_empty(),
2115 "Templater with arguments should not be flagged. Got: {result:?}"
2116 );
2117 }
2118
2119 #[test]
2120 fn test_md033_templater_javascript_execution_not_flagged() {
2121 let rule = MD033NoInlineHtml::default();
2123 let content = "<%* const today = tp.date.now(); tR += today; %>";
2124 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2125 let result = rule.check(&ctx).unwrap();
2126 assert!(
2127 result.is_empty(),
2128 "Templater JS execution block should not be flagged. Got: {result:?}"
2129 );
2130 }
2131
2132 #[test]
2133 fn test_md033_templater_dynamic_execution_not_flagged() {
2134 let rule = MD033NoInlineHtml::default();
2136 let content = "Dynamic: <%+ tp.date.now() %>";
2137 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2138 let result = rule.check(&ctx).unwrap();
2139 assert!(
2140 result.is_empty(),
2141 "Templater dynamic execution should not be flagged. Got: {result:?}"
2142 );
2143 }
2144
2145 #[test]
2146 fn test_md033_templater_whitespace_trim_all_not_flagged() {
2147 let rule = MD033NoInlineHtml::default();
2149 let content = "<%_ tp.date.now() _%>";
2150 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2151 let result = rule.check(&ctx).unwrap();
2152 assert!(
2153 result.is_empty(),
2154 "Templater trim-all whitespace should not be flagged. Got: {result:?}"
2155 );
2156 }
2157
2158 #[test]
2159 fn test_md033_templater_whitespace_trim_newline_not_flagged() {
2160 let rule = MD033NoInlineHtml::default();
2162 let content = "<%- tp.date.now() -%>";
2163 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2164 let result = rule.check(&ctx).unwrap();
2165 assert!(
2166 result.is_empty(),
2167 "Templater trim-newline should not be flagged. Got: {result:?}"
2168 );
2169 }
2170
2171 #[test]
2172 fn test_md033_templater_combined_modifiers_not_flagged() {
2173 let rule = MD033NoInlineHtml::default();
2175 let contents = [
2176 "<%-* const x = 1; -%>", "<%_+ tp.date.now() _%>", "<%- tp.file.title -%>", "<%_ tp.file.title _%>", ];
2181 for content in contents {
2182 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2183 let result = rule.check(&ctx).unwrap();
2184 assert!(
2185 result.is_empty(),
2186 "Templater combined modifiers should not be flagged: {content}. Got: {result:?}"
2187 );
2188 }
2189 }
2190
2191 #[test]
2192 fn test_md033_templater_multiline_block_not_flagged() {
2193 let rule = MD033NoInlineHtml::default();
2195 let content = r#"<%*
2196const x = 1;
2197const y = 2;
2198tR += x + y;
2199%>"#;
2200 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2201 let result = rule.check(&ctx).unwrap();
2202 assert!(
2203 result.is_empty(),
2204 "Templater multi-line block should not be flagged. Got: {result:?}"
2205 );
2206 }
2207
2208 #[test]
2209 fn test_md033_templater_with_angle_brackets_in_condition_not_flagged() {
2210 let rule = MD033NoInlineHtml::default();
2213 let content = "<%* if (x < 5) { tR += 'small'; } %>";
2214 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2215 let result = rule.check(&ctx).unwrap();
2216 assert!(
2217 result.is_empty(),
2218 "Templater with angle brackets in conditions should not be flagged. Got: {result:?}"
2219 );
2220 }
2221
2222 #[test]
2223 fn test_md033_templater_mixed_with_html_only_html_flagged() {
2224 let rule = MD033NoInlineHtml::default();
2226 let content = "<% tp.date.now() %> is today's date. <div>This is HTML</div>";
2227 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2228 let result = rule.check(&ctx).unwrap();
2229 assert_eq!(result.len(), 1, "Should only flag the HTML div tag");
2230 assert!(
2231 result[0].message.contains("<div>"),
2232 "Should flag <div>, got: {}",
2233 result[0].message
2234 );
2235 }
2236
2237 #[test]
2238 fn test_md033_templater_in_heading_not_flagged() {
2239 let rule = MD033NoInlineHtml::default();
2241 let content = "# <% tp.file.title %>";
2242 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2243 let result = rule.check(&ctx).unwrap();
2244 assert!(
2245 result.is_empty(),
2246 "Templater in heading should not be flagged. Got: {result:?}"
2247 );
2248 }
2249
2250 #[test]
2251 fn test_md033_templater_multiple_on_same_line_not_flagged() {
2252 let rule = MD033NoInlineHtml::default();
2254 let content = "From <% tp.date.now() %> to <% tp.date.tomorrow() %> we have meetings.";
2255 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2256 let result = rule.check(&ctx).unwrap();
2257 assert!(
2258 result.is_empty(),
2259 "Multiple Templater blocks should not be flagged. Got: {result:?}"
2260 );
2261 }
2262
2263 #[test]
2264 fn test_md033_templater_in_code_block_not_flagged() {
2265 let rule = MD033NoInlineHtml::default();
2267 let content = "```\n<% tp.date.now() %>\n```";
2268 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2269 let result = rule.check(&ctx).unwrap();
2270 assert!(
2271 result.is_empty(),
2272 "Templater in code block should not be flagged. Got: {result:?}"
2273 );
2274 }
2275
2276 #[test]
2277 fn test_md033_templater_in_inline_code_not_flagged() {
2278 let rule = MD033NoInlineHtml::default();
2280 let content = "Use `<% tp.date.now() %>` for current date.";
2281 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2282 let result = rule.check(&ctx).unwrap();
2283 assert!(
2284 result.is_empty(),
2285 "Templater in inline code should not be flagged. Got: {result:?}"
2286 );
2287 }
2288
2289 #[test]
2290 fn test_md033_templater_also_works_in_standard_flavor() {
2291 let rule = MD033NoInlineHtml::default();
2294 let content = "<% tp.date.now() %> works everywhere.";
2295 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2296 let result = rule.check(&ctx).unwrap();
2297 assert!(
2298 result.is_empty(),
2299 "Templater should not be flagged even in Standard flavor. Got: {result:?}"
2300 );
2301 }
2302
2303 #[test]
2304 fn test_md033_templater_empty_tag_not_flagged() {
2305 let rule = MD033NoInlineHtml::default();
2307 let content = "<%>";
2308 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2309 let result = rule.check(&ctx).unwrap();
2310 assert!(
2311 result.is_empty(),
2312 "Empty Templater-like tag should not be flagged. Got: {result:?}"
2313 );
2314 }
2315
2316 #[test]
2317 fn test_md033_templater_unclosed_not_flagged() {
2318 let rule = MD033NoInlineHtml::default();
2320 let content = "<% tp.date.now() without closing tag";
2321 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2322 let result = rule.check(&ctx).unwrap();
2323 assert!(
2324 result.is_empty(),
2325 "Unclosed Templater should not be flagged as HTML. Got: {result:?}"
2326 );
2327 }
2328
2329 #[test]
2330 fn test_md033_templater_with_newlines_inside_not_flagged() {
2331 let rule = MD033NoInlineHtml::default();
2333 let content = r#"<% tp.date.now("YYYY") +
2334"-" +
2335tp.date.now("MM") %>"#;
2336 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2337 let result = rule.check(&ctx).unwrap();
2338 assert!(
2339 result.is_empty(),
2340 "Templater with internal newlines should not be flagged. Got: {result:?}"
2341 );
2342 }
2343
2344 #[test]
2345 fn test_md033_erb_style_tags_not_flagged() {
2346 let rule = MD033NoInlineHtml::default();
2349 let content = "<%= variable %> and <% code %> and <%# comment %>";
2350 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2351 let result = rule.check(&ctx).unwrap();
2352 assert!(
2353 result.is_empty(),
2354 "ERB/EJS style tags should not be flagged as HTML. Got: {result:?}"
2355 );
2356 }
2357
2358 #[test]
2359 fn test_md033_templater_complex_expression_not_flagged() {
2360 let rule = MD033NoInlineHtml::default();
2362 let content = r#"<%*
2363const file = tp.file.title;
2364const date = tp.date.now("YYYY-MM-DD");
2365const folder = tp.file.folder();
2366tR += `# ${file}\n\nCreated: ${date}\nIn: ${folder}`;
2367%>"#;
2368 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
2369 let result = rule.check(&ctx).unwrap();
2370 assert!(
2371 result.is_empty(),
2372 "Complex Templater expression should not be flagged. Got: {result:?}"
2373 );
2374 }
2375
2376 #[test]
2377 fn test_md033_percent_sign_variations_not_flagged() {
2378 let rule = MD033NoInlineHtml::default();
2380 let patterns = [
2381 "<%=", "<%#", "<%%", "<%!", "<%@", "<%--", ];
2388 for pattern in patterns {
2389 let content = format!("{pattern} content %>");
2390 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
2391 let result = rule.check(&ctx).unwrap();
2392 assert!(
2393 result.is_empty(),
2394 "Pattern {pattern} should not be flagged. Got: {result:?}"
2395 );
2396 }
2397 }
2398
2399 #[test]
2405 fn test_md033_fix_a_wrapping_markdown_image_no_escaped_brackets() {
2406 let rule = MD033NoInlineHtml::with_fix(true);
2409 let content = r#"<a href="https://example.com"></a>"#;
2410 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2411 let fixed = rule.fix(&ctx).unwrap();
2412
2413 assert_eq!(fixed, "[](https://example.com)",);
2414 assert!(!fixed.contains(r"\["), "Must not escape brackets: {fixed}");
2415 assert!(!fixed.contains(r"\]"), "Must not escape brackets: {fixed}");
2416 }
2417
2418 #[test]
2419 fn test_md033_fix_a_wrapping_markdown_image_with_alt() {
2420 let rule = MD033NoInlineHtml::with_fix(true);
2422 let content =
2423 r#"<a href="https://github.com/repo"></a>"#;
2424 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2425 let fixed = rule.fix(&ctx).unwrap();
2426
2427 assert_eq!(
2428 fixed,
2429 "[](https://github.com/repo)"
2430 );
2431 }
2432
2433 #[test]
2434 fn test_md033_fix_img_without_alt_produces_empty_alt() {
2435 let rule = MD033NoInlineHtml::with_fix(true);
2436 let content = r#"<img src="photo.jpg" />"#;
2437 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2438 let fixed = rule.fix(&ctx).unwrap();
2439
2440 assert_eq!(fixed, "");
2441 }
2442
2443 #[test]
2444 fn test_md033_fix_a_with_plain_text_still_escapes_brackets() {
2445 let rule = MD033NoInlineHtml::with_fix(true);
2447 let content = r#"<a href="https://example.com">text with [brackets]</a>"#;
2448 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2449 let fixed = rule.fix(&ctx).unwrap();
2450
2451 assert!(
2452 fixed.contains(r"\[brackets\]"),
2453 "Plain text brackets should be escaped: {fixed}"
2454 );
2455 }
2456
2457 #[test]
2458 fn test_md033_fix_a_with_image_plus_extra_text_escapes_brackets() {
2459 let rule = MD033NoInlineHtml::with_fix(true);
2462 let content = r#"<a href="/link"> see [docs]</a>"#;
2463 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2464 let fixed = rule.fix(&ctx).unwrap();
2465
2466 assert!(
2468 fixed.contains(r"\[docs\]"),
2469 "Brackets in mixed image+text content should be escaped: {fixed}"
2470 );
2471 }
2472
2473 #[test]
2474 fn test_md033_fix_img_in_a_end_to_end() {
2475 use crate::config::Config;
2478 use crate::fix_coordinator::FixCoordinator;
2479
2480 let rule = MD033NoInlineHtml::with_fix(true);
2481 let rules: Vec<Box<dyn crate::rule::Rule>> = vec![Box::new(rule)];
2482
2483 let mut content =
2484 r#"<a href="https://github.com/org/repo"><img src="https://contrib.rocks/image?repo=org/repo" /></a>"#
2485 .to_string();
2486 let config = Config::default();
2487 let coordinator = FixCoordinator::new();
2488
2489 let result = coordinator
2490 .apply_fixes_iterative(&rules, &[], &mut content, &config, 10, None)
2491 .unwrap();
2492
2493 assert_eq!(
2494 content, "[](https://github.com/org/repo)",
2495 "End-to-end: <a><img></a> should become valid linked image"
2496 );
2497 assert!(result.converged);
2498 assert!(!content.contains(r"\["), "No escaped brackets: {content}");
2499 }
2500
2501 #[test]
2502 fn test_md033_fix_img_in_a_with_alt_end_to_end() {
2503 use crate::config::Config;
2504 use crate::fix_coordinator::FixCoordinator;
2505
2506 let rule = MD033NoInlineHtml::with_fix(true);
2507 let rules: Vec<Box<dyn crate::rule::Rule>> = vec![Box::new(rule)];
2508
2509 let mut content =
2510 r#"<a href="https://github.com/org/repo"><img src="https://contrib.rocks/image" alt="Contributors" /></a>"#
2511 .to_string();
2512 let config = Config::default();
2513 let coordinator = FixCoordinator::new();
2514
2515 let result = coordinator
2516 .apply_fixes_iterative(&rules, &[], &mut content, &config, 10, None)
2517 .unwrap();
2518
2519 assert_eq!(
2520 content,
2521 "[](https://github.com/org/repo)",
2522 );
2523 assert!(result.converged);
2524 }
2525}