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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ because it turns other people's test suites red.
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.
- **HTML: links, and an indented page.** `links` puts a list of that many
links above the pictures and the blocks. They go through 18 kinds in turn -
a link to the list itself, relative, to the folder above, from the root, a
query alone, absolute with a query and a fragment, `http`, without a
scheme, percent encoded, an international domain, a port, `mailto:`,
`tel:`, `ftp:`, a download, a new window, `nofollow` and `data:` - and the
text of each says which it is. Every address is on a domain kept for
examples. The Nu HTML Checker finds nothing wrong with any of them, and
jsdom, lxml, BeautifulSoup and Python's `urljoin` resolve them alike.
DOMPurify with its defaults takes out the `data:` address and the target
that opens a new window, and it ran out of 8 GB of memory over a million
links, which lxml read in 3 seconds. The manifest records `links`.
`formatting=indented` puts two spaces in front of a line for every element
it sits in and leaves the text of `pre` as it is - Chromium places every
element of it where it places them on the page of lines, and the Nu HTML
Checker finds nothing wrong with it. With `nesting` the spaces grow with
every level, so `nesting` 1000 makes a page of at least 1.9 MB. 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 Expand Up @@ -299,6 +317,9 @@ because it turns other people's test suites red.

### Fixed

- **The window called the byte order mark setting "Bom".** It reads "BOM" now,
on `txt`, `md`, `xml` and `html` alike.

- **Ctrl+C waited for a file that holds thousands of files to be planned.**
A zip, a tar.gz, a mail message or a page with ten thousand small pictures
inside took over a minute to plan, and a run stopped in that minute went
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,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`, `formatting`, `styles`, `inline`, `inline_format`, `inline_size` |
| `html` | `structure`, `nesting`, `characters`, `elements`, `links`, `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
25 changes: 17 additions & 8 deletions internal/format/htmlfile/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,23 @@ import (
// or a list cut in the middle still renders, and it says something other than
// it meant.
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
// lead is the indentation of a block and inner the one of a list's items
// and a table's rows, both empty unless the page is indented.
lead, inner string
}

// Shortest is the smallest closing record: an empty paragraph plus whatever
// this shape closes after it.
func (b blocks) Shortest() int64 { return int64(len(b.m[paraOpen]) + len(b.tail)) }
// Shortest is the smallest closing record: an empty paragraph.
func (b blocks) Shortest() int64 {
return int64(len(b.lead) + len(b.m[paraOpen]) + len(b.m[paraClose]))
}

