rama_http/protocols/html/
core.rs1#![expect(
12 clippy::allow_attributes,
13 reason = "vendored from `vy-core`: macro-internal `#[allow(non_snake_case)]` attrs whose underlying lint fires only for some tuple-arity expansions"
14)]
15
16use std::{
17 borrow::Cow,
18 fmt::{self, Write as _},
19};
20
21pub trait IntoHtml {
64 fn into_html(self) -> impl IntoHtml;
67
68 #[inline]
70 fn escape_and_write(self, buf: &mut String)
71 where
72 Self: Sized,
73 {
74 self.into_html().escape_and_write(buf);
75 }
76
77 #[inline]
80 fn size_hint(&self) -> usize {
81 0
82 }
83
84 fn into_string(self) -> String
86 where
87 Self: Sized,
88 {
89 let html = self.into_html();
90 let size = html.size_hint();
91 let mut buf = String::with_capacity(size + (size / 10));
92 html.escape_and_write(&mut buf);
93 buf
94 }
95}
96
97#[inline]
104pub fn escape_into(output: &mut String, input: &str) {
105 let bytes = input.as_bytes();
106 let mut start = 0;
107 for (i, &b) in bytes.iter().enumerate() {
108 let replacement = match b {
109 b'&' => "&",
110 b'<' => "<",
111 b'>' => ">",
112 b'"' => """,
113 b'\'' => "'",
114 _ => continue,
115 };
116 output.push_str(&input[start..i]);
118 output.push_str(replacement);
119 start = i + 1;
120 }
121 output.push_str(&input[start..]);
122}
123
124pub(crate) fn escape_attr_value_into(output: &mut Vec<u8>, value: &[u8]) {
128 let mut start = 0;
129 for (i, &b) in value.iter().enumerate() {
130 let replacement: &[u8] = match b {
131 b'&' => b"&",
132 b'"' => b""",
133 _ => continue,
134 };
135 output.extend_from_slice(&value[start..i]);
136 output.extend_from_slice(replacement);
137 start = i + 1;
138 }
139 output.extend_from_slice(&value[start..]);
140}
141
142#[inline]
146pub fn escape(input: &str) -> Cow<'_, str> {
147 if !input
149 .bytes()
150 .any(|b| matches!(b, b'&' | b'<' | b'>' | b'"' | b'\''))
151 {
152 return Cow::Borrowed(input);
153 }
154 let mut output = String::with_capacity(input.len() + 8);
155 escape_into(&mut output, input);
156 Cow::Owned(output)
157}
158
159const MAX_ENTITY_LEN: usize = 32;
162
163#[must_use]
171pub fn decode_entities(input: &str) -> Cow<'_, str> {
172 let Some(first) = input.find('&') else {
173 return Cow::Borrowed(input);
174 };
175 let mut out = String::with_capacity(input.len());
176 out.push_str(&input[..first]);
177 let mut rest = &input[first..];
178 loop {
179 let after = &rest[1..];
181 if let Some(semi) = after.find(';').filter(|&i| i <= MAX_ENTITY_LEN)
182 && let Some(ch) = decode_entity(&after[..semi])
183 {
184 out.push(ch);
185 rest = &after[semi + 1..];
186 } else {
187 out.push('&');
188 rest = after;
189 }
190 let Some(next) = rest.find('&') else {
191 out.push_str(rest);
192 break;
193 };
194 out.push_str(&rest[..next]);
195 rest = &rest[next..];
196 }
197 Cow::Owned(out)
198}
199
200fn decode_entity(body: &str) -> Option<char> {
203 if let Some(num) = body.strip_prefix('#') {
204 let code = match num.strip_prefix(['x', 'X']) {
205 Some(hex) => u32::from_str_radix(hex, 16).ok()?,
206 None => num.parse::<u32>().ok()?,
207 };
208 return char::from_u32(code);
209 }
210 Some(match body {
211 "amp" => '&',
212 "lt" => '<',
213 "gt" => '>',
214 "quot" => '"',
215 "apos" => '\'',
216 "nbsp" => '\u{a0}',
217 "hellip" => '…',
218 "mdash" => '—',
219 "ndash" => '–',
220 "lsquo" => '\u{2018}',
221 "rsquo" => '\u{2019}',
222 "ldquo" => '\u{201C}',
223 "rdquo" => '\u{201D}',
224 "laquo" => '«',
225 "raquo" => '»',
226 "copy" => '©',
227 "reg" => '®',
228 "trade" => '™',
229 "deg" => '°',
230 "middot" | "bull" => '•',
231 "euro" => '€',
232 "pound" => '£',
233 "cent" => '¢',
234 "sect" => '§',
235 "times" => '×',
236 "divide" => '÷',
237 _ => return None,
238 })
239}
240
241#[inline]
247pub fn marker<S: AsRef<str>>(name: S) -> Marker<S> {
248 Marker(name)
249}
250
251#[derive(Debug, Clone, Copy)]
254pub struct Marker<S>(pub S);
255
256impl<S: AsRef<str>> IntoHtml for Marker<S> {
257 #[inline]
258 fn into_html(self) -> impl IntoHtml {
259 self
260 }
261 fn escape_and_write(self, buf: &mut String) {
262 buf.push_str(r#"<?marker name=""#);
263 escape_into(buf, self.0.as_ref());
264 buf.push_str(r#"">"#);
265 }
266 fn size_hint(&self) -> usize {
267 17 + self.0.as_ref().len()
269 }
270}
271
272#[inline]
279pub fn start<S: AsRef<str>>(name: S) -> Start<S> {
280 Start(name)
281}
282
283#[derive(Debug, Clone, Copy)]
285pub struct Start<S>(pub S);
286
287impl<S: AsRef<str>> IntoHtml for Start<S> {
288 #[inline]
289 fn into_html(self) -> impl IntoHtml {
290 self
291 }
292 fn escape_and_write(self, buf: &mut String) {
293 buf.push_str(r#"<?start name=""#);
294 escape_into(buf, self.0.as_ref());
295 buf.push_str(r#"">"#);
296 }
297 fn size_hint(&self) -> usize {
298 16 + self.0.as_ref().len()
300 }
301}
302
303#[inline]
307pub fn end() -> End {
308 End
309}
310
311#[derive(Debug, Clone, Copy)]
313pub struct End;
314
315impl IntoHtml for End {
316 #[inline]
317 fn into_html(self) -> impl IntoHtml {
318 self
319 }
320 fn escape_and_write(self, buf: &mut String) {
321 buf.push_str(r#"<?end>"#);
322 }
323 fn size_hint(&self) -> usize {
324 6 }
326}
327
328#[derive(Debug, Clone, Copy)]
335pub struct PreEscaped<T>(pub T);
336
337impl<T: fmt::Display> fmt::Display for PreEscaped<T> {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 self.0.fmt(f)
340 }
341}
342
343impl IntoHtml for PreEscaped<&str> {
344 #[inline]
345 fn into_html(self) -> impl IntoHtml {
346 self
347 }
348 #[inline]
349 fn escape_and_write(self, buf: &mut String) {
350 buf.push_str(self.0);
351 }
352 #[inline]
353 fn size_hint(&self) -> usize {
354 self.0.len()
355 }
356}
357
358impl IntoHtml for PreEscaped<String> {
359 #[inline]
360 fn into_html(self) -> impl IntoHtml {
361 self
362 }
363 #[inline]
364 fn escape_and_write(self, buf: &mut String) {
365 buf.push_str(&self.0);
366 }
367 #[inline]
368 fn size_hint(&self) -> usize {
369 self.0.len()
370 }
371}
372
373impl IntoHtml for PreEscaped<char> {
374 #[inline]
375 fn into_html(self) -> impl IntoHtml {
376 self
377 }
378 #[inline]
379 fn escape_and_write(self, buf: &mut String) {
380 buf.push(self.0);
381 }
382 #[inline]
383 fn size_hint(&self) -> usize {
384 self.0.len_utf8()
385 }
386}
387
388impl IntoHtml for PreEscaped<Cow<'static, str>> {
389 #[inline]
390 fn into_html(self) -> impl IntoHtml {
391 self
392 }
393 #[inline]
394 fn escape_and_write(self, buf: &mut String) {
395 buf.push_str(&self.0);
396 }
397 #[inline]
398 fn size_hint(&self) -> usize {
399 self.0.len()
400 }
401}
402
403impl IntoHtml for PreEscaped<Box<str>> {
404 #[inline]
405 fn into_html(self) -> impl IntoHtml {
406 self
407 }
408 #[inline]
409 fn escape_and_write(self, buf: &mut String) {
410 buf.push_str(&self.0);
411 }
412 #[inline]
413 fn size_hint(&self) -> usize {
414 self.0.len()
415 }
416}
417
418impl IntoHtml for &str {
421 #[inline]
422 fn into_html(self) -> impl IntoHtml {
423 self
424 }
425 #[inline]
426 fn escape_and_write(self, buf: &mut String) {
427 escape_into(buf, self)
428 }
429 #[inline]
430 fn size_hint(&self) -> usize {
431 self.len()
432 }
433}
434
435impl IntoHtml for char {
436 #[inline]
437 fn into_html(self) -> impl IntoHtml {
438 self
439 }
440 #[inline]
441 fn escape_and_write(self, buf: &mut String) {
442 escape_into(buf, self.encode_utf8(&mut [0; 4]));
443 }
444 #[inline]
445 fn size_hint(&self) -> usize {
446 self.len_utf8()
447 }
448}
449
450impl IntoHtml for String {
451 #[inline]
452 fn into_html(self) -> impl IntoHtml {
453 self
454 }
455 #[inline]
456 fn escape_and_write(self, buf: &mut String) {
457 escape_into(buf, &self)
458 }
459 #[inline]
460 fn size_hint(&self) -> usize {
461 self.len()
462 }
463}
464
465impl IntoHtml for &String {
466 #[inline]
467 fn into_html(self) -> impl IntoHtml {
468 self
469 }
470 #[inline]
471 fn escape_and_write(self, buf: &mut String) {
472 escape_into(buf, self)
473 }
474 #[inline]
475 fn size_hint(&self) -> usize {
476 self.len()
477 }
478}
479
480impl IntoHtml for Box<str> {
481 #[inline]
482 fn into_html(self) -> impl IntoHtml {
483 self
484 }
485 #[inline]
486 fn escape_and_write(self, buf: &mut String) {
487 escape_into(buf, &self)
488 }
489 #[inline]
490 fn size_hint(&self) -> usize {
491 self.len()
492 }
493}
494
495impl IntoHtml for Cow<'static, str> {
496 #[inline]
497 fn into_html(self) -> impl IntoHtml {
498 self
499 }
500 #[inline]
501 fn escape_and_write(self, buf: &mut String) {
502 escape_into(buf, self.as_ref())
503 }
504 #[inline]
505 fn size_hint(&self) -> usize {
506 self.as_ref().len()
507 }
508}
509
510impl IntoHtml for bool {
511 #[inline]
512 fn into_html(self) -> impl IntoHtml {
513 if self { "true" } else { "false" }
514 }
515 #[inline]
516 fn size_hint(&self) -> usize {
517 5
518 }
519}
520
521impl<T: IntoHtml> IntoHtml for Option<T> {
522 #[inline]
523 fn into_html(self) -> impl IntoHtml {
524 self
525 }
526 #[inline]
527 fn escape_and_write(self, buf: &mut String) {
528 if let Some(x) = self {
529 x.escape_and_write(buf)
530 }
531 }
532 #[inline]
533 fn size_hint(&self) -> usize {
534 match self {
535 Some(x) => x.size_hint(),
536 None => 0,
537 }
538 }
539}
540
541impl IntoHtml for () {
542 #[inline]
543 fn into_html(self) -> impl IntoHtml {
544 self
545 }
546 #[inline]
547 fn escape_and_write(self, _: &mut String) {}
548}
549
550impl<F: FnOnce(&mut String)> IntoHtml for F {
551 #[inline]
552 fn into_html(self) -> impl IntoHtml {
553 self
554 }
555 #[inline]
556 fn escape_and_write(self, buf: &mut String) {
557 (self)(buf)
558 }
559}
560
561impl<B: IntoHtml, I: ExactSizeIterator, F> IntoHtml for std::iter::Map<I, F>
562where
563 F: FnMut(I::Item) -> B,
564{
565 #[inline]
566 fn into_html(self) -> impl IntoHtml {
567 self
568 }
569 #[inline]
570 fn escape_and_write(self, buf: &mut String) {
571 let len = self.len();
572 for (i, x) in self.enumerate() {
573 if i == 0 {
574 buf.reserve(len * x.size_hint());
575 }
576 x.escape_and_write(buf);
577 }
578 }
579}
580
581impl<T: IntoHtml> IntoHtml for Vec<T> {
582 #[inline]
583 fn into_html(self) -> impl IntoHtml {
584 self
585 }
586 #[inline]
587 fn escape_and_write(self, buf: &mut String) {
588 for x in self {
589 x.escape_and_write(buf);
590 }
591 }
592 #[inline]
593 fn size_hint(&self) -> usize {
594 self.iter().map(IntoHtml::size_hint).sum()
595 }
596}
597
598impl<T: IntoHtml, const N: usize> IntoHtml for [T; N] {
599 #[inline]
600 fn into_html(self) -> impl IntoHtml {
601 self
602 }
603 #[inline]
604 fn escape_and_write(self, buf: &mut String) {
605 for x in self {
606 x.escape_and_write(buf);
607 }
608 }
609 #[inline]
610 fn size_hint(&self) -> usize {
611 self.iter().map(IntoHtml::size_hint).sum()
612 }
613}
614
615macro_rules! impl_tuple {
618 ( ( $($i:ident,)+ ) ) => {
619 impl<$($i,)+> IntoHtml for ($($i,)+)
620 where
621 $($i: IntoHtml,)+
622 {
623 #[inline]
624 fn into_html(self) -> impl IntoHtml {
625 #[allow(non_snake_case)]
626 let ($($i,)+) = self;
627 ($($i.into_html(),)+)
628 }
629
630 #[inline]
631 fn escape_and_write(self, buf: &mut String) {
632 #[allow(non_snake_case)]
633 let ($($i,)+) = self;
634 $( $i.escape_and_write(buf); )+
635 }
636
637 #[inline]
638 fn size_hint(&self) -> usize {
639 #[allow(non_snake_case)]
640 let ($($i,)+) = self;
641 let mut n = 0;
642 $( n += $i.size_hint(); )+
643 n
644 }
645 }
646 };
647 ($f:ident) => {
648 impl_tuple!(($f,));
649 };
650 ($f:ident $($i:ident)+) => {
651 impl_tuple!(($f, $($i,)+));
652 impl_tuple!($($i)+);
653 };
654}
655
656impl_tuple!(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A_ B_ C_ D_ E_ F_ G_ H_ I_ J_ K_);
657
658macro_rules! via_display {
663 ($($ty:ty)*) => {
664 $(
665 impl IntoHtml for $ty {
666 #[inline]
667 fn into_html(self) -> impl IntoHtml { self }
668 #[inline]
669 fn escape_and_write(self, buf: &mut String) {
670 _ = write!(buf, "{self}");
671 }
672 }
673 )*
674 };
675}
676
677via_display! { isize i8 i16 i32 i64 i128 usize u8 u16 u32 u64 u128 f32 f64 }