max / makeover-webview
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
1 file changed,
+50 insertions,
-4 deletions
| @@ -688,7 +688,13 @@ | |||
| 688 | 688 | /// nobody chose. goingson hit exactly that with a backup-retention default of | |
| 689 | 689 | /// 10 against a 1/3/7/14/0 list, and grew this stray-option fix locally; it is | |
| 690 | 690 | /// here so the second app gets it without hitting the bug first. | |
| 691 | - | fn push_options(out: &mut String, field: &Field<'_>, options: &[Choice<'_>], value: &str) { | |
| 691 | + | fn push_options( | |
| 692 | + | out: &mut String, | |
| 693 | + | field: &Field<'_>, | |
| 694 | + | options: &[Choice<'_>], | |
| 695 | + | value: &str, | |
| 696 | + | mut placed: Option<&mut Vec<core::ops::Range<usize>>>, | |
| 697 | + | ) { | |
| 692 | 698 | // The unanswered state, which HTML has no attribute for: `placeholder` is | |
| 693 | 699 | // not a `<select>` attribute, and the idiom is an empty option that cannot | |
| 694 | 700 | // be chosen back. `disabled` is what stops it being re-selected once the | |
| @@ -721,6 +727,7 @@ | |||
| 721 | 727 | ); | |
| 722 | 728 | } | |
| 723 | 729 | for opt in options { | |
| 730 | + | let at = out.len(); | |
| 724 | 731 | out.push_str("<option value=\""); | |
| 725 | 732 | escape_into(opt.value, out); | |
| 726 | 733 | out.push('"'); | |
| @@ -762,6 +769,9 @@ | |||
| 762 | 769 | escape_into(reason, out); | |
| 763 | 770 | } | |
| 764 | 771 | out.push_str("</option>"); | |
| 772 | + | if let Some(placed) = placed.as_deref_mut() { | |
| 773 | + | placed.push(at..out.len()); | |
| 774 | + | } | |
| 765 | 775 | } | |
| 766 | 776 | } | |
| 767 | 777 | ||
| @@ -855,7 +865,13 @@ | |||
| 855 | 865 | } | |
| 856 | 866 | ||
| 857 | 867 | /// The control itself, without its label, hint or error. | |
| 858 | - | fn push_control(out: &mut String, field: &Field<'_>, filling: &Filling<'_>, opts: &Emit) { | |
| 868 | + | fn push_control( | |
| 869 | + | out: &mut String, | |
| 870 | + | field: &Field<'_>, | |
| 871 | + | filling: &Filling<'_>, | |
| 872 | + | opts: &Emit, | |
| 873 | + | placed: Option<&mut Vec<core::ops::Range<usize>>>, | |
| 874 | + | ) { | |
| 859 | 875 | // Emitted before anything else is computed: a radio group carries its | |
| 860 | 876 | // descriptions on the group rather than on a control, so none of the | |
| 861 | 877 | // attributes below belong to it. | |
| @@ -921,7 +937,7 @@ | |||
| 921 | 937 | // A select described with no options emits an empty select, which | |
| 922 | 938 | // says so on screen rather than in a log. That is the description's | |
| 923 | 939 | // own position on `Field::options`, not a fallback invented here. | |
| 924 | - | push_options(out, field, field.options, filling.value.as_text()); | |
| 940 | + | push_options(out, field, field.options, filling.value.as_text(), placed); | |
| 925 | 941 | out.push_str("</select>"); | |
| 926 | 942 | } | |
| 927 | 943 | // The one place this renderer emits `<optgroup>`, and it emits it | |
| @@ -1308,6 +1324,36 @@ | |||
| 1308 | 1324 | /// these, so a host building one should hold a single buffer and append each | |
| 1309 | 1325 | /// field into it rather than take a `String` per field and concatenate. | |
| 1310 | 1326 | pub fn field_html_into(field: &Field<'_>, filling: &Filling<'_>, opts: &Emit, out: &mut String) { | |
| 1327 | + | emit_field(field, filling, opts, out, None); | |
| 1328 | + | } | |
| 1329 | + | ||
| 1330 | + | /// One field, saying where each of its options landed. | |
| 1331 | + | /// | |
| 1332 | + | /// Byte-identical to [`field_html_into`], and it appends one entry to `placed` | |
| 1333 | + | /// per option of a select, in order: the offsets in `out` between which that | |
| 1334 | + | /// `<option>` was written. Nothing is appended for a field that offers no | |
| 1335 | + | /// options. | |
| 1336 | + | /// | |
| 1337 | + | /// Same reason as [`crate::list::cells_html_placed`]: a caller compiling a | |
| 1338 | + | /// described screen into a template has to know which bytes one option | |
| 1339 | + | /// produced, and two options with the same label are the same bytes. | |
| 1340 | + | pub fn field_html_placed( | |
| 1341 | + | field: &Field<'_>, | |
| 1342 | + | filling: &Filling<'_>, | |
| 1343 | + | opts: &Emit, | |
| 1344 | + | out: &mut String, | |
| 1345 | + | placed: &mut Vec<core::ops::Range<usize>>, | |
| 1346 | + | ) { | |
| 1347 | + | emit_field(field, filling, opts, out, Some(placed)); | |
| 1348 | + | } | |
| 1349 | + | ||
| 1350 | + | fn emit_field( | |
| 1351 | + | field: &Field<'_>, | |
| 1352 | + | filling: &Filling<'_>, | |
| 1353 | + | opts: &Emit, | |
| 1354 | + | out: &mut String, | |
| 1355 | + | placed: Option<&mut Vec<core::ops::Range<usize>>>, | |
| 1356 | + | ) { | |
| 1311 | 1357 | let id = filling.id_for(field.name); | |
| 1312 | 1358 | ||
| 1313 | 1359 | if !field.kind.visible() { | |
| @@ -1351,7 +1397,7 @@ | |||
| 1351 | 1397 | out.push_str("</label>"); | |
| 1352 | 1398 | } | |
| 1353 | 1399 | ||
| 1354 | - | push_control(out, field, filling, opts); | |
| 1400 | + | push_control(out, field, filling, opts, placed); | |
| 1355 | 1401 | ||
| 1356 | 1402 | // Adjacent text, because HTML has no unit attribute and inventing one would | |
| 1357 | 1403 | // be markup nothing reads. Pointed at by `aria-describedby` so it is not |