func (b blocks) Append(dst []byte, rng *rand.Rand) []byte {
m := b.m
dst = append(dst, b.lead...)
switch rng.IntN(5) {
case 0:
dst = append(dst, m[h2Open]...)
Expand All @@ -45,14 +47,19 @@ func (b blocks) Append(dst []byte, rng *rand.Rand) []byte {
case 1:
dst = append(dst, m[ulOpen]...)
for i, n := 0, 2+rng.IntN(3); i < n; i++ {
dst = append(dst, b.inner...)
dst = append(dst, m[liOpen]...)
dst = appendPhrase(dst, rng, 3+rng.IntN(4))
dst = append(dst, m[liClose]...)
}
dst = append(dst, b.lead...)
return append(dst, m[ulClose]...)
case 2:
dst = append(dst, m[tableOpen]...)
dst = append(dst, b.inner...)
dst = append(dst, m[tableHead]...)
for i, n := 0, 2+rng.IntN(3); i < n; i++ {
dst = append(dst, b.inner...)
dst = append(dst, m[rowOpen]...)
dst = strconv.AppendInt(dst, int64(i+1), 10)
dst = append(dst, m[cellSep]...)
Expand All @@ -61,6 +68,7 @@ func (b blocks) Append(dst []byte, rng *rand.Rand) []byte {
dst = append(dst, words[rng.IntN(len(words))]...)
dst = append(dst, m[rowClose]...)
}
dst = append(dst, b.lead...)
return append(dst, m[tableClose]...)
case 3:
// A real page carries entities, and an ampersand left raw is the
Expand All @@ -84,10 +92,11 @@ func (blocks) Discard() {}

func (b blocks) AppendExact(dst []byte, rng *rand.Rand, n int64) []byte {
start := len(dst)
dst = append(dst, b.lead...)
dst = append(dst, b.m[paraOpen]...)
used := int64(len(dst)-start) + int64(len(b.tail))
used := int64(len(dst)-start) + int64(len(b.m[paraClose]))
dst = appendFiller(dst, n-used)
return append(dst, b.tail...)
return append(dst, b.m[paraClose]...)
}

func appendPhrase(dst []byte, rng *rand.Rand, n int) []byte {
Expand Down
28 changes: 23 additions & 5 deletions internal/format/htmlfile/catalogue.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,28 @@ func (s settings) catalogueHeadFor() string {
return s.spelling()[headCatalogue]
}

// catalogueBlocks is what the catalogue adds to the body.
func (s settings) catalogueBlocks() string {
if !s.every {
return ""
// appendCatalogue appends what the catalogue adds to the body, at the depth of
// its content.
func (c content) appendCatalogue(dst []byte, lead string) []byte {
s := c.s
switch {
case !s.every:
return dst
case s.indented():
return appendLaidOut(dst, s.laid().catalogue, lead)
}
return append(dst, s.spelling()[bodyCatalogue]...)
}

// catalogueBytes is what appendCatalogue appends, worked out without building
// it.
func (c content) catalogueBytes() int64 {
s := c.s
switch {
case !s.every:
return 0
case s.indented():
return laidOutBytes(s.laid().catalogue, s.leadBytes())
}
return s.spelling()[bodyCatalogue]
return int64(len(s.spelling()[bodyCatalogue]))
}
29 changes: 23 additions & 6 deletions internal/format/htmlfile/characters.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,28 @@ const (
`and entities &amp; &lt; &gt; &quot; &nbsp; &copy; &euro; &#8364; &#x1F600;.</p>` + "\n"
)

// internationalBlocks is what the setting adds, nothing for ascii, in the
// spelling of the page.
func (s settings) internationalBlocks() string {
if !s.international {
return ""
// appendInternational appends what the setting adds, nothing for ascii, in the
// spelling of the page and at the depth of its content.
func (c content) appendInternational(dst []byte, lead string) []byte {
s := c.s
switch {
case !s.international:
return dst
case s.indented():
return appendLaidOut(dst, s.laid().scripts, lead)
}
return s.spelling()[scripts]
return append(dst, s.spelling()[scripts]...)
}

// internationalBytes is what appendInternational appends, worked out without
// building it.
func (c content) internationalBytes() int64 {
s := c.s
switch {
case !s.international:
return 0
case s.indented():
return laidOutBytes(s.laid().scripts, s.leadBytes())
}
return int64(len(s.spelling()[scripts]))
}
156 changes: 156 additions & 0 deletions internal/format/htmlfile/content.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package htmlfile

import (
"context"
"io"

"github.com/donislawdev/TestingFilesGenerator/internal/core"
)

// content is what a page writes after its label - the levels of nesting, the
// paragraphs in other scripts, the catalogue, the links, the blocks and what
// closes them - each one written from the settings and counted without being
// built. Split from settings on 2026-10-14, when indentation and links took
// that type past the methods one type may carry (typeshape_test.go): settings
// says what was asked for, and this writes it.
type content struct{ s settings }

// content is the writing of a page of these settings.
func (s settings) content() content { return content{s} }

// chunks builds what a page repeats and writes it each time it passes
// writeChunk, asking between two writes whether the run was stopped. After the
// first error it builds nothing more, and close says what the error was.
type chunks struct {
ctx context.Context
w io.Writer
buf []byte
err error
}

func newChunks(ctx context.Context, w io.Writer) *chunks {
return &chunks{ctx: ctx, w: w, buf: make([]byte, 0, writeChunk+1024)}
}

// add appends two pieces, one after the other - most often an indentation and
// what stands at it.
func (c *chunks) add(first, second string) {
if c.err != nil {
return
}
c.buf = append(append(c.buf, first...), second...)
if len(c.buf) >= writeChunk {
c.err = flush(c.ctx, c.w, c.buf)
c.buf = c.buf[:0]
}
}

// close writes what is left, or says why nothing more was written.
func (c *chunks) close() error {
if c.err != nil {
return c.err
}
return core.WriteAll(c.w, c.buf)
}

// flush writes what was built so far, after asking whether the run was
// interrupted.
func flush(ctx context.Context, w io.Writer, buf []byte) error {
if err := ctx.Err(); err != nil {
return err
}
return core.WriteAll(w, buf)
}

// levelIndent is the indentation of the level k levels in, cut from the
// deepest one rather than built for every level, nothing unless the page is
// indented.
func (c content) levelIndent(in indents, k int) string {
return in.lead[:min(len(in.lead), int(indentationBytes(c.s.bodyDepth()+k)))]
}

// levelsBytes is what the levels take opened, or closed - each one at the depth
// it opens at, which an indented page writes in front of it. Indented, a level
// k deep has 2(b+k) spaces, so the sum is worked out rather than added up.
func (c content) levelsBytes(piece string) int64 {
s := c.s
n := int64(s.nesting)
total := n * int64(len(piece))
if s.indented() {
b := int64(s.bodyDepth())
total += int64(len(indentUnit)) * (n*b + n*(n-1)/2)
}
return total
}

// writeOpening writes what follows the label: the levels of nesting, then the
// paragraphs in other scripts and the catalogue. The levels are written a
// chunk at a time - a hundred thousand of them indented are 10 GB.
func (c content) writeOpening(ctx context.Context, w io.Writer, in indents) error {
s, m := c.s, c.s.spelling()
out := newChunks(ctx, w)
for k := 0; k < s.nesting && out.err == nil; k++ {
out.add(c.levelIndent(in, k), m[divOpen])
}
if out.err == nil {
out.buf = c.appendInternational(out.buf, in.lead)
out.buf = c.appendCatalogue(out.buf, in.lead)
}
return out.close()
}

// openingBytes is what writeOpening writes, worked out without building it.
func (c content) openingBytes() int64 {
return c.levelsBytes(c.s.spelling()[divOpen]) + c.internationalBytes() + c.catalogueBytes()
}

// writeClosing writes what closes the levels from the innermost out, then the
// body and the document. It follows the last block rather than ending it, so a
// page closing a hundred thousand indented levels never builds them as one
// record.
func (c content) writeClosing(ctx context.Context, w io.Writer, in indents) error {
s, m := c.s, c.s.spelling()
out := newChunks(ctx, w)
for k := s.nesting - 1; k >= 0 && out.err == nil; k-- {
out.add(c.levelIndent(in, k), m[divClose])
}
out.add("", c.documentClose())
return out.close()
}

// closingBytes is what writeClosing writes, worked out without building it.
func (c content) closingBytes() int64 {
return c.levelsBytes(c.s.spelling()[divClose]) + int64(len(c.documentClose()))
}

// documentClose closes the body and the document of a page, and nothing in a
// fragment, which has neither.
func (c content) documentClose() string {
s := c.s
if s.shape != structureDocument {
return ""
}
closing := s.spelling()[bodyClose]
if s.indented() {
return joined(layOut(closing, s.bodyDepth()))
}
return closing
}

// blocks is the body builder for these settings.
//
// What closes the levels, the body and the document follows the last record
// (writeClosing) and is counted out of the bytes the records fill, so a change
// to it has to reach both. Until 2026-10-14 it sat in the last record, and the
// golden hashes - a fragment and a page nested on one line among them - hold
// the bytes to what they were then.
func (c content) blocks(in indents) blocks {
return blocks{m: c.s.spelling(), lead: in.lead, inner: in.inner}
}

// shortestRecord is the smallest closing record, worked out without building
// the indentation: an empty paragraph at the depth of the content.
func (c content) shortestRecord() int64 {
m := c.s.spelling()
return c.s.leadBytes() + int64(len(m[paraOpen])+len(m[paraClose]))
}
Loading
Loading