Skip to content

Commit 5458a46

Browse files
authored
Merge pull request #2 from mahirshah/json-formatter
Add JsonGrammarFormatter and basic format json grammars script
2 parents 1a53ec7 + 8584c8f commit 5458a46

17 files changed

+259
-29
lines changed

src/constants/grammars.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
R_GRAMMAR_IDENT: /^<(([a-z-]+)(\(\))?)>$/,
3+
LEXICAL_BASE_KEY: '__base__',
4+
SYNTACTIC_BASE_KEY: '__Base__',
5+
DOUBLE_BAR_PARAMETERIZED_RULE_NAME: 'UnorderedOptionalTuple',
6+
};

src/constants/paths.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
// TODO: use __dirname for all paths
12
module.exports = {
2-
JSON_GRAMMAR_PATH: './src/grammars/',
3-
OHM_GRAMMAR_PATH: './ohm-grammars/',
3+
JSON_GRAMMAR_PATH: './src/jsonGrammars/',
4+
OHM_GRAMMAR_PATH: './ohm-jsonGrammars/',
45
DATA_PATH: './data',
56
FORMATTED_DATA_PATH: './formatted-data/',
67
};

src/data/acquire.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
22
* Script used to acquire CSS data from MDN repo. Writes json files out to DATA_PATH.
3+
* TODO: once MDN publishes data via npm, we can remove this
34
*/
45
const fetch = require('node-fetch');
56
const fs = require('fs-extra');

src/formatFormalSyntaxes.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Format each formal syntax into a json grammar
3+
*/
4+
const fs = require('fs-extra');
5+
const ohm = require('ohm-js');
6+
const JsonGrammarFormatter = require('./formatters/JsonGrammarFormatter');
7+
8+
9+
const grammarContents = fs.readFileSync('./src/grammars/formalSyntax.ohm');
10+
const formalSyntaxGrammar = ohm.grammar(grammarContents);
11+
const s = new JsonGrammarFormatter(formalSyntaxGrammar)
12+
.formatFormalSyntax('background', '<bg-image> || <position> [ / <bg-size> ]? || <repeat-style> || <attachment> || <box>{1,2}');
13+
console.log(s);

src/formatGrammars.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
/**
2+
* Format each JSON grammar into an Ohm grammar
3+
*/
14
const fs = require('fs-extra');
25
const OhmGrammarFormatter = require('./formatters/OhmGrammarFormatter');
36
const PATHS = require('./constants/paths');
47

5-
const json = fs.readJsonSync('./src/grammars/angle.json');
8+
const json = fs.readJsonSync('./src/jsonGrammars/angle.json');
69

