From eb9e600eb8f9701f419b6b62b2c75b8f95a91460 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Tue, 8 Sep 2026 16:54:41 +0200 Subject: [PATCH] format: an HTML file can be a fragment instead of a whole page A new setting on html: structure, taking document or fragment. It defaults to the whole page these files have always been, so the ten pinned hashes are unchanged whether a recipe says nothing or says document out loud. A fragment is the same blocks without the skeleton - no doctype, no html element, no head and no body. It is what a content field, an email body or a partial render really holds. Half of this change is what was not built. The obvious next setting after XML was a third turn of the textenc work, since HTML carries a meta charset in band the way XML carries a declaration. The standard rules it out, checked at the source rather than recalled: the document encoding must be UTF-8, and the charset attribute must match "utf-8". So a UTF-16 page is not a document whose declaration is ignored, it is a non-conforming document. Measured beside that: html.parser takes text rather than bytes, so a UTF-16 file announcing utf-8 parses without complaint and reports charset utf-8. A mismatch nothing refuses. What the change cost. The bytes that close the body and the document sit in the last RECORD rather than in a footer, so the shape has to reach the block builder and not only the prologue - swapping the prologue alone gives a fragment ending in , the right size and nonsense. That is its own mutation. Each shape answers for its own minimum, 118 B against 8 B, and the registry declares the default shape's the way it does for the JSON layouts. The label rides in the heading alone in a fragment, since there is no head to put a title in, and the format still declares its label visible because a heading is. The oracle had to be told which shape to expect. check_html hardcoded a doctype prefix and a closing html tag, so a fragment turned both over - and a checker that worked the shape out from the file would agree with a fragment produced where a page was ordered and the other way round. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 16 ++ README.md | 2 +- internal/format/htmlfile/html.go | 187 ++++++++++++++++++------ internal/guard/htmlfragment_test.go | 217 ++++++++++++++++++++++++++++ internal/guard/parity_test.go | 1 + internal/oracle/strict.py | 41 ++++-- web/public/formats/index.html | 5 + web/public/pl/formaty/index.html | 5 + 8 files changed, 423 insertions(+), 51 deletions(-) create mode 100644 internal/guard/htmlfragment_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc63b8..fa7de7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,22 @@ because it turns other people's test suites red. ### Added +- **HTML files can be a fragment instead of a whole page.** A new setting on + `html`: `structure`, which takes `document` or `fragment`. It defaults to the + whole page these files have always been, so a recipe that says nothing gets + the same bytes it got before. + + A fragment is the same blocks without the skeleton - no doctype, no `html` + element, no head and no body. It is what a content field, an email body or a + partial render really holds, and it is what a system under test is handed when + something else owns the page around it. + + Two things follow from carrying less. The smallest fragment is 8 B rather than + 118 B, because there is no skeleton to pay for. And the label, which a page + carries twice - once in the title for the tab and once in a heading for the + reader - rides in the heading alone, since a fragment has no head to put a + title in. It is still visible. + - **XML documents can be written in UTF-16.** Two new settings on `xml`: `encoding`, which takes `utf-8`, `utf-16le` or `utf-16be`, and `bom`, which says whether the file opens with a byte order mark. Both default to what diff --git a/README.md b/README.md index 7aa4b5a..6a8d7ab 100644 --- a/README.md +++ b/README.md @@ -479,7 +479,7 @@ recipe. `tfg formats ` prints the allowed range or list for each: | `txt`, `md`, `xml` | `encoding`, `bom` | | `json` | `formatting` | | `svg` | `width`, `height` | -| `html` | none | +| `html` | `structure` | ``` tfg generate --format jpg --size 500kb --set width=1920 --set height=1080 --set quality=85 diff --git a/internal/format/htmlfile/html.go b/internal/format/htmlfile/html.go index 19b69a5..b6af1f5 100644 --- a/internal/format/htmlfile/html.go +++ b/internal/format/htmlfile/html.go @@ -56,10 +56,64 @@ const ( tailLast = paraClose + bodyClose - // fixedWidth is every literal byte of the closing record. - fixedWidth = len(paraOpen) + len(tailLast) + // tailFragment is what a fragment's closing record ends with. A fragment + // closes its last paragraph and stops, because it has no body and no + // document to close. + tailFragment = paraClose ) +// The shape of the file: a whole page, or only the blocks that would sit in one. +const ( + settingStructure = "structure" + structureDocument = "document" + structureFragment = "fragment" +) + +// structureOf reads the setting. +// +// A value outside the declared set has already been refused by the registry, +// which checks every format against its declaration in one place. This branch +// stays for the same reason the CSV dialect and the text encoding keep theirs: +// this function is callable directly, a guard is such a caller, and a generator +// that trusts its input is one registry change away from writing a file nobody +// ordered. +func structureOf(props map[string]string) (string, error) { + v, ok := props[settingStructure] + if !ok || v == "" { + return structureDocument, nil + } + switch v { + case structureDocument, structureFragment: + return v, nil + } + return "", &format.PropertyValueError{ + Format: "html", Key: settingStructure, Value: v, + Reason: "it has to be " + structureDocument + " or " + structureFragment, + } +} + +// prologueFor is the skeleton down to the body, empty for a fragment. +func prologueFor(shape string) string { + if shape == structureFragment { + return "" + } + return prologue +} + +// blocksFor is the body builder for one shape. +// +// The shape has to reach the BUILDER and not only the prologue, and that is the +// part of this easy to miss: the bytes that close the body and the document sit +// in the last RECORD rather than in a footer. A change that swapped only the +// prologue would end a fragment with - the right size, +// deterministic, and nonsense. +func blocksFor(shape string) blocks { + if shape == structureFragment { + return blocks{tail: tailFragment} + } + return blocks{tail: tailLast} +} + func init() { format.Register(format.Descriptor{ ID: "html", @@ -82,10 +136,16 @@ func init() { // than a comment. Label: format.LabelVisible, Oracle: "python-html", - // Fragment mode, element counts, inline CSS and JS, images, forms and - // the "every HTML5 tag" variant come later. Declaring none now makes a - // recipe asking for them fail loudly. - Properties: nil, + // Element counts, inline CSS and JS, images, forms and the "every HTML5 + // tag" variant come later. Declaring only what is here is what makes a + // recipe asking for them fail loudly instead of quietly producing + // something else. + Properties: []format.Property{{ + Name: settingStructure, Kind: format.PropertyChoice, + Choices: []string{structureDocument, structureFragment}, + Default: structureDocument, + Detail: "Whether the file is a whole page or only the blocks that would sit inside one. A fragment has no doctype, no html element and no body, which is what a content field or the body of an email really holds. It is far smaller, so the smallest fragment sits well below the smallest page.", + }}, GeneratorVersion: generatorVersion, Generator: generator{}, }) @@ -97,49 +157,56 @@ type memo struct { head string // the whole skeleton down to , title included labelLine string // the visible heading, empty when absent seed uint64 + shape string } func (generator) Plan(r format.Request) (format.Plan, error) { - min := minimumBytes() + shape, err := structureOf(r.Properties) + if err != nil { + return format.Plan{}, err + } + + min := minimumFor(shape) if r.Bytes < min { + reason := "a page holds a head, a body and whole blocks, and one of each needs that much" + if shape == structureFragment { + reason = "a fragment holds whole blocks, and one of them needs that much" + } return format.Plan{}, &format.BelowMinimumError{ Format: "HTML", Requested: r.Bytes, Minimum: min, - Reason: "a page holds a head, a body and whole blocks, and one of each needs that much", + Reason: reason, Hint: fmt.Sprintf("Ask for %d B or more.", min), } } + // A fragment has no doctype, and saying it has one would be the manifest + // describing a file that is not there. + doctype := "html" + if shape == structureFragment { + doctype = "none" + } + p := format.Plan{ Bytes: r.Bytes, Exact: true, Determinism: format.DeterminismByte, Properties: map[string]any{ - "encoding": "utf-8", - "line_ending": "lf", - "doctype": "html", - "language": "en", + "encoding": "utf-8", + "line_ending": "lf", + "doctype": doctype, + "language": "en", + settingStructure: shape, }, } - m := memo{seed: r.Seed, head: prologue} + m := memo{seed: r.Seed, head: prologueFor(shape), shape: shape} if r.Label { - // The title and the heading say the same thing, which is what a page - // does - one for the tab and one for the reader. - label := core.Label("html", r.Bytes, r.Seed) - withTitle := strings.Replace(prologue, emptyTitle, ""+label+"", 1) - heading := "

