| 183 |
183 |
|
/// `open` is a half-written document waiting to happen. What goes in is the
|
| 184 |
184 |
|
/// inside of `<body>`; the `<body>` tag itself, its classes and the morph
|
| 185 |
185 |
|
/// registration are the shell's, the same as on the described path.
|
|
186 |
+ |
///
|
|
187 |
+ |
/// A host whose own templating writes into the head cannot hand a body over
|
|
188 |
+ |
/// as a string, and takes [`parts`](Self::parts) instead.
|
| 186 |
189 |
|
#[must_use]
|
| 187 |
190 |
|
pub fn document(&self, title: &str, body: &str) -> String {
|
| 188 |
191 |
|
let mut out = String::with_capacity(body.len() + 1024);
|
| 192 |
195 |
|
out
|
| 193 |
196 |
|
}
|
| 194 |
197 |
|
|
|
198 |
+ |
/// The shell's half of a document the host assembles itself.
|
|
199 |
+ |
///
|
|
200 |
+ |
/// [`document`](Self::document) is the one to reach for. This is for a host
|
|
201 |
+ |
/// whose templating emits into the head from inside the page — a template
|
|
202 |
+ |
/// language with inheritance, where the per-page title and metadata are
|
|
203 |
+ |
/// blocks the parent renders in place and no caller ever holds as a string.
|
|
204 |
+ |
/// Askama is the case this was measured on, converting a server one screen
|
|
205 |
+ |
/// at a time; the parent template assembles:
|
|
206 |
+ |
///
|
|
207 |
+ |
/// ```html
|
|
208 |
+ |
/// {{ parts.head }}<title>...</title>...</head>
|
|
209 |
+ |
/// <body{{ parts.body_attrs }} class="...">
|
|
210 |
+ |
/// ...
|
|
211 |
+ |
/// </body></html>
|
|
212 |
+ |
/// ```
|
|
213 |
+ |
///
|
|
214 |
+ |
/// The split is by what could silently diverge, not by what is convenient.
|
|
215 |
+ |
/// The host owes `<title>`, `</head>`, the `<body>` tag and the close, which
|
|
216 |
+ |
/// are structure a template cannot get subtly wrong. Everything with an
|
|
217 |
+ |
/// opinion in it — the layer statement's position, the htmx config, the
|
|
218 |
+ |
/// morph registration, the order of sheets against scripts — is here, and
|
|
219 |
+ |
/// stays the same markup the described path emits.
|
|
220 |
+ |
#[must_use]
|
|
221 |
+ |
pub fn parts(&self) -> Parts {
|
|
222 |
+ |
let mut head = String::with_capacity(1024);
|
|
223 |
+ |
self.open_head(None, &mut head);
|
|
224 |
+ |
Parts {
|
|
225 |
+ |
head,
|
|
226 |
+ |
body_attrs: self.body_attrs(),
|
|
227 |
+ |
}
|
|
228 |
+ |
}
|
|
229 |
+ |
|
| 195 |
230 |
|
/// Everything from `<!doctype>` to the open `<body>` tag.
|
| 196 |
231 |
|
pub(crate) fn open(&self, title: &str, out: &mut String) {
|
|
232 |
+ |
self.open_head(Some(title), out);
|
|
233 |
+ |
out.push_str("</head><body");
|
|
234 |
+ |
out.push_str(&self.body_attrs());
|
|
235 |
+ |
out.push('>');
|
|
236 |
+ |
}
|
|
237 |
+ |
|
|
238 |
+ |
/// The head, less its close. `None` leaves the `<title>` to the caller.
|
|
239 |
+ |
fn open_head(&self, title: Option<&str>, out: &mut String) {
|
| 197 |
240 |
|
out.push_str("<!doctype html><html lang=\"");
|
| 198 |
241 |
|
out.push_str(&escape(&self.lang));
|
| 199 |
242 |
|
out.push_str("\"><head><meta charset=\"utf-8\">");
|
| 200 |
243 |
|
out.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
|
| 201 |
|
- |
out.push_str("<title>");
|
| 202 |
|
- |
out.push_str(&escape(title));
|
| 203 |
|
- |
out.push_str("</title>");
|
|
244 |
+ |
if let Some(title) = title {
|
|
245 |
+ |
out.push_str("<title>");
|
|
246 |
+ |
out.push_str(&escape(title));
|
|
247 |
+ |
out.push_str("</title>");
|
|
248 |
+ |
}
|
| 204 |
249 |
|
|
| 205 |
250 |
|
// Decision 9's gap, closed in one place. A 4xx that does not swap is a
|
| 206 |
251 |
|
// banner the user never sees, so this tag is required rather than a
|
| 249 |
294 |
|
if let Some(head) = &self.head {
|
| 250 |
295 |
|
out.push_str(head);
|
| 251 |
296 |
|
}
|
|
297 |
+ |
}
|
| 252 |
298 |
|
|
| 253 |
|
- |
out.push_str("</head><body");
|
|
299 |
+ |
/// The attributes the shell owns on `<body>`, each one space-prefixed so
|
|
300 |
+ |
/// they compose with whatever else the host puts on the tag.
|
|
301 |
+ |
fn body_attrs(&self) -> String {
|
|
302 |
+ |
let mut out = String::new();
|
| 254 |
303 |
|
if self.morphs() {
|
| 255 |
304 |
|
// Registered once on the body rather than per element: the
|
| 256 |
305 |
|
// extension is inherited, and an app that has to remember it per
|
| 262 |
311 |
|
out.push_str(&escape(class));
|
| 263 |
312 |
|
out.push('"');
|
| 264 |
313 |
|
}
|
| 265 |
|
- |
out.push('>');
|
|
314 |
+ |
out
|
| 266 |
315 |
|
}
|
| 267 |
316 |
|
|
| 268 |
317 |
|
/// The close of what [`open`](Self::open) opened.
|
| 270 |
319 |
|
out.push_str("</body></html>");
|
| 271 |
320 |
|
}
|
| 272 |
321 |
|
}
|
|
322 |
+ |
|
|
323 |
+ |
/// The shell's half of a host-assembled document. See [`Shell::parts`].
|
|
324 |
+ |
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
325 |
+ |
pub struct Parts {
|
|
326 |
+ |
/// `<!doctype>` through the head's contents, without `</head>`. The host
|
|
327 |
+ |
/// appends its own head markup and closes the element.
|
|
328 |
+ |
pub head: String,
|
|
329 |
+ |
/// The attributes the shell owns on `<body>`, space-prefixed, for a host
|
|
330 |
+ |
/// writing the tag itself: `<body{body_attrs} class="...">`.
|
|
331 |
+ |
pub body_attrs: String,
|
|
332 |
+ |
}
|