oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "net/http"
5 "strconv"
6)
7
8// formatFromForm decodes a formatPatch out of an ordinary HTML form.
9//
10// The second decoder for one route (this slice's decision D). Both
11// converge on a formatPatch and NOTHING is validated here — validate,
12// normalize and the maxFormatCells bound all run afterwards, in the
13// handler, on whichever patch arrived. If this function checked an
14// alignment or clamped a rectangle, the form path and the JSON path
15// would be two implementations of the rules and would drift, and the
16// drift would only surface when somebody formatted in one view and read
17// the result in the other.
18//
19// **Every toggle is tri-state, and that is not a style choice.**
20// formatPatch's fields are pointers precisely so that "leave bold
21// alone" and "turn bold off" are different requests — and an unchecked
22// checkbox is simply ABSENT from a form submission, indistinguishable
23// from a field the form never had. A checkbox-shaped bold control can
24// therefore express "make it bold" and can never express "make it not
25// bold", which is half a control. So each toggle posts an explicit
26// word: "set", "clear", or nothing at all.
27func formatFromForm(r *http.Request) (formatPatch, bool) {
28 var p formatPatch
29
30 // ParseForm's error is checked, and that is not defensive noise.
31 // r.FormValue swallows it — including the one MaxBytesReader raises
32 // on an oversized body — and Go still populates r.Form from the URL
33 // QUERY. So without this, a post whose body was refused had its
34 // patch read from the query string instead and the write landed:
35 // the cap was enforced on the body and bypassed by moving the
36 // fields. sizeFromForm has always checked; this one had not, so the
37 // two decoders disagreed about what a malformed form is. Found by
38 // an independent review (Codex, 2026-09-05).
39 if err := r.ParseForm(); err != nil {
40 return p, false
41 }
42
43 rect := []struct {
44 name string
45 into *int
46 }{{"r1", &p.R1}, {"c1", &p.C1}, {"r2", &p.R2}, {"c2", &p.C2}}
47 for _, f := range rect {
48 v, err := strconv.Atoi(r.FormValue(f.name))
49 if err != nil {
50 return p, false
51 }
52 *f.into = v
53 }
54
55 if b, ok := triState(r.FormValue("bold")); ok {
56 p.Bold = &b
57 }
58 if b, ok := triState(r.FormValue("italic")); ok {
59 p.Italic = &b
60 }
61 // The three string fields carry their own "leave alone", which is
62 // the empty submission — but "" is also a MEANINGFUL value for
63 // align and numfmt (it clears back to the engine's own choice), so
64 // they cannot use the same convention as the booleans. A separate
65 // word does the job: the control posts "keep" when it is not
66 // changing anything.
67 for _, f := range []struct {
68 name string
69 into **string
70 }{{"align", &p.Align}, {"numfmt", &p.NumFmt}, {"fill", &p.Fill}, {"ink", &p.Ink}} {
71 v := r.FormValue(f.name)
72 if v == "keep" {
73 continue
74 }
75 if _, present := r.Form[f.name]; !present {
76 continue
77 }
78 val := v
79 *f.into = &val
80 }
81 return p, true
82}
83
84// triState reads a boolean toggle. "set" and "clear" are the two
85// changes; anything else — including the empty string a "leave alone"
86// option posts, and including a value nobody expected — leaves the
87// field absent.
88//
89// Deliberately not accepting "true"/"false"/"on": "on" is what a
90// checkbox posts, and quietly accepting it would let somebody build the
91// checkbox-shaped control this function exists to rule out, and have it
92// appear to work for exactly the half of its job that it can do.
93func triState(v string) (bool, bool) {
94 switch v {
95 case "set":
96 return true, true
97 case "clear":
98 return false, true
99 }
100 return false, false
101}
102