New:Microsoft Teams Notifications Are Now Available in Socket.Learn more
Get Started

@jesscss/less-parser

Package Overview
Dependencies
Maintainers
1
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@jesscss/less-parser

Jess LESS parser

npmnpm
Version
2.0.0-alpha.22
Version published
Weekly downloads
672
-31.29%
Maintainers
1
Weekly downloads
 
Created
Source

@jesscss/less-parser

The Less grammar, layered on the CSS base parser, with core-free CST entry points.

Status: alpha. Part of Jess. The broader language/tooling picture is still early. Expect gaps and report bugs. Docs live at jesscss.github.io.

What it is

The Less grammar extends the spec-aligned CSS base in @jesscss/css-parser: unchanged CSS structure remains CSS-owned, and Less changes only the smallest child, value slot, or reference its syntax requires. It adds @variable / @{interpolation}, mixins, and the rest of Less. Parseman currently compiles the CSS and Less host factories from shared recognition artifacts rather than literally composing a terminal cssGrammar artifact; that macro boundary does not relax the ownership rule. It is built on parsemanthe fastest general-purpose JavaScript parser in its published benchmarks (see @jesscss/css-parser for figures and engineering details). It is the parser Jess uses when it compiles .less — the "Now" tier of the language roadmap, and the one dialect shipping in the alpha.

The default parse() operation constructs canonical AST v2 Stylesheet directly through parser-local Parseman reductions. Use the explicit ./cst entry when a language-service or document consumer needs a CST. The package has no core-owned parser driver or AST construction host.

Install

npm install @jesscss/less-parser

@jesscss/core is an optional peer for consumers using the default AST v2 parse() result. The explicit CST and grammar subpaths remain core-free. Those explicit entries expose Parseman types and grammar values; Parseman ships as a bundled dependency, so it installs with the package automatically.

Canonical AST parsing

import { parse } from '@jesscss/less-parser'

const stylesheet = parse('@c: red;\n.foo { color: @c; }')

stylesheet.type // 'Stylesheet'

Standalone usage (core-free)

import { parseLessCst } from '@jesscss/less-parser/cst'

const result = parseLessCst('@c: red;\n.foo { color: @c; }')

result.ok               // true
result.errors           // ParseError[] (empty when ok)
result.unconsumedFrom   // index of first unparsed char, or null
result.tree             // the CST root (a StyleSheet node)

Signature:

parseLessCst(input: string, startRule = 'Stylesheet', options?: { collapse?: boolean }): LessCstParseResult

Pass a different startRule (any capitalized grammar rule, e.g. 'SelectorList', 'Declaration') to parse a fragment.

Public API

EntryExportPurpose
@jesscss/less-parser/cstparseLessCstCore-free parse of a Less string to a CST.
@jesscss/less-parser/cstLessCstNode, LessCstLeaf, LessCstError, LessCstChild, LessCstParseResult, LessCstType (types)CST type definitions (aliases of the shared @jesscss/css-parser/cst types).
@jesscss/less-parser/grammarlessGrammarThe compiled Less AST grammar (a rule map). Extend it with compose() or drive it directly with parseman's run. See the variant table below.
@jesscss/less-parser (.)parseParse Less directly to canonical AST v2 Stylesheet. It does not load the CST grammar.

Line-aware entries

parse and the CST parsers come in two bindings, one per compiled table, so an entry never loads a table it does not parse with:

EntryExportTreePositions
@jesscss/less-parser (.)parseASTno
@jesscss/less-parser/positionsparseASTyes
@jesscss/less-parser/cstparseLessCst, parseLessDocCSTno
@jesscss/less-parser/cst/positionsparseLessCst, parseLessDocCSTyes

The /positions entries export the same names bound to the line-aware table: switching is a change of import specifier, not of call site.

Choosing a grammar build

Each compiled grammar is a standalone multi-megabyte artifact, so the four variants ship as four separate files. Importing one never loads the others. Pick by the two questions the subpath name answers — which tree, and whether source positions are tracked:

SubpathExportTreePositions
@jesscss/less-parser/grammar/astlessGrammarASTno
@jesscss/less-parser/grammar/ast/positionslessPositionsGrammarASTyes
@jesscss/less-parser/grammar/cstlessCstGrammarCSTno
@jesscss/less-parser/grammar/cst/positionslessCstPositionsGrammarCSTyes

@jesscss/less-parser/grammar is an alias for /grammar/ast, the build the shipping parse() route uses. It is not a barrel: it exposes the AST variant only, so importing it cannot pull the other three in.

The positions variants set startLine/startColumn on every span. There is no trackLines option: an option would force one module to name both tables, and Node executes every module it statically imports, so the choice is which entry you import. Error tolerance is not a property of a build — the CST runner collects result.errors on either CST variant.

Default CST shape