710
fs.outputFile(`${PATHS.OHM_GRAMMAR_PATH}angle.ohm`, OhmGrammarFormatter.formatOhmGrammarFromJson(json, 'Angle'))
811
.then(() => console.log(`Successfully wrote grammar to ${PATHS.OHM_GRAMMAR_PATH}`));
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
const GRAMMAR_CONSTANTS = require('../constants/grammars');
2+
3+
const INTERMEDIATE_GRAMMAR_PREFIX = 'IntermediateRule';
4+
const TERMINAL_GRAMMARS = ['dataName', 'literal', 'node'];
5+
6+
/**
7+
* Class to convert a CSS formal syntax into a JSON grammar.
8+
* See https://developer.mozilla.org/en-US/docs/Web/CSS/Value_definition_syntax for more info on CSS value definition
9+
* syntax.
10+
* @type {JsonGrammarFormatter}
11+
*/
12+
module.exports = class JsonGrammarFormatter {
13+
14+
/**
15+
* Creates a JsonGrammarFormatter which can be used to convert a CSS formal syntax string into a JSON grammar.
16+
*
17+
* @param {Object} formalSyntaxGrammar - an ohm grammar to parse a CSS formal syntax
18+
*/
19+
constructor(formalSyntaxGrammar) {
20+
this.formalSyntaxGrammar = formalSyntaxGrammar;
21+
this.formalSyntaxSemantics = this._generateSemantics();
22+
}
23+
24+
/**
25+
* Attempts to format a formal syntax string into a JSON grammar. Returns a JSON grammar if the formal syntax
26+
* matches the given formal syntax grammar. If the match fails, an error in thrown.
27+
*
28+
* @param {string} propertyName - the property this formal syntax is associated with. Should be in kebab-case format.
29+
* For example, "bg-color" or "color()".
30+
* @param {string} formalSyntax - the css formal syntax string. For example, "white | blue | red || green"
31+
* @returns {Object} - a JSON grammar if the formal syntax matches the formal syntax grammar, else throws
32+
* a an error indicating why the match failed.
33+
*/
34+
formatFormalSyntax(propertyName, formalSyntax) {
35+
this.intermediateGrammarIndex = 0;
36+
this.intermediateGrammars = [];
37+
this.grammarsToResolve = new Set();
38+
this.propertyName = propertyName;
39+
const match = this.formalSyntaxGrammar.match(formalSyntax);
40+
41+
if (match.succeeded()) {
42+
const baseOhmGrammar = this.formalSyntaxSemantics(match).eval();
43+
const grammarsToResolve = Array.from(this.grammarsToResolve).map(grammarName => [grammarName]);
44+
return [
45+
[GRAMMAR_CONSTANTS.LEXICAL_BASE_KEY, baseOhmGrammar],
46+
...this.intermediateGrammars,
47+
...grammarsToResolve,
48+
];
49+
}
50+
51+
throw new Error(`Formal syntax: ${formalSyntax}, failed to match: ${match.message}`);
52+
}
53+
54+
/* eslint-disable no-unused-vars */
55+
/**
56+
* Generates the Ohm Semantics object corresponding to the formal syntax grammar found in
57+
* "../grammars/formalSyntax.ohm".
58+
*
59+
* @returns {Object} - the Ohm Semantics object based on the formal syntax grammar
60+
* @private
61+
*/
62+
_generateSemantics() {
63+
const grammarFormatter = this;
64+
65+
return this.formalSyntaxGrammar.createSemantics().addOperation('eval', {
66+
// simply the root formal syntax
67+
Exp(baseExpression) {
68+
return baseExpression.eval();
69+
},
70+
71+
// syntax of the form: "[ <expression> ]"
72+
Brackets(leftBracket, e, rightBracket) {
73+
return `( ${e.eval()} )`;
74+
},
75+
76+
// syntax of the form: "<expression> <expression>"
77+
Juxtaposition(expression1, expression2) {
78+
return `${expression1.eval()} ${expression2.eval()}`;
79+
},
80+
81+
// syntax of the form: "<expression> && <expression>"
82+
DoubleAmpersand(expression1, doubleAmp, expression2) {
83+
const expression1Eval = expression1.eval();
84+
const expression2Eval = expression2.eval();
85+
86+
return `( ${expression1Eval} ${expression2Eval} ) | ( ${expression2Eval} ${expression1} )`;
87+
},
88+
89+
// syntax of the form: "<expression> || <expression>"
90+
DoubleBar(expression1, doubleBar, expression2) {
91+
const expressionEvaluations = [expression1, expression2]
92+
.map(expression => [expression, grammarFormatter._getNodeName(expression)])
93+
.map(([expression, nodeName]) => [expression, TERMINAL_GRAMMARS.includes(nodeName)])
94+
.map(([expression, isTerminal]) => {
95+
if (!isTerminal) {
96+
const intermediateGrammarRuleName = grammarFormatter._generateIntermediateGrammarRuleName();
97+
grammarFormatter.intermediateGrammars.push([intermediateGrammarRuleName, expression.eval()]);
98+
return intermediateGrammarRuleName;
99+
}
100+
101+
return expression.eval();
102+
})
103+
.join(' , ');
104+
105+
return `${GRAMMAR_CONSTANTS.DOUBLE_BAR_PARAMETERIZED_RULE_NAME}< ${expressionEvaluations} >`;
106+
},
107+
108+
// syntax of the form: "<expression> | <expression>"
109+
SingleBar(expression1, singleBar, expression2) {
110+
return `${expression1.eval()} | ${expression2.eval()}`;
111+
},
112+
113+
// syntax of the form: "<expression>*"
114+
Asterisk(expression, asterisk) {
115+
return `${expression.eval()}*`;
116+
},
117+
118+
// syntax of the form: "<expression>+"
119+
Plus(expression, plus) {
120+
return `${expression.eval()}+`;
121+
},
122+
123+
// syntax of the form: "<expression>?"
124+
QuestionMark(expression, questionMark) {
125+
return `${expression.eval()}?`;
126+
},
127+
128+
// syntax of the form: "<expression>{<integer>, <integer>}"
129+
CurlyBraces(expression, b1, lowerLimit, comma, upperLimit, b2) {
130+
const min = +lowerLimit.sourceString;
131+
const max = +upperLimit.sourceString;
132+
const minimumString = new Array(min).fill().map(() => expression.eval()).join(' ');
133+
const maximumString = new Array(max - min).fill().map(() => `${expression.eval()}?`).join(' ');
134+
135+
return `${minimumString} ${maximumString}`;
136+
},
137+
138+
// syntax of the form: "<data-name>" or "<'data-name'>
139+
node(leftBracket, leftQuote, dataName, rightQuote, rightBracket) {
140+
const dataNameValue = dataName.eval();
141+
142+
grammarFormatter.grammarsToResolve.add(`<${dataNameValue}>`);
143+
return `<${dataNameValue}>`;
144+
},
145+
146+
// any string
147+
dataName(e) {
148+
return this.sourceString;
149+
},
150+
151+
// a character literal like "," or "/"
152+
literal(e) {
153+
return `"${this.sourceString}"`;
154+
},
155+
});
156+
}
157+
158+
/**
159+
* Returns the node name of the given node
160+
* @param {Object} node - an Ohm AST node
161+
* @returns {string} - the node name of the given node
162+
* @private
163+
*/
164+
_getNodeName(node) {
165+
return node.numChildren && node.children[0].ctorName;
166+
}
167+
168+
/**
169+
* Generates the next intermediate grammar rule name. This is used in when parsing double bar expressions
170+
* that don't contain terminal expressions. For example, given the formal syntax "(a | b) || c", we would have
171+
* to create a intermediate rule for the left hand side of the double bar expression. Thus we would want something
172+
* like:
173+
* exp = UnorderedOptionalTuple< IntermediateRule1, c >
174+
* IntermediateRule1 = (a | b)
175+
*
176+
* @returns {string}
177+
* @private
178+
*/
179+
_generateIntermediateGrammarRuleName() {
180+
return `${this.propertyName}${INTERMEDIATE_GRAMMAR_PREFIX}${this.intermediateGrammarIndex++}`;
181+
}
182+
};