" + label + "

\n" - if int64(len(withTitle)-len(prologue)+len(heading))+minimumBytes() <= r.Bytes { - m.head = withTitle - m.labelLine = heading - } else { - p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label needs %d B and this file has no room for it beside a whole block. Its name and the manifest still identify it.", - len(heading)), - }) + var note *format.Note + m.head, m.labelLine, note = labelledHead(shape, r, min) + if note != nil { + p.Notes = append(p.Notes, *note) } } @@ -148,6 +215,37 @@ func (generator) Plan(r format.Request) (format.Plan, error) { return p, nil } +// labelledHead works out what a labelled file carries: the head, the visible +// heading, and a note instead of both when there is no room beside a whole +// block. +// +// Split out of Plan when that function reached the crowding threshold. The line +// is what a part does rather than how long it is: this answers "does the label +// fit and what does it cost", and Plan answers "is this request askable at all". +func labelledHead(shape string, r format.Request, min int64) (head, heading string, note *format.Note) { + label := core.Label("html", r.Bytes, r.Seed) + heading = "

" + label + "

\n" + + head, extra := prologueFor(shape), int64(0) + if shape == structureDocument { + // The title and the heading say the same thing, which is what a page + // does - one for the tab and one for the reader. A fragment has no head + // to put a title in, so it carries only the heading, and the label is + // still visible because a heading is. + head = strings.Replace(prologue, emptyTitle, ""+label+"", 1) + extra = int64(len(head) - len(prologue)) + } + if extra+int64(len(heading))+min <= r.Bytes { + return head, heading, nil + } + return prologueFor(shape), "", &format.Note{ + Code: "label_omitted", + Detail: fmt.Sprintf( + "The label needs %d B and this file has no room for it beside a whole block. Its name and the manifest still identify it.", + len(heading)), + } +} + func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { @@ -160,7 +258,7 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { } rng := core.NewRand(m.seed) - return core.FillRecords(ctx, w, rng, p.Bytes-int64(len(head)), blocks{}) + return core.FillRecords(ctx, w, rng, p.Bytes-int64(len(head)), blocksFor(m.shape)) } // blocks builds the body. A natural record is one complete block element and @@ -169,11 +267,15 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { // Whole blocks only, for the same reason Markdown writes whole blocks: a table // or a list cut in the middle still renders, and it says something other than // it meant. -type blocks struct{} +type blocks struct { + // tail is what the closing record ends with, which is the only thing the + // shape of the file changes down here. + tail string +} -// Shortest is the smallest closing record: an empty paragraph plus the bytes -// that close the body and the document. -func (blocks) Shortest() int64 { return int64(fixedWidth) } +// Shortest is the smallest closing record: an empty paragraph plus whatever +// this shape closes after it. +func (b blocks) Shortest() int64 { return int64(len(paraOpen) + len(b.tail)) } func (blocks) Append(dst []byte, rng *rand.Rand) []byte { switch rng.IntN(5) { @@ -221,12 +323,12 @@ func (blocks) Append(dst []byte, rng *rand.Rand) []byte { // next, so throwing one away leaves no trace to undo. func (blocks) Discard() {} -func (blocks) AppendExact(dst []byte, rng *rand.Rand, n int64) []byte { +func (b blocks) AppendExact(dst []byte, rng *rand.Rand, n int64) []byte { start := len(dst) dst = append(dst, paraOpen...) - used := int64(len(dst)-start) + int64(len(tailLast)) + used := int64(len(dst)-start) + int64(len(b.tail)) dst = appendFiller(dst, n-used) - return append(dst, tailLast...) + return append(dst, b.tail...) } func appendPhrase(dst []byte, rng *rand.Rand, n int) []byte { @@ -249,11 +351,14 @@ func appendFiller(dst []byte, n int64) []byte { return core.AppendFiller(dst, words, n, nil) } -// minimumBytes is the skeleton and one whole block, computed rather than -// written down so it cannot drift away from the template. -func minimumBytes() int64 { - var b blocks - return int64(len(prologue)) + b.Shortest() +// minimumBytes is the smallest whole page, which is what the registry declares. +// Every shape answers for its own, the way the JSON layouts do. +func minimumBytes() int64 { return minimumFor(structureDocument) } + +// minimumFor is the skeleton of one shape and one whole block, computed rather +// than written down so it cannot drift away from the template. +func minimumFor(shape string) int64 { + return int64(len(prologueFor(shape))) + blocksFor(shape).Shortest() } // words is the filler vocabulary. English by default, like the rest of the text diff --git a/internal/guard/htmlfragment_test.go b/internal/guard/htmlfragment_test.go new file mode 100644 index 0000000..3c0a1fe --- /dev/null +++ b/internal/guard/htmlfragment_test.go @@ -0,0 +1,217 @@ +package guard + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/oracle" +) + +// HTML gained a shape on 2026-09-08: a whole page, or only the blocks that +// would sit inside one. +// +// The axis is structural on purpose. HTML is the weakest format in this project +// for checking, because the specification requires a parser to recover from +// almost anything - so "it parsed" carries close to no information, and an axis +// visible as the ABSENCE of named elements is worth more here than one visible +// only in the content. +// +// Encoding was the obvious next setting after XML and it is deliberately NOT +// here. The standard says the document encoding must be UTF-8 and the charset +// attribute must match "utf-8", so a UTF-16 page would break the specification, +// and nothing in this project could go red on one. See docs/OBSERVATIONS.md +// O191 and docs/HTML-STRUCTURE-2026-09-08.md. + +const ( + shapeSetting = "structure" + shapeDocument = "document" + shapeFragment = "fragment" +) + +func shapeProps(shape string) map[string]string { + return map[string]string{shapeSetting: shape} +} + +// skeleton is what a whole page carries and a fragment must not. +var skeleton = []string{"", ""} + +// TestAFragmentCarriesNoSkeletonAndAPageCarriesOne is the claim itself. +// +// Read from the bytes rather than through a parser, and that is the point: the +// tolerant reader beside this one accepts both shapes happily, so it cannot +// tell them apart. What separates them is which elements are THERE. +func TestAFragmentCarriesNoSkeletonAndAPageCarriesOne(t *testing.T) { + d, err := format.Get("html") + if err != nil { + t.Fatal(err) + } + + for _, shape := range []string{shapeDocument, shapeFragment} { + floor := d.SmallestAccepted(format.Request{Seed: 7741, Label: true, Properties: shapeProps(shape)}) + for _, size := range []int64{floor, floor + 400, 4096} { + body := strings.ToLower(string(writeEncoded(t, "html", size, shapeProps(shape)))) + + for _, part := range skeleton { + has := strings.Contains(body, part) + if shape == shapeDocument && !has { + t.Errorf("a page of %d B is missing %q", size, part) + } + if shape == shapeFragment && has { + t.Errorf("a fragment of %d B holds %q, which belongs to a whole page", size, part) + } + } + } + } +} + +// TestEachHTMLShapeAnswersForItsOwnMinimum holds the arithmetic. +// +// The registry declares the DEFAULT shape's minimum, the way it does for the +// JSON layouts, and every other shape answers for its own. Without this a +// fragment would inherit a page's floor and 110 B of every file would be a +// skeleton that is not there. +func TestEachHTMLShapeAnswersForItsOwnMinimum(t *testing.T) { + d, err := format.Get("html") + if err != nil { + t.Fatal(err) + } + + page := d.SmallestAccepted(format.Request{Seed: 7741, Label: true, Properties: shapeProps(shapeDocument)}) + part := d.SmallestAccepted(format.Request{Seed: 7741, Label: true, Properties: shapeProps(shapeFragment)}) + + if page != d.MinBytes { + t.Errorf("the registry declares %d B and a page starts at %d - the declared minimum is the default shape's", + d.MinBytes, page) + } + if part >= page { + t.Errorf("a fragment starts at %d B and a page at %d - a fragment carries less, so it has to start lower", + part, page) + } + + // "Lower" is not enough, and the mutation run said so rather than the + // reading: a fragment that inherited a page's prologue still starts lower + // than a page, so the check above stayed green on exactly the defect it + // names. What has to hold is that the gap IS the skeleton. + // + // Derived from the two files rather than repeated from the generator's + // constants, which would be the same number written twice and would agree + // with itself however wrong it was. + pageBody := writeEncoded(t, "html", page, shapeProps(shapeDocument)) + partBody := writeEncoded(t, "html", part, shapeProps(shapeFragment)) + + stripped := pageBody + if i := bytes.Index(stripped, []byte("\n")); i >= 0 { + stripped = stripped[i+len("\n"):] + } + stripped = bytes.TrimSuffix(stripped, []byte("\n\n")) + if !bytes.Equal(stripped, partBody) { + t.Errorf("the smallest page without its skeleton is %q and the smallest fragment is %q - "+ + "a fragment is the blocks of a page and nothing else, so at the floor the two have to meet", + stripped, partBody) + } + + // Both floors are actually writable, and one below each is refused. A floor + // the format announces and then turns down is the defect this pins. + for _, c := range []struct { + shape string + floor int64 + }{{shapeDocument, page}, {shapeFragment, part}} { + if n := int64(len(writeEncoded(t, "html", c.floor, shapeProps(c.shape)))); n != c.floor { + t.Errorf("%s: asked for its own floor of %d B and got %d", c.shape, c.floor, n) + } + _, err := d.Generator.Plan(format.Request{ + Bytes: c.floor - 1, Seed: 7741, Label: true, Properties: shapeProps(c.shape)}) + var below *format.BelowMinimumError + if !errors.As(err, &below) { + t.Errorf("%s: one byte under its floor was answered with %v, not a BelowMinimumError", c.shape, err) + continue + } + if below.Minimum != c.floor { + t.Errorf("%s: refusing %d B named %d as its minimum, not %d", c.shape, c.floor-1, below.Minimum, c.floor) + } + } +} + +// TestTheDefaultHTMLShapeIsTheBytesItAlwaysWrote is the way back. +// +// A setting whose default changes the file is a breaking change wearing the +// clothes of a feature. Saying nothing and saying "document" out loud are two +// different routes through the parser and have to meet. +func TestTheDefaultHTMLShapeIsTheBytesItAlwaysWrote(t *testing.T) { + for _, size := range []int64{118, 119, 400, 4096} { + silent := writeEncoded(t, "html", size, nil) + spoken := writeEncoded(t, "html", size, shapeProps(shapeDocument)) + if !bytes.Equal(silent, spoken) { + t.Errorf("at %d B: saying nothing and saying %s produce different bytes", size, shapeDocument) + } + } +} + +// TestTheLabelIsVisibleInBothHTMLShapes holds the label across the change. +// +// A page carries it twice, in the title for the tab and the heading for the +// reader. A fragment has no head to put a title in, so it carries the heading +// alone - and the format still declares its label VISIBLE, which stays true +// only because a heading is. +func TestTheLabelIsVisibleInBothHTMLShapes(t *testing.T) { + for _, shape := range []string{shapeDocument, shapeFragment} { + body := string(writeEncoded(t, "html", 4096, shapeProps(shape))) + + if !strings.Contains(body, "

