Skip to content

Commit 3f5301e

Browse files
committed
docs: add Functions section to CSS_SUPPORT.md
Documents the ~30 CSS functional values Folio recognizes, grouped by category: math (calc/min/max/clamp), color (rgb/rgba/hsl/hsla/cmyk), gradients (linear/radial + repeating- variants), content/counters (var, attr, content, counter, counters, string), transforms (10 2D forms), and url. Each table calls out known limitations alongside related issue numbers (#222, #265, #266, #274, #275) so an evaluator hits known gaps from the doc, not from a failed render. Same hand-written + drift-guard pattern as the At-rules section: function dispatch is spread across 6 files (properties.go, converter_style_parsers.go, css_props.go, page.go, converter_style.go, bookmark.go) so a single AST grep isn't tractable. Instead, TestFunctionsDocCoverage maintains a static expected list and asserts (a) each name is referenced in the rendered doc and (b) one form per category is actually accepted by the relevant parser — a sanity net that fires if a parser is removed under a documented function.
1 parent 646d83f commit 3f5301e

3 files changed

Lines changed: 236 additions & 2 deletions

File tree

docs/CSS_SUPPORT.md

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Folio CSS support
22

3-
> Auto-generated from `html/css_props.go` and `html/css.go`. Do not edit by hand.
3+
> Auto-generated from `html/css_props.go`, `html/css.go`, and the function parsers in `html/`. Do not edit by hand.
44
> Run `go generate ./html/...` to regenerate after changing the registry.
55
66
Folio's HTML-to-PDF converter recognizes the CSS properties listed below.
@@ -314,6 +314,94 @@ these produce a warning — the rule and its body are dropped during parsing.
314314
| `@namespace`, `@charset` | Not interpreted. |
315315
| `@layer`, `@scope`, `@container`, `@property` | Newer CSS spec features; not interpreted. |
316316

317+
## Functions
318+
319+
CSS functional values recognized by Folio's parsers, grouped by category.
320+
Functions not listed here pass through as opaque text and almost always
321+
cause the containing declaration to be discarded.
322+
323+
### Math
324+
325+
Accepted everywhere a `<length>` or `<percentage>` is expected.
326+
Folio's parser preserves these as single tokens through shorthand splitting,
327+
so they survive inside `margin`, `padding`, `flex`, `transform()`, etc.
328+
329+
| Function | Notes |
330+
|---|---|
331+
| `calc()` | Supports `+`, `-`, `*`, `/` with operator precedence and nested parentheses. Mixed units (e.g. `calc(100% - 20px)`) resolve at layout time. |
332+
| `min()` | Comma-separated argument list. Returns the smallest resolved value. |
333+
| `max()` | Comma-separated argument list. Returns the largest resolved value. |
334+
| `clamp()` | `clamp(<min>, <preferred>, <max>)`. |
335+
336+
Known limitations: `calc()` does not yet expand inside `rotate()`, `scale()`, `skew()`, `background-position`, or `linear-gradient()` color stops — see issues #265, #266, #274, #275.
337+
338+
### Color
339+
340+
Accepted everywhere a `<color>` is expected. Output is sRGB regardless of input form.
341+
342+
| Function | Notes |
343+
|---|---|
344+
| `rgb()` | `rgb(R, G, B)` or `rgb(R G B)`. Components are 0-255 integers or 0-100% percentages. |
345+
| `rgba()` | `rgba(R, G, B, A)`. Alpha is 0-1 or 0-100%. |
346+
| `hsl()` | `hsl(H, S%, L%)`. Hue in degrees. |
347+
| `hsla()` | `hsla(H, S%, L%, A)`. |
348+
| `cmyk()` / `device-cmyk()` | `cmyk(C, M, Y, K)` with components as 0-1 or 0-100%. Folio converts to sRGB for raster compositing; the original CMYK is preserved in the PDF color space for print pipelines. |
349+
350+
Known unsupported color functions: `oklch()`, `oklab()`, `lch()`, `lab()`, `color-mix()`, `color()` — see [Known unsupported features](#known-unsupported-features) for workarounds.
351+
352+
### Gradients
353+
354+
Accepted as `background-image` values.
355+
356+
| Function | Notes |
357+
|---|---|
358+
| `linear-gradient()` | Direction (`to right`, `45deg`, etc.) plus 2+ `<color>` stops. |
359+
| `repeating-linear-gradient()` | Same syntax; tiles the gradient pattern. |
360+
| `radial-gradient()` | Shape (`circle`, `ellipse`), size, and `<color>` stops. |
361+
| `repeating-radial-gradient()` | Same syntax; tiles the gradient pattern. |
362+
363+
`conic-gradient()` is not supported.
364+
365+
### Content and counters
366+
367+
Used in `string-set`, `bookmark-label`, `content`, and `@page` margin boxes.
368+
369+
| Function | Notes |
370+
|---|---|
371+
| `var()` | CSS custom property reference. Supports a fallback as the second argument: `var(--c, #000)`. Resolved BEFORE per-property dispatch, so functions and gradients receive resolved values. |
372+
| `attr()` | Reads an HTML attribute. Used in `bookmark-label`. |
373+
| `content()` | Substitutes the element's text content. Used in `string-set` and `bookmark-label`. |
374+
| `counter()` | `counter(<name>)` or `counter(<name>, <list-style>)`. Page counter `counter(page)` is supported in `@page` margin boxes. |
375+
| `counters()` | `counters(<name>, <separator>)` for nested counter chains. |
376+
| `string()` | Reads the latest value of a named string set via `string-set` (used in running headers). |
377+
378+
Known unsupported: `target-counter()` for cross-references — tracked as #222.
379+
380+
### Transform
381+
382+
Used in `transform`. Multiple functions compose in the listed order.
383+
384+
| Function | Notes |
385+
|---|---|
386+
| `translate()` | `translate(<tx>)` or `translate(<tx>, <ty>)`. Lengths in any supported unit; bare numbers treated as px. |
387+
| `translateX()` | Single `<length>` argument. |
388+
| `translateY()` | Single `<length>` argument. |
389+
| `rotate()` | Single `<angle>`: `deg`, `rad`, `grad`, `turn`, or bare number (degrees). |
390+
| `scale()` | `scale(<s>)` (uniform) or `scale(<sx>, <sy>)`. |
391+
| `scaleX()` | Single `<number>` argument. |
392+
| `scaleY()` | Single `<number>` argument. |
393+
| `skew()` | `skew(<ax>)` or `skew(<ax>, <ay>)`. |
394+
| `skewX()` | Single `<angle>` argument. |
395+
| `skewY()` | Single `<angle>` argument. |
396+
397+
Known unsupported: `matrix()`, `matrix3d()`, `translate3d()`, `rotate3d()`, `scale3d()`, `perspective()` — Folio renders 2D only.
398+
399+
### Other
400+
401+
| Function | Notes |
402+
|---|---|
403+
| `url()` | Used in `background-image`, `@font-face` `src`, and asset references. Resolves through Folio's asset loader (BaseFS or HTTP via Client, subject to `Options.URLPolicy`). |
404+
317405
## Known unsupported features
318406

319407
These properties / values are commonly requested but NOT recognized by Folio.

html/css_props_doc.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func RenderCSSPropertiesMarkdown() string {
2323
var b strings.Builder
2424

2525
b.WriteString("# Folio CSS support\n\n")
26-
b.WriteString("> Auto-generated from `html/css_props.go` and `html/css.go`. Do not edit by hand.\n")
26+
b.WriteString("> Auto-generated from `html/css_props.go`, `html/css.go`, and the function parsers in `html/`. Do not edit by hand.\n")
2727
b.WriteString("> Run `go generate ./html/...` to regenerate after changing the registry.\n\n")
2828
b.WriteString("Folio's HTML-to-PDF converter recognizes the CSS properties listed below.\n")
2929
b.WriteString("Properties not in this document are silently ignored at render time.\n\n")
@@ -187,6 +187,80 @@ func RenderCSSPropertiesMarkdown() string {
187187
b.WriteString("| `@namespace`, `@charset` | Not interpreted. |\n")
188188
b.WriteString("| `@layer`, `@scope`, `@container`, `@property` | Newer CSS spec features; not interpreted. |\n\n")
189189

190+
// Functions — hand-curated section, same rationale as At-rules. The
191+
// set of functional values Folio recognizes is small (~30 across
192+
// math/color/gradients/content/transforms/url) and spread across
193+
// half a dozen parsers, so a function-dispatch registry would be a
194+
// substantial refactor for a doc-only payoff. TestFunctionsDocCoverage
195+
// is the drift guard: it maintains a static list of the function
196+
// names that this section claims to document and asserts each is
197+
// referenced here AND that the relevant parser actually recognizes
198+
// the form.
199+
b.WriteString("## Functions\n\n")
200+
b.WriteString("CSS functional values recognized by Folio's parsers, grouped by category.\n")
201+
b.WriteString("Functions not listed here pass through as opaque text and almost always\n")
202+
b.WriteString("cause the containing declaration to be discarded.\n\n")
203+
b.WriteString("### Math\n\n")
204+
b.WriteString("Accepted everywhere a `<length>` or `<percentage>` is expected.\n")
205+
b.WriteString("Folio's parser preserves these as single tokens through shorthand splitting,\n")
206+
b.WriteString("so they survive inside `margin`, `padding`, `flex`, `transform()`, etc.\n\n")
207+
b.WriteString("| Function | Notes |\n")
208+
b.WriteString("|---|---|\n")
209+
b.WriteString("| `calc()` | Supports `+`, `-`, `*`, `/` with operator precedence and nested parentheses. Mixed units (e.g. `calc(100% - 20px)`) resolve at layout time. |\n")
210+
b.WriteString("| `min()` | Comma-separated argument list. Returns the smallest resolved value. |\n")
211+
b.WriteString("| `max()` | Comma-separated argument list. Returns the largest resolved value. |\n")
212+
b.WriteString("| `clamp()` | `clamp(<min>, <preferred>, <max>)`. |\n\n")
213+
b.WriteString("Known limitations: `calc()` does not yet expand inside `rotate()`, `scale()`, `skew()`, `background-position`, or `linear-gradient()` color stops — see issues #265, #266, #274, #275.\n\n")
214+
b.WriteString("### Color\n\n")
215+
b.WriteString("Accepted everywhere a `<color>` is expected. Output is sRGB regardless of input form.\n\n")
216+
b.WriteString("| Function | Notes |\n")
217+
b.WriteString("|---|---|\n")
218+
b.WriteString("| `rgb()` | `rgb(R, G, B)` or `rgb(R G B)`. Components are 0-255 integers or 0-100% percentages. |\n")
219+
b.WriteString("| `rgba()` | `rgba(R, G, B, A)`. Alpha is 0-1 or 0-100%. |\n")
220+
b.WriteString("| `hsl()` | `hsl(H, S%, L%)`. Hue in degrees. |\n")
221+
b.WriteString("| `hsla()` | `hsla(H, S%, L%, A)`. |\n")
222+
b.WriteString("| `cmyk()` / `device-cmyk()` | `cmyk(C, M, Y, K)` with components as 0-1 or 0-100%. Folio converts to sRGB for raster compositing; the original CMYK is preserved in the PDF color space for print pipelines. |\n\n")
223+
b.WriteString("Known unsupported color functions: `oklch()`, `oklab()`, `lch()`, `lab()`, `color-mix()`, `color()` — see [Known unsupported features](#known-unsupported-features) for workarounds.\n\n")
224+
b.WriteString("### Gradients\n\n")
225+
b.WriteString("Accepted as `background-image` values.\n\n")
226+
b.WriteString("| Function | Notes |\n")
227+
b.WriteString("|---|---|\n")
228+
b.WriteString("| `linear-gradient()` | Direction (`to right`, `45deg`, etc.) plus 2+ `<color>` stops. |\n")
229+
b.WriteString("| `repeating-linear-gradient()` | Same syntax; tiles the gradient pattern. |\n")
230+
b.WriteString("| `radial-gradient()` | Shape (`circle`, `ellipse`), size, and `<color>` stops. |\n")
231+
b.WriteString("| `repeating-radial-gradient()` | Same syntax; tiles the gradient pattern. |\n\n")
232+
b.WriteString("`conic-gradient()` is not supported.\n\n")
233+
b.WriteString("### Content and counters\n\n")
234+
b.WriteString("Used in `string-set`, `bookmark-label`, `content`, and `@page` margin boxes.\n\n")
235+
b.WriteString("| Function | Notes |\n")
236+
b.WriteString("|---|---|\n")
237+
b.WriteString("| `var()` | CSS custom property reference. Supports a fallback as the second argument: `var(--c, #000)`. Resolved BEFORE per-property dispatch, so functions and gradients receive resolved values. |\n")
238+
b.WriteString("| `attr()` | Reads an HTML attribute. Used in `bookmark-label`. |\n")
239+
b.WriteString("| `content()` | Substitutes the element's text content. Used in `string-set` and `bookmark-label`. |\n")
240+
b.WriteString("| `counter()` | `counter(<name>)` or `counter(<name>, <list-style>)`. Page counter `counter(page)` is supported in `@page` margin boxes. |\n")
241+
b.WriteString("| `counters()` | `counters(<name>, <separator>)` for nested counter chains. |\n")
242+
b.WriteString("| `string()` | Reads the latest value of a named string set via `string-set` (used in running headers). |\n\n")
243+
b.WriteString("Known unsupported: `target-counter()` for cross-references — tracked as #222.\n\n")
244+
b.WriteString("### Transform\n\n")
245+
b.WriteString("Used in `transform`. Multiple functions compose in the listed order.\n\n")
246+
b.WriteString("| Function | Notes |\n")
247+
b.WriteString("|---|---|\n")
248+
b.WriteString("| `translate()` | `translate(<tx>)` or `translate(<tx>, <ty>)`. Lengths in any supported unit; bare numbers treated as px. |\n")
249+
b.WriteString("| `translateX()` | Single `<length>` argument. |\n")
250+
b.WriteString("| `translateY()` | Single `<length>` argument. |\n")
251+
b.WriteString("| `rotate()` | Single `<angle>`: `deg`, `rad`, `grad`, `turn`, or bare number (degrees). |\n")
252+
b.WriteString("| `scale()` | `scale(<s>)` (uniform) or `scale(<sx>, <sy>)`. |\n")
253+
b.WriteString("| `scaleX()` | Single `<number>` argument. |\n")
254+
b.WriteString("| `scaleY()` | Single `<number>` argument. |\n")
255+
b.WriteString("| `skew()` | `skew(<ax>)` or `skew(<ax>, <ay>)`. |\n")
256+
b.WriteString("| `skewX()` | Single `<angle>` argument. |\n")
257+
b.WriteString("| `skewY()` | Single `<angle>` argument. |\n\n")
258+
b.WriteString("Known unsupported: `matrix()`, `matrix3d()`, `translate3d()`, `rotate3d()`, `scale3d()`, `perspective()` — Folio renders 2D only.\n\n")
259+
b.WriteString("### Other\n\n")
260+
b.WriteString("| Function | Notes |\n")
261+
b.WriteString("|---|---|\n")
262+
b.WriteString("| `url()` | Used in `background-image`, `@font-face` `src`, and asset references. Resolves through Folio's asset loader (BaseFS or HTTP via Client, subject to `Options.URLPolicy`). |\n\n")
263+
190264
// Known unsupported list — hardcoded for now; future work could
191265
// derive this from a separate registry.
192266
b.WriteString("## Known unsupported features\n\n")

html/css_props_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,3 +1104,75 @@ func TestAtRulesDocCoverage(t *testing.T) {
11041104
}
11051105
}
11061106
}
1107+
1108+
// TestFunctionsDocCoverage is the drift guard for the Functions section
1109+
// of CSS_SUPPORT.md. The list below mirrors what the doc claims to
1110+
// support; the test asserts (a) each name appears as a code-fenced
1111+
// reference in the rendered doc and (b) for every category, at least
1112+
// one representative form is actually recognized by the relevant
1113+
// parser. Adding a new function value to a Folio parser without
1114+
// updating the doc requires also updating this list — that's the
1115+
// forcing function.
1116+
//
1117+
// Function-call dispatch in Folio is spread across half a dozen files
1118+
// (properties.go for color/math, converter_style_parsers.go for
1119+
// transform, css_props.go for gradients, page.go for page-counter,
1120+
// converter_style.go for var/counter), so a single AST walk like
1121+
// TestAtRulesDocCoverage isn't tractable. A static list is the
1122+
// pragmatic alternative.
1123+
func TestFunctionsDocCoverage(t *testing.T) {
1124+
want := []string{
1125+
// Math
1126+
"calc()", "min()", "max()", "clamp()",
1127+
// Color
1128+
"rgb()", "rgba()", "hsl()", "hsla()", "cmyk()",
1129+
// Gradients
1130+
"linear-gradient()", "repeating-linear-gradient()",
1131+
"radial-gradient()", "repeating-radial-gradient()",
1132+
// Content / counters
1133+
"var()", "attr()", "content()", "counter()", "counters()", "string()",
1134+
// Transforms
1135+
"translate()", "translateX()", "translateY()",
1136+
"rotate()", "scale()", "scaleX()", "scaleY()",
1137+
"skew()", "skewX()", "skewY()",
1138+
// Other
1139+
"url()",
1140+
}
1141+
1142+
doc := RenderCSSPropertiesMarkdown()
1143+
for _, name := range want {
1144+
if !strings.Contains(doc, "`"+name+"`") {
1145+
t.Errorf("function %q is in the documented-functions list but not present in CSS_SUPPORT.md — add it to the Functions section in html/css_props_doc.go", name)
1146+
}
1147+
}
1148+
1149+
// Behavioral smoke checks: one representative form per category to
1150+
// catch the case where a function is documented but its parser was
1151+
// removed. Per-function parity is covered exhaustively elsewhere
1152+
// (parseLength, parseColor, parseTransform have their own test
1153+
// suites); these assertions are a bare sanity net.
1154+
if l := parseLength("calc(10px + 5px)"); l == nil {
1155+
t.Error("parseLength rejected calc(10px + 5px) — Math section is documenting an unsupported form")
1156+
}
1157+
if l := parseLength("min(10px, 20px)"); l == nil {
1158+
t.Error("parseLength rejected min(10px, 20px)")
1159+
}
1160+
if l := parseLength("max(10px, 20px)"); l == nil {
1161+
t.Error("parseLength rejected max(10px, 20px)")
1162+
}
1163+
if l := parseLength("clamp(5px, 10px, 20px)"); l == nil {
1164+
t.Error("parseLength rejected clamp(5px, 10px, 20px)")
1165+
}
1166+
if _, ok := parseColor("rgb(0, 0, 0)"); !ok {
1167+
t.Error("parseColor rejected rgb(0, 0, 0)")
1168+
}
1169+
if _, ok := parseColor("hsl(0, 0%, 0%)"); !ok {
1170+
t.Error("parseColor rejected hsl(0, 0%, 0%)")
1171+
}
1172+
if _, ok := parseColor("cmyk(0, 0, 0, 1)"); !ok {
1173+
t.Error("parseColor rejected cmyk(0, 0, 0, 1)")
1174+
}
1175+
if ops := parseTransform("translate(5px, 10px) rotate(45deg)"); len(ops) != 2 {
1176+
t.Errorf("parseTransform produced %d ops for translate+rotate; want 2", len(ops))
1177+
}
1178+
}

0 commit comments

Comments
 (0)