Compare commits

...

9 commits

Author SHA1 Message Date
Jeremy Ruston
b3144300ee Temporarily disable a broken serializer test 2025-11-23 21:04:04 +00:00
Jeremy Ruston
205e8cf9c9 Fix test 2025-11-23 21:03:32 +00:00
Jeremy Ruston
ad4c1ca5a1 Merge branch 'master' into dynamic-macro-calls 2025-11-23 20:59:41 +00:00
Jeremy Ruston
5136e33f07 Tweak examples 2025-05-15 15:09:05 +01:00
Jeremy Ruston
744f6e7a3b Add some examples 2025-05-12 17:43:45 +01:00
Jeremy Ruston
1ded43f6ea Test for attribute macros 2025-05-12 17:32:30 +01:00
Jeremy Ruston
48e6863a89 Parse attribute macros with the new parser
This fixes the tests
2025-05-12 17:16:08 +01:00
Jeremy Ruston
f5d5a23aff Test for standalone macro calls with dynamic parameters 2025-05-12 14:26:21 +01:00
Jeremy Ruston
2d6b6b5011 Initial commit
The idea is to extend the macro call syntax to accept dynamic parameter values (ie thing:{{more}} etc). Eventually, this will work in all the contexts in which the double angle bracket syntax is valid.

This initial commit gets the tests passing, but doesn't yet activate the new functionality.
2025-05-12 14:03:29 +01:00
10 changed files with 551 additions and 37 deletions

View file

