blob: 756fe88b73cf3a98b47e8900c95150c3c827bcb1 [file] [log] [blame]
options {
LOOKAHEAD = 1;
STATIC = false;
UNICODE_INPUT = true;
MULTI = true;
BUILD_NODE_FILES = false;
NODE_PREFIX = "";
NODE_DEFAULT_VOID = true;
NODE_USES_PARSER = false;
NODE_PACKAGE = "org.tautua.markdownpapers.ast";
DEBUG_PARSER = false;
DEBUG_TOKEN_MANAGER = false;
DEBUG_LOOKAHEAD = false;
}
PARSER_BEGIN(Parser)
package org.tautua.markdownpapers.parser;
import org.tautua.markdownpapers.ast.*;
import org.tautua.markdownpapers.util.*;
public class Parser {
private static final String EMPTY_STRING = "";
private static final String QUOTE = '"' + "";
private Stack<Node> stack = new DequeStack<Node>();
private Stack<Node> markupStack = new DequeStack<Node>();
private int currentQuoteLevel = 0;
private int parentheses;
private int brackets;
public Document parse() throws ParseException {
jj_input_stream.setTabSize(4);
Document();
return (Document)getRootNode();
}
public Node getRootNode() {
return jjtree.rootNode();
}
String val(Token t) {
String i = t.image;
if (t.kind == EMPHASIS_BOLD || (t.kind == CODE_SPAN && i.startsWith("``"))) {
i = i.substring(2, i.length() - 2);
} else if (t.any(CODE_SPAN, EMPHASIS_ITALIC)) {
i = i.substring(1, i.length() - 1);
} else if (t.any(EMPHASIS_ITALIC_BOLD)) {
i = i.substring(3, i.length() - 3);
} else if(t.kind == ESCAPED_CHAR) {
i = String.valueOf(i.charAt(1));
}
return i;
}
String toWhitespace(Token prev, Token tab) {
int x = (4 - ((prev == null ? 1 : prev.endColumn + 1) % 4)) + 1;
switch(x) {
case 1:
return " ";
case 2:
return " ";
case 3:
return " ";
default:
return " ";
}
}
boolean ParagraphLookahead() {
if (getToken(1).kind != EOL) {
return false;
}
int i = 2;
int quoteLevel = 0;
Token t;
do {
t = getToken(i++);
if (t.kind == GT) {
quoteLevel++;
} else if (t.kind == EOL) {
quoteLevel = 0;
}
} while(t.any(EOL, SPACE, TAB, GT));
if (t.any(PLUS, MINUS, STAR, NUMBERING, EOF)) {
return false;
}
return currentQuoteLevel == quoteLevel && stack.size() > 0 && stack.peek() instanceof Item
&& ((Item)stack.peek()).getIndentation() < t.beginColumn;
}
boolean LineLookahead() {
if (getToken(1).kind != EOL) {
return false;
}
int i = 2;
int quoteLevel = 0;
Token t;
do {
t = getToken(i++);
if(t.kind == GT) {
quoteLevel++;
}
} while (t.any(SPACE, TAB, GT));
if (t.any(EOL, EOF)) {
return false;
}
if (t.any(PLUS, MINUS, STAR, NUMBERING) && stack.peek() instanceof Item) {
return false;
}
return currentQuoteLevel >= quoteLevel;
}
boolean CodeLineLookahead() {
if (getToken(1).kind != EOL) {
return false;
}
int i = 2;
int quoteLevel = 0;
int _indent = 0;
Token t;
do {
t = getToken(i++);
if(t.kind == GT) {
quoteLevel++;
_indent = 0;
} else if(t.kind == SPACE) {
_indent++;
} else if(t.kind == TAB) {
_indent += 4;
}
} while (t.any(SPACE, TAB, GT) && _indent < 4);
if (t.any(EOL, EOF)) {
return true;
}
return currentQuoteLevel >= quoteLevel && _indent >= 4;
}
boolean QuotedElementLookahead() {
if (getToken(1).none(EOL)) {
return false;
}
int i = 2;
int quoteLevel = 0;
Token t;
do {
t = getToken(i++);
if (t.any(GT)) {
quoteLevel++;
}
} while (t.any(SPACE, TAB, GT));
if (t.any(EOL, EOF)) {
return true;
}
return currentQuoteLevel <= quoteLevel;
}
boolean LooseLookahead() {
if (getToken(1).none(EOL)) {
return false;
}
int i = 2;
boolean newline = false;
Token t;
do {
t = getToken(i++);
if (t.any(EOL)) {
newline = true;
}
} while(t.any(SPACE, TAB, GT, EOL));
Item item = (Item)stack.peek();
return newline && t.any(PLUS, MINUS, STAR, NUMBERING) && item.getIndentation() == t.beginColumn;
}
boolean TextLookahead() {
if (stack.size() > 0 && stack.peek() instanceof Header) {
int i = 1;
Token t;
do {
t = getToken(i++);
} while(t.any(SHARP));
return t.none(EOL,EOF);
}
return getToken(1).none(EOL, EOF);
}
boolean ListLookahead() {
if (getToken(1).none(EOL)) {
return false;
}
int i = 2;
int quoteLevel = 0;
Token t;
do {
t = getToken(i++);
if (t.any(GT)) {
quoteLevel++;
}
} while (t.any(EOL, SPACE, TAB, GT));
Item item = (Item)stack.peek();
return t.any(PLUS, MINUS, STAR, NUMBERING) && item.getIndentation() < t.beginColumn
&& quoteLevel <= currentQuoteLevel;
}
boolean ItemLookahead() {
if (getToken(1).none(EOL)) {
return false;
}
int i = 2;
int quoteLevel = 0;
Token t;
do {
t = getToken(i++);
if (t.any(GT)) {
quoteLevel++;
}
} while (t.any(EOL, SPACE, TAB, GT));
List list = (List)stack.peek();
return t.any(PLUS, MINUS, STAR, NUMBERING) && list.getIndentation() == t.beginColumn;
}
boolean QuoteInsideTitleLookahead(int quoteKind) {
if (getToken(1).kind == quoteKind) {
Token t;
int i = 2;
do {
t = getToken(i++);
} while (t.none(quoteKind, RPAREN, EOL, EOF));
return t.kind == quoteKind;
}
return getToken(1).none(EOL, EOF);
}
}
PARSER_END(Parser)
/* WHITESPACE */
TOKEN : {
< SPACE : " " >
| < TAB : "\t" >
| < EOL : "\r" | "\n" | "\r\n" >
}
/* PUNCTUATION */
TOKEN : {
< AMPERSAND : "&" >
| < BACKSLASH : "\\" >
| < BACKTICK : "`" >
| < BANG : "!" >
| < COLON : ":" >
| < DOUBLE_QUOTE : "\"" >
| < EQ : "=" >
| < GT : ">" >
| < LBRACKET : "[" >
| < LPAREN : "(" >
| < LT : "<" >
| < MINUS : "-" >
| < PLUS : "+" >
| < RBRACKET : "]" >
| < RPAREN : ")" >
| < SHARP : "#" >
| < SINGLE_QUOTE : "'" >
| < SLASH : "/" >
| < STAR : "*" >
| < UNDERSCORE : "_" >
}
TOKEN : {
< MINUS_RULER : "-" ( (" "){0,2} )? "-" ( ( (" "){0,2} )? "-" )+ >
| < STAR_RULER : "*" ( (" "){0,2} )? "*" ( ( (" "){0,2} )? "*" )+ >
| < UNDERSCORE_RULER : "_" ( (" "){0,2} )? "_" ( ( (" "){0,2} )? "_" )+ >
}
TOKEN : {
< COMMENT_OPEN : "<!--" >
| < COMMENT_CLOSE : "-->" >
}
TOKEN : {
< CODE_SPAN : "`" ( ~["`", "\r", "\n"] )+ "`" | "`" "`" ( ~["'", "\r", "\n"] )+ "`" "`" >
| < EMPHASIS_ITALIC : "*" <EMP_A> ( ( ~["*", "\r", "\n"] )* <EMP_A> )? "*"
| "_" <EMP_U> ( ( ~["_", "\r", "\n"] )* <EMP_U> )? "_">
| < EMPHASIS_BOLD : "**" <EMP_A> ( ( ~["*", "\r", "\n"] )* <EMP_A> )? "**"
| "__" <EMP_U> ( ( ~["_", "\r", "\n"] )* <EMP_U> )? "__">
| < EMPHASIS_ITALIC_BOLD : "***" <EMP_A> ( ( ~["*", "\r", "\n"] )* <EMP_A> )? "***"
| "___" <EMP_U> ( ( ~["_", "\r", "\n"] )* <EMP_U> )? "___">
| < NUMBERING : ( ["0"-"9"] )+ "." >
| < #EMP_A : ~["*", " ", "\t", "\r", "\n"] >
| < #EMP_U : ~["_", " ", "\t", "\r", "\n"] >
}
TOKEN : {
< CHAR_ENTITY_REF : "&" ( ["a"-"z", "A"-"Z"] )+ ";" >
| < NUMERIC_CHAR_REF : "&" ( ( ["0"-"9"] ){1,4} | "x" ( ["0"-"9", "a"-"f", "A"-"F"] ){1,4} ) ";" >
| < ESCAPED_CHAR : "\\" ["{", "}", "[", "]", "(", ")", "\\", "`", "_", ">", "#", ".", "!", "+", "-", "*"] >
| < CHAR_SEQUENCE : ( ~["=", "#", "&", "*", "\"", "'", "`", ":", "<", ">", "(", ")", "[", "]", " ", "\\", "/", "\t", "\r", "\n", "!", "_"] )+ >
}
void Document() #Document : {} {
(
<EOL>
| Element() ( LOOKAHEAD(2) <EOL> Element() )*
)*
<EOF>
}
void Element() : {} {
LOOKAHEAD( ResourceDefinition() ) DocumentElement() | BlockElement()
}
void DocumentElement() : {} {
ResourceDefinition()
}
void BlockElement() : {} {
LOOKAHEAD( Ruler() ( <EOL> | <EOF> ) ) Ruler()
| LOOKAHEAD( Header() ( <EOL> | <EOF> ) ) Header()
| LOOKAHEAD( EmptyLine() ) Whitespace()
| LOOKAHEAD( CodeLine() ) Code()
| LOOKAHEAD( QuotePrefix() ) Quote()
| LOOKAHEAD( ( InsignificantWhitespace() )? ( <PLUS> | <MINUS> | <STAR> | <NUMBERING> ) ( <SPACE> | <TAB> ) ) List()
| LOOKAHEAD( Comment() ) Comment()
| Paragraph()
}
/*
void HtmlBlock() : {} {
Tag()
}
*/
void Whitespace() : {} {
( <SPACE> | <TAB> )+
}
void InsignificantWhitespace() : {} {
<SPACE> ( <SPACE> ( <SPACE> )? )?
}
void EmptyLine() : {} {
( Whitespace() )? ( <EOL> | <EOF> )
}
void Header() #Header : {
int level = 1;
stack.push(jjtThis);
} {
(
level = HeaderPrefix() Line() ( "#" )*
| Line() <EOL> level = HeaderSuffix()
) ( Whitespace() )?
{
jjtThis.setLevel(level);
stack.pop();
}
}
int HeaderPrefix() : {
int level = 1;
} {
"#" ( "#" { level++; } ( "#" { level++; } ( "#" { level++; } ( "#" { level++; } ( "#" { level++; } )? )? )? )? )?
{
return level;
}
}
int HeaderSuffix() : {
int level = 1;
}{
(
( < EQ > )+ { level = 1; }
| < MINUS_RULER > { level = 2; }
| < MINUS > { level = 2; }
)
{
return level;
}
}
void Ruler() #Ruler : {} {
( InsignificantWhitespace() )? ( <UNDERSCORE_RULER> | <MINUS_RULER> | <STAR_RULER> ) ( Whitespace() )?
}
void Quote() #Quote : {
stack.push(jjtThis);
currentQuoteLevel++;
} {
QuotePrefix() BlockElement()
( LOOKAHEAD( {QuotedElementLookahead()} ) <EOL>
(
LOOKAHEAD( ( Whitespace() )? <EOL> | <EOF> ) Whitespace() #Line
| LOOKAHEAD( QuotePrefix() ) QuotePrefix() ( BlockElement() )?
)?
)*
{
currentQuoteLevel--;
stack.pop();
}
}
void QuotePrefix() : {} {
( InsignificantWhitespace() )? <GT> ( <SPACE> )?
}
void Code() #Code : {} {
CodeLine()
( LOOKAHEAD( {CodeLineLookahead()} ) <EOL>
( LOOKAHEAD( QuotePrefix() ) QuotePrefix() )*
(
LOOKAHEAD( ( Whitespace() )? <EOL> | <EOF> ) ( Whitespace() )? #Line
| CodeLine()
)
)*
}
void CodeLine() #Line : {} {
CodeLinePrefix() CodeText()
}
void CodeLinePrefix() : {} {
<SPACE> <SPACE> <SPACE> <SPACE> | <TAB>
}
void CodeText() #CodeText : {
Token t;
Token prev = null;
} {
(
(
(
t = <AMPERSAND>
| t = <BACKTICK>
| t = <BACKSLASH>
| t = <BANG>
| t = <CHAR_ENTITY_REF>
| t = <CHAR_SEQUENCE>
| t = <CODE_SPAN>
| t = <COMMENT_OPEN>
| t = <COMMENT_CLOSE>
| t = <COLON>
| t = <DOUBLE_QUOTE>
| t = <EMPHASIS_ITALIC>
| t = <EMPHASIS_BOLD>
| t = <EMPHASIS_ITALIC_BOLD>
| t = <EQ>
| t = <ESCAPED_CHAR>
| t = <GT>
| t = <NUMBERING>
| t = <NUMERIC_CHAR_REF>
| t = <LBRACKET>
| t = <LPAREN>
| t = <LT>
| t = <MINUS>
| t = <MINUS_RULER>
| t = <PLUS>
| t = <RBRACKET>
| t = <RPAREN>
| t = <SHARP>
| t = <SINGLE_QUOTE>
| t = <SLASH>
| t = <SPACE>
| t = <STAR>
| t = <STAR_RULER>
| t = <UNDERSCORE>
| t = <UNDERSCORE_RULER>
) { jjtThis.append(t.image); }
| t = <TAB> { jjtThis.append(toWhitespace(prev, t)); }
) { prev = t; }
)*
}
void ResourceDefinition() #ResourceDefinition : {
String n = null;
Resource resource;
} {
( <SPACE> ( <SPACE> ( <SPACE> )? )? )?
"[" n = refname() "]" { jjtThis.setId(n); }
( <SPACE> )? ":"
( Whitespace() )?
resource = Resource() { jjtThis.setResource(resource); }
}
void List() #List : {
stack.push(jjtThis);
} {
( <SPACE> | <TAB> | <GT> )*
Item()
(
LOOKAHEAD( {ItemLookahead()} ) <EOL>
( <SPACE> | <TAB> | <GT> )* ( Item() )?
)*
{
stack.pop();
}
}
void Item() #Item : {
stack.push(jjtThis);
Token t;
} {
(
(
t = <PLUS>
| t = <MINUS>
| t = <STAR>
| t = <NUMBERING> { jjtThis.makeOrdered(); }
)
( <SPACE> | <TAB> )
) { jjtThis.setIndentation(t.beginColumn); }
Paragraph()
(
LOOKAHEAD( {ParagraphLookahead()} ) <EOL> ( LOOKAHEAD( EmptyLine() ) ( Whitespace() )? <EOL> )* Paragraph() { jjtThis.makeLoose(); }
| LOOKAHEAD( {LooseLookahead()} ) <EOL> ( Whitespace() )? { jjtThis.makeLoose(); }
| LOOKAHEAD( {ListLookahead()} ) <EOL> ( LOOKAHEAD( EmptyLine() ) ( Whitespace() )? <EOL> )* List()
)*
{
Item item = (Item)stack.pop();
List list = (List)stack.peek();
if (list.getIndentation() == 0) {
list.setIndentation(item.getIndentation());
}
}
}
void Paragraph() #Paragraph : {} {
Line()
( LOOKAHEAD( {LineLookahead()} ) <EOL> ( <SPACE> | <TAB> | <GT> )* Line() )*
}
void Line() #Line : {} {
(LOOKAHEAD( {TextLookahead()} )
(
CharRef()
| CodeSpan()
| Emphasis()
| LOOKAHEAD( InlineURL() ) InlineURL()
| LOOKAHEAD( Tag() ) Tag()
| LOOKAHEAD( Image() ) Image()
| LOOKAHEAD( Link() ) Link()
| LOOKAHEAD( LineBreak() <EOL> ) LineBreak()
| Text()
)
)+
}
void LineBreak() #LineBreak : {} {
<SPACE> <SPACE>
}
void Text() #Text : {
Token t;
String v;
} {
v = Anything() { jjtThis.append(v); }
}
void CharRef() #CharRef : {
Token t;
} {
( t = <NUMERIC_CHAR_REF> | t = <CHAR_ENTITY_REF> ) { jjtThis.setValue(t.image); }
}
void CodeSpan() #CodeSpan : {
Token t;
} {
t = <CODE_SPAN> { jjtThis.setText(val(t)); }
}
void Emphasis() #Emphasis : {
Token t;
} {
t = <EMPHASIS_ITALIC> { jjtThis.makeItalic(val(t)); }
| t = <EMPHASIS_BOLD> { jjtThis.makeBold(val(t)); }
| t = <EMPHASIS_ITALIC_BOLD> { jjtThis.makeItalicAndBold(val(t)); }
}
void Comment() #Comment : {
StringBuilder buff = new StringBuilder();
String v;
Token t;
} {
<COMMENT_OPEN>
(
(
LOOKAHEAD( {getToken(1).none(EOL,COMMENT_CLOSE)} ) v = Anything() { buff.append(v); }
| t = <EOL> { buff.append(t.image); }
)
)*
{ jjtThis.setText(buff.toString()); }
<COMMENT_CLOSE>
}
void InlineURL() #InlineUrl : {
Token t;
StringBuilder buff = new StringBuilder();
} {
"<"
t = <CHAR_SEQUENCE> { buff.append(t.image); }
":" { buff.append(":"); }
( "/" { buff.append("/"); } )*
t = <CHAR_SEQUENCE> { buff.append(t.image); }
(
t = <CHAR_SEQUENCE> { buff.append(t.image); }
| "/" { buff.append("/"); }
| "&" { buff.append("&"); }
| "=" { buff.append("="); }
)*
">"
{ jjtThis.setUrl(buff.toString()); }
}
void Link() #Link : {
String text, reference = "";
Resource resource = null;
} {
"["
(
CharRef()
| CodeSpan()
| Emphasis()
| LOOKAHEAD( InlineURL() ) InlineURL()
| LOOKAHEAD( Image() ) Image()
| LOOKAHEAD( Link() ) Link()
| LOOKAHEAD( {getToken(1).none(RBRACKET, EOL, EOF)} ) Text()
| <EOL> #Text { ((Text)jjtree.peekNode()).append("\n"); }
)+
"]"
(
LOOKAHEAD(3)
(
( <SPACE> { jjtThis.setWhitespaceAtMiddle(); } )? ( <EOL> )?
"["
( reference = refname() )?
{ jjtThis.setReference(reference); }
"]"
|
"("
( resource = Resource() )?
( Whitespace() )?
{ jjtThis.setResource(resource); }
")"
)
)?
}
void Image() #Image : {
String text, reference;
Resource resource = null;
} {
"!" "["
text = refname() { jjtThis.setText(text); }
"]"
(
( <SPACE> )?
"["
reference = refname() { jjtThis.setReference(reference); }
"]"
|
"("
(resource = Resource() { jjtThis.setResource(resource); })?
( Whitespace() )?
")"
)?
}
void Tag() : {
Token t;
TagAttribute attribute;
java.util.List<TagAttribute> attributes = new java.util.ArrayList<TagAttribute>();
} {
(
LOOKAHEAD(2)
"<" "/" t = <CHAR_SEQUENCE> <GT> #ClosingTag
{
ClosingTag closing = ((ClosingTag)jjtree.peekNode());
closing.setName(t.image);
if (markupStack.size() > 0) {
OpeningTag opening = (OpeningTag)markupStack.peek();
if (opening.getName().equals(closing.getName())) {
opening.setClosingTag(closing);
markupStack.pop();
} else {
markupStack.clear();
}
}
}
|
"<" t = <CHAR_SEQUENCE>
(
LOOKAHEAD( ( <SPACE> )+ <CHAR_SEQUENCE> )
( <SPACE> )+ attribute = TagAttribute() { attributes.add(attribute); }
)* ( <SPACE> )*
(
"/" ">" #EmptyTag { EmptyTag emptytag = (EmptyTag)jjtree.peekNode(); emptytag.setName(t.image); emptytag.setAttributes(attributes); }
| ">" #OpeningTag { OpeningTag opening = (OpeningTag)jjtree.peekNode(); opening.setName(t.image); opening.setAttributes(attributes); markupStack.push(opening); })
)
}
TagAttribute TagAttribute() : {
StringBuilder buff = new StringBuilder();
Token attrName;
Token t;
} {
attrName = <CHAR_SEQUENCE> "="
(
"\""
(
(
t = <CODE_SPAN>
| t = <ESCAPED_CHAR>
| t = <SPACE>
| t = <TAB>
| t = <CHAR_SEQUENCE>
| t = "&"
| t = "("
| t = ")"
| t = "<"
| t = ">"
| t = "/"
| t = "\\"
| t = "'"
| t = "`"
) { buff.append(t.image); }
)*
"\""
|
"'"
(
(
t = <CODE_SPAN>
| t = <ESCAPED_CHAR>
| t = <SPACE>
| t = <TAB>
| t = <CHAR_SEQUENCE>
| t = "&"
| t = "("
| t = ")"
| t = "<"
| t = ">"
| t = "/"
| t = "\\"
| t = "\""
| t = "`"
) { buff.append(t.image); }
)*
"'"
)
{
return new TagAttribute(attrName.image, buff.toString());
}
}
String refname() : {
Token t;
StringBuilder buff = new StringBuilder();
} {
(
(
t = <AMPERSAND>
| t = <BACKSLASH>
| t = <COLON>
| t = <EQ>
| t = <GT>
| t = <LBRACKET>
| t = <LPAREN>
| t = <LT>
| t = <RPAREN>
| t = <SLASH>
| t = <SPACE>
| t = <TAB>
| t = <CHAR_SEQUENCE>
) { buff.append(t.image); }
)+
{ return buff.toString(); }
}
Resource Resource() : {
String url;
String hint = null;
} {
url = url()
(
Whitespace()
hint = title()
)?
{ return new Resource(url, hint); }
}
String url() : {
String text;
} {
(
"<"
text = urltext()
">"
|
text = urltext()
)
{ return text; }
}
String urltext() : {
Token t;
StringBuilder buff = new StringBuilder();
} {
(
(
t = <CHAR_SEQUENCE>
| t = "&"
| t = <EQ>
| t = "(" { parentheses++; }
| LOOKAHEAD({parentheses > 0}) t = ")" { parentheses--; }
| t = "["
| t = "]"
| t = ":"
| t = "/"
| t = "\\"
| t = "#"
| t = <UNDERSCORE>
) { buff.append(t.image); }
) +
{ parentheses = 0; return buff.toString(); }
}
String title() : {
String text;
} {
(
(
"\""
text = titletext(DOUBLE_QUOTE)
"\""
)
|
(
"'"
text = titletext(SINGLE_QUOTE)
"'"
)
)
{ return text; }
}
String titletext(int quoteType) : {
StringBuilder buff = new StringBuilder();
Token t;
} {
(
LOOKAHEAD( {QuoteInsideTitleLookahead(quoteType)} )
(
t = <CHAR_SEQUENCE>
| t = <SPACE>
| t = <TAB>
| t = "&"
| t = "("
| t = ")"
| t = "/"
| t = "\\"
| t = "'"
| t = "\""
| t = <UNDERSCORE>
) { buff.append(t.image); }
)*
{ return buff.toString(); }
}
String Anything() : {
Token t;
} {
(
t = <AMPERSAND>
| t = <BACKSLASH>
| t = <BACKTICK>
| t = <BANG>
| t = <CHAR_SEQUENCE>
| t = <COMMENT_CLOSE>
| t = <COMMENT_OPEN>
| t = <COLON>
| t = <DOUBLE_QUOTE>
| t = <EQ>
| t = <ESCAPED_CHAR> { t.image = val(t); }
| t = <GT>
| t = <LBRACKET>
| t = <LPAREN>
| t = <LT>
| t = <MINUS>
| t = <MINUS_RULER>
| t = <NUMBERING>
| t = <PLUS>
| t = <RBRACKET>
| t = <RPAREN>
| t = <SHARP>
| t = <SINGLE_QUOTE>
| t = <SLASH>
| t = <SPACE>
| t = <STAR>
| t = <STAR_RULER>
| t = <TAB>
| t = <UNDERSCORE>
| t = <UNDERSCORE_RULER>
) { return t.image; }
}