src/formatters/OhmGrammarFormatter.js

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
const CaseConverterUtils = require('../utils/CaseConverterUtils');
22
const fs = require('fs-extra');
33
const PATHS = require('../constants/paths');
4+
const GRAMMAR_CONSTANTS = require('../constants/grammars');
45

56
const BASE_GRAMMAR_FORMATTER_MAP = {
6-
__base__: 'exp',
7-
__Base__: 'Exp',
7+
[GRAMMAR_CONSTANTS.LEXICAL_BASE_KEY]: 'exp',
8+
[GRAMMAR_CONSTANTS.SYNTACTIC_BASE_KEY]: 'Exp',
89
};
9-
const R_GRAMMAR_IDENT = /<(([a-z-]+)(\(\))?)>/;
1010

1111
/**
1212
* Class to format a JSON Grammar into an Ohm Grammar
@@ -25,15 +25,15 @@ module.exports = class OhmGrammarFormatter {
2525
OhmGrammarFormatter._isGrammarValid(jsonGrammar);
2626

2727
// get the file name for each grammar that needs to be pulled into this grammar. Grab the files for those
28-
// grammars and pull in the rule definitions in those grammars.
28+
// jsonGrammars and pull in the rule definitions in those jsonGrammars.
2929
const recursivelyResolvedGrammarArr = OhmGrammarFormatter
3030
._getGrammarsToResolve(jsonGrammar)
3131
.map(fileToResolve => [fileToResolve, fs.readJsonSync(`${PATHS.JSON_GRAMMAR_PATH}${fileToResolve}.json`)])
3232
.filter(([, json]) => OhmGrammarFormatter._isGrammarValid(json))
3333
.map(([fileName, json]) => json
34-
.filter(grammarPair => grammarPair.length === 2) // filter out any grammars that need resolution
34+
.filter(grammarPair => grammarPair.length === 2) // filter out any jsonGrammars that need resolution
3535
.map(([ruleName, ruleBody]) => (Object.keys(BASE_GRAMMAR_FORMATTER_MAP).includes(ruleName)
36-
? [OhmGrammarFormatter._formatJsonRuleName(`<${fileName}>`), ruleBody]
36+
? [CaseConverterUtils.formalSyntaxIdentToOhmIdent(`<${fileName}>`), ruleBody]
3737
: [ruleName, ruleBody])));
3838
const [baseKey, baseValue] = jsonGrammar[0];
3939
// the base key for this grammar should be mapped to exp or Exp, then concat the rest of the rules and
@@ -49,17 +49,17 @@ module.exports = class OhmGrammarFormatter {
4949
}
5050

5151
/**
52-
* Given a json grammar recursively finds all additional grammars that the grammar depends on. Returns a list
53-
* of unique file names indicating which grammars need to be resolved.
52+
* Given a json grammar recursively finds all additional jsonGrammars that the grammar depends on. Returns a list
53+
* of unique file names indicating which jsonGrammars need to be resolved.
5454
*
5555
* @param {Array} jsonGrammar - a json structure representing a grammar
56-
* @returns {Array} - an set of unique file names indicating which grammars need to resolved.
56+
* @returns {Array} - an set of unique file names indicating which jsonGrammars need to resolved.
5757
* @private
5858
*/
5959
static _getGrammarsToResolve(jsonGrammar) {
6060
const resolutions = jsonGrammar
6161
.filter(grammarLine => grammarLine.length === 1)
62-
.map(([grammarName]) => R_GRAMMAR_IDENT.exec(grammarName)[1]);
62+
.map(([grammarName]) => GRAMMAR_CONSTANTS.R_GRAMMAR_IDENT.exec(grammarName)[1]);
6363

6464
if (resolutions.length === 0) {
6565
return [];
@@ -73,27 +73,14 @@ module.exports = class OhmGrammarFormatter {
7373

7474
/**
7575
* Formats the given rule body into a string that is compatible with Ohm.
76+
*
7677
* @param {string} ruleBody - the JSON grammar body
7778
* @returns {string} - the formatted rule body
7879
* @private
7980
*/
8081
static _formatJsonRuleBody(ruleBody) {
81-
return ruleBody.split(' ').map(OhmGrammarFormatter._formatJsonRuleName).join(' ');
82-
}
83-
84-
/**
85-
* Formats the given rule name into a string that is compatible with Ohm.
86-
* @param {string} ruleName - the JSON grammar rule name
87-
* @returns {string} - the formatted rule name in camelCase format, with an optional Func suffix
88-
* @private
89-
*/
90-
static _formatJsonRuleName(ruleName) {
91-
if (R_GRAMMAR_IDENT.test(ruleName)) {
92-
const [, , name, parens] = R_GRAMMAR_IDENT.exec(ruleName);
93-
return `${CaseConverterUtils.kebabToCamel(name)}${parens ? 'Func' : ''}`;
94-
}
95-
96-
return ruleName;
82+
console.log(ruleBody.split(' ').map(CaseConverterUtils.formalSyntaxIdentToOhmIdent));
83+
return ruleBody.split(' ').map(CaseConverterUtils.formalSyntaxIdentToOhmIdent).join(' ');
9784
}
9885

9986
/**

src/grammars/formalSyntax.ohm

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FormalSyntax {
2+
Exp = Brackets | Juxtaposition | DoubleAmpersand | DoubleBar | SingleBar | Asterisk | Plus | QuestionMark | CurlyBraces | HashMark | node | dataName | literal
3+
Brackets = "[" Exp "]"
4+
Juxtaposition = Exp Exp
5+
DoubleAmpersand = Exp "&&" Exp
6+
DoubleBar = Exp "||" Exp
7+
SingleBar = Exp "|" Exp
8+
Asterisk = Exp #"*"
9+
Plus = Exp #"+"
10+
QuestionMark = Exp #"?"
11+
CurlyBraces = Exp #"{" digit "," digit "}"
12+
HashMark = Exp #"#"
13+
node = "<" "'"? dataName "'"? ">"
14+
dataName = (letter | "-" | "(" | ")" )+
15+
literal = "," | "/"
16+
}
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)