Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,31 @@ because it turns other people's test suites red.
- **HTML: pictures inside the page.** `inline`, `inline_format` and
`inline_size` - the same settings, with the same meaning, as the pictures
of an `eml` message - write real pictures of any picture format into the
page as `data:` URLs, each on a line of its own above the blocks, so
page as `data:` URLs, each in a paragraph of its own above the blocks, so
opening the page fetches nothing. Each picture is the very file its format
writes on its own. The Nu HTML Checker finds nothing wrong with a page of
any of the nine formats, Chromium shows every format but `tiff`, and
DOMPurify keeps them all. lxml, and BeautifulSoup on top of it, leave out
a picture past about 7.1 MB unless told to read huge trees - on Linux lxml
took over 20 seconds over such a page, and with a 15 MB picture it lost the
rest of the page as well. Files made without pictures keep their bytes.
- **HTML: a page on one line, and styles.** `formatting=minified` puts the
whole page on one line with no newline at the end, the way a bundler
writes it, and the page shows the same as one written in lines. A program
reading line by line gets the whole page as one line - Go's
`bufio.Scanner` refuses a line past 64 KB unless given a larger buffer -
and text read by joining what the elements hold runs the last word of one
block into the first word of the next. The manifest says `line_ending:
none` for such a page. `styles=embedded` puts CSS rules in a style element,
in the head of a page and at the very start of a fragment, and
`styles=inline` puts the same rules in a style attribute on every heading,
paragraph, list, table and quote the page is filled with. The Nu HTML
Checker finds nothing wrong with either, and Chromium shows both the same.
DOMPurify with its defaults drops the style element without listing it as
removed and keeps the attributes, and lxml's `text_content` and
BeautifulSoup on html5lib count the rules as text of the page. No rule
names anything to fetch. Files made with the earlier settings keep their
bytes.
- **A length of time as a setting.** `duration`, `change_interval` and `keyframe_interval` take
`10s`, `1m30s`, `1h`, `500ms` or `59.9s`, to the millisecond. A bare number
is refused rather than read as seconds, and so is a length that does not
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ recipe. `tfg formats <id>` prints the allowed range or list for each:
| `geojson` | `geometry`, `formatting`, `precision`, `altitude`, `vertices`, `holes`, `winding`, `antimeridian`, `unlocated`, `ids`, `bbox` |
| `eml` | `body`, `text_encoding`, `charset`, `headers`, `recipients`, `line_endings`, `attachments`, `attachment_format`, `attachment_size`, `filename_style`, `message_depth`, `inline`, `inline_format`, `inline_size` |
| `svg` | `width`, `height` |
| `html` | `structure`, `nesting`, `characters`, `elements`, `bom`, `inline`, `inline_format`, `inline_size` |
| `html` | `structure`, `nesting`, `characters`, `elements`, `bom`, `formatting`, `styles`, `inline`, `inline_format`, `inline_size` |
| `yaml`, `toml` | none in this build - the document is a fixed shape, and a size is the only thing to ask for |

