Skip to main content

max / makeover-webview

0.51.0: emit what a file field accepts, and whether it takes several Task f7261a5a's webview half, on makeover-layout 0.31. The accept list is comma-joined because that is the attribute's own format, and each entry writes itself through Accepted::as_str: a family is its wildcard media type, a media type and a suffix are themselves. Nothing is normalised on the way through -- .tar.gz is two dots and the browser is fine with it. An empty list emits no attribute at all, which is the browser's own "any file" and is what the description means by listing nothing. accept="" is a filter that means nothing on one browser and everything on another. Escaped like every other string that reaches an attribute value. Nothing in the tree writes a quote into an accept list; that it cannot is the point.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-18 21:15 UTC
Signed with PGP, not checked
Commit: 55f7b371aa3ebfc492e141a473eebeb125b82709
Parent: cd6a611
2 files changed, +81 insertions, -5 deletions
M Cargo.toml +3 -3
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.50.0"
3 + version = "0.51.0"
4 4 edition = "2024"
5 5 # One copy of this renderer per dependency graph, enforced by cargo rather than
6 6 # by remembering. Two versions means the generated stylesheet and the emitted
@@ -17,7 +17,7 @@
17 17 # patch satisfy the requirement and still fail to compile. That happened once
18 18 # with `form::radio_html` calling `FieldKind::Radio`, and makeover-build is where
19 19 # it surfaced, one release later.
20 - makeover-layout = "0.30.0"
20 + makeover-layout = "0.31.0"
21 21 # The capability axis. `makeover-touch` decides whether a hover rule should be
22 22 # gated at all; `makeover-geometry` spells the gate as a media condition. Both
23 23 # answers are owned elsewhere and neither is re-derived here.
@@ -27,7 +27,7 @@
27 27 # satisfies "0.8" and keeps a second makeover-geometry in the graph next to the
28 28 # 0.7 this crate asks for. `Density` is nominally distinct across the two and
29 29 # the build fails on a type that reads as identical.
30 - makeover-touch = "0.21.0"
30 + makeover-touch = "0.22.0"
31 31 makeover-geometry = "0.7"
32 32
33 33 [lints.rust]
M src/form.rs +78 -2
@@ -233,6 +233,36 @@
233 233 /// name is what submits and is fixed by the description; the id has to be
234 234 /// unique in the document and so carries [`Filling::id_prefix`] when a form
235 235 /// appears more than once.
236 + /// The `accept` attribute, from the description's accept list.
237 + ///
238 + /// makeover-layout 0.31.0. The list is comma-joined because that is the
239 + /// attribute's own format, and each entry writes itself: a family is its
240 + /// wildcard media type, a media type is itself, a suffix is itself with its
241 + /// leading dot. Nothing is normalised on the way through -- `.tar.gz` is two
242 + /// dots and the browser is fine with it.
243 + ///
244 + /// An empty list emits no attribute at all, which is the browser's own "any
245 + /// file" and is what the description means by listing nothing. Emitting
246 + /// `accept=""` instead would be a filter that matches nothing on some browsers
247 + /// and everything on others.
248 + ///
249 + /// It is a filter and not a guarantee, on the browser's side as much as here:
250 + /// the picker keeps an "All Files" escape and the user may take it. Whoever
251 + /// validated still validates.
252 + fn push_accept(out: &mut String, field: &Field<'_>) {
253 + if field.accept.is_empty() {
254 + return;
255 + }
256 + out.push_str(" accept=\"");
257 + for (index, one) in field.accept.iter().enumerate() {
258 + if index > 0 {
259 + out.push(',');
260 + }
261 + escape_into(one.as_str(), out);
262 + }
263 + out.push('"');
264 + }
265 +
236 266 fn push_control_attributes(out: &mut String, field: &Field<'_>, id: &str, name: &str) {
237 267 let _ = write!(out, " id=\"{id}\" name=\"");
238 268 escape_into(name, out);
@@ -540,6 +570,10 @@
540 570 push_class(out, "field", opts);
541 571 out.push('"');
542 572 push_control_attributes(out, field, &id, field.name);
573 + push_accept(out, field);
574 + if field.multiple {
575 + out.push_str(" multiple");
576 + }
543 577 out.push('>');
544 578 }
545 579 kind => {
@@ -664,6 +698,7 @@
664 698 #[cfg(test)]
665 699 mod tests {
666 700 use super::*;
701 + use makeover_layout::{Accepted, Family};
667 702
668 703 fn field(kind: FieldKind) -> Field<'static> {
669 704 Field::new(kind, "title", "Title")
@@ -1266,8 +1301,9 @@
1266 1301
1267 1302 #[test]
1268 1303 fn a_file_field_is_a_file_input() {
1269 - // `844b5ae0`. It carries no `accept`, which is measured rather than
1270 - // deferred: zero sites in either app.
1304 + // `844b5ae0`. A field that takes any file emits no `accept` at all,
1305 + // which is the browser's own "any file". `accept=""` is a filter that
1306 + // means nothing on one browser and everything on another.
1271 1307 let html = field_html(
1272 1308 &Field::new(FieldKind::File, "attachment", "Attachment"),
1273 1309 &Filling::default(),
@@ -1275,11 +1311,51 @@
1275 1311 );
1276 1312 assert!(html.contains(r#"type="file""#));
1277 1313 assert!(!html.contains("accept="));
1314 + assert!(!html.contains("multiple"));
1278 1315 // And it never carries a value: a file input's value is not settable
1279 1316 // from markup, and the browser refuses one that tries.
1280 1317 assert!(!html.contains("value="));
1281 1318 }
1282 1319
1320 + #[test]
1321 + fn an_accept_list_is_comma_joined_in_the_attributes_own_format() {
1322 + // `f7261a5a`, makeover-layout 0.31.0. Each entry writes itself: a
1323 + // family is its wildcard, a media type is itself, a suffix keeps its
1324 + // leading dot and however many more it has.
1325 + const MIXED: &[Accepted<'_>] = &[
1326 + Accepted::Family(Family::Image),
1327 + Accepted::Type("text/csv"),
1328 + Accepted::Suffix(".tar.gz"),
1329 + ];
1330 + let html = field_html(
1331 + &Field {
1332 + multiple: true,
1333 + ..Field::upload("drop", "Drop files", MIXED)
1334 + },
1335 + &Filling::default(),
1336 + &Emit::default(),
1337 + );
1338 + assert!(
1339 + html.contains(r#"accept="image/*,text/csv,.tar.gz""#),
1340 + "{html}"
1341 + );
1342 + assert!(html.contains(" multiple"), "{html}");
1343 + }
1344 +
1345 + #[test]
1346 + fn an_accept_entry_cannot_end_the_attribute_it_sits_in() {
1347 + // The list reaches an attribute value, so it is escaped like every
1348 + // other string that does. Nothing in the tree writes a quote into one;
1349 + // that it cannot is the point.
1350 + const HOSTILE: &[Accepted<'_>] = &[Accepted::Type(r#"image/x" onload="x"#)];
1351 + let html = field_html(
1352 + &Field::upload("cover", "Cover", HOSTILE),
1353 + &Filling::default(),
1354 + &Emit::default(),
1355 + );
1356 + assert!(!html.contains(r#"onload="x"#), "{html}");
1357 + }
1358 +
1283 1359 #[test]
1284 1360 fn the_typed_text_kinds_keep_their_input_type() {
1285 1361 for (kind, expected) in [