Commit 9a7f06aa authored by di68kap's avatar di68kap
Browse files

- DHParser/parser.py: support for "anonymizing" named parsers, so that they do not emit named nodes

parent 67b14eb4
...@@ -931,7 +931,7 @@ class EBNFCompiler(Compiler): ...@@ -931,7 +931,7 @@ class EBNFCompiler(Compiler):
definitions = [] # type: List[Tuple[str, str]] definitions = [] # type: List[Tuple[str, str]]
# drop the wrapping sequence node # drop the wrapping sequence node
if len(node.children) == 1 and node.children[0].is_anonymous(): if len(node.children) == 1 and node.children[0].anonymous:
node = node.children[0] node = node.children[0]
# compile definitions and directives and collect definitions # compile definitions and directives and collect definitions
......
...@@ -32,7 +32,7 @@ for an example. ...@@ -32,7 +32,7 @@ for an example.
from collections import defaultdict from collections import defaultdict
import copy import copy
from typing import Callable, cast, List, Tuple, Set, Iterator, Dict, \ from typing import Callable, cast, List, Tuple, Set, Container, Dict, \
DefaultDict, Union, Optional, Any DefaultDict, Union, Optional, Any
from DHParser.configuration import get_config_value from DHParser.configuration import get_config_value
...@@ -195,6 +195,13 @@ ApplyFunc = Callable[['Parser'], None] ...@@ -195,6 +195,13 @@ ApplyFunc = Callable[['Parser'], None]
FlagFunc = Callable[[ApplyFunc, Set[ApplyFunc]], bool] FlagFunc = Callable[[ApplyFunc, Set[ApplyFunc]], bool]
def copy_parser_attrs(src: 'Parser', duplicate: 'Parser'):
"""Duplicates all parser attributes from source to dest."""
duplicate.pname = src.pname
duplicate.anonymous = src.anonymous
duplicate.tag_name = src.tag_name
class Parser: class Parser:
""" """
(Abstract) Base class for Parser combinator parsers. Any parser (Abstract) Base class for Parser combinator parsers. Any parser
...@@ -234,9 +241,14 @@ class Parser: ...@@ -234,9 +241,14 @@ class Parser:
contained parser is repeated zero times. contained parser is repeated zero times.
Attributes and Properties: Attributes and Properties:
pname: The parser name or the empty string in case the parser pname: The parser's name or a (possibly empty) alias name in case
remains anonymous. of an anonymous parser.
tag_name: The tag_name for the nodes that are created by anonymous: A property indicating that the parser remains anynomous
anonymous with respect to the nodes it returns. For performance
reasons this is implemented as an object variable rather
than a property. This property must always be equal to
`self.tag_name[0] == ":"`.
tag_name: The tag_name for the nodes that are created by
the parser. If the parser is named, this is the same as the parser. If the parser is named, this is the same as
`pname`, otherwise it is the name of the parser's type. `pname`, otherwise it is the name of the parser's type.
visited: Mapping of places this parser has already been to visited: Mapping of places this parser has already been to
...@@ -261,6 +273,7 @@ class Parser: ...@@ -261,6 +273,7 @@ class Parser:
def __init__(self) -> None: def __init__(self) -> None:
# assert isinstance(name, str), str(name) # assert isinstance(name, str), str(name)
self.pname = '' # type: str self.pname = '' # type: str
self.anonymous = True # type: bool
self.tag_name = self.ptype # type: str self.tag_name = self.ptype # type: str
self.cycle_detection = set() # type: Set[ApplyFunc] self.cycle_detection = set() # type: Set[ApplyFunc]
try: try:
...@@ -277,8 +290,7 @@ class Parser: ...@@ -277,8 +290,7 @@ class Parser:
calling the same method from the superclass) by the derived class. calling the same method from the superclass) by the derived class.
""" """
duplicate = self.__class__() duplicate = self.__class__()
duplicate.pname = self.pname copy_parser_attrs(self, duplicate)
duplicate.tag_name = self.tag_name
return duplicate return duplicate
def __repr__(self): def __repr__(self):
...@@ -349,7 +361,7 @@ class Parser: ...@@ -349,7 +361,7 @@ class Parser:
if history_tracking__: if history_tracking__:
grammar.call_stack__.append( grammar.call_stack__.append(
((self.repr if self.tag_name in (':RegExp', ':Token', ':DropToken') ((self.repr if self.tag_name in (':RegExp', ':Token', ':DropToken')
else self.tag_name), location)) else (self.pname or self.tag_name)), location))
grammar.moving_forward__ = True grammar.moving_forward__ = True
error = None error = None
...@@ -652,14 +664,14 @@ class Grammar: ...@@ -652,14 +664,14 @@ class Grammar:
Upon instantiation the parser objects are deep-copied to the Upon instantiation the parser objects are deep-copied to the
Grammar object and assigned to object variables of the same name. Grammar object and assigned to object variables of the same name.
Any parser that is directly assigned to a class variable is a For any parser that is directly assigned to a class variable the
'named' parser and its field `parser.pname` contains the variable field `parser.pname` contains the variable name after instantiation
name after instantiation of the Grammar class. All other parsers, of the Grammar class. The parser will never the less remain anonymous
i.e. parsers that are defined within a `named` parser, remain with respect to the tag names of the nodes it generates, if its name
"anonymous parsers" where `parser.pname` is the empty string. is matched by the `anonymous__` regular expression.
If one and the same parser is assigned to several class variables If one and the same parser is assigned to several class variables
such as, for example, the parser `expression` in the example above, such as, for example, the parser `expression` in the example above,
the first name sticks. which is also assigned to `root__`, the first name sticks.
Grammar objects are callable. Calling a grammar object with a UTF-8 Grammar objects are callable. Calling a grammar object with a UTF-8
encoded document, initiates the parsing of the document with the encoded document, initiates the parsing of the document with the
...@@ -682,11 +694,11 @@ class Grammar: ...@@ -682,11 +694,11 @@ class Grammar:
that act as rules to find the reentry point if a ParserError was that act as rules to find the reentry point if a ParserError was
thrown during the execution of the parser with the respective name. thrown during the execution of the parser with the respective name.
anonymous__: Either a regular expression or a set of strings that anonymous__: A regular expression to identify names of parsers that are
identify names of parsers that shall be treated as anonymous parsers, assigned to class fields but shall never the less yield anonymous
even though they are assigned to a class field (see nodes (i.e. nodes the tag name of which starts with a colon ":"
`:func:_assign_parser_names()`). The default is to treat all parsers followed by the parser's class name). The default is to treat all
starting with an underscore as anonymous in addition to those parsers starting with an underscore as anonymous in addition to those
parsers that are not directly assigned to a class field. parsers that are not directly assigned to a class field.
parser_initialization__: Before the grammar class (!) has been initialized, parser_initialization__: Before the grammar class (!) has been initialized,
...@@ -811,7 +823,7 @@ class Grammar: ...@@ -811,7 +823,7 @@ class Grammar:
# root__ must be overwritten with the root-parser by grammar subclass # root__ must be overwritten with the root-parser by grammar subclass
parser_initialization__ = ["pending"] # type: List[str] parser_initialization__ = ["pending"] # type: List[str]
resume_rules__ = dict() # type: Dict[str, ResumeList] resume_rules__ = dict() # type: Dict[str, ResumeList]
anonymous__ = re.compile(r'_\w+') # type: Union[RxPatternType, Set[str]] anonymous__ = re.compile(r'_') # type: RxPatternType
# some default values # some default values
# COMMENT__ = r'' # type: str # r'#.*(?:\n|$)' # COMMENT__ = r'' # type: str # r'#.*(?:\n|$)'
# WSP_RE__ = mixin_comment(whitespace=r'[\t ]*', comment=COMMENT__) # type: str # WSP_RE__ = mixin_comment(whitespace=r'[\t ]*', comment=COMMENT__) # type: str
...@@ -846,11 +858,14 @@ class Grammar: ...@@ -846,11 +858,14 @@ class Grammar:
cdict = cls.__dict__ cdict = cls.__dict__
for entry, parser in cdict.items(): for entry, parser in cdict.items():
if isinstance(parser, Parser) and sane_parser_name(entry): if isinstance(parser, Parser) and sane_parser_name(entry):
anonymous = True if cls.anonymous__.match(entry) else False
if isinstance(parser, Forward): if isinstance(parser, Forward):
if not cast(Forward, parser).parser.pname: if not cast(Forward, parser).parser.pname:
cast(Forward, parser).parser.pname = entry cast(Forward, parser).parser.pname = entry
cast(Forward, parser).parser.anonymous = anonymous
else: # if not parser.pname: else: # if not parser.pname:
parser.pname = entry parser.pname = entry
parser.anonymous = anonymous
cls.parser_initialization__[0] = "done" cls.parser_initialization__[0] = "done"
...@@ -958,7 +973,7 @@ class Grammar: ...@@ -958,7 +973,7 @@ class Grammar:
'already exists in grammar object: %s!' 'already exists in grammar object: %s!'
% (parser.pname, str(self.__dict__[parser.pname]))) % (parser.pname, str(self.__dict__[parser.pname])))
setattr(self, parser.pname, parser) setattr(self, parser.pname, parser)
parser.tag_name = parser.pname or parser.ptype parser.tag_name = parser.ptype if parser.anonymous else parser.pname
self.all_parsers__.add(parser) self.all_parsers__.add(parser)
parser.grammar = self parser.grammar = self
...@@ -1239,10 +1254,13 @@ class PreprocessorToken(Parser): ...@@ -1239,10 +1254,13 @@ class PreprocessorToken(Parser):
assert RX_TOKEN_NAME.match(token) assert RX_TOKEN_NAME.match(token)
super(PreprocessorToken, self).__init__() super(PreprocessorToken, self).__init__()
self.pname = token self.pname = token
if token:
self.anonymous = False
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
duplicate = self.__class__(self.pname) duplicate = self.__class__(self.pname)
# duplicate.pname = self.pname # will be written by the constructor, anyway # duplicate.pname = self.pname
duplicate.anonymous = self.anonymous
duplicate.tag_name = self.tag_name duplicate.tag_name = self.tag_name
return duplicate return duplicate
...@@ -1295,13 +1313,12 @@ class Token(Parser): ...@@ -1295,13 +1313,12 @@ class Token(Parser):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
duplicate = self.__class__(self.text) duplicate = self.__class__(self.text)
duplicate.pname = self.pname copy_parser_attrs(self, duplicate)
duplicate.tag_name = self.tag_name
return duplicate return duplicate
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]: def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
if text.startswith(self.text): if text.startswith(self.text):
if self.text or self.pname: if self.text or not self.anonymous:
return Node(self.tag_name, self.text, True), text[self.len:] return Node(self.tag_name, self.text, True), text[self.len:]
return EMPTY_NODE, text[0:] return EMPTY_NODE, text[0:]
return None, text return None, text
...@@ -1316,7 +1333,7 @@ class DropToken(Token): ...@@ -1316,7 +1333,7 @@ class DropToken(Token):
string on a match. Violates the invariant: str(parse(text)) == text ! string on a match. Violates the invariant: str(parse(text)) == text !
""" """
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]: def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
assert not self.pname, "DropToken must not be used for named parsers!" assert self.anonymous, "DropToken must not be used for named parsers!"
if text.startswith(self.text): if text.startswith(self.text):
return EMPTY_NODE, text[self.len:] return EMPTY_NODE, text[self.len:]
# return Node(self.tag_name, self.text, True), text[self.len:] # return Node(self.tag_name, self.text, True), text[self.len:]
...@@ -1354,15 +1371,14 @@ class RegExp(Parser): ...@@ -1354,15 +1371,14 @@ class RegExp(Parser):
except TypeError: except TypeError:
regexp = self.regexp.pattern regexp = self.regexp.pattern
duplicate = self.__class__(regexp) duplicate = self.__class__(regexp)
duplicate.pname = self.pname copy_parser_attrs(self, duplicate)
duplicate.tag_name = self.tag_name
return duplicate return duplicate
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]: def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
match = text.match(self.regexp) match = text.match(self.regexp)
if match: if match:
capture = match.group(0) capture = match.group(0)
if capture or self.pname: if capture or not self.anonymous:
end = text.index(match.end()) end = text.index(match.end())
return Node(self.tag_name, capture, True), text[end:] return Node(self.tag_name, capture, True), text[end:]
assert text.index(match.end()) == 0 assert text.index(match.end()) == 0
...@@ -1414,7 +1430,7 @@ class Whitespace(RegExp): ...@@ -1414,7 +1430,7 @@ class Whitespace(RegExp):
match = text.match(self.regexp) match = text.match(self.regexp)
if match: if match:
capture = match.group(0) capture = match.group(0)
if capture or self.pname: if capture or not self.anonymous:
end = text.index(match.end()) end = text.index(match.end())
return Node(self.tag_name, capture, True), text[end:] return Node(self.tag_name, capture, True), text[end:]
else: else:
...@@ -1433,7 +1449,7 @@ class DropWhitespace(Whitespace): ...@@ -1433,7 +1449,7 @@ class DropWhitespace(Whitespace):
""" """
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]: def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
assert not self.pname, "DropWhitespace must not be used for named parsers!" assert self.anonymous, "DropWhitespace must not be used for named parsers!"
match = text.match(self.regexp) match = text.match(self.regexp)
if match: if match:
# capture = match.group(0) # capture = match.group(0)
...@@ -1476,14 +1492,14 @@ class MetaParser(Parser): ...@@ -1476,14 +1492,14 @@ class MetaParser(Parser):
assert node is None or isinstance(node, Node) assert node is None or isinstance(node, Node)
if self._grammar.flatten_tree__: if self._grammar.flatten_tree__:
if node: if node:
if self.pname: if self.anonymous:
if node.tag_name[0] == ':': # faster than node.is_anonymous() return node
return Node(self.tag_name, node._result) if node.tag_name[0] == ':': # faster than node.is_anonymous()
return Node(self.tag_name, node) return Node(self.tag_name, node._result)
return node return Node(self.tag_name, node)
elif self.pname: elif self.anonymous:
return Node(self.tag_name, ()) return EMPTY_NODE # avoid creation of a node object for anonymous empty nodes
return EMPTY_NODE # avoid creation of a node object for anonymous empty nodes return Node(self.tag_name, ())
return Node(self.tag_name, node or ()) # unoptimized code return Node(self.tag_name, node or ()) # unoptimized code
@cython.locals(N=cython.int) @cython.locals(N=cython.int)
...@@ -1510,9 +1526,9 @@ class MetaParser(Parser): ...@@ -1510,9 +1526,9 @@ class MetaParser(Parser):
elif N == 1: elif N == 1:
return self._return_value(results[0]) return self._return_value(results[0])
elif self._grammar.flatten_tree__: elif self._grammar.flatten_tree__:
if self.pname: if self.anonymous:
return Node(self.tag_name, ()) return EMPTY_NODE # avoid creation of a node object for anonymous empty nodes
return EMPTY_NODE # avoid creation of a node object for anonymous empty nodes return Node(self.tag_name, ())
return Node(self.tag_name, results) # unoptimized code return Node(self.tag_name, results) # unoptimized code
...@@ -1535,8 +1551,7 @@ class UnaryParser(MetaParser): ...@@ -1535,8 +1551,7 @@ class UnaryParser(MetaParser):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
parser = copy.deepcopy(self.parser, memo) parser = copy.deepcopy(self.parser, memo)
duplicate = self.__class__(parser) duplicate = self.__class__(parser)
duplicate.pname = self.pname copy_parser_attrs(self, duplicate)
duplicate.tag_name = self.tag_name
return duplicate return duplicate
def _apply(self, func: ApplyFunc, flip: FlagFunc) -> bool: def _apply(self, func: ApplyFunc, flip: FlagFunc) -> bool:
...@@ -1566,8 +1581,7 @@ class NaryParser(MetaParser): ...@@ -1566,8 +1581,7 @@ class NaryParser(MetaParser):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
parsers = copy.deepcopy(self.parsers, memo) parsers = copy.deepcopy(self.parsers, memo)
duplicate = self.__class__(*parsers) duplicate = self.__class__(*parsers)
duplicate.pname = self.pname copy_parser_attrs(self, duplicate)
duplicate.tag_name = self.tag_name
return duplicate return duplicate
def _apply(self, func: ApplyFunc, flip: FlagFunc) -> bool: def _apply(self, func: ApplyFunc, flip: FlagFunc) -> bool:
...@@ -1840,8 +1854,7 @@ class Series(NaryParser): ...@@ -1840,8 +1854,7 @@ class Series(NaryParser):
parsers = copy.deepcopy(self.parsers, memo) parsers = copy.deepcopy(self.parsers, memo)
duplicate = self.__class__(*parsers, mandatory=self.mandatory, duplicate = self.__class__(*parsers, mandatory=self.mandatory,
err_msgs=self.err_msgs, skip=self.skip) err_msgs=self.err_msgs, skip=self.skip)
duplicate.pname = self.pname copy_parser_attrs(self, duplicate)
duplicate.tag_name = self.tag_name
return duplicate return duplicate
@cython.locals(pos=cython.int, reloc=cython.int) @cython.locals(pos=cython.int, reloc=cython.int)
...@@ -2058,8 +2071,7 @@ class AllOf(NaryParser): ...@@ -2058,8 +2071,7 @@ class AllOf(NaryParser):
duplicate = self.__class__(*parsers, mandatory=self.mandatory, duplicate = self.__class__(*parsers, mandatory=self.mandatory,
err_msgs=self.err_msgs, skip=self.skip) err_msgs=self.err_msgs, skip=self.skip)
duplicate.pname = self.pname duplicate.pname = self.pname
duplicate.tag_name = self.tag_name copy_parser_attrs(self, duplicate)
duplicate.num_parsers = self.num_parsers
return duplicate return duplicate
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]: def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
...@@ -2219,7 +2231,7 @@ class Lookahead(FlowParser): ...@@ -2219,7 +2231,7 @@ class Lookahead(FlowParser):
if self.sign(node is not None): if self.sign(node is not None):
# static analysis requires lookahead to be disabled at document end # static analysis requires lookahead to be disabled at document end
# or (self.grammar.static_analysis_pending__ and not text)): # or (self.grammar.static_analysis_pending__ and not text)):
return Node(self.tag_name, '') if self.pname else EMPTY_NODE, text return (EMPTY_NODE if self.anonymous else Node(self.tag_name, '')), text
else: else:
return None, text return None, text
...@@ -2364,8 +2376,7 @@ class Retrieve(Parser): ...@@ -2364,8 +2376,7 @@ class Retrieve(Parser):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
duplicate = self.__class__(self.symbol, self.filter) duplicate = self.__class__(self.symbol, self.filter)
duplicate.pname = self.pname copy_parser_attrs(self, duplicate)
duplicate.tag_name = self.tag_name
return duplicate return duplicate
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]: def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
...@@ -2417,8 +2428,7 @@ class Pop(Retrieve): ...@@ -2417,8 +2428,7 @@ class Pop(Retrieve):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
duplicate = self.__class__(self.symbol, self.filter) duplicate = self.__class__(self.symbol, self.filter)
duplicate.pname = self.pname copy_parser_attrs(self, duplicate)
duplicate.tag_name = self.tag_name
duplicate.values = self.values[:] duplicate.values = self.values[:]
return duplicate return duplicate
...@@ -2498,6 +2508,7 @@ class Forward(Parser): ...@@ -2498,6 +2508,7 @@ class Forward(Parser):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
duplicate = self.__class__() duplicate = self.__class__()
# duplicate.pname = self.pname # Forward-Parsers should never have a name! # duplicate.pname = self.pname # Forward-Parsers should never have a name!
duplicate.anonymous = self.anonymous
duplicate.tag_name = self.tag_name duplicate.tag_name = self.tag_name
memo[id(self)] = duplicate memo[id(self)] = duplicate
parser = copy.deepcopy(self.parser, memo) parser = copy.deepcopy(self.parser, memo)
......
...@@ -347,7 +347,8 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil ...@@ -347,7 +347,8 @@ class Node: # (collections.abc.Sized): Base class omitted for cython-compatibil
except KeyError: except KeyError:
return surrogate return surrogate
def is_anonymous(self) -> bool: @property
def anonymous(self) -> bool:
"""Returns True, if the Node is an "anonymous" Node, i.e. a node that """Returns True, if the Node is an "anonymous" Node, i.e. a node that
has not been created by a named parser. has not been created by a named parser.
......
...@@ -403,12 +403,12 @@ def is_single_child(context: List[Node]) -> bool: ...@@ -403,12 +403,12 @@ def is_single_child(context: List[Node]) -> bool:
def is_named(context: List[Node]) -> bool: def is_named(context: List[Node]) -> bool:
"""Returns ``True`` if the current node's parser is a named parser.""" """Returns ``True`` if the current node's parser is a named parser."""
return not context[-1].is_anonymous() return not context[-1].anonymous
def is_anonymous(context: List[Node]) -> bool: def is_anonymous(context: List[Node]) -> bool:
"""Returns ``True`` if the current node's parser is an anonymous parser.""" """Returns ``True`` if the current node's parser is an anonymous parser."""
return context[-1].is_anonymous() return context[-1].anonymous
def is_insignificant_whitespace(context: List[Node]) -> bool: def is_insignificant_whitespace(context: List[Node]) -> bool:
...@@ -563,7 +563,7 @@ def _replace_by(node: Node, child: Node): ...@@ -563,7 +563,7 @@ def _replace_by(node: Node, child: Node):
""" """
Replaces node's contents by child's content including the tag name. Replaces node's contents by child's content including the tag name.
""" """
if node.is_anonymous() or not child.is_anonymous(): if node.anonymous or not child.anonymous:
node.tag_name = child.tag_name node.tag_name = child.tag_name
# name, ptype = (node.tag_name.split(':') + [''])[:2] # name, ptype = (node.tag_name.split(':') + [''])[:2]
# child.parser = MockParser(name, ptype) # child.parser = MockParser(name, ptype)
......
[match:json] [match:json]
M1*: """ M1*: """{
"object":
{
"one": 1,
"two": 2,
"three": ["3"],
"fraction": 1.5,
"unicode": "Text with \uc4a3(unicode)"
},
"array": ["one", 2, 3],
"string": " string example ",
"true": true,
"false": false,
"null": null
}"""
M2: """
{ {
"leading and trailing whitespace": true "leading and trailing whitespace": true
...@@ -11,7 +28,7 @@ M1*: """ ...@@ -11,7 +28,7 @@ M1*: """
[ast:json] [ast:json]
[fail:json] [fail:json]
M2: """ F1: """
{ {
"leading and trailing whitespace": True, "leading and trailing whitespace": True,
...@@ -20,6 +37,21 @@ M2: """ ...@@ -20,6 +37,21 @@ M2: """
""" """
F2: """{
"object":
{
"one": 1,
"two": 2,
"three": ["3"]
"fraction": 1.5,
"unicode": "\xc4a3"
},
"array": ["one", 2, 3],
"string": " string example ",
"true": true,
"false": false,
"null": null
}"""
[match:element] [match:element]
......
...@@ -21,21 +21,25 @@ ...@@ -21,21 +21,25 @@
@ object_resume = /\}\s*/ @ object_resume = /\}\s*/
@ member_error = /\w+/, 'Possible non-numerical and non-string values are `true`, `false` or `null` (always written with small letters and without quotation marks).'
@ member_error = /["\'`´]/, 'String values must be enclosed by double-quotation marks: "..."!' @ member_error = /["\'`´]/, 'String values must be enclosed by double-quotation marks: "..."!'
@ member_error = /\\/, 'Possible escaped values are /, \\, b, n, r, t, or u.'
@ member_error = /\d/, '{1} does not represent a valid number or other value.'
@ member_resume = /(?=,|\})/ @ member_resume = /(?=,|\})/
@ _members_resume = /(?="[^"\n]+":)/
@ string_error = /\\/, 'Possible escaped values are \\/, \\\\, \\b, \\n, \\r, \\t, or \\u, but not {1}'