```
Expand Down
43 changes: 24 additions & 19 deletions internal/format/htmlfile/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,51 +25,56 @@ 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
// m is how the blocks are spelled. The words drawn are the same in every
// spelling, so one seed fills a page of one line and a page of many with
// the same blocks.
m *markup
}

// 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 (b blocks) Shortest() int64 { return int64(len(b.m[paraOpen]) + len(b.tail)) }

func (blocks) Append(dst []byte, rng *rand.Rand) []byte {
func (b blocks) Append(dst []byte, rng *rand.Rand) []byte {
m := b.m
switch rng.IntN(5) {
case 0:
dst = append(dst, "<h2>"...)
dst = append(dst, m[h2Open]...)
dst = appendPhrase(dst, rng, 2+rng.IntN(3))
return append(dst, "</h2>\n"...)
return append(dst, m[h2Close]...)
case 1:
dst = append(dst, "<ul>\n"...)
dst = append(dst, m[ulOpen]...)
for i, n := 0, 2+rng.IntN(3); i < n; i++ {
dst = append(dst, "<li>"...)
dst = append(dst, m[liOpen]...)
dst = appendPhrase(dst, rng, 3+rng.IntN(4))
dst = append(dst, "</li>\n"...)
dst = append(dst, m[liClose]...)
}
return append(dst, "</ul>\n"...)
return append(dst, m[ulClose]...)
case 2:
dst = append(dst, "<table>\n<tr><th>id</th><th>field</th><th>value</th></tr>\n"...)
dst = append(dst, m[tableOpen]...)
for i, n := 0, 2+rng.IntN(3); i < n; i++ {
dst = append(dst, "<tr><td>"...)
dst = append(dst, m[rowOpen]...)
dst = strconv.AppendInt(dst, int64(i+1), 10)
dst = append(dst, "</td><td>"...)
dst = append(dst, m[cellSep]...)
dst = append(dst, words[rng.IntN(len(words))]...)
dst = append(dst, "</td><td>"...)
dst = append(dst, m[cellSep]...)
dst = append(dst, words[rng.IntN(len(words))]...)
dst = append(dst, "</td></tr>\n"...)
dst = append(dst, m[rowClose]...)
}
return append(dst, "</table>\n"...)
return append(dst, m[tableClose]...)
case 3:
// A real page carries entities, and an ampersand left raw is the
// classic way a document stops being well formed. The fixture carries
// one on purpose.
dst = append(dst, "<blockquote><p>"...)
dst = append(dst, m[quoteOpen]...)
dst = appendPhrase(dst, rng, 4+rng.IntN(4))
dst = append(dst, " &amp; "...)
dst = appendPhrase(dst, rng, 2)
return append(dst, "</p></blockquote>\n"...)
return append(dst, m[quoteClose]...)
default:
dst = append(dst, paraOpen...)
dst = append(dst, m[paraOpen]...)
dst = appendPhrase(dst, rng, 12+rng.IntN(12))
return append(dst, paraClose...)
return append(dst, m[paraClose]...)
}
}

Expand All @@ -79,7 +84,7 @@ func (blocks) Discard() {}

func (b blocks) AppendExact(dst []byte, rng *rand.Rand, n int64) []byte {
start := len(dst)
dst = append(dst, paraOpen...)
dst = append(dst, b.m[paraOpen]...)
used := int64(len(dst)-start) + int64(len(b.tail))
dst = appendFiller(dst, n-used)
return append(dst, b.tail...)
Expand Down
12 changes: 8 additions & 4 deletions internal/format/htmlfile/catalogue.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const (
smallPicture = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEUlEQVR42mNwaDiAFTEMLQkAYvNgAQlPC90AAAAASUVORK5CYII="
sound = "data:audio/wav;base64,UklGRjQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YRAAAACAgICAgICAgICAgICAgICA"

// preformatted is the text of the pre element, the one place in the
// catalogue where a line break is text rather than layout. A page on one
// line has to keep it, so it is named here and found by name.
preformatted = "preformatted text\n keeps its spaces"

// catalogueHead is what the catalogue puts in the head of a page. base
// names no address, so it changes nothing a link resolves to, and the icon
// is empty data so a browser asks nobody for one.
Expand All @@ -52,8 +57,7 @@ const (
<h5>Fifth level</h5>
<h6>Sixth level</h6>
<blockquote cite="#text"><p>A quotation in a block.</p></blockquote>
<pre><code>preformatted text
keeps its spaces</code></pre>
<pre><code>` + preformatted + `</code></pre>
<hr>
<dl><dt>Term</dt><dd>Its description.</dd></dl>
<ol start="3" reversed><li>three</li><li>two</li></ol>
Expand Down Expand Up @@ -143,13 +147,13 @@ func (s settings) catalogueHeadFor() string {
if !s.every || s.shape != structureDocument {
return ""
}
return catalogueHead
return s.spelling()[headCatalogue]
}

// catalogueBlocks is what the catalogue adds to the body.
func (s settings) catalogueBlocks() string {
if !s.every {
return ""
}
return catalogueBody
return s.spelling()[bodyCatalogue]
}
5 changes: 3 additions & 2 deletions internal/format/htmlfile/characters.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ const (
`and entities &amp; &lt; &gt; &quot; &nbsp; &copy; &euro; &#8364; &#x1F600;.</p>` + "\n"
)