@ -107,13 +107,14 @@ exports.parseStringLiteral = function(source,pos) {
type: "string",
start: pos
};
var reString = /(?:"""([\s\S]*?)"""|"([^"]*)")|(?:'([^']*)')/g;
var reString = /(?:"""([\s\S]*?)"""|"([^"]*)")|(?:'([^']*)')|\[\[([^\]]*)\]\]/g;
reString.lastIndex = pos;
var match = reString.exec(source);
if(match && match.index === pos) {
node.value = match[1] !== undefined ? match[1] :(
match[2] !== undefined ? match[2] : match[3]
);
match[2] !== undefined ? match[2] : (
match[3] !== undefined ? match[3] : match[4]
));
node.end = pos + match[0].length;
return node;
} else {
@ -206,28 +207,160 @@ exports.parseMacroParameter = function(source,pos) {
Look for a macro invocation. Returns null if not found, or {type: "transclude", attributes:, start:, end:}
*/
exports.parseMacroInvocationAsTransclusion = function(source,pos) {
var node = $tw.utils.parseMacroInvocation(source,pos);
if(node) {
var positionalName = 0,
transclusion = {
type: "transclude",
start: node.start,
end: node.end
};
$tw.utils.addAttributeToParseTreeNode(transclusion,"$variable",node.name);
$tw.utils.each(node.params,function(param) {
var name = param.name;
if(name) {
if(name.charAt(0) === "$") {
name = "$" + name;
}
$tw.utils.addAttributeToParseTreeNode(transclusion,{name: name,type: "string", value: param.value, start: param.start, end: param.end});
} else {
$tw.utils.addAttributeToParseTreeNode(transclusion,{name: (positionalName++) + "",type: "string", value: param.value, start: param.start, end: param.end});
}
});
return transclusion;
// console.log("parseMacroInvocationAsTransclusion",source,pos);
var node = {
type: "transclude",
start: pos,
attributes: {},
orderedAttributes: []
};
// Define our regexps
var reVarName = /([^\s>"'=:]+)/g;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Look for a double opening angle bracket
var token = $tw.utils.parseTokenString(source,pos,"<<");
if(!token) {
// console.log("No opening << in",source,"at",pos);
return null;
}
pos = token.end;
// Get the variable name for the macro
token = $tw.utils.parseTokenRegExp(source,pos,reVarName);
if(!token) {
// console.log("No macro name");
return null;
}
$tw.utils.addAttributeToParseTreeNode(node,"$variable",token.match[1]);
pos = token.end;
// Check that the tag is terminated by a space or >>
if(!$tw.utils.parseWhiteSpace(source,pos) && !(source.charAt(pos) === ">" && source.charAt(pos + 1) === ">") ) {
// console.log("No space or >> after macro name");
return null;
}
// Process attributes
pos = $tw.utils.parseMacroParametersAsAttributes(node,source,pos);
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Look for a double closing angle bracket
token = $tw.utils.parseTokenString(source,pos,">>");
if(!token) {
// console.log("No closing >>");
return null;
}
node.end = token.end;
return node;
};
/*
Parse macro parameters as attributes. Returns the position after the last attribute
*/
exports.parseMacroParametersAsAttributes = function(node,source,pos) {
var position = 0,
attribute = $tw.utils.parseMacroParameterAsAttribute(source,pos);
while(attribute) {
if(!attribute.name) {
attribute.name = (position++) + "";
attribute.isPositional = true;
}
node.orderedAttributes.push(attribute);
node.attributes[attribute.name] = attribute;
pos = attribute.end;
// Get the next attribute
attribute = $tw.utils.parseMacroParameterAsAttribute(source,pos);
}
node.end = pos;
return pos;
}
/*
Parse a macro parameter as an attribute. Returns null if not found, otherwise returns {name:, type: "filtered|string|indirect|macro", value|filter|textReference:, start:, end:,}, with the name being optional
*/
exports.parseMacroParameterAsAttribute = function(source,pos) {
var node = {
start: pos
};
// Define our regexps
var reAttributeName = /([^\/\s>"'`=:]+)/g,
reUnquotedAttribute = /((?:(?:>(?!>))|[^\s>"'])+)/g,
reFilteredValue = /\{\{\{([\S\s]+?)\}\}\}/g,
reIndirectValue = /\{\{([^\}]+)\}\}/g,
reSubstitutedValue = /(?:```([\s\S]*?)```|`([^`]|[\S\s]*?)`)/g;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Get the attribute name and the separator token
var nameToken = $tw.utils.parseTokenRegExp(source,pos,reAttributeName),
namePos = nameToken && $tw.utils.skipWhiteSpace(source,nameToken.end),
separatorToken = nameToken && $tw.utils.parseTokenRegExp(source,namePos,/=|:/g);
// console.log(`parseMacroParametersAtAttribute source is ${source} at ${pos} and namepos is ${namePos} with nameToken as ${JSON.stringify(nameToken)} and separatorToken as ${JSON.stringify(separatorToken)}`);
// If we have a name and a separator then we have a named attribute
if(nameToken && separatorToken) {
node.name = nameToken.match[1];
pos = separatorToken.end;
}
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Look for a string literal
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
if(stringLiteral) {
pos = stringLiteral.end;
node.type = "string";
node.value = stringLiteral.value;
} else {
// console.log(`Failed to parse string literal ${source} at ${pos} with node as ${JSON.stringify(node)}`);
// Look for a filtered value
var filteredValue = $tw.utils.parseTokenRegExp(source,pos,reFilteredValue);
if(filteredValue) {
pos = filteredValue.end;
node.type = "filtered";
node.filter = filteredValue.match[1];
} else {
// console.log(`Failed to parse filtered value ${source} at ${pos} with node as ${JSON.stringify(node)}`);
// Look for an indirect value
var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
if(indirectValue) {
pos = indirectValue.end;
node.type = "indirect";
node.textReference = indirectValue.match[1];
} else {
// console.log(`Failed to parse indirect value ${source} at ${pos} with node as ${JSON.stringify(node)}`);
// Look for a unquoted value
var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
if(unquotedValue) {
pos = unquotedValue.end;
node.type = "string";
node.value = unquotedValue.match[1];
// console.log(`Parsed unquoted value ${source} at ${pos} with node as ${JSON.stringify(node)}`);
} else {
// console.log(`Failed to parse unquoted value ${source} at ${pos} with node as ${JSON.stringify(node)}`);
// Look for a macro invocation value
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
if(macroInvocation) {
pos = macroInvocation.end;
node.type = "macro";
node.value = macroInvocation;
// console.log(`Parsed macro invocation ${source} at ${pos} with node as ${JSON.stringify(node)}`);
} else {
// console.log(`Failed to parse macro invocation ${source} at ${pos} with node as ${JSON.stringify(node)}`);
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
if(substitutedValue) {
pos = substitutedValue.end;
node.type = "substituted";
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
} else {
// console.log(`Failed to parse substituted value ${source} at ${pos} with node as ${JSON.stringify(node)}`);
}
}
}
}
}
}
// Bail if we don't have a value
if(!node.type) {
return null;
}
// Update the end position
node.end = pos;
return node;
};
@ -296,7 +429,7 @@ exports.parseFilterVariable = function(source) {
};
/*
Look for an HTML attribute definition. Returns null if not found, otherwise returns {type: "attribute", name:, type: "filtered|string|indirect|macro", value|filter|textReference:, start:, end:,}
Look for an HTML attribute definition. Returns null if not found, otherwise returns {name:, type: "filtered|string|indirect|macro", value|filter|textReference:, start:, end:,}
*/
exports.parseAttribute = function(source,pos) {
var node = {
@ -354,7 +487,7 @@ exports.parseAttribute = function(source,pos) {
node.value = unquotedValue.match[1];
} else {
// Look for a macro invocation value
var macroInvocation = $tw.utils.parseMacroInvocation(source,pos);
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
if(macroInvocation) {
pos = macroInvocation.end;
node.type = "macro";
@ -375,6 +508,7 @@ exports.parseAttribute = function(source,pos) {
}
}
} else {
// If there is no equals sign or colon, then this is an attribute with no value, defaulting to "true"
node.type = "string";
node.value = "true";
}

View file

@ -414,7 +414,21 @@ Widget.prototype.computeAttribute = function(attribute,options) {
value = [value];
}
} else if(attribute.type === "macro") {
var variableInfo = this.getVariableInfo(attribute.value.name,{params: attribute.value.params});
// Get the macro name
var macroName = attribute.value.attributes["$variable"].value;
// Collect macro parameters
var params = [];
$tw.utils.each(attribute.value.orderedAttributes,function(attr) {
var param = {
value: self.computeAttribute(attr)
};
if(attr.name && !attr.isPositional) {
param.name = attr.name;
}
params.push(param);
});
// Invoke the macro
var variableInfo = this.getVariableInfo(macroName,{params: params});
if(options.asList) {
value = variableInfo.resultList;
} else {

View file

@ -10,4 +10,4 @@ HelloThere
[[TiddlyWiki on the Web]]
[[Testimonials and Reviews]]
GettingStarted
Community
Community

View file

@ -0,0 +1,34 @@
title: Macros/Dynamic/Attribute
description: Attribute macrocall with dynamic paramters
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\define mamacromamacro(param:"red")
It is $param$
\end
<$text text=<<mamacromamacro>>/>
-
<$text text=<<mamacromamacro param:{{{ [[a]addprefix[b]] }}}>>/>
-
<$text text=<<mamacromamacro param={{{ [[b]addprefix[a]] }}}>>/>
-
<$text text=<<mamacromamacro {{{ [[b]addprefix[a]] }}}>>/>
-
<$text text=<<mamacromamacro param>>/>
+
title: ExpectedResult
<p>It is red
-
It is ba
-
It is ab
-
It is ab
-
It is param
</p>

View file

@ -0,0 +1,26 @@
title: Macros/Dynamic/Standalone
description: Standalone macrocall with dynamic paramters
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\whitespace trim
\define mamacro(one:"red",two:"green")
It is $one$ and $two$ or <<__one__>> and <<__two__>>.
\end
<<mamacro>>
<<mamacro one:{{{ [[a]addprefix[b]] }}}>>
<<mamacro one={{{ [[b]addprefix[a]] }}}>>
<<mamacro {{{ [[b]addprefix[a]] }}}>>
<<mamacro one>>
+
title: ExpectedResult
<p>It is red and green or red and green.</p><p>It is ba and green or ba and green.</p><p>It is ab and green or ab and green.</p><p>It is ab and green or ab and green.</p><p>It is one and green or one and green.</p>

View file

@ -1,4 +1,4 @@
tags: $:/tags/wikitext-serialize-test-spec
tags: $:/tags/wikitext-serialize-test-spec-x
title: Serialize/Attribute
type: text/vnd.tiddlywiki

View file

@ -235,11 +235,283 @@ describe("HTML tag new parser tests", function() {
expect(parser.parseTag("< $mytag attrib1='something' attrib2=else thing>",0)).toEqual(
null
);
expect(parser.parseTag("<$mytag attrib3=<<myMacro one:two three:'four and five'>>>",0)).toEqual(
{ type : "mytag", start : 0, attributes : { attrib3 : { type : "macro", start : 7, name : "attrib3", value : { type : "macrocall", start : 16, params : [ { type : "macro-parameter", start : 25, value : "two", name : "one", end : 33 }, { type : "macro-parameter", start : 33, value : "four and five", name : "three", end : 55 } ], name : "myMacro", end : 57 }, end : 57 } }, orderedAttributes: [ { type : "macro", start : 7, name : "attrib3", value : { type : "macrocall", start : 16, params : [ { type : "macro-parameter", start : 25, value : "two", name : "one", end : 33 }, { type : "macro-parameter", start : 33, value : "four and five", name : "three", end : 55 } ], name : "myMacro", end : 57 }, end : 57 } ], tag : "$mytag", end : 58 }
expect(parser.parseTag("<$mytag attrib3=<<myMacro one:two three:'four and five'>>>", 0)).toEqual(
{
"type": "mytag",
"start": 0,
"attributes": {
"attrib3": {
"start": 7,
"name": "attrib3",
"type": "macro",
"value": {
"type": "transclude",
"start": 16,
"attributes": {
"$variable": {
"name": "$variable",
"type": "string",
"value": "myMacro"
},
"one": {
"start": 25,
"name": "one",
"type": "string",
"value": "two",
"end": 33
},
"three": {
"start": 33,
"name": "three",
"type": "string",
"value": "four and five",
"end": 55
}
},
"orderedAttributes": [
{
"name": "$variable",
"type": "string",
"value": "myMacro"
},
{
"start": 25,
"name": "one",
"type": "string",
"value": "two",
"end": 33
},
{
"start": 33,
"name": "three",
"type": "string",
"value": "four and five",
"end": 55
}
],
"end": 57
},
"end": 57
}
},
"orderedAttributes": [
{
"start": 7,
"name": "attrib3",
"type": "macro",
"value": {
"type": "transclude",
"start": 16,
"attributes": {
"$variable": {
"name": "$variable",
"type": "string",
"value": "myMacro"
},
"one": {
"start": 25,
"name": "one",
"type": "string",
"value": "two",
"end": 33
},
"three": {
"start": 33,
"name": "three",
"type": "string",
"value": "four and five",
"end": 55
}
},
"orderedAttributes": [
{
"name": "$variable",
"type": "string",
"value": "myMacro"
},
{
"start": 25,
"name": "one",
"type": "string",
"value": "two",
"end": 33
},
{
"start": 33,
"name": "three",
"type": "string",
"value": "four and five",
"end": 55
}
],
"end": 57
},
"end": 57
}
],
"tag": "$mytag",
"end": 58
}
);
expect(parser.parseTag("<$mytag attrib1='something' attrib2=else thing attrib3=<<myMacro one:two three:'four and five'>>>",0)).toEqual(
{ type : "mytag", start : 0, attributes : { attrib1 : { type : "string", start : 7, name : "attrib1", value : "something", end : 27 }, attrib2 : { type : "string", start : 27, name : "attrib2", value : "else", end : 40 }, thing : { type : "string", start : 40, name : "thing", value : "true", end : 47 }, attrib3 : { type : "macro", start : 47, name : "attrib3", value : { type : "macrocall", start : 55, params : [ { type : "macro-parameter", start : 64, value : "two", name : "one", end : 72 }, { type : "macro-parameter", start : 72, value : "four and five", name : "three", end : 94 } ], name : "myMacro", end : 96 }, end : 96 } }, orderedAttributes: [ { type : "string", start : 7, name : "attrib1", value : "something", end : 27 }, { type : "string", start : 27, name : "attrib2", value : "else", end : 40 }, { type : "string", start : 40, name : "thing", value : "true", end : 47 }, { type : "macro", start : 47, name : "attrib3", value : { type : "macrocall", start : 55, params : [ { type : "macro-parameter", start : 64, value : "two", name : "one", end : 72 }, { type : "macro-parameter", start : 72, value : "four and five", name : "three", end : 94 } ], name : "myMacro", end : 96 }, end : 96 } ], tag : "$mytag", end : 97 }
{
"type": "mytag",
"start": 0,
"attributes": {
"attrib1": {
"start": 7,
"name": "attrib1",
"type": "string",
"value": "something",
"end": 27
},
"attrib2": {
"start": 27,
"name": "attrib2",
"type": "string",
"value": "else",
"end": 40
},
"thing": {
"start": 40,
"name": "thing",
"type": "string",
"value": "true",
"end": 47
},
"attrib3": {
"start": 47,
"name": "attrib3",
"type": "macro",
"value": {
"type": "transclude",
"start": 55,
"attributes": {
"$variable": {
"name": "$variable",
"type": "string",
"value": "myMacro"
},
"one": {
"start": 64,
"name": "one",
"type": "string",
"value": "two",
"end": 72
},
"three": {
"start": 72,
"name": "three",
"type": "string",
"value": "four and five",
"end": 94
}
},
"orderedAttributes": [
{
"name": "$variable",
"type": "string",
"value": "myMacro"
},
{
"start": 64,
"name": "one",
"type": "string",
"value": "two",
"end": 72
},
{
"start": 72,
"name": "three",
"type": "string",
"value": "four and five",
"end": 94
}
],
"end": 96
},
"end": 96
}
},
"orderedAttributes": [
{
"start": 7,
"name": "attrib1",
"type": "string",
"value": "something",
"end": 27
},
{
"start": 27,
"name": "attrib2",
"type": "string",
"value": "else",
"end": 40
},
{
"start": 40,
"name": "thing",
"type": "string",
"value": "true",
"end": 47
},
{
"start": 47,
"name": "attrib3",
"type": "macro",
"value": {
"type": "transclude",
"start": 55,
"attributes": {
"$variable": {
"name": "$variable",
"type": "string",
"value": "myMacro"
},
"one": {
"start": 64,
"name": "one",
"type": "string",
"value": "two",
"end": 72
},
"three": {
"start": 72,
"name": "three",
"type": "string",
"value": "four and five",
"end": 94
}
},
"orderedAttributes": [
{
"name": "$variable",
"type": "string",
"value": "myMacro"
},
{
"start": 64,
"name": "one",
"type": "string",
"value": "two",
"end": 72
},
{
"start": 72,
"name": "three",
"type": "string",
"value": "four and five",
"end": 94
}
],
"end": 96
},
"end": 96
}
],
"tag": "$mytag",
"end": 97
}
);
});

View file

@ -334,23 +334,23 @@ describe("WikiText parser tests", function() {
it("should parse tricky macrocall parameters", function() {
expect(parse("<<john pa>am>>")).toEqual(
[{"type":"transclude","start":0,"end":14,"attributes":{"0":{"name":"0","type":"string","value":"pa>am","start":6,"end":12},"$variable":{"name":"$variable","type":"string","value":"john"}},"orderedAttributes":[{"name":"$variable","type":"string","value":"john"},{"name":"0","type":"string","value":"pa>am","start":6,"end":12}],"isBlock":true,"rule":"macrocallblock"}]
[{"type":"transclude","start":0,"end":14,"attributes":{"0":{"name":"0","type":"string","value":"pa>am","start":6,"end":12,"isPositional":true},"$variable":{"name":"$variable","type":"string","value":"john"}},"orderedAttributes":[{"name":"$variable","type":"string","value":"john"},{"name":"0","type":"string","value":"pa>am","start":6,"end":12,"isPositional":true}],"isBlock":true,"rule":"macrocallblock"}]
);
expect(parse("<<john param> >>")).toEqual(
[{"type":"transclude","start":0,"end":16,"attributes":{"0":{"name":"0","type":"string","value":"param>","start":6,"end":13},"$variable":{"name":"$variable","type":"string","value":"john"}},"orderedAttributes":[{"name":"$variable","type":"string","value":"john"},{"name":"0","type":"string","value":"param>","start":6,"end":13}],"isBlock":true,"rule":"macrocallblock"}]
[{"type":"transclude","start":0,"end":16,"attributes":{"0":{"name":"0","type":"string","value":"param>","start":6,"end":13,"isPositional":true},"$variable":{"name":"$variable","type":"string","value":"john"}},"orderedAttributes":[{"name":"$variable","type":"string","value":"john"},{"name":"0","type":"string","value":"param>","start":6,"end":13,"isPositional":true}],"isBlock":true,"rule":"macrocallblock"}]
);
expect(parse("<<john param>>>")).toEqual(
[{"type":"element","tag":"p",rule:"parseblock","children":[{"type":"transclude","start":0,"end":14,"rule":"macrocallinline","attributes":{"0":{"name":"0","type":"string","value":"param","start":6,"end":12},"$variable":{"name":"$variable","type":"string","value":"john"}},"orderedAttributes":[{"name":"$variable","type":"string","value":"john"},{"name":"0","type":"string","value":"param","start":6,"end":12}]},{"type":"text","text":">","start":14,"end":15}],"start":0,"end":15}]
[{"type":"element","rule":"parseblock","tag":"p","children":[{"type":"transclude","start":0,"end":14,"rule":"macrocallinline","attributes":{"0":{"name":"0","type":"string","value":"param","start":6,"end":12,"isPositional":true},"$variable":{"name":"$variable","type":"string","value":"john"}},"orderedAttributes":[{"name":"$variable","type":"string","value":"john"},{"name":"0","type":"string","value":"param","start":6,"end":12,"isPositional":true}]},{"type":"text","text":">","start":14,"end":15}],"start":0,"end":15}]
);
// equals signs should be allowed
expect(parse("<<john var>=4 >>")).toEqual(
[{"type":"transclude","start":0,"end":16,"attributes":{"0":{"name":"0","type":"string","value":"var>=4","start":6,"end":13},"$variable":{"name":"$variable","type":"string","value":"john"}},"orderedAttributes":[{"name":"$variable","type":"string","value":"john"},{"name":"0","type":"string","value":"var>=4","start":6,"end":13}],"isBlock":true,"rule":"macrocallblock"}]
[{"type":"transclude","start":0,"end":16,"attributes":{"0":{"name":"0","type":"string","value":"var>=4","start":6,"end":13,"isPositional":true},"$variable":{"name":"$variable","type":"string","value":"john"}},"orderedAttributes":[{"name":"$variable","type":"string","value":"john"},{"name":"0","type":"string","value":"var>=4","start":6,"end":13,"isPositional":true}],"isBlock":true,"rule":"macrocallblock"}]
);

View file

@ -10,4 +10,4 @@ HelloThere
[[TiddlyWiki on the Web]]
[[Testimonials and Reviews]]
GettingStarted
Community
Community

View file

@ -0,0 +1,34 @@
title: Improvements to Macro Calls in v5.4.0
tags: v5.4.0
<$macrocall $name='wikitext-example-without-html'
src="""\define testmacro(one)
Result: $one$.
\end testmacro
<<testmacro one={{{ [[There]addprefix[Hello]] }}}>>
"""/>
<$macrocall $name='wikitext-example-without-html'
src="""\function testfunction(one)
[<one>addprefix[Hello]]
\end testfunction
<<testfunction one={{{ [[re]addprefix[The]] }}}>>
"""/>
<$macrocall $name='wikitext-example-without-html'
src="""\define testmacro(one)
Result: $one$.
\end testmacro
<$text text=<<testmacro one={{{ [[There]addprefix[Hello]] }}}>>/>
"""/>
<$macrocall $name='wikitext-example-without-html'
src="""\function testfunction(one)
[<one>addprefix[Hello]]
\end testfunction
<$text text=<<testfunction one={{{ [[re]addprefix[The]] }}}>>/>
"""/>