1#[cfg(feature = "onig")]
34extern crate onig as regex;
35
36mod options;
37
38#[cfg(not(feature = "onig"))]
39use std::borrow::Cow;
40use std::{fmt::Write, str::from_utf8_unchecked, sync::LazyLock};
41
42pub use options::*;
43use regex::Regex;
44use trim_in_place::TrimInPlace;
45
46macro_rules! all_blocks_tag_names_except_p {
47 () => {
48 "table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|form|map|area|blockquote|address|math|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary"
49 }
50}
51
52macro_rules! all_blocks_tag_names {
53 () => {
54 concat!(all_blocks_tag_names_except_p!(), "|p")
55 };
56}
57
58macro_rules! all_preserved_tag_names {
59 () => {
60 "textarea|script|style|svg"
61 };
62}
63
64macro_rules! all_block_and_preserved_tag_names {
65 () => {
66 concat!(all_blocks_tag_names!(), "|", all_preserved_tag_names!())
67 };
68}
69
70macro_rules! pattern_all_blocks_except_p {
71 () => {
72 concat!("(?i:", all_blocks_tag_names_except_p!(), ")")
73 };
74}
75
76macro_rules! pattern_all_blocks {
77 () => {
78 concat!("(?i:", all_blocks_tag_names!(), ")")
79 };
80}
81
82macro_rules! pattern_all_block_and_preserved_tag_names {
83 () => {
84 concat!("(?i:", all_block_and_preserved_tag_names!(), ")")
85 };
86}
87
88macro_rules! pattern_attributes {
89 () => {
90 "(?:\\s+[^<>\\s=]+(?:=(?:|(?:[^'\"])|(?:[^'\"][^\\s<>]*[^'\"])|(?:\"[^\"]*\")|(?:'[^']*'\
91 )))?)*\\s*"
92 };
93}
94
95static RE_PRE_ELEMENT: LazyLock<Regex> = LazyLock::new(|| {
96 Regex::new(concat!("(?i)", "(<pre", pattern_attributes!(), r">)([\s\S]*?)(</pre\s*>)")).unwrap()
97});
98static RE_TEXTAREA_ELEMENT: LazyLock<Regex> = LazyLock::new(|| {
99 Regex::new(concat!(
100 "(?i)",
101 "(<textarea",
102 pattern_attributes!(),
103 r">)([\s\S]*?)(</textarea\s*>)"
104 ))
105 .unwrap()
106});
107static RE_SCRIPT_ELEMENT: LazyLock<Regex> = LazyLock::new(|| {
108 Regex::new(concat!("(?i)", "(<script", pattern_attributes!(), r">)([\s\S]*?)(</script\s*>)"))
109 .unwrap()
110});
111static RE_STYLE_ELEMENT: LazyLock<Regex> = LazyLock::new(|| {
112 Regex::new(concat!("(?i)", "(<style", pattern_attributes!(), r">)([\s\S]*?)(</style\s*>)"))
113 .unwrap()
114});
115static RE_SVG_ELEMENT: LazyLock<Regex> = LazyLock::new(|| {
116 Regex::new(concat!("(?i)", "(<svg", pattern_attributes!(), r">)([\s\S]*?)(</svg\s*>)")).unwrap()
117});
118static RE_BR_ELEMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)<br\s*/?>").unwrap());
119
120static RE_TAG: LazyLock<Regex> =
121 LazyLock::new(|| Regex::new(concat!(r"</?[^\s<]+(", pattern_attributes!(), r")/?>")).unwrap());
122
123static RE_OTHER_NEWLINE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?:\r\n|\r)").unwrap());
124#[allow(clippy::trivial_regex)]
125static RE_EMPTY_PARAGRAPH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<p></p>").unwrap());
126
127static RE_P_END_TAG_MISSING_START: LazyLock<Regex> = LazyLock::new(|| {
128 Regex::new(concat!(
129 "(?i)",
130 r"(<",
131 pattern_all_blocks_except_p!(),
132 pattern_attributes!(),
133 r">)(\s*)([^<]+)</p>"
134 ))
135 .unwrap()
136});
137static RE_P_START_TAG_MISSING_END: LazyLock<Regex> = LazyLock::new(|| {
138 Regex::new(concat!("(?i)", r"<p>([^<]+)(\s*)(</", pattern_all_blocks_except_p!(), r"\s*>)"))
139 .unwrap()
140});
141
142static RE_LI_IN_PARAGRAPH: LazyLock<Regex> = LazyLock::new(|| {
143 Regex::new(concat!("(?i)", r"<p>(<li", pattern_attributes!(), r">[\s\S]*)</p>")).unwrap()
144});
145
146static RE_BLOCK_AND_PRESERVED_TAG_AFTER_P_START_TAG: LazyLock<Regex> = LazyLock::new(|| {
147 Regex::new(concat!(
148 "(?i)",
149 r"<p>(</?",
150 pattern_all_block_and_preserved_tag_names!(),
151 pattern_attributes!(),
152 r">)"
153 ))
154 .unwrap()
155});
156static RE_BLOCK_AND_PRESERVED_TAG_BEFORE_P_END_TAG: LazyLock<Regex> = LazyLock::new(|| {
157 Regex::new(concat!(
158 "(?i)",
159 r"(</?",
160 pattern_all_block_and_preserved_tag_names!(),
161 pattern_attributes!(),
162 r">)</p>"
163 ))
164 .unwrap()
165});
166
167static RE_BR_ELEMENT_AFTER_BLOCK_TAG: LazyLock<Regex> = LazyLock::new(|| {
168 Regex::new(concat!("(?i)", r"(</?", pattern_all_blocks!(), pattern_attributes!(), r">)<br>\n"))
169 .unwrap()
170});
171static RE_BR_ELEMENT_BEFORE_BLOCK_TAG: LazyLock<Regex> = LazyLock::new(|| {
172 Regex::new(concat!("(?i)", r"<br>\n(</?", pattern_all_blocks!(), pattern_attributes!(), r">)"))
173 .unwrap()
174});
175
176pub fn auto_p<S: Into<String>>(pee: S, options: Options) -> String {
182 let mut pee = pee.into();
183
184 pee.trim_in_place();
185
186 if pee.is_empty() {
187 return pee;
188 }
189
190 let mut pre_inner_html_buffer: Vec<(String, usize, usize)> = Vec::new();
191 let mut script_inner_html_buffer: Vec<(String, usize, usize)> = Vec::new();
192 let mut style_inner_html_buffer: Vec<(String, usize, usize)> = Vec::new();
193 let mut textarea_inner_html_buffer: Vec<(String, usize, usize)> = Vec::new();
194 let mut svg_inner_html_buffer: Vec<(String, usize, usize)> = Vec::new();
195
196 {
198 fn reserve(pee: &mut String, regex: &Regex, buffer: &mut Vec<(String, usize, usize)>) {
199 for captures in regex.captures_iter(pee) {
200 let (s, start, end) = get(&captures, 2);
201
202 buffer.push((String::from(s), start, end));
203 }
204
205 let bytes = unsafe { pee.as_mut_vec() };
206
207 for (_, start, end) in buffer.iter() {
208 for e in bytes[*start..*end].iter_mut() {
209 *e = b'0';
210 }
211 }
212 }
213
214 reserve(&mut pee, &RE_PRE_ELEMENT, &mut pre_inner_html_buffer);
215 reserve(&mut pee, &RE_TEXTAREA_ELEMENT, &mut textarea_inner_html_buffer);
216 reserve(&mut pee, &RE_SCRIPT_ELEMENT, &mut script_inner_html_buffer);
217 reserve(&mut pee, &RE_STYLE_ELEMENT, &mut style_inner_html_buffer);
218 reserve(&mut pee, &RE_SVG_ELEMENT, &mut svg_inner_html_buffer);
219 }
220
221 let mut pee = replace_all(&RE_OTHER_NEWLINE, pee, "\n");
223
224 {
226 let mut newlines_in_tags: Vec<usize> = Vec::new();
227
228 for captures in RE_TAG.captures_iter(&pee) {
229 let (s, start, _) = get(&captures, 1);
230
231 for (i, e) in s.bytes().enumerate() {
232 if e == b'\n' {
233 newlines_in_tags.push(i + start);
234 }
235 }
236 }
237
238 let bytes = unsafe { pee.as_mut_vec() };
239
240 for newline_index in newlines_in_tags {
241 bytes[newline_index] = b'\r';
242 }
243 }
244
245 let pees = pee.split("\n\n");
247
248 let separator_count = pee.matches("\n\n").count();
250 let chunk_count = separator_count + 1;
251 let mut pee = String::with_capacity(pee.len() - separator_count * 2 + chunk_count * 8);
252
253 for tinkle in pees {
255 pee.write_fmt(format_args!("<p>{}</p>\n", tinkle.trim())).unwrap();
256 }
257
258 let mut pee = replace_all(&RE_EMPTY_PARAGRAPH, pee, "");
260
261 pee.trim_matches_in_place('\n');
262
263 let pee = replace_all(&RE_P_END_TAG_MISSING_START, pee, "$1$2<p>$3</p>");
265
266 let pee = replace_all(&RE_P_START_TAG_MISSING_END, pee, "<p>$1</p>$2$3");
268
269 let pee = replace_all(&RE_LI_IN_PARAGRAPH, pee, "$1");
271
272 let pee = replace_all(&RE_BLOCK_AND_PRESERVED_TAG_AFTER_P_START_TAG, pee, "$1");
274
275 let pee = replace_all(&RE_BLOCK_AND_PRESERVED_TAG_BEFORE_P_END_TAG, pee, "$1");
277
278 #[allow(clippy::let_and_return)]
280 let mut pee = if options.br {
281 let mut pee = replace_all(&RE_BR_ELEMENT, pee, "<br>");
283
284 let mut v = Vec::new();
286
287 {
288 let bytes = pee.as_bytes();
289
290 let mut p = bytes.len();
291
292 loop {
293 if p == 0 {
294 break;
295 }
296
297 p -= 1;
298
299 let e = bytes[p];
300
301 if e == b'\n' {
302 let mut pp = p;
303
304 loop {
305 if pp == 0 {
306 break;
307 }
308
309 pp -= 1;
310
311 let e = bytes[pp];
312
313 if !e.is_ascii_whitespace() {
314 break;
315 }
316 }
317
318 if pp < 3 || &bytes[(pp - 3)..=pp] != b"<br>" {
319 v.push((pp + 1)..p);
320 }
321
322 p = pp;
323 }
324 }
325 }
326
327 for range in v.into_iter() {
328 pee.replace_range(range, "<br>");
329 }
330
331 let pee = replace_all(&RE_BR_ELEMENT_AFTER_BLOCK_TAG, pee, "$1\n");
333
334 let pee = replace_all(&RE_BR_ELEMENT_BEFORE_BLOCK_TAG, pee, "\n$1");
336
337 pee
338 } else {
339 pee
340 };
341
342 {
344 fn recover(pee: &mut String, regex: &Regex, buffer: &[(String, usize, usize)]) {
345 let mut v = Vec::with_capacity(buffer.len());
346
347 for (captures, inner_html) in regex.captures_iter(pee).zip(buffer.iter()) {
348 let (_, start, end) = get(&captures, 2);
349
350 v.push((start..end, inner_html.0.as_str()));
351 }
352
353 for (range, inner_html) in v.into_iter().rev() {
354 pee.replace_range(range, inner_html);
355 }
356 }
357
358 recover(&mut pee, &RE_SVG_ELEMENT, &svg_inner_html_buffer);
359 recover(&mut pee, &RE_STYLE_ELEMENT, &style_inner_html_buffer);
360 recover(&mut pee, &RE_SCRIPT_ELEMENT, &script_inner_html_buffer);
361 recover(&mut pee, &RE_TEXTAREA_ELEMENT, &textarea_inner_html_buffer);
362
363 if options.esc_pre || options.remove_useless_newlines_in_pre {
364 let mut v = Vec::with_capacity(pre_inner_html_buffer.len());
365
366 for (captures, inner_html) in
367 RE_PRE_ELEMENT.captures_iter(pee.as_str()).zip(pre_inner_html_buffer.iter())
368 {
369 let (_, start, end) = get(&captures, 2);
370
371 v.push((start..end, inner_html.0.as_str()));
372 }
373
374 if options.esc_pre {
375 if options.remove_useless_newlines_in_pre {
376 for (range, inner_html) in v.into_iter().rev() {
377 pee.replace_range(
378 range,
379 html_escape::encode_safe(trim_newline_exactly_one(inner_html)).as_ref(),
380 );
381 }
382 } else {
383 for (range, inner_html) in v.into_iter().rev() {
384 pee.replace_range(range, html_escape::encode_safe(inner_html).as_ref());
385 }
386 }
387 } else if options.remove_useless_newlines_in_pre {
388 for (range, inner_html) in v.into_iter().rev() {
389 pee.replace_range(range, trim_newline_exactly_one(inner_html));
390 }
391 } else {
392 for (range, inner_html) in v.into_iter().rev() {
393 pee.replace_range(range, inner_html);
394 }
395 }
396 } else {
397 recover(&mut pee, &RE_PRE_ELEMENT, &pre_inner_html_buffer);
398 }
399 }
400
401 {
403 let bytes = unsafe { pee.as_mut_vec() };
404
405 for e in bytes {
406 if *e == b'\r' {
407 *e = b'\n';
408 }
409 }
410 }
411
412 pee
413}
414
415fn trim_newline_exactly_one<S: ?Sized + AsRef<str>>(s: &S) -> &str {
416 let s = s.as_ref();
417 let bytes = s.as_bytes();
418 let length = bytes.len();
419
420 if length == 0 {
421 return "";
422 }
423
424 let bytes = match bytes[0] {
426 b'\n' => {
427 if length == 1 {
428 return "";
429 } else if bytes[1] != b'\n' && bytes[1] != b'\r' {
430 &bytes[1..]
431 } else {
432 bytes
433 }
434 },
435 b'\r' => {
436 if length == 1 {
437 return "";
438 } else if bytes[1] == b'\n' {
439 if length == 2 {
440 return "";
441 } else if bytes[2] != b'\n' && bytes[2] != b'\r' {
442 &bytes[2..]
443 } else {
444 bytes
445 }
446 } else if bytes[1] != b'\r' {
447 &bytes[1..]
448 } else {
449 bytes
450 }
451 },
452 _ => bytes,
453 };
454
455 let length = bytes.len();
456
457 let bytes = match bytes[length - 1] {
459 b'\n' => {
460 if length == 1 {
461 return "";
462 } else if bytes[length - 2] != b'\n' && bytes[length - 2] != b'\r' {
463 &bytes[..(length - 1)]
464 } else {
465 bytes
466 }
467 },
468 b'\r' => {
469 if length == 1 {
470 return "";
471 } else if bytes[length - 2] == b'\n' {
472 if length == 2 {
473 return "";
474 } else if bytes[length - 3] != b'\n' && bytes[length - 3] != b'\r' {
475 &bytes[..(length - 2)]
476 } else {
477 bytes
478 }
479 } else if bytes[length - 2] != b'\r' {
480 &bytes[..(length - 1)]
481 } else {
482 bytes
483 }
484 },
485 _ => bytes,
486 };
487
488 unsafe { from_utf8_unchecked(bytes) }
489}
490
491#[cfg(feature = "onig")]
492#[inline]
493fn replace_all(regex: &Regex, pee: String, rep: &str) -> String {
494 regex.replace_all(pee.as_str(), |caps: ®ex::Captures| {
495 let mut s = String::with_capacity(rep.len());
496
497 let mut chars = rep.chars();
498
499 while let Some(c) = chars.next() {
500 if c == '$' {
501 let index = (chars.next().unwrap() as u8 - b'0') as usize;
502
503 s.push_str(caps.at(index).unwrap());
504 } else {
505 s.push(c);
506 }
507 }
508
509 s
510 })
511}
512
513#[cfg(not(feature = "onig"))]
514#[inline]
515fn replace_all(regex: &Regex, pee: String, rep: &str) -> String {
516 match regex.replace_all(pee.as_str(), rep) {
517 Cow::Owned(pee) => pee,
518 Cow::Borrowed(_) => pee,
519 }
520}
521
522#[cfg(feature = "onig")]
523#[inline]
524fn get<'a>(captures: ®ex::Captures<'a>, index: usize) -> (&'a str, usize, usize) {
525 let (start, end) = captures.pos(index).unwrap();
526
527 (captures.at(index).unwrap(), start, end)
528}
529
530#[cfg(not(feature = "onig"))]
531#[inline]
532fn get<'a>(captures: ®ex::Captures<'a>, index: usize) -> (&'a str, usize, usize) {
533 let captures = captures.get(index).unwrap();
534
535 (captures.as_str(), captures.start(), captures.end())
536}