Module: github.com/titpetric/lessgo
The dst/ package contains the parser and formatter. Before making ANY changes to files in dst/:
- Request explicit confirmation from the user
- Understand that changes may impact parsing behavior globally
- Document the change in this AGENTS.md file
- Test against all fixtures
- Use
expression/package for token parsing and function evaluation evaluator/package tokenizer is for guard conditions (simple expressions)expression/package should handle quoted strings and nested function calls properly- Prefer to solve problems in
renderer/,expression/functions/, and evaluator chains
Potential Future Cleanup: Merge evaluator/ → expression/
- The
evaluator/package only contains a tokenizer for guard conditions - The
expression/package is the full expression evaluator with variables & functions - They solve related problems but at different abstraction levels
- When consolidating, move evaluator tokenizer into expression/ (or deprecate in favor of expression)
- This is NOT urgent - the current separation works fine and maintains clarity
Fix missing/unused imports automatically:
goimports -w .This should be used instead of manually adjusting imports to ensure consistency.
Run all tests with coverage (via task runner):
task testTest specific fixture:
go test -v -run 'TestFixtures/NNN-description' ./...Clear test cache before running tests with -count=1:
go clean -testcache && go test ./... -count=1Use -count=1 to disable caching and force fresh test runs. Without clearing the cache first, you'll see "(cached)" in output even with -count=1.
Run benchmarks with multi-CPU profiling:
task benchGenerate coverage reports:
task coverNote: Fixture tests now read pre-computed .css files (via lessc) instead of calling lessc at test time. This speeds up tests 280x (15s → 53ms). The .css files are the ground truth and are maintained in version control.
Render via lessgo (direct CSS output):
./bin/lessgo generate 'testdata/fixtures/FILE.less' > /tmp/lessgo_out.cssVerify identical CSS output:
diff /tmp/lessc_out.css /tmp/lessgo_out.cssFormat single file (stdout):
./bin/lessgo fmt testdata/fixtures/FILE.lessFormat in-place:
./bin/lessgo fmt -w testdata/fixtures/FILE.lessFormat multiple files:
./bin/lessgo fmt -w testdata/fixtures/*.lessGenerate CSS from single file:
./bin/lessgo generate 'testdata/fixtures/FILE.less'Generate CSS from multiple files:
./bin/lessgo generate 'testdata/fixtures/*.less' -o output.cssTo inspect the AST for a .less file:
go run ./cmd/lessgo ast testdata/fixtures/FILE.lessgo build -o bin/lessgo ./cmd/lessgogo test ./dst ./expr ./fn ./renderer -vTest fixtures are organized by feature number (e.g., 004-operations.less, 050-math-functions-basic.less).
Run against all fixtures:
for f in testdata/fixtures/*.less; do
echo "Testing $f..."
./bin/lessgo fmt "$f" | lessc - > /tmp/out.css 2>/dev/null
lessc "$f" > /tmp/expected.css 2>/dev/null
diff -q /tmp/out.css /tmp/expected.css || echo "FAIL: $f"
done- Add feature to
FEATURES.mdin "In Progress" section - Create test fixture in
testdata/fixtures/NNN-description.less - Verify fixture compiles with lessc
- Implement feature in appropriate package (dst, expr, fn, etc)
- Validate:
./bin/lessgo fmt fixture.less | lessc - | diff - <(lessc fixture.less) - Move feature to "Done" in FEATURES.md
Run all fixtures against lessc to identify failures:
Failfast mode (stop on first failure with diff):
./bin/lessgo fmt fixture.less | lessc - > /tmp/out.css 2>/dev/null
lessc fixture.less > /tmp/expected.css 2>/dev/null
diff /tmp/expected.css /tmp/out.cssCount passing/failing:
for f in testdata/fixtures/*.less; do
./bin/lessgo fmt "$f" | lessc - > /tmp/out.css 2>/dev/null
lessc "$f" > /tmp/expected.css 2>/dev/null
if ! diff -q /tmp/out.css /tmp/expected.css > /dev/null 2>&1; then
echo "FAIL: $f"
fi
done-
dst/: Data structure tree (parser, formatter, node types)
parser.go: Parses .less files into DSTformatter.go: Formats DST back to .less (less->less)node.go: Node types (Block, Decl, Comment, MixinCall)resolver.go: Variable and expression resolution (used by formatter)stack.go: Variable scope stack management
-
renderer/: CSS code generation (less->css)
renderer.go: Renders DST to CSS with expression evaluation- Handles nested selectors, parent references (&), mixin expansion
- Evaluates embedded LESS functions in property values
-
expr/: Expression evaluation (Value, Parse, Evaluator)
parse.go: Parses numeric values with unitsevaluator.go: Evaluates expressions with variables and functionscolor.go: Color representation and parsingvalue.go: Value type with arithmetic operations
-
expression/functions/: LessCSS functions (math, color, type, list, image functions)
func_math.go: ceil, floor, round, abs, sqrt, pow, min, maxfunc_colors.go: lighten, darken, saturate, desaturate, rgb, rgbafunc_types.go: isnumber, isstring, iscolor, etc.func_strings.go: escape, e, replace, formatfunc_images.go: image-width, image-height, image-size (for local files)
-
cmd/lessgo/: CLI tool
fmtcommand: Format .less files for consistent indentationgeneratecommand: Compile .less files to CSS
The expr package handles arithmetic with units:
v, _ := expr.Parse("10px")
v2, _ := expr.Parse("5")
result, _ := v.Multiply(v2) // 50px
// Percentages convert to decimals
pct, _ := expr.Parse("50%") // 0.5 (no unit)Math functions are separated into the fn package for independent testing:
go test ./fn -v # Test only functions
go test ./expr -v # Test only expression evaluation
go test ./dst -v # Test only DST parsing
go test ./... -v # Test everythingWhen implementing a feature:
- Create fixture:
testdata/fixtures/NNN-description.less - Verify with lessc:
lessc testdata/fixtures/NNN-description.less > /tmp/expected.css - Implement feature in appropriate package (dst, expr, fn, renderer)
- Test with lessgo:
./bin/lessgo generate testdata/fixtures/NNN-description.less > /tmp/actual.css - Verify match:
diff /tmp/expected.css /tmp/actual.css
The NodeContext struct holds rendering state including:
Buf: Output bufferStack: Variable scope stackBaseDir: Base directory for resolving relative file paths (e.g., for image functions)
When implementing file-aware features (like image functions):
- Store context in
NodeContext.BaseDir - Call
renderer.RenderWithBaseDir(astFile, baseDir)to set the directory - Functions can access
functions.BaseDirglobal variable - The global is set during rendering and automatically propagated through NodeContext
The image-width(), image-height(), and image-size() functions read local image files to extract dimensions:
- Registered as
image-width,image-height,image-sizein expression/html_template.go - Implemented in expression/functions/func_images.go
- Support PNG, JPEG, GIF formats via Go's standard image packages
- Resolve relative paths from the base directory passed to Renderer
- Return unimplemented error for external URLs (http://, https://)
- Cache dimensions to avoid re-reading files
Example usage:
.hero {
width: image-width('hero.png');
height: image-height('hero.png');
background-size: image-size('hero.png');
}- DST is data model: Parse .less as declarative structure, not imperative
- Lessc is source of truth: All CSS output must match lessc compilation
- Separate concerns:
dsthandles LESS->LESS formattingrendererhandles LESS->CSS code generationexprhandles expression evaluationfnhandles LESS/CSS functions
- Unit test packages separately: expr and fn packages should have isolated tests
- Minimal parser: Keep parser simple, complexity goes in expr/fn/renderer packages