// internationalBlocks is what the setting adds, nothing for ascii.
// internationalBlocks is what the setting adds, nothing for ascii, in the
// spelling of the page.
func (s settings) internationalBlocks() string {
if !s.international {
return ""
}
return international
return s.spelling()[scripts]
}
25 changes: 14 additions & 11 deletions internal/format/htmlfile/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ func init() {
// than a comment.
Label: format.LabelVisible,
Oracle: "python-html",
// Style sheets and a page on one line come later
// (docs/HTML-2-2026-10-14.md section 8). Declaring only what is here
// is what makes a recipe asking for them fail loudly instead of
// quietly producing something else.
// Links come later, if at all (docs/HTML-2-2026-10-14.md section 3,
// B4). Declaring only what is here is what makes a recipe asking for
// them fail loudly instead of quietly producing something else.
Properties: properties(),
GeneratorVersion: generatorVersion,
Generator: generator{},
Expand Down Expand Up @@ -89,7 +88,7 @@ func (generator) PlanContext(ctx context.Context, r format.Request) (format.Plan
return format.Plan{}, err
}

pictures, pictureBytes, err := planPictures(ctx, r, s.pictures)
pictures, pictureBytes, err := planPictures(ctx, r, s.pictures, s.spelling())
if err != nil {
return format.Plan{}, err
}
Expand All @@ -110,8 +109,10 @@ func (generator) PlanContext(ctx context.Context, r format.Request) (format.Plan
Exact: true,
Determinism: format.DeterminismByte,
Properties: map[string]any{
"encoding": "utf-8",
"line_ending": "lf",
"encoding": "utf-8",
// none for a page on one line: it has no line to end, and lf
// would describe a file that is not there.
"line_ending": s.lineEnding(),
"doctype": doctype,
"language": "en",
},
Expand Down Expand Up @@ -154,9 +155,10 @@ func belowMinimum(s settings, requested, min int64) error {
// the reason names, whatever else was asked.
reason = core.Says("html.MinimumWithPictures", "the file holds whole blocks and every picture in base64, which is about a third larger than the picture itself, and together they need that much")
case s.adds():
reason = core.Says("html.MinimumWithSettings", "the file holds whole blocks and what %s, %s, %s and %s add to them, and together they need that much",
reason = core.Says("html.MinimumWithSettings", "the file holds whole blocks and what %s, %s, %s, %s and %s add to them, and together they need that much",
core.A("Nesting", core.LabelTerm(settingNesting)), core.A("Characters", core.LabelTerm(settingCharacters)),
core.A("Elements", core.LabelTerm(settingElements)), core.A("BOM", core.LabelTerm(textenc.SettingBOM)))
core.A("Elements", core.LabelTerm(settingElements)), core.A("BOM", core.LabelTerm(textenc.SettingBOM)),
core.A("Styles", core.LabelTerm(settingStyles)))
case s.shape == structureFragment:
reason = core.Says("html.MinimumFragment", "a fragment holds whole blocks, and one of them needs that much")
}
Expand All @@ -178,7 +180,8 @@ func belowMinimum(s settings, requested, min int64) error {
// fit and what does it cost", and Plan answers "is this request askable at all".
func labelledHead(s settings, r format.Request, min int64) (head, heading string, note *format.Note) {
label := core.Label("html", r.Bytes, r.Seed)
heading = "<h1>" + label + "</h1>\n"
m := s.spelling()
heading = m[labelOpen] + label + m[labelClose]

plain := s.prologue()
head, extra := plain, int64(0)
Expand Down Expand Up @@ -229,5 +232,5 @@ func minimumBytes() int64 { return minimumFor(settings{shape: structureDocument}
// whole block, computed from the pieces Write uses rather than written down so
// it cannot drift away from the template.
func minimumFor(s settings) int64 {
return int64(len(s.prologue())) + s.openingBytes() + int64(len(paraOpen)) + s.tailBytes()
return int64(len(s.prologue())) + s.openingBytes() + int64(len(s.spelling()[paraOpen])) + s.tailBytes()
}
Loading
Loading