use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::task::{Context, Poll, Waker};
use serde::Serialize;
use crate::html::Html;
use crate::slots::{HOLE, Slot};
use crate::{json, state};
#[derive(Debug, Default)]
pub struct Page {
record: Shared,
}
pub struct Part<'p> {
name: &'static str,
props: String,
render: Box<dyn Fn(&mut String) + 'p>,
}
pub struct PageSlot<'p> {
render: Box<SlotContent<'p>>,
}
pub struct PageHole {
record: Shared,
index: usize,
open: bool,
}
#[derive(Debug)]
pub struct PageRecord {
record: Shared,
}
#[derive(Debug)]
pub struct PageScript {
record: Shared,
id: String,
}
type Shared = Arc<Mutex<Recorded>>;
type SlotContent<'p> = dyn Fn(&mut String, &str) -> bool + 'p;
#[derive(Debug, Default)]
struct Recorded {
props: String,
names: Vec<&'static str>,
slots: Vec<(String, Option<String>)>,
open: usize,
waker: Option<Waker>,
}
fn lock(record: &Shared) -> MutexGuard<'_, Recorded> {
record.lock().unwrap_or_else(PoisonError::into_inner)
}
impl Page {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn slot<'p>(
&mut self,
slot: &str,
parts: impl IntoIterator<Item = Part<'p>>,
) -> PageSlot<'p> {
let parts: Vec<Part<'p>> = parts.into_iter().collect();
self.add(slot, Some(parts_json(&parts)));
lock(&self.record).name(&parts);
PageSlot {
render: Box::new(move |out, slot_scope_id| {
refuse_slotted(slot_scope_id);
for part in &parts {
(part.render)(out);
}
true
}),
}
}
pub fn hole(&mut self, slot: &str) -> PageHole {
let index = self.add(slot, None);
lock(&self.record).open += 1;
PageHole {
record: Arc::clone(&self.record),
index,
open: true,
}
}
pub fn render_to<P: Serialize, F: Fn(&mut String, &P)>(
self,
out: &mut String,
layout: Html<'_, P, F>,
) -> PageRecord {
let props = layout.props.get();
lock(&self.record).props = json::to_string_or_null(props);
(layout.render)(out, props);
PageRecord {
record: self.record,
}
}
fn add(&mut self, slot: &str, parts: Option<String>) -> usize {
let mut record = lock(&self.record);
assert!(
record.slots.iter().all(|(name, _)| name != slot),
"the page already has a slot called {slot:?}"
);
record.slots.push((slot.to_owned(), parts));
record.slots.len() - 1
}
}
impl<'p> Part<'p> {
#[must_use]
pub fn new<P: Serialize + 'p, F: Fn(&mut String, &P) + 'p>(
name: &'static str,
html: Html<'p, P, F>,
) -> Self {
let props = json::to_string_or_null(html.props.get());
Part {
name,
props,
render: Box::new(move |out| (html.render)(out, html.props.get())),
}
}
#[must_use]
pub fn name(&self) -> &'static str {
self.name
}
}
impl PageSlot<'_> {
#[must_use]
pub fn slot(&self) -> Slot<'_> {
Slot::slotted(&*self.render)
}
}
impl PageHole {
#[must_use]
pub fn slot(&self) -> Slot<'static> {
Slot::slotted(&write_hole)
}
#[must_use]
pub fn fill<'p>(mut self, parts: impl IntoIterator<Item = Part<'p>>) -> String {
let parts: Vec<Part<'p>> = parts.into_iter().collect();
let mut out = String::new();
for part in &parts {
(part.render)(&mut out);
}
lock(&self.record).name(&parts);
self.close(parts_json(&parts));
out
}
fn close(&mut self, parts: String) {
if !std::mem::replace(&mut self.open, false) {
return;
}
let mut record = lock(&self.record);
record.slots[self.index].1 = Some(parts);
record.open -= 1;
if record.open == 0
&& let Some(waker) = record.waker.take()
{
waker.wake();
}
}
}
impl Drop for PageHole {
fn drop(&mut self) {
self.close(String::from("[]"));
}
}
impl PageRecord {
#[must_use]
pub fn island_names(&self) -> Vec<&'static str> {
lock(&self.record).names.clone()
}
pub fn script_into(&self, out: &mut String, id: &str) {
let record = lock(&self.record);
assert!(
record.open == 0,
"a hole of the page is not filled yet: write the record with `PageRecord::script`"
);
state::json_script_into(out, id, &record.to_json());
}
#[must_use]
pub fn script(self, id: impl Into<String>) -> PageScript {
PageScript {
record: self.record,
id: id.into(),
}
}
}
impl Future for PageScript {
type Output = String;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<String> {
let mut record = lock(&self.record);
if record.open > 0 {
record.waker = Some(cx.waker().clone());
return Poll::Pending;
}
let mut out = String::new();
state::json_script_into(&mut out, &self.id, &record.to_json());
Poll::Ready(out)
}
}
impl Recorded {
fn name(&mut self, parts: &[Part<'_>]) {
for part in parts {
if !self.names.contains(&part.name) {
self.names.push(part.name);
}
}
}
fn to_json(&self) -> String {
let mut out = String::from("{\"props\":");
out.push_str(&self.props);
out.push_str(",\"slots\":{");
for (i, (name, parts)) in self.slots.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&json::to_string(name));
out.push(':');
out.push_str(parts.as_deref().unwrap_or("[]"));
}
out.push_str("}}");
out
}
}
fn parts_json(parts: &[Part<'_>]) -> String {
let mut out = String::from("[");
for (i, part) in parts.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str("{\"c\":");
out.push_str(&json::to_string(part.name));
out.push_str(",\"p\":");
out.push_str(&part.props);
out.push('}');
}
out.push(']');
out
}
fn refuse_slotted(slot_scope_id: &str) {
assert!(
slot_scope_id.is_empty(),
"a page part is written into an outlet that gives its content the slot scope id{slot_scope_id}, \
which Vue writes onto the part's root and its `html()` cannot: fill that slot without `Page`"
);
}
fn write_hole(out: &mut String, slot_scope_id: &str) -> bool {
refuse_slotted(slot_scope_id);
out.push_str(HOLE);
true
}
impl std::fmt::Debug for Part<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Part")
.field("name", &self.name)
.field("props", &self.props)
.finish_non_exhaustive()
}
}
impl std::fmt::Debug for PageSlot<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PageSlot").finish_non_exhaustive()
}
}
impl std::fmt::Debug for PageHole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PageHole")
.field("index", &self.index)
.field("open", &self.open)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests;