Pegjs: μƒμ„±λœ νŒŒμ„œμ˜ μΌκ΄€λ˜μ§€ μ•Šμ€ λ™μž‘

에 λ§Œλ“  2018λ…„ 03μ›” 08일  Β·  15μ½”λ©˜νŠΈ  Β·  좜처: pegjs/pegjs

문제 μœ ν˜•

  • 버그 λ³΄κ³ μ„œ:

μ „μ œ 쑰건

  • 문제λ₯Ό μž¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆκΉŒ?: _예_
  • μ €μž₯μ†Œ 문제λ₯Ό κ²€μƒ‰ν•˜μ…¨μŠ΅λ‹ˆκΉŒ?: _yes_
  • ν¬λŸΌμ„ ν™•μΈν–ˆμŠ΅λ‹ˆκΉŒ?: _no_
  • μ›Ή 검색(google, yahoo λ“±)을 μˆ˜ν–‰ν•˜μ…¨μŠ΅λ‹ˆκΉŒ?: _yes_

μ„€λͺ…

ν˜„μž¬ JS APIλ₯Ό μ‚¬μš©ν•˜μ—¬ λŸ°νƒ€μž„μ— νŒŒμ„œλ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 이것은 잘 μž‘λ™ν•©λ‹ˆλ‹€.

그런 λ‹€μŒ λŸ°νƒ€μž„ 쀑에 μƒμ„±λ˜μ§€ μ•Šλ„λ‘ CLIλ₯Ό μ‚¬μš©ν•˜μ—¬ νŒŒμ„œλ₯Ό μƒμ„±ν•˜λ €κ³  ν–ˆμŠ΅λ‹ˆλ‹€. μ‚¬μš©ν•˜λ©΄ 였λ₯˜κ°€ λ°œμƒν•©λ‹ˆλ‹€(λ¬Έμžμ—΄ ꡬ문 뢄석을 μœ„ν•œ ν…ŒμŠ€νŠΈμ˜ ~ 절반).

λ²ˆμ‹ 단계

  1. 문법을 자체 파일둜 이동 grammar.pegjs
  2. CLIλ₯Ό μ‚¬μš©ν•˜μ—¬ νŒŒμ„œ 생성
pegjs -o parser.js grammar.pegjs
  1. peg.generate('...') μ œκ±°ν•˜κ³  μƒˆ νŒŒμ„œλ‘œ κ΅μ²΄ν•˜μ‹­μ‹œμ˜€.
const parser = require('./parser');
parser.parse('...');
  1. ν…ŒμŠ€νŠΈ μ‹€ν–‰

μ˜ˆμƒλ˜λŠ” λ™μž‘:
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) 

μ†Œν”„νŠΈμ›¨μ–΄

  • PEG.js: 0.10.0
  • Node.js: 8.9.1
  • NPM λ˜λŠ” 원사: [email protected]
  • λΈŒλΌμš°μ €: 크둬
  • OS: OSX
  • νŽΈμ§‘κΈ°: VSCode
question

λͺ¨λ“  15 λŒ“κΈ€

μ’‹μŠ΅λ‹ˆλ‹€. μ •ν™•ν•˜κ²Œ μ±„μ› μŠ΅λ‹ˆλ‹€ πŸ‘ 이제 λ¬Έλ²•λ§Œ 있으면 λ„μ™€λ“œλ¦΄ 수 μžˆμŠ΅λ‹ˆλ‹€ πŸ˜„

μ—¬κΈ° μžˆμŠ΅λ‹ˆλ‹€:

// 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가지 경둜λ₯Ό 톡해 이것을 μ‹œλ„ν–ˆμŠ΅λ‹ˆλ‹€.

  1. https://pegjs.org/online
  2. PEG.js API
  3. pegjs CLI

3개 λͺ¨λ‘ λ™μΌν•œ 였λ₯˜λ₯Ό ν‘œμ‹œν–ˆμŠ΅λ‹ˆλ‹€: 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]

κ³ μ • κ·œμΉ™μ„ μ μš©ν•œ ν›„ λ‹€μŒ 사항을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

  • λ™μΌν•œ 였λ₯˜ λ©”μ‹œμ§€ λ˜λŠ” λ‹€λ₯Έ 였λ₯˜κ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.
  • λ‚΄κ°€ μ–ΈκΈ‰ν•œ 것과 λ‹€λ₯Έ(λ˜λŠ” μΆ”κ°€ 단계)λ₯Ό μˆ˜ν–‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
  • PEG.js APIλ₯Ό μ‚¬μš©ν•  λ•Œ μ˜΅μ…˜μ„ μ‚¬μš© μ€‘μž…λ‹ˆκΉŒ?

λ˜ν•œ μž…λ ₯을 μ•½κ°„ μ‘°μ •ν•œ ν›„ 문법이 쀄 λ°”κΏˆμ„ 곡백으둜 μ˜¬λ°”λ₯΄κ²Œ μ„€λͺ…ν•˜μ§€ μ•ŠλŠ”λ‹€λŠ” 것을 κΉ¨λ‹¬μ•˜μŠ΅λ‹ˆλ‹€. μ΄λŠ” 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" λ…Έλ“œλ₯Ό 좜λ ₯ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

λ§Žμ€ 도움과 μ‘°μ–Έ λΆ€νƒλ“œλ¦½λ‹ˆλ‹€. μš°λ¦¬κ°€ 가진 문제λ₯Ό ν•΄κ²°ν•˜λŠ” 것 κ°™μŠ΅λ‹ˆλ‹€. "쑰건" λ…Έλ“œμ— λŒ€ν•œ 쑰언을 μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

λ°˜κ°‘μŠ΅λ‹ˆλ‹€ πŸ˜„

이 νŽ˜μ΄μ§€κ°€ 도움이 λ˜μ—ˆλ‚˜μš”?
0 / 5 - 0 λ“±κΈ‰