1use core::fmt;
2
3#[cfg(feature = "http")]
4use http::{HeaderMap, StatusCode};
5use topcoat_core::context::Cx;
6
7use crate::{
8 AttributeCollector, CollectedPart, HtmlContext, HtmlWriter, RegionId, ViewHandle,
9 buffer::ViewBuffer,
10};
11
12pub trait DynViewPart: 'static + fmt::Debug + Send + Sync {
20 #[track_caller]
22 fn render(&self, cx: &Cx, w: &mut HtmlWriter<'_, '_>);
23
24 #[inline]
29 fn size_hint(&self) -> usize {
30 0
31 }
32}
33
34macro_rules! impl_push_primitive {
35 ($method:ident, $ty:ty, $size_hint:expr) => {
36 #[doc = concat!("Appends a `", stringify!($ty), "` rendered as text.")]
37 #[inline]
41 pub fn $method(&mut self, value: $ty) -> &mut Self {
42 self.size_hint += $size_hint;
43 self.sink.$method(value);
44 self
45 }
46 };
47}
48
49pub struct PartsWriter<'a> {
72 sink: Sink<'a>,
73 context: HtmlContext,
74 size_hint: usize,
75}
76
77impl<'a> PartsWriter<'a> {
78 #[inline]
80 pub(super) fn new(buffer: &'a mut ViewBuffer, context: HtmlContext) -> Self {
81 Self {
82 sink: Sink::Buffer(buffer),
83 context,
84 size_hint: 0,
85 }
86 }
87
88 #[inline]
91 pub(crate) fn collecting(
92 collector: &'a mut AttributeCollector,
93 cx: &'a Cx,
94 context: HtmlContext,
95 ) -> Self {
96 Self {
97 sink: Sink::Collector { collector, cx },
98 context,
99 size_hint: 0,
100 }
101 }
102
103 #[inline]
105 pub(super) fn size_hint(&self) -> usize {
106 self.size_hint
107 }
108
109 #[inline]
119 pub(crate) fn in_context<R>(
120 &mut self,
121 context: HtmlContext,
122 f: impl FnOnce(&mut Self) -> R,
123 ) -> R {
124 let previous = std::mem::replace(&mut self.context, context);
125 let result = f(self);
126 self.context = previous;
127 result
128 }
129
130 fn str_size_hint(value: &str, context: HtmlContext) -> usize {
132 match context {
133 HtmlContext::Unescaped => value.len(),
134 _ => value.len() + value.len() / 8,
136 }
137 }
138
139 #[inline]
141 pub fn push_str(&mut self, value: &str) -> &mut Self {
142 self.size_hint += Self::str_size_hint(value, self.context);
143 self.sink.push_str(value, self.context);
144 self
145 }
146
147 #[inline]
149 pub fn push_static_str(&mut self, value: &'static str) -> &mut Self {
150 self.size_hint += Self::str_size_hint(value, self.context);
151 self.sink.push_static_str(value, self.context);
152 self
153 }
154
155 #[inline]
163 pub fn push_promoted_str(&mut self, value: &'static &'static str) -> &mut Self {
164 self.size_hint += Self::str_size_hint(value, self.context);
165 self.sink.push_promoted_str(value, self.context);
166 self
167 }
168
169 #[inline]
171 pub fn push_string(&mut self, value: String) -> &mut Self {
172 self.size_hint += Self::str_size_hint(&value, self.context);
173 self.sink.push_string(value, self.context);
174 self
175 }
176
177 #[inline]
183 pub fn push_str_unescaped(&mut self, value: &str) -> &mut Self {
184 self.size_hint += value.len();
185 self.sink.push_str(value, HtmlContext::Unescaped);
186 self
187 }
188
189 #[inline]
195 pub fn push_static_str_unescaped(&mut self, value: &'static str) -> &mut Self {
196 self.size_hint += value.len();
197 self.sink.push_static_str(value, HtmlContext::Unescaped);
198 self
199 }
200
201 #[inline]
213 pub fn push_promoted_str_unescaped(&mut self, value: &'static &'static str) -> &mut Self {
214 self.size_hint += value.len();
215 self.sink.push_promoted_str(value, HtmlContext::Unescaped);
216 self
217 }
218
219 #[inline]
225 pub fn push_string_unescaped(&mut self, value: String) -> &mut Self {
226 self.size_hint += value.len();
227 self.sink.push_string(value, HtmlContext::Unescaped);
228 self
229 }
230
231 #[inline]
244 pub fn push_comment(&mut self, build: impl FnOnce(&mut PartsWriter<'_>)) -> &mut Self {
245 assert!(
246 self.context == HtmlContext::Text,
247 "tried to push comment in html context {:?}",
248 self.context,
249 );
250 self.push_promoted_str_unescaped(&"<!--");
251 self.in_context(HtmlContext::Comment, build);
252 self.push_promoted_str_unescaped(&"-->");
253 self
254 }
255
256 #[inline]
258 pub fn push_char(&mut self, value: char) -> &mut Self {
259 self.size_hint += 3;
261 self.sink.push_char(value, self.context);
262 self
263 }
264
265 impl_push_primitive!(push_bool, bool, 5);
272 impl_push_primitive!(push_i8, i8, 3);
273 impl_push_primitive!(push_i16, i16, 4);
274 impl_push_primitive!(push_i32, i32, 6);
275 impl_push_primitive!(push_i64, i64, 11);
276 impl_push_primitive!(push_i128, i128, 21);
277 impl_push_primitive!(push_isize, isize, 11);
278 impl_push_primitive!(push_u8, u8, 2);
279 impl_push_primitive!(push_u16, u16, 3);
280 impl_push_primitive!(push_u32, u32, 6);
281 impl_push_primitive!(push_u64, u64, 11);
282 impl_push_primitive!(push_u128, u128, 20);
283 impl_push_primitive!(push_usize, usize, 11);
284 impl_push_primitive!(push_f32, f32, 9);
285 impl_push_primitive!(push_f64, f64, 13);
286
287 #[inline]
293 pub(crate) fn push_region_start(&mut self, region: RegionId) -> &mut Self {
294 assert!(
295 self.context == HtmlContext::Text,
296 "tried to push region start in html context {:?}",
297 self.context,
298 );
299 self.size_hint += 42;
301 self.sink.push_region_start(region);
302 self
303 }
304
305 #[inline]
311 pub(crate) fn push_region_end(&mut self, region: RegionId) -> &mut Self {
312 assert!(
313 self.context == HtmlContext::Text,
314 "tried to push region end in html context {:?}",
315 self.context,
316 );
317 self.size_hint += 40;
319 self.sink.push_region_end(region);
320 self
321 }
322
323 #[inline]
326 pub fn push_dyn(&mut self, part: Box<dyn DynViewPart>) -> &mut Self {
327 self.size_hint += part.size_hint();
328 self.sink.push_dyn(part, self.context);
329 self
330 }
331
332 #[inline]
342 pub fn push_view_handle(&mut self, handle: ViewHandle) -> &mut Self {
343 self.size_hint += handle.size_hint();
344 self.sink.push_view(handle);
345 self
346 }
347
348 #[cfg(feature = "http")]
350 #[inline]
351 pub fn push_status_code(&mut self, status_code: StatusCode) -> &mut Self {
352 self.sink.push_status_code(status_code);
353 self
354 }
355
356 #[cfg(feature = "http")]
358 #[inline]
359 pub fn push_headers(&mut self, headers: HeaderMap) -> &mut Self {
360 self.sink.push_headers(headers);
361 self
362 }
363}
364
365macro_rules! impl_sink_primitive {
366 ($method:ident, $ty:ty, $part:expr) => {
367 #[inline]
368 fn $method(&mut self, value: $ty) {
369 match self {
370 Self::Buffer(buffer) => buffer.$method(value),
371 Self::Collector { collector, cx } => collector.push(cx, $part(value)),
372 }
373 }
374 };
375}
376
377enum Sink<'a> {
379 Buffer(&'a mut ViewBuffer),
381 Collector {
384 collector: &'a mut AttributeCollector,
385 cx: &'a Cx,
386 },
387}
388
389impl Sink<'_> {
390 #[inline]
391 fn push_str(&mut self, value: &str, context: HtmlContext) {
392 match self {
393 Self::Buffer(buffer) => buffer.push_str(value, context),
394 Self::Collector { collector, cx } => collector.push_str(cx, value, context),
395 }
396 }
397
398 #[inline]
399 fn push_static_str(&mut self, value: &'static str, context: HtmlContext) {
400 match self {
401 Self::Buffer(buffer) => buffer.push_static_str(value, context),
402 Self::Collector { collector, cx } => {
403 collector.push(cx, CollectedPart::StaticStr { value, context });
404 }
405 }
406 }
407
408 #[inline]
409 fn push_promoted_str(&mut self, value: &'static &'static str, context: HtmlContext) {
410 match self {
411 Self::Buffer(buffer) => buffer.push_promoted_str(value, context),
412 Self::Collector { collector, cx } => {
413 collector.push(cx, CollectedPart::PromotedStr { value, context });
414 }
415 }
416 }
417
418 #[inline]
419 fn push_string(&mut self, value: String, context: HtmlContext) {
420 match self {
421 Self::Buffer(buffer) => buffer.push_string(value, context),
422 Self::Collector { collector, cx } => {
423 collector.push(cx, CollectedPart::String { value, context });
424 }
425 }
426 }
427
428 #[inline]
429 fn push_char(&mut self, value: char, context: HtmlContext) {
430 match self {
431 Self::Buffer(buffer) => buffer.push_char(value, context),
432 Self::Collector { collector, cx } => {
433 collector.push(cx, CollectedPart::Char { value, context });
434 }
435 }
436 }
437
438 impl_sink_primitive!(push_bool, bool, CollectedPart::Bool);
439 impl_sink_primitive!(push_i8, i8, |value| CollectedPart::Int(i128::from(value)));
440 impl_sink_primitive!(push_i16, i16, |value| CollectedPart::Int(i128::from(value)));
441 impl_sink_primitive!(push_i32, i32, |value| CollectedPart::Int(i128::from(value)));
442 impl_sink_primitive!(push_i64, i64, |value| CollectedPart::Int(i128::from(value)));
443 impl_sink_primitive!(push_i128, i128, CollectedPart::Int);
444 impl_sink_primitive!(push_isize, isize, |value| CollectedPart::Int(value as i128));
445 impl_sink_primitive!(push_u8, u8, |value| CollectedPart::Uint(u128::from(value)));
446 impl_sink_primitive!(push_u16, u16, |value| CollectedPart::Uint(u128::from(
447 value
448 )));
449 impl_sink_primitive!(push_u32, u32, |value| CollectedPart::Uint(u128::from(
450 value
451 )));
452 impl_sink_primitive!(push_u64, u64, |value| CollectedPart::Uint(u128::from(
453 value
454 )));
455 impl_sink_primitive!(push_u128, u128, CollectedPart::Uint);
456 impl_sink_primitive!(push_usize, usize, |value| CollectedPart::Uint(
457 value as u128
458 ));
459 impl_sink_primitive!(push_f32, f32, CollectedPart::F32);
460 impl_sink_primitive!(push_f64, f64, CollectedPart::F64);
461
462 #[inline]
463 fn push_region_start(&mut self, region: RegionId) {
464 match self {
465 Self::Buffer(buffer) => buffer.push_region_start(region),
466 Self::Collector { .. } => panic!("tried to push a region into an attribute"),
467 }
468 }
469
470 #[inline]
471 fn push_region_end(&mut self, region: RegionId) {
472 match self {
473 Self::Buffer(buffer) => buffer.push_region_end(region),
474 Self::Collector { .. } => panic!("tried to push a region into an attribute"),
475 }
476 }
477
478 #[inline]
479 fn push_dyn(&mut self, part: Box<dyn DynViewPart>, context: HtmlContext) {
480 match self {
481 Self::Buffer(buffer) => buffer.push_dyn(part, context),
482 Self::Collector { collector, cx } => {
483 collector.push(cx, CollectedPart::Dyn { part, context });
484 }
485 }
486 }
487
488 #[inline]
489 fn push_view(&mut self, handle: ViewHandle) {
490 match self {
491 Self::Buffer(buffer) => buffer.push_view(handle),
492 Self::Collector { collector, cx } => collector.push(cx, CollectedPart::View(handle)),
493 }
494 }
495
496 #[cfg(feature = "http")]
498 #[inline]
499 fn push_status_code(&mut self, status_code: StatusCode) {
500 if let Self::Buffer(buffer) = self {
501 buffer.push_status_code(status_code);
502 }
503 }
504
505 #[cfg(feature = "http")]
507 #[inline]
508 fn push_headers(&mut self, headers: HeaderMap) {
509 if let Self::Buffer(buffer) = self {
510 buffer.push_headers(headers);
511 }
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518
519 fn render_with(context: HtmlContext, f: impl FnOnce(&mut PartsWriter<'_>)) -> String {
521 ViewBuffer::build(|parts| parts.in_context(context, f)).render(&Cx::default())
522 }
523
524 #[test]
525 fn push_str_seals_the_writer_context() {
526 let out = render_with(HtmlContext::Text, |w| {
527 w.push_str("<b> & \"q\"");
528 });
529 assert_eq!(out, "<b> & \"q\"");
530
531 let out = render_with(HtmlContext::AttributeValue, |w| {
532 w.push_str("<b> & \"q\"");
533 });
534 assert_eq!(out, "<b> & "q"");
535 }
536
537 #[test]
538 fn push_str_unescaped_bypasses_the_context() {
539 let out = render_with(HtmlContext::Text, |w| {
540 w.push_str_unescaped("<b>raw</b>");
541 });
542 assert_eq!(out, "<b>raw</b>");
543 }
544
545 #[test]
546 fn push_promoted_str_seals_the_writer_context() {
547 let out = render_with(HtmlContext::Text, |w| {
548 w.push_promoted_str(&"<b> & \"q\"");
549 });
550 assert_eq!(out, "<b> & \"q\"");
551
552 let out = render_with(HtmlContext::AttributeValue, |w| {
553 w.push_promoted_str(&"<b> & \"q\"");
554 });
555 assert_eq!(out, "<b> & "q"");
556 }
557
558 #[test]
559 fn push_promoted_str_unescaped_bypasses_the_context() {
560 let out = render_with(HtmlContext::Text, |w| {
561 w.push_promoted_str_unescaped(&"<b>raw</b>");
562 });
563 assert_eq!(out, "<b>raw</b>");
564 }
565
566 #[test]
567 fn push_promoted_str_skips_empty_strings() {
568 let out = render_with(HtmlContext::Text, |w| {
569 w.push_promoted_str(&"a").push_promoted_str(&"");
570 w.push_promoted_str_unescaped(&"").push_promoted_str(&"b");
571 });
572 assert_eq!(out, "ab");
573 }
574
575 #[test]
576 fn push_char_seals_the_writer_context() {
577 let out = render_with(HtmlContext::Text, |w| {
578 w.push_char('<');
579 });
580 assert_eq!(out, "<");
581 }
582
583 #[test]
584 #[should_panic(expected = "invalid attribute key")]
585 fn ident_context_panics_on_forbidden_characters_at_render() {
586 render_with(HtmlContext::AttributeKey, |w| {
587 w.push_str("on click");
588 });
589 }
590
591 #[test]
592 fn push_primitives_render_as_text() {
593 let out = render_with(HtmlContext::Text, |w| {
594 w.push_i32(-42).push_str_unescaped(" ");
595 w.push_bool(true).push_str_unescaped(" ");
596 w.push_f64(1.5).push_str_unescaped(" ");
597 w.push_i128(-1 << 100).push_str_unescaped(" ");
598 w.push_u128(1 << 100);
599 });
600 assert_eq!(
601 out,
602 "-42 true 1.5 -1267650600228229401496703205376 1267650600228229401496703205376"
603 );
604 }
605}