νμ¬ JS APIλ₯Ό μ¬μ©νμ¬ λ°νμμ νμλ₯Ό μμ±νκ³ μμ΅λλ€. μ΄κ²μ μ μλν©λλ€.
κ·Έλ° λ€μ λ°νμ μ€μ μμ±λμ§ μλλ‘ CLIλ₯Ό μ¬μ©νμ¬ νμλ₯Ό μμ±νλ €κ³ νμ΅λλ€. μ¬μ©νλ©΄ μ€λ₯κ° λ°μν©λλ€(λ¬Έμμ΄ κ΅¬λ¬Έ λΆμμ μν ν μ€νΈμ ~ μ λ°).
grammar.pegjs
pegjs -o parser.js grammar.pegjs
peg.generate('...')
μ κ±°νκ³ μ νμλ‘ κ΅μ²΄νμμμ€.const parser = require('./parser');
parser.parse('...');
μμλλ λμ:
CLIμμ μμ±λ νμλ JS APIμμ μμ±λ νμμ λμΌνκ² μλν κ²μΌλ‘ μμν©λλ€.
μ€μ νλ:
JS APIλ₯Ό μ¬μ©νμ¬ μ΄ λ¬Έμμ΄( 'foo = "bar"'
)μ νμμ μ λ¬ν λ λ€μ ASTλ₯Ό μ»μ΅λλ€.
{
kind: 'condition',
target: 'foo',
operator: '=',
value: 'bar',
valueType: 'string',
attributeType: undefined
}
κ·Έλ¬λ CLIλ₯Ό μ¬μ©νμ¬ "μμ±λ" νμλ₯Ό μ¬μ©νκ³ λμΌν λ¬Έμμ΄( 'foo = "bar"'
)μ μ λ¬νλ©΄ λ€μ μ€λ₯κ° λ°μν©λλ€.
SyntaxError: Expected "(", boolean, date, datetime, number, string, or time but "\"" found.
at peg$buildStructuredError (/Users/emmenko/xxx/parser.js:446:12)
at Object.peg$parse [as parse] (/Users/emmenko/xxx/parser.js:2865:11)
at repl:1:7
at ContextifyScript.Script.runInThisContext (vm.js:50:33)
at REPLServer.defaultEval (repl.js:240:29)
at bound (domain.js:301:14)
at REPLServer.runBound [as eval] (domain.js:314:12)
at REPLServer.onLine (repl.js:441:10)
at emitOne (events.js:121:20)
at REPLServer.emit (events.js:211:7)
0.10.0
8.9.1
[email protected]
μ’μ΅λλ€. μ ννκ² μ±μ μ΅λλ€ π μ΄μ λ¬Έλ²λ§ μμΌλ©΄ λμλ릴 μ μμ΅λλ€ π
μ¬κΈ° μμ΅λλ€:
// GRAMMAR
const parser = peg.generate(`
{
function getFlattenedValue (value) {
if (!value) return undefined
return Array.isArray(value)
? value.map(function(v){return v.value})
: value.value
}
function getValueType (value) {
if (!value) return undefined
var rawType = value.type
if (Array.isArray(value))
rawType = value[0].type
switch (rawType) {
case 'string':
case 'number':
case 'boolean':
return rawType
default:
return 'string'
}
}
function getAttributeType (target, op, val) {
if (typeof target === 'string' && target.indexOf('attributes.') === 0) {
if (!val)
return undefined
switch (op) {
case 'in':
case 'not in':
return val[0].type;
case 'contains':
return 'set-' + val.type
default:
return Array.isArray(val) ? 'set-' + val[0].type : val.type;
}
}
}
function transformToCondition (target, op, val) {
return {
kind: "condition",
target: target,
operator: op,
value: getFlattenedValue(val),
valueType: getValueType(val),
attributeType: getAttributeType(target, op, val),
}
}
function createIdentifier (body) {
return body
.map(identifiers => identifiers.filter(identifier => (identifier && identifier !== '.'))) // gets raw_identifiers without dots and empty identifiers
.filter(identifiers => identifiers.length > 0) // filter out empty identifiers arrays
.map(identifiers => identifiers.join('.'))
.join('.') // join back to construct the path
}
}
// ----- DSL Grammar -----
predicate
= ws exp:expression ws { return exp; }
expression
= head:term tail:("or" term)*
{
if (tail.length === 0) {
return head;
}
return {
kind: "logical",
logical: "or",
conditions: [head].concat(tail.map(function(el){return el[1];})),
};
}
term
= head:factor tail:("and" factor)*
{
if (tail.length === 0) {
return head;
}
return {
kind: "logical",
logical: "and",
conditions: [head].concat(tail.map(function(el){return el[1];})),
};
}
factor
= ws negation:"not" ws primary:primary ws
{
return {
kind: "negation",
condition: primary,
};
}
/ ws primary:primary ws { return primary; }
primary
= basic_comparison
/ list_comparison
/ empty_comparison
/ parens
// ----- Comparators -----
basic_comparison
= target:val_expression ws op:single_operators ws val:value
{ return transformToCondition(target, op, val); }
list_comparison
= target:val_expression ws op:list_operators ws val:list_of_values
{ return transformToCondition(target, op, val); }
empty_comparison
= target:val_expression ws op:empty_operators
{ return transformToCondition(target, op); }
// ----- Operators -----
single_operators
= "!="
/ "="
/ "<>"
/ ">="
/ ">"
/ "<="
/ "<"
/ "contains"
list_operators
= "!="
/ "="
/ "<>"
/ "not in"
/ "in"
/ "contains all"
/ "contains any"
empty_operators
= "is not empty"
/ "is empty"
/ "is not defined"
/ "is defined"
list_of_values
= ws "(" ws head:value tail:(ws "," ws value)* ws ")" ws
{
if (tail.length === 0) {
return [head];
}
return [head].concat(tail.map(function(el){ return el[el.length -1];}));
}
// ----- Expressions -----
val_expression
= application_expression
/ constant_expression
/ field_expression
application_expression
= identifier ws "(" ws function_argument (ws "," ws function_argument)* ws ")"
constant_expression = ws val:value ws { return val; }
field_expression = ws i:identifier ws { return i; }
function_argument
= expression
/ constant_expression
/ field_expression
value
= v:boolean { return { type: 'boolean', value: v }; }
/ v:datetime { return { type: 'datetime', value: v }; }
/ v:date { return { type: 'date', value: v }; }
/ v:time { return { type: 'time', value: v }; }
/ v:number { return { type: 'number', value: v }; }
/ v:string { return { type: 'string', value: v }; }
// ----- Common rules -----
parens
= ws "(" ws ex:expression ws ")" ws { return ex; }
identifier
= body:((raw_identifier "." escaped_identifier)+ / (raw_identifier "." raw_identifier)+)
{
return createIdentifier(body)
}
/ i:raw_identifier { return i; }
escaped_identifier
= "\`" head:raw_identifier tail:("-" raw_identifier)* "\`"
{ return [head].concat(tail.map(function(el){return el.join('');})).join(''); }
raw_identifier = i:[a-zA-Z0-9_]* { return i.join(''); }
ws "whitespace" = [ \\t\\n\\r]*
// ----- Types: booleans -----
boolean "boolean"
= "false" { return false; }
/ "true" { return true; }
// ----- Types: datetime -----
datetime "datetime"
= quotation_mark datetime:datetime_format quotation_mark
{ return datetime.map(function(el){return Array.isArray(el) ? el.join('') : el;}).join(''); }
datetime_format = date_format time_mark time_format zulu_mark
time_mark = "T"
zulu_mark = "Z"
// ----- Types: date -----
date "date"
= quotation_mark date:date_format quotation_mark { return date.join("");}
date_format = [0-9][0-9][0-9][0-9] minus [0-9][0-9] minus [0-9][0-9]
// ----- Types: time -----
time "time"
= quotation_mark time:time_format quotation_mark { return time.join("");}
time_format = [0-2][0-9] colon [0-5][0-9] colon [0-5][0-9] decimal_point [0-9][0-9][0-9]
colon = ":"
// ----- Types: numbers -----
number "number"
= minus? int frac? exp? { return parseFloat(text()); }
decimal_point = "."
digit1_9 = [1-9]
e = [eE]
exp = e (minus / plus)? DIGIT+
frac = decimal_point DIGIT+
int = zero / (digit1_9 DIGIT*)
minus = "-"
plus = "+"
zero = "0"
// ----- Types: strings -----
string "string"
= quotation_mark chars:char* quotation_mark { return chars.join(""); }
char
= unescaped
/ escape
sequence:(
'"'
/ "\\\\"
/ "/"
/ "b" { return "\\b"; }
/ "f" { return "\\f"; }
/ "n" { return "\\n"; }
/ "r" { return "\\r"; }
/ "t" { return "\\t"; }
/ "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG)
{ return String.fromCharCode(parseInt(digits, 16)); }
)
{ return sequence; }
escape = "\\\\"
quotation_mark = '"'
unescaped = [^\\0-\\x1F\\x22\\x5C]
// See RFC 4234, Appendix B (http://tools.ietf.org/html/rfc4234).
DIGIT = [0-9]
HEXDIG = [0-9a-f]i
λ²κ·Έμ μ½κ°μ κ΄λ ¨ μΆκ°. pegjs-loader λ₯Ό ν΅ν΄ pegjs
λ₯Ό μ€μ νμ΅λλ€. parser.generate
νΈμΆνλ νλ μλ JS APIμμ μλνλ©° λμΌν μ€λ₯κ° λ°μν©λλ€.
그건 κ·Έλ κ³ νλ‘μ νΈμ λν΄ λ§μ κ°μ¬λ립λλ€!
@emmenko κ·νμ λ¬Έλ²μ΄ APIμ ν¨κ» μλνλ μ΄μ λ₯Ό λͺ¨λ₯΄κ² μ΅λλ€(μ΄μ λ₯Ό κ³μ μ°ΎμΌλ €κ³ λ
Έλ ₯ν κ²μ
λλ€). νμ§λ§ κ·νμ λ¬Έλ²μ΄ μ¬λ°λ₯΄μ§ μμ κ²½μ° unescaped
κ·μΉμ λ€μκ³Ό κ°μμΌ ν©λλ€.
unescaped = !'"' [^\\0-\\x1F\\x22\\x5C]
μ΄λ κ² νλ©΄ λ¬Έμ κ° ν΄κ²°λλμ§ μλ €μ£Όμμμ€.
@tdeekens λμΌν μ€λ₯(μ: Expected ... but "\"" found.
)μΈ κ²½μ° λ¬Έλ²μ΄ μ¬λ°λ₯Έμ§ νμΈνκ±°λ μ¬κΈ°μ κ²μνμμμ€.
@futagoza μ μ @tdeekens λ κ°μ νμ΄λΌ κ°μ λ¬Έμ μ λλ€ π
κ³μ μλ €λλ¦¬κ² μ΅λλ€! μ§κΈκΉμ§ μμν΄μ£Όμ μ κ°μ¬ν©λλ€π
κ·νμ λ¬Έλ²μ΄ APIμ ν¨κ» μλνλ μ΄μ λ₯Ό λͺ¨λ₯΄κ² μ΅λλ€.
μ°λ¦¬λ μμ§ν κ·Έκ²μ λν΄ λ¬Έμ κ° μμμ΅λλ€. κ·Έλλ μ§μ ν΄μ£Όμ μ κ°μ¬ν©λλ€!
μ§κΈ μλν©λκΉ?
λΆννλ λμμ΄λμ§ μμμ΅λλ€ βΉοΈ
λ¬Έλ², PEG.js 0.10, Node 8.9.0 λ° foo = "bar"
μ
λ ₯μ μ¬μ©νμ¬ 3κ°μ§ κ²½λ‘λ₯Ό ν΅ν΄ μ΄κ²μ μλνμ΅λλ€.
pegjs
CLI3κ° λͺ¨λ λμΌν μ€λ₯λ₯Ό νμνμ΅λλ€: Line 1, column 7: Expected "(", boolean, date, datetime, number, string, or time but "\"" found.
λ¬Έλ²μ λ³κ²½νλ©΄ 3κ°μ§ κ²½λ‘ λͺ¨λμ λν΄ μ΄ μ€λ₯κ° μμ λ©λλ€.
// orignal
unescaped = [^\\0-\\x1F\\x22\\x5C]
// fixed
unescaped = !'"' [^\\0-\\x1F\\x22\\x5C]
κ³ μ κ·μΉμ μ μ©ν ν λ€μ μ¬νμ νμΈν μ μμ΅λλ€.
λν μ
λ ₯μ μ½κ° μ‘°μ ν ν λ¬Έλ²μ΄ μ€ λ°κΏμ 곡백μΌλ‘ μ¬λ°λ₯΄κ² μ€λͺ
νμ§ μλλ€λ κ²μ κΉ¨λ¬μμ΅λλ€. μ΄λ ws
κ·μΉ λλ¬ΈμΌ κ°λ₯μ±μ΄ ν½λλ€.
νΈμ§: λ΄ ν μ€νΈ μ€ν¬λ¦½νΈλ λ€μκ³Ό κ°μ΅λλ€.
/* eslint node/no-unsupported-features: 0 */
"use strict";
const { exec } = require( "child_process" );
const { readFileSync } = require( "fs" );
const { join } = require( "path" );
const { generate } = require( "pegjs" );
function test( parser ) {
try {
console.log( parser.parse( `foo = "bar"` ) );
} catch ( error ) {
if ( error.name !== "SyntaxError" ) throw error;
const loc = error.location.start;
console.log( `Line ${ loc.line }, column ${ loc.column }: ${ error.message }` );
}
}
const COMMAND = process.argv[ 2 ];
switch ( COMMAND ) {
case "api":
test( generate( readFileSync( join( __dirname, "grammar.pegjs" ), "utf8" ) ) );
break;
case "cli":
exec( "node node_modules/pegjs/bin/pegjs -o parser.js grammar.pegjs", error => {
if ( error ) console.error( error ), process.exit( 1 );
test( require( "./parser" ) );
} );
break;
default:
console.error( `Invalid command "${ COMMAND }" passed to test script.` );
process.exit( 1 );
}
νΌλλ°± μ£Όμ μ κ°μ¬ν©λλ€! κ·νμ μ μμΌλ‘ λ΄μΌ μλνκ³ λμμ΄ λμλ€λ©΄ μ΅λν 빨리 μλ € λλ¦¬κ² μ΅λλ€. π
νΌλλ°±μ μ£Όμ μ κ°μ¬ν©λλ€. νΌλμ λλ € λ¨Όμ μ¬κ³Όλ립λλ€. λλ λ¬Έμ κ° webpack-loaderμλ μλ€λ κ²μ μ§μ νκ³ μΆμμ΅λλ€. μ΄ λ¬Έμ λ‘ νΌλμ λλ € μ£μ‘ν©λλ€.
μ°λ¦¬λ κ°μ μ μλνμ΅λλ€. μΌλ°μ μΌλ‘ νμλ₯Ό μμ νμ§λ§ μλ‘μ΄ λ¬Έμ κ° λ°μνμ¬ κ·Έ μ΄μ λ₯Ό μ΄ν΄νλ λ° μ΄λ €μμ κ²ͺμ΅λλ€.
ν μ€νΈμ μλ λ€μκ³Ό κ°μ΅λλ€(μμΈν λ΄μ©μ μλ μ°Έμ‘°).
Object {
+ "attributeType": undefined,
"kind": "condition",
"operator": "=",
"target": "foo",
- "value": "bar",
+ "value": ",b,a,r",
"valueType": "string",
}
μ°λ¦¬λ μ€λ₯κ° μ°λ¦¬ νΈμΌ κ°λ₯μ±μ΄ μλ€κ³ μκ°ν©λλ€. μμ§ μ΄λ μλμ§ νμ€νμ§ μμ΅λλ€. μ΄κ²μ μλ₯Ό λ€μ΄ λ€μ μ λ ₯μμ λ°μν©λλ€.
categories.id != ("b33f8e3a-f8d1-476f-a595-2615c4b57556")
λλ
categories.id != (",b,3,3,f,8,e,3,a,-,f,8,d,1,-,4,7,6,f,-,a,5,9,5,-,2,6,1,5,c,4,b,5,7,5,5,6")
ꡬ문 λΆμν λ.
μ°λ¦¬λ λΆλͺ ν λ¨μμ λν΄ λ§€μ° κ°μ¬ν κ²μ΄μ§λ§ κ±°κΈ°μμ μ°λ¦¬λ₯Ό μ§μν μ μλμ§ μ΄ν΄ν©λλ€.
μ΄λ°, λ΄ μ€μ π¨, μ΄κ²μ κ·Έκ²μ κ³ μΉ κ²μ λλ€
unescaped = !'"' value:[^\\0-\\x1F\\x22\\x5C] { return value; }
λ§€μ° λΉ λ₯Έ μλ΅μ κ°μ¬λ립λλ€. SyntaxError: Expected "(", boolean, date, datetime, number, string, or time but "\"" found.
μ μ΄κΈ° μ€λ₯λ₯Ό μμ£Ό λ°ννλ CLI λλ webpack-loaderλ₯Ό μ¬μ©ν λλ λμμ΄ λμ§λ§ λμμ΄ λμ§ μμ΅λλ€. μλ₯Ό λ€μ΄ not(sku = "123")
λλ λ 볡μ‘ν μμ lineItemTotal(sku = "SKU1" or list contains all (1,2,3), field.name, "third arg") = "10 EUR"
μ
λλ€. νμΆκ³Ό μ¬μ ν κ΄λ ¨μ΄ μμ΅λκΉ?
λ€, μ΄μ€ νμΆ λλ¬ΈμΈ κ²μΌλ‘ λ°νμ‘μ΅λλ€. λ€μμ κ³ μ κ·μΉμ λλ€.
ws "whitespace" = [ \t\n\r]*
char
= unescaped
/ escape
sequence:(
'"'
/ "\\"
/ "/"
/ "b" { return "\b"; }
/ "f" { return "\f"; }
/ "n" { return "\n"; }
/ "r" { return "\r"; }
/ "t" { return "\t"; }
/ "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG)
{ return String.fromCharCode(parseInt(digits, 16)); }
)
{ return sequence; }
escape = "\\"
unescaped = !'"' value:[^\0-\x1F\x22\x5C] { return value; }
νΈμ§: 볡μ‘ν μμ λ₯Ό ꡬ문 λΆμνλ κ·μΉμ λν΄ μμ
νλ €λ κ² κ°μ΅λλ€. lineItemTotal(sku = "SKU1" or list contains all (1,2,3), field.name, "third arg") = "10 EUR"
, νμ¬ μ΄μν "kind":"condition"
λ
Έλλ₯Ό μΆλ ₯νκ³ μμ΅λλ€.
λ§μ λμκ³Ό μ‘°μΈ λΆνλ립λλ€. μ°λ¦¬κ° κ°μ§ λ¬Έμ λ₯Ό ν΄κ²°νλ κ² κ°μ΅λλ€. "쑰건" λ Έλμ λν μ‘°μΈμ μ΄ν΄λ³΄κ² μ΅λλ€.
λ°κ°μ΅λλ€ π