javascriptgrammarjison

Defining Grammar for import statements for a jison generated css präprozessor languages


Im trying to generate a stylsheet parser with some extras to experiment with jison. How can I implement the import directive to load other files into the main file? I'm a little bit confused. Is there a way to use the lexer in the grammer file? Can I read the file and then tokenize it?

grammar.jison

%{
  var nodes = require('./nodes')
%}

%%

// Parsing starts here.
stylesheet:
  statements EOF                   { return new nodes.StyleSheet($1) }          
;

statements:
  /* empty */                      { $$ = [] }
| statementGroup                   { $$ = $1 }
| statements ';' statementGroup    { $$ = $1.concat($3) }
| statements ';'                   { $$ = $1 }
;

statementGroup:
  statement                        { $$ = [ $1 ] }
| rules
| rules statement                  { $$ = $1.concat($2) }
;

statement:
  variableDeclaration
;


rules:
  rule                             { $$ = [ $1 ] }
| rules rule                       { $$ = $1.concat($2) }
;

rule:
  selector '{' declarations '}'    { $$ = new nodes.Rule($1, $3) }
;

selector:
  IDENTIFIER
| SELECTOR
;

declarations:
  /* empty */                       { $$ = [] }
| declarationGroup                  { $$ = $1 }
| declarations ';' declarationGroup { $$ = $1.concat($3) }
| declarations ';'                  { $$ = $1 }
;

declarationGroup:
  declaration                       { $$ = [ $1 ] }
| rules
| rules declaration                 { $$ = $1.concat($2) }
;

declaration:
  property
| variableDeclaration
;

property:
  IDENTIFIER ':' values            { $$ = new nodes.Property($1, $3) }
;

variableDeclaration:
  VARIABLE ':' values              { $$ = new nodes.Assign($1, $3) }
;

values:
  value                            { $$ = [ $1 ] }
| values value                     { $$ = $1.concat($2) }
;

value:
  IDENTIFIER                       { $$ = new nodes.Literal($1) }
| COLOR                            { $$ = new nodes.Literal($1) }
| NUMBER                           { $$ = new nodes.Literal($1) }
| DIMENSION                        { $$ = new nodes.Literal($1) }
| VARIABLE                         { $$ = new nodes.Variable($1) }
;

tokens.jisonlex

// Order is important. Rules are matches from top to bottom.

//// Macros
DIGIT                 [0-9]
NUMBER                {DIGIT}+(\.{DIGIT}+)? // matches: 10 and 3.14
NAME                  [a-zA-Z][\w\-]*       // matches: body, background-color and myClassName
SELECTOR              (\.|\#|\:\:|\:){NAME} // matches: #id, .class, :hover and ::before
PATH                  (.+)/([^/]+)          // matches ./bla/bla/nested.sss

%%

//// Rules
\s+                   // ignore spaces, line breaks

// Numbers
{NUMBER}(px|em|\%)    return 'DIMENSION' // 10px, 1em, 50%
{NUMBER}              return 'NUMBER' // 0
\#[0-9A-Fa-f]{3,6}    return 'COLOR' // #fff, #f0f0f0

// Selectors
{SELECTOR}            return 'SELECTOR' // .class, #id
{NAME}{SELECTOR}      return 'SELECTOR' // div.class, body#id

\@{NAME}              return 'VARIABLE' // @variable


{NAME}                return 'IDENTIFIER' // body, font-size

.                     return yytext // {, }, +, :, ;

<<EOF>>               return 'EOF'

nodes.js

var Context = require('./context').Context

var compressed

function StyleSheet(rules, ss) {
  this.rules = rules
  this.ss = ss ? ss : []
}
exports.StyleSheet = StyleSheet

StyleSheet.prototype.toCSS = function(output) {
  compressed = output || false

  var context = new Context()

  var ret = this.rules.map(function (rule) { 
return rule.toCSS(context) }).filter(function (value) { return typeof value !== 'undefined' }).join('\n')

  return compressed ? ret.replace(/\s+/g, '') : ret
}

function Rule(selector, declarations) {
  this.selector = selector
  this.declarations = declarations
}
exports.Rule = Rule

Rule.prototype.toCSS = function(parentContext) {
  var propertiesCSS = [],
      nestedRulesCSS = [],
      context = new Context(this, parentContext)

  this.declarations.forEach(function(declaration) {
    var css = declaration.toCSS(context)

    if (declaration instanceof Property) {
      propertiesCSS.push(css)
    } else if (declaration instanceof Rule) {
      nestedRulesCSS.push(css)
    }
  })

  return [ context.selector() + ' { ' + propertiesCSS.join(' ') +  ' }' ].
         concat(nestedRulesCSS).
         join('\n')
}


function Property(name, values) {
  this.name = name
  this.values = values
}
exports.Property = Property

Property.prototype.toCSS = function(context) {
  var valuesCSS = this.values.map(function(value) { return value.toCSS(context) })
  return this.name + ': ' + valuesCSS.join(' ') + ';'
}


function Literal(value) {
  this.value = value
}
exports.Literal = Literal


Literal.prototype.toCSS = function() {
  return this.value
}


function Variable(name) {
  this.name = name
}
exports.Variable = Variable

Variable.prototype.toCSS = function(context) {
  return context.get(this.name)
}


function Assign(name, values) {
  this.name = name
  this.values = values
}
exports.Assign = Assign

Assign.prototype.toCSS = function(context) {
  var valuesCSS = this.values.map(function(value) { return value.toCSS(context) })
  context.set(this.name, valuesCSS.join(' '))
}

Solution

  • As far as I know, the basic jison lexer does not allow for incremental input; you need to give it a single string which it tokenises.

    You can, however, use your own custom lexer (including calling into the jison lexer) in order to do incremental lexing. So your custom lexer would need to implement the input stack to implement to include command. It shouldn't be particularly difficult, although I don't have an example near at hand.