662 lines
16 KiB
JavaScript
662 lines
16 KiB
JavaScript
var acorn = require("../acorn");
|
|
|
|
var tt = acorn.tokTypes;
|
|
var tc = acorn.tokContexts;
|
|
|
|
tc.j_oTag = new acorn.TokContext("<tag", false);
|
|
tc.j_cTag = new acorn.TokContext("</tag", false);
|
|
tc.j_expr = new acorn.TokContext("<tag>...</tag>", true, true);
|
|
|
|
tt.jsxName = new acorn.TokenType("jsxName");
|
|
tt.jsxText = new acorn.TokenType("jsxText", {beforeExpr: true});
|
|
tt.jsxTagStart = new acorn.TokenType("jsxTagStart");
|
|
tt.jsxTagEnd = new acorn.TokenType("jsxTagEnd");
|
|
|
|
tt.jsxTagStart.updateContext = function() {
|
|
this.context.push(tc.j_expr); // treat as beginning of JSX expression
|
|
this.context.push(tc.j_oTag); // start opening tag context
|
|
this.exprAllowed = false;
|
|
};
|
|
tt.jsxTagEnd.updateContext = function(prevType) {
|
|
var out = this.context.pop();
|
|
if (out === tc.j_oTag && prevType === tt.slash || out === tc.j_cTag) {
|
|
this.context.pop();
|
|
this.exprAllowed = this.curContext() === tc.j_expr;
|
|
} else {
|
|
this.exprAllowed = true;
|
|
}
|
|
};
|
|
|
|
var pp = acorn.Parser.prototype;
|
|
|
|
// Reads inline JSX contents token.
|
|
|
|
pp.jsx_readToken = function() {
|
|
var out = "", chunkStart = this.pos;
|
|
for (;;) {
|
|
if (this.pos >= this.input.length)
|
|
this.raise(this.start, "Unterminated JSX contents");
|
|
var ch = this.input.charCodeAt(this.pos);
|
|
|
|
switch (ch) {
|
|
case 60: // '<'
|
|
case 123: // '{'
|
|
if (this.pos === this.start) {
|
|
if (ch === 60 && this.exprAllowed) {
|
|
++this.pos;
|
|
return this.finishToken(tt.jsxTagStart);
|
|
}
|
|
return this.getTokenFromCode(ch);
|
|
}
|
|
out += this.input.slice(chunkStart, this.pos);
|
|
return this.finishToken(tt.jsxText, out);
|
|
|
|
case 38: // '&'
|
|
out += this.input.slice(chunkStart, this.pos);
|
|
out += this.jsx_readEntity();
|
|
chunkStart = this.pos;
|
|
break;
|
|
|
|
default:
|
|
if (acorn.isNewLine(ch)) {
|
|
out += this.input.slice(chunkStart, this.pos);
|
|
++this.pos;
|
|
if (ch === 13 && this.input.charCodeAt(this.pos) === 10) {
|
|
++this.pos;
|
|
out += "\n";
|
|
} else {
|
|
out += String.fromCharCode(ch);
|
|
}
|
|
if (this.options.locations) {
|
|
++this.curLine;
|
|
this.lineStart = this.pos;
|
|
}
|
|
chunkStart = this.pos;
|
|
} else {
|
|
++this.pos;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
pp.jsx_readString = function(quote) {
|
|
var out = "", chunkStart = ++this.pos;
|
|
for (;;) {
|
|
if (this.pos >= this.input.length)
|
|
this.raise(this.start, "Unterminated string constant");
|
|
var ch = this.input.charCodeAt(this.pos);
|
|
if (ch === quote) break;
|
|
if (ch === 38) { // '&'
|
|
out += this.input.slice(chunkStart, this.pos);
|
|
out += this.jsx_readEntity();
|
|
chunkStart = this.pos;
|
|
} else {
|
|
++this.pos;
|
|
}
|
|
}
|
|
out += this.input.slice(chunkStart, this.pos++);
|
|
return this.finishToken(tt.string, out);
|
|
};
|
|
|
|
var XHTMLEntities = {
|
|
quot: '\u0022',
|
|
amp: '&',
|
|
apos: '\u0027',
|
|
lt: '<',
|
|
gt: '>',
|
|
nbsp: '\u00A0',
|
|
iexcl: '\u00A1',
|
|
cent: '\u00A2',
|
|
pound: '\u00A3',
|
|
curren: '\u00A4',
|
|
yen: '\u00A5',
|
|
brvbar: '\u00A6',
|
|
sect: '\u00A7',
|
|
uml: '\u00A8',
|
|
copy: '\u00A9',
|
|
ordf: '\u00AA',
|
|
laquo: '\u00AB',
|
|
not: '\u00AC',
|
|
shy: '\u00AD',
|
|
reg: '\u00AE',
|
|
macr: '\u00AF',
|
|
deg: '\u00B0',
|
|
plusmn: '\u00B1',
|
|
sup2: '\u00B2',
|
|
sup3: '\u00B3',
|
|
acute: '\u00B4',
|
|
micro: '\u00B5',
|
|
para: '\u00B6',
|
|
middot: '\u00B7',
|
|
cedil: '\u00B8',
|
|
sup1: '\u00B9',
|
|
ordm: '\u00BA',
|
|
raquo: '\u00BB',
|
|
frac14: '\u00BC',
|
|
frac12: '\u00BD',
|
|
frac34: '\u00BE',
|
|
iquest: '\u00BF',
|
|
Agrave: '\u00C0',
|
|
Aacute: '\u00C1',
|
|
Acirc: '\u00C2',
|
|
Atilde: '\u00C3',
|
|
Auml: '\u00C4',
|
|
Aring: '\u00C5',
|
|
AElig: '\u00C6',
|
|
Ccedil: '\u00C7',
|
|
Egrave: '\u00C8',
|
|
Eacute: '\u00C9',
|
|
Ecirc: '\u00CA',
|
|
Euml: '\u00CB',
|
|
Igrave: '\u00CC',
|
|
Iacute: '\u00CD',
|
|
Icirc: '\u00CE',
|
|
Iuml: '\u00CF',
|
|
ETH: '\u00D0',
|
|
Ntilde: '\u00D1',
|
|
Ograve: '\u00D2',
|
|
Oacute: '\u00D3',
|
|
Ocirc: '\u00D4',
|
|
Otilde: '\u00D5',
|
|
Ouml: '\u00D6',
|
|
times: '\u00D7',
|
|
Oslash: '\u00D8',
|
|
Ugrave: '\u00D9',
|
|
Uacute: '\u00DA',
|
|
Ucirc: '\u00DB',
|
|
Uuml: '\u00DC',
|
|
Yacute: '\u00DD',
|
|
THORN: '\u00DE',
|
|
szlig: '\u00DF',
|
|
agrave: '\u00E0',
|
|
aacute: '\u00E1',
|
|
acirc: '\u00E2',
|
|
atilde: '\u00E3',
|
|
auml: '\u00E4',
|
|
aring: '\u00E5',
|
|
aelig: '\u00E6',
|
|
ccedil: '\u00E7',
|
|
egrave: '\u00E8',
|
|
eacute: '\u00E9',
|
|
ecirc: '\u00EA',
|
|
euml: '\u00EB',
|
|
igrave: '\u00EC',
|
|
iacute: '\u00ED',
|
|
icirc: '\u00EE',
|
|
iuml: '\u00EF',
|
|
eth: '\u00F0',
|
|
ntilde: '\u00F1',
|
|
ograve: '\u00F2',
|
|
oacute: '\u00F3',
|
|
ocirc: '\u00F4',
|
|
otilde: '\u00F5',
|
|
ouml: '\u00F6',
|
|
divide: '\u00F7',
|
|
oslash: '\u00F8',
|
|
ugrave: '\u00F9',
|
|
uacute: '\u00FA',
|
|
ucirc: '\u00FB',
|
|
uuml: '\u00FC',
|
|
yacute: '\u00FD',
|
|
thorn: '\u00FE',
|
|
yuml: '\u00FF',
|
|
OElig: '\u0152',
|
|
oelig: '\u0153',
|
|
Scaron: '\u0160',
|
|
scaron: '\u0161',
|
|
Yuml: '\u0178',
|
|
fnof: '\u0192',
|
|
circ: '\u02C6',
|
|
tilde: '\u02DC',
|
|
Alpha: '\u0391',
|
|
Beta: '\u0392',
|
|
Gamma: '\u0393',
|
|
Delta: '\u0394',
|
|
Epsilon: '\u0395',
|
|
Zeta: '\u0396',
|
|
Eta: '\u0397',
|
|
Theta: '\u0398',
|
|
Iota: '\u0399',
|
|
Kappa: '\u039A',
|
|
Lambda: '\u039B',
|
|
Mu: '\u039C',
|
|
Nu: '\u039D',
|
|
Xi: '\u039E',
|
|
Omicron: '\u039F',
|
|
Pi: '\u03A0',
|
|
Rho: '\u03A1',
|
|
Sigma: '\u03A3',
|
|
Tau: '\u03A4',
|
|
Upsilon: '\u03A5',
|
|
Phi: '\u03A6',
|
|
Chi: '\u03A7',
|
|
Psi: '\u03A8',
|
|
Omega: '\u03A9',
|
|
alpha: '\u03B1',
|
|
beta: '\u03B2',
|
|
gamma: '\u03B3',
|
|
delta: '\u03B4',
|
|
epsilon: '\u03B5',
|
|
zeta: '\u03B6',
|
|
eta: '\u03B7',
|
|
theta: '\u03B8',
|
|
iota: '\u03B9',
|
|
kappa: '\u03BA',
|
|
lambda: '\u03BB',
|
|
mu: '\u03BC',
|
|
nu: '\u03BD',
|
|
xi: '\u03BE',
|
|
omicron: '\u03BF',
|
|
pi: '\u03C0',
|
|
rho: '\u03C1',
|
|
sigmaf: '\u03C2',
|
|
sigma: '\u03C3',
|
|
tau: '\u03C4',
|
|
upsilon: '\u03C5',
|
|
phi: '\u03C6',
|
|
chi: '\u03C7',
|
|
psi: '\u03C8',
|
|
omega: '\u03C9',
|
|
thetasym: '\u03D1',
|
|
upsih: '\u03D2',
|
|
piv: '\u03D6',
|
|
ensp: '\u2002',
|
|
emsp: '\u2003',
|
|
thinsp: '\u2009',
|
|
zwnj: '\u200C',
|
|
zwj: '\u200D',
|
|
lrm: '\u200E',
|
|
rlm: '\u200F',
|
|
ndash: '\u2013',
|
|
mdash: '\u2014',
|
|
lsquo: '\u2018',
|
|
rsquo: '\u2019',
|
|
sbquo: '\u201A',
|
|
ldquo: '\u201C',
|
|
rdquo: '\u201D',
|
|
bdquo: '\u201E',
|
|
dagger: '\u2020',
|
|
Dagger: '\u2021',
|
|
bull: '\u2022',
|
|
hellip: '\u2026',
|
|
permil: '\u2030',
|
|
prime: '\u2032',
|
|
Prime: '\u2033',
|
|
lsaquo: '\u2039',
|
|
rsaquo: '\u203A',
|
|
oline: '\u203E',
|
|
frasl: '\u2044',
|
|
euro: '\u20AC',
|
|
image: '\u2111',
|
|
weierp: '\u2118',
|
|
real: '\u211C',
|
|
trade: '\u2122',
|
|
alefsym: '\u2135',
|
|
larr: '\u2190',
|
|
uarr: '\u2191',
|
|
rarr: '\u2192',
|
|
darr: '\u2193',
|
|
harr: '\u2194',
|
|
crarr: '\u21B5',
|
|
lArr: '\u21D0',
|
|
uArr: '\u21D1',
|
|
rArr: '\u21D2',
|
|
dArr: '\u21D3',
|
|
hArr: '\u21D4',
|
|
forall: '\u2200',
|
|
part: '\u2202',
|
|
exist: '\u2203',
|
|
empty: '\u2205',
|
|
nabla: '\u2207',
|
|
isin: '\u2208',
|
|
notin: '\u2209',
|
|
ni: '\u220B',
|
|
prod: '\u220F',
|
|
sum: '\u2211',
|
|
minus: '\u2212',
|
|
lowast: '\u2217',
|
|
radic: '\u221A',
|
|
prop: '\u221D',
|
|
infin: '\u221E',
|
|
ang: '\u2220',
|
|
and: '\u2227',
|
|
or: '\u2228',
|
|
cap: '\u2229',
|
|
cup: '\u222A',
|
|
'int': '\u222B',
|
|
there4: '\u2234',
|
|
sim: '\u223C',
|
|
cong: '\u2245',
|
|
asymp: '\u2248',
|
|
ne: '\u2260',
|
|
equiv: '\u2261',
|
|
le: '\u2264',
|
|
ge: '\u2265',
|
|
sub: '\u2282',
|
|
sup: '\u2283',
|
|
nsub: '\u2284',
|
|
sube: '\u2286',
|
|
supe: '\u2287',
|
|
oplus: '\u2295',
|
|
otimes: '\u2297',
|
|
perp: '\u22A5',
|
|
sdot: '\u22C5',
|
|
lceil: '\u2308',
|
|
rceil: '\u2309',
|
|
lfloor: '\u230A',
|
|
rfloor: '\u230B',
|
|
lang: '\u2329',
|
|
rang: '\u232A',
|
|
loz: '\u25CA',
|
|
spades: '\u2660',
|
|
clubs: '\u2663',
|
|
hearts: '\u2665',
|
|
diams: '\u2666'
|
|
};
|
|
|
|
var hexNumber = /^[\da-fA-F]+$/;
|
|
var decimalNumber = /^\d+$/;
|
|
|
|
pp.jsx_readEntity = function() {
|
|
var str = "", count = 0, entity;
|
|
var ch = this.input[this.pos];
|
|
if (ch !== "&")
|
|
this.raise(this.pos, "Entity must start with an ampersand");
|
|
var startPos = ++this.pos;
|
|
while (this.pos < this.input.length && count++ < 10) {
|
|
ch = this.input[this.pos++];
|
|
if (ch === ";") {
|
|
if (str[0] === "#") {
|
|
if (str[1] === "x") {
|
|
str = str.substr(2);
|
|
if (hexNumber.test(str))
|
|
entity = String.fromCharCode(parseInt(str, 16));
|
|
} else {
|
|
str = str.substr(1);
|
|
if (decimalNumber.test(str))
|
|
entity = String.fromCharCode(parseInt(str, 10));
|
|
}
|
|
} else {
|
|
entity = XHTMLEntities[str];
|
|
}
|
|
break;
|
|
}
|
|
str += ch;
|
|
}
|
|
if (!entity) {
|
|
this.pos = startPos;
|
|
return "&";
|
|
}
|
|
return entity;
|
|
};
|
|
|
|
|
|
// Read a JSX identifier (valid tag or attribute name).
|
|
//
|
|
// Optimized version since JSX identifiers can't contain
|
|
// escape characters and so can be read as single slice.
|
|
// Also assumes that first character was already checked
|
|
// by isIdentifierStart in readToken.
|
|
|
|
pp.jsx_readWord = function() {
|
|
var ch, start = this.pos;
|
|
do {
|
|
ch = this.input.charCodeAt(++this.pos);
|
|
} while (acorn.isIdentifierChar(ch) || ch === 45); // '-'
|
|
return this.finishToken(tt.jsxName, this.input.slice(start, this.pos));
|
|
};
|
|
|
|
// Transforms JSX element name to string.
|
|
|
|
function getQualifiedJSXName(object) {
|
|
if (object.type === "JSXIdentifier")
|
|
return object.name;
|
|
|
|
if (object.type === "JSXNamespacedName")
|
|
return object.namespace.name + ':' + object.name.name;
|
|
|
|
if (object.type === "JSXMemberExpression")
|
|
return getQualifiedJSXName(object.object) + '.' +
|
|
getQualifiedJSXName(object.property);
|
|
}
|
|
|
|
// Parse next token as JSX identifier
|
|
|
|
pp.jsx_parseIdentifier = function() {
|
|
var node = this.startNode();
|
|
if (this.type === tt.jsxName)
|
|
node.name = this.value;
|
|
else if (this.type.keyword)
|
|
node.name = this.type.keyword;
|
|
else
|
|
this.unexpected();
|
|
this.next();
|
|
return this.finishNode(node, "JSXIdentifier");
|
|
};
|
|
|
|
// Parse namespaced identifier.
|
|
|
|
pp.jsx_parseNamespacedName = function() {
|
|
var start = this.currentPos();
|
|
var name = this.jsx_parseIdentifier();
|
|
if (!this.eat(tt.colon)) return name;
|
|
var node = this.startNodeAt(start);
|
|
node.namespace = name;
|
|
node.name = this.jsx_parseIdentifier();
|
|
return this.finishNode(node, "JSXNamespacedName");
|
|
};
|
|
|
|
// Parses element name in any form - namespaced, member
|
|
// or single identifier.
|
|
|
|
pp.jsx_parseElementName = function() {
|
|
var start = this.currentPos();
|
|
var node = this.jsx_parseNamespacedName();
|
|
while (this.eat(tt.dot)) {
|
|
var newNode = this.startNodeAt(start);
|
|
newNode.object = node;
|
|
newNode.property = this.jsx_parseIdentifier();
|
|
node = this.finishNode(newNode, "JSXMemberExpression");
|
|
}
|
|
return node;
|
|
};
|
|
|
|
// Parses any type of JSX attribute value.
|
|
|
|
pp.jsx_parseAttributeValue = function() {
|
|
switch (this.type) {
|
|
case tt.braceL:
|
|
var node = this.jsx_parseExpressionContainer();
|
|
if (node.expression.type === "JSXEmptyExpression")
|
|
this.raise(node.start, "JSX attributes must only be assigned a non-empty expression");
|
|
return node;
|
|
|
|
case tt.jsxTagStart:
|
|
case tt.string:
|
|
return this.parseExprAtom();
|
|
|
|
default:
|
|
this.raise(this.start, "JSX value should be either an expression or a quoted JSX text");
|
|
}
|
|
};
|
|
|
|
// JSXEmptyExpression is unique type since it doesn't actually parse anything,
|
|
// and so it should start at the end of last read token (left brace) and finish
|
|
// at the beginning of the next one (right brace).
|
|
|
|
pp.jsx_parseEmptyExpression = function() {
|
|
var tmp = this.start;
|
|
this.start = this.lastTokEnd;
|
|
this.lastTokEnd = tmp;
|
|
|
|
tmp = this.startLoc;
|
|
this.startLoc = this.lastTokEndLoc;
|
|
this.lastTokEndLoc = tmp;
|
|
|
|
return this.finishNode(this.startNode(), "JSXEmptyExpression");
|
|
};
|
|
|
|
// Parses JSX expression enclosed into curly brackets.
|
|
|
|
|
|
pp.jsx_parseExpressionContainer = function() {
|
|
var node = this.startNode();
|
|
this.next();
|
|
node.expression = this.type === tt.braceR
|
|
? this.jsx_parseEmptyExpression()
|
|
: this.parseExpression();
|
|
this.expect(tt.braceR);
|
|
return this.finishNode(node, "JSXExpressionContainer");
|
|
};
|
|
|
|
// Parses following JSX attribute name-value pair.
|
|
|
|
pp.jsx_parseAttribute = function() {
|
|
var node = this.startNode();
|
|
if (this.eat(tt.braceL)) {
|
|
this.expect(tt.ellipsis);
|
|
node.argument = this.parseMaybeAssign();
|
|
this.expect(tt.braceR);
|
|
return this.finishNode(node, "JSXSpreadAttribute");
|
|
}
|
|
node.name = this.jsx_parseNamespacedName();
|
|
node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
|
|
return this.finishNode(node, "JSXAttribute");
|
|
};
|
|
|
|
// Parses JSX opening tag starting after '<'.
|
|
|
|
pp.jsx_parseOpeningElementAt = function(start) {
|
|
var node = this.startNodeAt(start);
|
|
node.attributes = [];
|
|
node.name = this.jsx_parseElementName();
|
|
while (this.type !== tt.slash && this.type !== tt.jsxTagEnd)
|
|
node.attributes.push(this.jsx_parseAttribute());
|
|
node.selfClosing = this.eat(tt.slash);
|
|
this.expect(tt.jsxTagEnd);
|
|
return this.finishNode(node, "JSXOpeningElement");
|
|
};
|
|
|
|
// Parses JSX closing tag starting after '</'.
|
|
|
|
pp.jsx_parseClosingElementAt = function(start) {
|
|
var node = this.startNodeAt(start);
|
|
node.name = this.jsx_parseElementName();
|
|
this.expect(tt.jsxTagEnd);
|
|
return this.finishNode(node, "JSXClosingElement");
|
|
};
|
|
|
|
// Parses entire JSX element, including it's opening tag
|
|
// (starting after '<'), attributes, contents and closing tag.
|
|
|
|
pp.jsx_parseElementAt = function(start) {
|
|
var node = this.startNodeAt(start);
|
|
var children = [];
|
|
var openingElement = this.jsx_parseOpeningElementAt(start);
|
|
var closingElement = null;
|
|
|
|
if (!openingElement.selfClosing) {
|
|
contents: for (;;) {
|
|
switch (this.type) {
|
|
case tt.jsxTagStart:
|
|
start = this.currentPos();
|
|
this.next();
|
|
if (this.eat(tt.slash)) {
|
|
closingElement = this.jsx_parseClosingElementAt(start);
|
|
break contents;
|
|
}
|
|
children.push(this.jsx_parseElementAt(start));
|
|
break;
|
|
|
|
case tt.jsxText:
|
|
children.push(this.parseExprAtom());
|
|
break;
|
|
|
|
case tt.braceL:
|
|
children.push(this.jsx_parseExpressionContainer());
|
|
break;
|
|
|
|
default:
|
|
this.unexpected();
|
|
}
|
|
}
|
|
if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name))
|
|
this.raise(
|
|
closingElement.start,
|
|
"Expected corresponding JSX closing tag for <" + getQualifiedJSXName(openingElement.name) + ">");
|
|
}
|
|
|
|
node.openingElement = openingElement;
|
|
node.closingElement = closingElement;
|
|
node.children = children;
|
|
return this.finishNode(node, "JSXElement");
|
|
};
|
|
|
|
// Parses entire JSX element from current position.
|
|
|
|
pp.jsx_parseElement = function() {
|
|
var start = this.currentPos();
|
|
this.next();
|
|
return this.jsx_parseElementAt(start);
|
|
};
|
|
|
|
acorn.plugins.jsx = function(instance) {
|
|
instance.extend("parseExprAtom", function(inner) {
|
|
return function(refShortHandDefaultPos) {
|
|
if (this.type === tt.jsxText)
|
|
return this.parseLiteral(this.value);
|
|
else if (this.type === tt.jsxTagStart)
|
|
return this.jsx_parseElement();
|
|
else
|
|
return inner.call(this, refShortHandDefaultPos);
|
|
};
|
|
});
|
|
|
|
instance.extend("readToken", function(inner) {
|
|
return function(code) {
|
|
if (!this.inType) {
|
|
var context = this.curContext();
|
|
|
|
if (context === tc.j_expr) return this.jsx_readToken();
|
|
|
|
if (context === tc.j_oTag || context === tc.j_cTag) {
|
|
if (acorn.isIdentifierStart(code)) return this.jsx_readWord();
|
|
|
|
if (code == 62) {
|
|
++this.pos;
|
|
return this.finishToken(tt.jsxTagEnd);
|
|
}
|
|
|
|
if ((code === 34 || code === 39) && context == tc.j_oTag)
|
|
return this.jsx_readString(code);
|
|
}
|
|
|
|
if (code === 60 && this.exprAllowed) {
|
|
++this.pos;
|
|
return this.finishToken(tt.jsxTagStart);
|
|
}
|
|
}
|
|
|
|
return inner.call(this, code);
|
|
};
|
|
});
|
|
|
|
instance.extend("updateContext", function(inner) {
|
|
return function(prevType) {
|
|
if (this.type == tt.braceL) {
|
|
var curContext = this.curContext();
|
|
if (curContext == tc.j_oTag) this.context.push(tc.b_expr);
|
|
else if (curContext == tc.j_expr) this.context.push(tc.b_tmpl);
|
|
else inner.call(this, prevType);
|
|
this.exprAllowed = true;
|
|
} else if (this.type === tt.slash && prevType === tt.jsxTagStart) {
|
|
this.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore
|
|
this.context.push(tc.j_cTag); // reconsider as closing tag context
|
|
this.exprAllowed = false;
|
|
} else {
|
|
return inner.call(this, prevType);
|
|
}
|
|
};
|
|
});
|
|
}
|