The CST is parseman's, produced by the shared cssCstBuildHost. Three kinds of node:

  • node{ _tag: 'node', type, grammarType, span: { start, end }, state, children } (grammarType = raw rule name; type = friendly public name).
  • leaf{ _tag: 'leaf', value, span } for terminals.
  • error{ _tag: 'error', type, span, expected, children, state } where recovery happened.

Spans are [start, end) offsets; whitespace, block comments, and Less line comments (//) are trivia and do not appear as children.

Parsing @c: red;\n.foo { color: @c; } yields (abridged):

{
  "_tag": "node", "type": "StyleSheet", "grammarType": "Stylesheet",
  "children": [
    { "_tag": "node", "type": "VarDeclaration", "grammarType": "VarDeclaration", "span": { "start": 0, "end": 8 },
      "children": [
        { "_tag": "leaf", "value": "@c" }, { "_tag": "leaf", "value": ":" },
        { "_tag": "node", "type": "Keyword", "grammarType": "Keyword",
          "children": [ { "_tag": "leaf", "value": "red" } ] },
        { "_tag": "leaf", "value": ";" }
      ] },
    { "_tag": "node", "type": "QualifiedRule", "grammarType": "Ruleset", "span": { "start": 9, "end": 28 },
      "children": [
        { "_tag": "leaf", "value": ".foo" }, { "_tag": "leaf", "value": "{" },
        { "_tag": "node", "type": "Declaration", "grammarType": "Declaration",
          "children": [
            { "_tag": "leaf", "value": "color" }, { "_tag": "leaf", "value": ":" },
            { "_tag": "node", "type": "Reference", "grammarType": "Reference",
              "children": [ { "_tag": "leaf", "value": "@c" } ] },
            { "_tag": "leaf", "value": ";" }
          ] },
        { "_tag": "leaf", "value": "}" }
      ] }
  ]
}

Note the Less-specific nodes: a top-level @c: … becomes a VarDeclaration, and a @c value becomes a Reference. The color keyword red parses as a plain Keyword — the same node every dialect uses (NamedColor→Keyword convergence); its colour-ness is resolved only when it is operated on.

Pass { collapse: true } to unwrap single-child wrapper types (Reference, InterpolatedSelector) into their child.

Name-independent condition arguments

A top-level condition operator (> < >= <= = and or not) inside any call's argument parses as a Condition node — there is no parse-time name-dispatch on if/boolean. if(@a > 5, 1, 2), boolean(not(2 < 1)), #ns.if(@a > 5), and foo(@a > 5 and @b < 2) all route through the ordinary function/mixin Call production; the shared call-arg rule (ArgConditionCondArgOr/CondArgAnd/CondArgTerm) layers the condition-operator precedence chain on top of the normal value production. The layer is structurally gated: it only matches when a real operator is present, so a plain value / space-list argument (and mixin-definition params) fall through to the unchanged valueSequence byte-identically. Eval treats if/boolean as ordinary registered functions that consume the parsed Condition, so this is a parse-only unification (a deliberate v5 loosening vs Less 4.x, which name-dispatched and errored on the namespaced/generic forms).

One known gap: a namespace/accessor call in value position (b: #ns.if(@a > 5), b: .if(@a > 5)) is reassembled from a raw permissive-paren capture (_buildRefCallArgs), a separate shallow path that does not run the condition layer — its args stay a value list. Statement-position (#ns.if(@a > 5) { } / bare #ns.if(@a > 5)) and all function-call forms are covered.

Extending with your own builders

The grammar is decoupled from the tree it builds. Every capitalized rule is a parseman node(); when you drive a grammar with a build host, each node() calls your host instead of constructing the default CST. Use parseman's run with your own host and the grammar's trivia rule:

import { run } from 'parseman'
import { lessGrammar } from '@jesscss/less-parser/grammar'

const myHost = (type, children, fields, span) => ({ type, span, rules: children.filter(Boolean) })

const result = run(lessGrammar.Stylesheet, '@c: red; .foo { color: @c; }', {
  build: myHost,
  trivia: lessGrammar.rw   // Less trivia = whitespace + block + line comments
})

result.value   // the root node your host returned

The BuildHost signature (from parseman):

type BuildHost = (
  type: string,
  rules: readonly unknown[],
  fields: FieldMap | undefined,
  span: { start: number; end: number },
  rawChildren: readonly unknown[],
  triviaLog: readonly number[],
  state: unknown
) => unknown

parseLessCst(...) is this pattern with the shared cssCstBuildHost (see @jesscss/css-parser, src/cst.ts) as a reference host.

Part of Jess

This package is developed as part of Jess. Jess translates a Less string into the core Jess AST, which the compiler then evaluates and renders to CSS. Licensed MIT.

FAQs

Package last updated on 20 Sep 2026

Related posts