Skip to content

Embedding · The addressable pipeline

compile is four stages chained together. Each stage is a public method, so tooling — editors, linters, language servers — can call exactly the stage it needs without paying for the rest.

Method Stage Input → Output
tokenize(source) lex str / SourceTokenStream
parse(source) parse str / Source / TokenStreamRuleset (AST; declaration values still raw text)
evaluate(root, src=…) eval RulesetRuleset (@import resolved, variables substituted, mixins expanded, :extend applied, at-rules bubbled)
emit(root, src=…) emit Ruleset → CSS str (or SourceMapResult)
compile(source) parse + eval + emit str → CSS str

Running the stages by hand

The cleanest way is to wrap your source in a Source once and thread it through, so error locations and source maps know the filename:

from lessish import Lessish, Source

ls = Lessish()
src = Source('@c: red; .a { color: @c; }', filename='styles.less')

tokens = ls.tokenize(src)        # lex
ast = ls.parse(tokens)           # parse — reuses the token stream
evaled = ls.evaluate(ast, src=src)   # variables / mixins resolved
css = ls.emit(evaled, src=src)       # final CSS
# => '.a {\n  color: red;\n}\n'

Note parse accepts the TokenStream returned by tokenize, so a language server that already tokenised for syntax highlighting doesn't pay the regex cost twice.

src is optional. If you only need a quick transform and don't care about filenames in diagnostics, omit it:

from lessish import Lessish

ls = Lessish()
css = ls.emit(ls.evaluate(ls.parse('@c: red; .a { color: @c; }')))
# => '.a {\n  color: red;\n}\n'

Stopping early — tokens and AST without a full compile

tokenize and parse are useful on their own for tooling that wants a position-aware view of the source. Crucially, they work even on input that evaluate would reject (backtick JS, @plugin, remote @import) — those only fail at the eval stage. See ../philosophy.md.

from lessish import Lessish, TokenStream, Ruleset

ls = Lessish()

tokens = ls.tokenize('@c: red; .a { color: @c; }')
assert isinstance(tokens, TokenStream)
assert len(tokens) > 0          # iterable, sized, indexable

ast = ls.parse('@c: red; .a { color: @c; }')
assert isinstance(ast, Ruleset)

Subclassing — override one stage

Because each stage is a method and compile calls them through self, overriding one stage automatically affects compile. Per-compile state (EvalContext, the emitter, guard state) is built fresh inside each method and never held on the instance, so overrides stay thread-safe.

from lessish import Lessish

class CountingLessish(Lessish):
    """A Lessish that counts how many times it emits CSS."""

    emit_calls = 0

    def emit(self, root, **kwargs):
        type(self).emit_calls += 1
        return super().emit(root, **kwargs)

ls = CountingLessish()
ls.compile('.a { color: red; }')   # routes through the overridden emit
ls.compile('.b { color: blue; }')
assert CountingLessish.emit_calls == 2

Typical uses: wrap emit with timing, swap evaluate for a sandboxed-imports variant, or instrument parse for an editor.

Prev: ← Source maps · Next: Errors →