Files and Assets
Paths, bytes, pictures, stylesheets, and making all of it smaller.
path
wattle.path.out(name) -- resolve against the output directory
wattle.path.join(a, b, ...)
wattle.path.basename(path, suffix) -- suffix is optional and stripped
wattle.path.dirname(path)
wattle.path.extname(path) -- includes the dot
wattle.path.shift(path) -- returns rest, first
wattle.path.slug(text)
wattle.path.variant(path, suffix)
wattle.path.cache(dir, inputs, ext)
o(path) is shorthand for wattle.path.out(path) and is the one you will type most.
shift splits the first component off a path, which is how you drop a source prefix and branch on it at the same time:
local rest, dir = wattle.path.shift(path) -- "static/fonts/a.woff2"
-- rest is "fonts/a.woff2", dir is "static"
variant("hero.jpg", "@2x") gives hero@2x.jpg. cache(dir, inputs, ext) builds a content-addressed path under the cache directory out of a list of things the result depends on, so a derived file names itself after its inputs.
file
wattle.file.read(path)
wattle.file.write(path, content)
wattle.file.copy(from, to)
wattle.file.exists(path)
wattle.file.hash(path)
wattle.file.mime(path)
wattle.file.size(path)
wattle.file.cache(path, fn)
write takes bytes as well as text, so compressed output can go straight to disk without a round trip through a string that is not valid UTF-8.
hash is fast and non-cryptographic. Do not use it for anything that has to agree with a hash computed somewhere else.
cache(path, fn) runs fn only when the file is missing, under $XDG_CACHE_HOME/wattle/. Pair it with http.get so a build does not re-fetch on every run.
image
local img = wattle.image.read(path)
wattle.image.dimensions(path) -- without decoding the whole file
wattle.image.exif(path)
Reads and writes JPEG, PNG, GIF, WebP, AVIF, TIFF, BMP and ICO. Not the scientific and game-texture formats the underlying crate also supports, which were three megabytes of binary for formats no browser opens.
An image carries width and height, and these methods:
crop fit fill rotate flip_horizontal flip_vertical
blur brighten contrast grayscale dither replace_colors write
Each returns a new image, so calls chain:
wattle.image.read("hero.jpg")
:fit(1200, 800)
:write(o("hero.jpg"))
write uses mozjpeg for JPEG and oxipng for PNG. Both are slow and both produce noticeably smaller files than the defaults, which is the right trade for output written once and served forever.
dither maps an image onto a palette of CSS colors using Atkinson, Floyd-Steinberg or Bayer, with optional serpentine scanning and a strength knob. replace_colors swaps the result onto a different palette.
scss
wattle.scss.render(path)
Compiles with grass, which is pure Rust, so there is no second toolchain to install. Partials resolve relative to the file that imports them, and a plain .css file loaded through @use is inlined — which is one way to fold a vendored design system into a single stylesheet instead of an @import chain the browser has to walk.
minify
wattle.minify.html(source)
wattle.minify.css(source)
CSS goes through lightningcss, which strips whitespace and shortens values (blue becomes #00f). It leaves your vendor prefixes exactly as you wrote them, adding none and removing none. That costs the merging of duplicate media queries, because the pass that does the merging is the same one that rewrites prefixes, and it gets them wrong more often than the merge is worth.
The HTML minifier is configured to keep its output spec-compliant rather than as small as possible, so <pre> and <script> contents come through untouched.
compress
wattle.compress.brotli(content, quality)
wattle.compress.gzip(content, level)
wattle.compress.zstd(content, level)
For writing .br and .gz files beside their originals at build time, so a server can hand over a pre-compressed body instead of compressing it per request. All three default near the top of their range.