") { + t.Errorf("%s: there is no heading, so the label is not visible", shape) + } + hasTitle := strings.Contains(body, "tfg") + if shape == shapeDocument && !hasTitle { + t.Error("a page carries the label in its title as well, and this one does not") + } + if shape == shapeFragment && hasTitle { + t.Error("a fragment has no head, so a title in it belongs to a page") + } + } +} + +// TestTheStructuralCheckIsToldWhichHTMLShapeToExpect is the canary. +// +// Every other guard here hands the checker a file of the shape it asked for, so +// all of them would stay green if it stopped looking at the shape at all. The +// two defects this setting can cause are a fragment where a page was ordered +// and a page where a fragment was, and only handing it the wrong one proves +// they would be caught. +func TestTheStructuralCheckIsToldWhichHTMLShapeToExpect(t *testing.T) { + dir := t.TempDir() + written := map[string]string{} + for _, shape := range []string{shapeDocument, shapeFragment} { + path := filepath.Join(dir, shape+".html") + if err := os.WriteFile(path, writeEncoded(t, "html", 4096, shapeProps(shape)), 0o600); err != nil { + t.Fatal(err) + } + written[shape] = path + } + + // The controls first. A check that refused everything would pass the half + // below without seeing anything at all. + for _, shape := range []string{shapeDocument, shapeFragment} { + res := oracle.Strict("html", written[shape], shapeSetting+"="+shape) + if !res.Available { + t.Skip("the structural check needs python") + } + if res.Err != nil { + t.Fatalf("%s handed to the check as %s was refused: %v", shape, shape, res.Err) + } + } + + for _, c := range []struct{ file, told string }{ + {shapeDocument, shapeFragment}, + {shapeFragment, shapeDocument}, + } { + if res := oracle.Strict("html", written[c.file], shapeSetting+"="+c.told); res.Err == nil { + t.Errorf("a %s handed to the check as a %s was called correct, so nothing here can see the shape", + c.file, c.told) + } + } +} diff --git a/internal/guard/parity_test.go b/internal/guard/parity_test.go index 7bb1694..c38a3e4 100644 --- a/internal/guard/parity_test.go +++ b/internal/guard/parity_test.go @@ -112,6 +112,7 @@ var reachableFromTheWindow = []string{ "property:gif.frames", "property:gif.height", "property:gif.width", + "property:html.structure", "property:ico.embed", "property:ico.height", "property:ico.width", diff --git a/internal/oracle/strict.py b/internal/oracle/strict.py index 033b8c8..deedb9e 100644 --- a/internal/oracle/strict.py +++ b/internal/oracle/strict.py @@ -655,8 +655,8 @@ def check_svg(data): ok(f"{shapes} drawable shapes out of {elements} elements") -def check_html(data): - """Balanced, complete and with real blocks in the body. +def check_html(data, settings): + """Balanced, the shape that was ordered, and with real blocks in it. HTML is the weakest format in this project for checking, and that is a property of the format rather than of this machine. A parser is required to @@ -675,10 +675,33 @@ def check_html(data): except UnicodeDecodeError as exc: fail(f"not valid UTF-8: {exc}") - if not text.lower().startswith("<!doctype html>"): - fail("the document does not open with an HTML5 doctype") - if not text.rstrip().endswith("</html>"): - fail("the document does not end with a closing html tag") + # TOLD which shape to expect, never sniffed, and this is the same lesson the + # JSON layouts taught: a checker that worked it out from the file would + # agree with a fragment produced where a page was ordered, and with a page + # produced where a fragment was. Those are the two defects this setting is + # able to cause, so guessing here would leave nothing to catch them. + shape = (settings or {}).get("structure", "document") + if shape not in ("document", "fragment"): + fail(f"the html check was told structure={shape!r}, which is not one this tool writes") + + opens = text.lower().startswith("<!doctype html>") + closes = text.rstrip().endswith("</html>") + if shape == "document": + if not opens: + fail("the document does not open with an HTML5 doctype") + if not closes: + fail("the document does not end with a closing html tag") + else: + # A fragment is what a content field or the body of an email holds. The + # skeleton being ABSENT is the whole of what was ordered, so its + # presence is the failure rather than a curiosity. + if opens: + fail("a fragment was ordered and the file opens with a doctype, so it is a whole page") + if closes: + fail("a fragment was ordered and the file ends with a closing html tag") + for tag in ("<html", "<head", "<body"): + if tag in text.lower(): + fail(f"a fragment was ordered and the file holds {tag}>, which belongs to a whole page") entity = re.compile(r"&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#[0-9]+|#x[0-9a-fA-F]+);") tag = re.compile(r"<(/?)([a-zA-Z][a-zA-Z0-9]*)([^>]*)>") @@ -709,8 +732,8 @@ def check_html(data): if stack: fail(f"the document ends with {', '.join('<' + s + '>' for s in stack)} still open") if blocks == 0: - fail("the body holds no block elements, so the page renders as nothing") - ok(f"{blocks} blocks, all tags balanced") + fail("there are no block elements, so this renders as nothing") + ok(f"{blocks} blocks, all tags balanced, shape is {shape}") def gzip_header_end(data): @@ -1703,7 +1726,7 @@ def check_md(data, settings=None): # Checks that take the shape of the file as well as its bytes. Everything else # is handed the bytes alone, so adding a setting to one check cannot change how # any other one is called. -TAKES_SETTINGS = {"csv", "txt", "md", "json", "xml"} +TAKES_SETTINGS = {"csv", "txt", "md", "json", "xml", "html"} if __name__ == "__main__": if len(sys.argv) < 3 or sys.argv[1] not in CHECKS: diff --git a/web/public/formats/index.html b/web/public/formats/index.html index ede0208..395cb95 100644 --- a/web/public/formats/index.html +++ b/web/public/formats/index.html @@ -385,6 +385,11 @@ <h2>Settings each format accepts</h2> <td><code>frames</code></td> <td>1 - 60</td> </tr> + <tr> + <td><code>html</code></td> + <td><code>structure</code></td> + <td>document, fragment</td> + </tr> <tr> <td><code>ico</code></td> <td><code>width</code></td> diff --git a/web/public/pl/formaty/index.html b/web/public/pl/formaty/index.html index c5c5d94..7bd8d9d 100644 --- a/web/public/pl/formaty/index.html +++ b/web/public/pl/formaty/index.html @@ -385,6 +385,11 @@ <h2>Ustawienia, które przyjmuje każdy format</h2> <td><code>frames</code></td> <td>1 - 60</td> </tr> + <tr> + <td><code>html</code></td> + <td><code>structure</code></td> + <td>document, fragment</td> + </tr> <tr> <td><code>ico</code></td> <td><code>width</code></td>