diff --git a/README.md b/README.md index 51e4258..d0db4d4 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ Inside a paragraph, list item, or header, the following inline formatting elemen **Variables** are written as `_x_` and are translated to `<var>x</var>`. Variables cannot contain whitespace or other formatting characters. +**Fields** are written as `[[f]]` and are translated as `<var class="field">[[f]]</var>`. Field names must match regular expression `/^[a-zA-Z0-9_]+$/`. + **Values** are written as `*x*` and are translated to `<emu-val>x</emu-val>`. Values cannot contain asterisks. **Code** is written as `` `x` `` and is translated to `<code>x</code>`. Code cannot contain backticks. diff --git a/src/emitter.ts b/src/emitter.ts index 11d313a..590203c 100644 --- a/src/emitter.ts +++ b/src/emitter.ts @@ -7,6 +7,7 @@ import type { TagNode, UnderscoreNode, StarNode, + DoubleBracketsNode, OrderedListItemNode, UnorderedListItemNode, OrderedListNode, @@ -72,6 +73,9 @@ export class Emitter { case 'tilde': this.emitTilde(node); break; + case 'double-brackets': + this.emitFieldOrSlot(node); + break; case 'comment': case 'tag': case 'opaqueTag': @@ -125,6 +129,10 @@ export class Emitter { this.str += `<var>${node.contents}</var>`; } + emitFieldOrSlot(node: DoubleBracketsNode) { + this.str += `<var class="field">[[${node.contents}]]</var>`; + } + emitTag(tag: OpaqueTagNode | CommentNode | TagNode) { this.str += tag.contents; } @@ -159,9 +167,9 @@ export class Emitter { this.str += '>' + pipe.nonTerminal + '</emu-nt>'; } - wrapFragment(wrapping: string, fragment: Node[]) { - this.str += `<${wrapping}>`; + wrapFragment(tagName: string, fragment: Node[], attrs: string = '') { + this.str += `<${tagName}${attrs}>`; this.emitFragment(fragment); - this.str += `</${wrapping}>`; + this.str += `</${tagName}>`; } } diff --git a/src/node-types.ts b/src/node-types.ts index 253a8f8..b056df7 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -47,6 +47,12 @@ export type WhitespaceToken = { location: LocationRange; }; +export type DoubleBracketsToken = { + name: 'double-brackets'; + contents: string; + location: LocationRange; +}; + export type TextToken = { name: 'text'; contents: string; @@ -96,6 +102,7 @@ export type Token = | ParabreakToken | LinebreakToken | WhitespaceToken + | DoubleBracketsToken | TextToken | CommentToken | TagToken @@ -168,6 +175,12 @@ export type PipeNode = { location: LocationRange; }; +export type DoubleBracketsNode = { + name: 'double-brackets'; + contents: string; + location: LocationRange; +}; + export type FormatNode = StarNode | UnderscoreNode | TickNode | TildeNode | PipeNode; export type UnorderedListNode = { @@ -201,7 +214,13 @@ export type OrderedListItemNode = { location: LocationRange; }; -export type FragmentNode = TextNode | FormatNode | CommentNode | TagNode | OpaqueTagNode; +export type FragmentNode = + | TextNode + | FormatNode + | CommentNode + | TagNode + | OpaqueTagNode + | DoubleBracketsNode; export type ListNode = UnorderedListNode | OrderedListNode; @@ -211,6 +230,7 @@ export type Node = | CommentNode | AlgorithmNode | TextNode + | DoubleBracketsNode | StarNode | UnderscoreNode | TickNode diff --git a/src/parser.ts b/src/parser.ts index 7a109d6..55f622e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -180,7 +180,12 @@ export class Parser { frag = frag.concat(f); } } - } else if (tok.name === 'comment' || tok.name === 'tag' || tok.name === 'opaqueTag') { + } else if ( + tok.name === 'comment' || + tok.name === 'tag' || + tok.name === 'opaqueTag' || + tok.name === 'double-brackets' + ) { frag.push(tok); this._t.next(); } else if (isList(tok)) { @@ -241,7 +246,12 @@ export class Parser { lastRealTok = lastWsTok; } - if (tok.name === 'opaqueTag' || tok.name === 'comment' || tok.name === 'tag') { + if ( + tok.name === 'opaqueTag' || + tok.name === 'comment' || + tok.name === 'tag' || + tok.name === 'double-brackets' + ) { break; } @@ -289,6 +299,7 @@ export class Parser { opts: ParseFragmentOpts ): (TextNode | CommentNode | TagNode | FormatNode)[] { const startTok = this._t.next() as FormatToken; + const start = this.getPos(startTok); let contents: (TextNode | CommentNode | TagNode)[] = []; if (format === 'underscore') { @@ -300,7 +311,6 @@ export class Parser { } const nextTok = this._t.peek(); - const start = this.getPos(startTok); // fragment ended but we don't have a close format. Convert this node into a text node. if (nextTok.name !== format) { diff --git a/src/tokenizer.ts b/src/tokenizer.ts index 8262a19..2522f64 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -1,5 +1,6 @@ import type { Unlocated, Token, AttrToken, Position } from './node-types'; +const fieldOrSlotRegexp = /^\[\[[a-zA-Z0-9_]+\]\]/; const tagRegexp = /^<[/!]?(\w[\w-]*)(\s+\w[\w-]*(\s*=\s*("[^"]*"|'[^']*'|[^><"'=`]+))?)*\s*>/; const commentRegexp = /^<!--[\w\W]*?-->/; const attrRegexp = /^\[ *[\w-]+ *= *"(?:[^"\\\x00-\x1F]|\\["\\/bfnrt]|\\u[a-fA-F]{4})*" *(?:, *[\w-]+ *= *"(?:[^"\\\x00-\x1F]|\\["\\/bfnrt]|\\u[a-fA-F]{4})*" *)*] /; @@ -79,6 +80,13 @@ export class Tokenizer { if (chr === '\\') { out += this.scanEscape(); } else if (isChars(chr)) { + out += chr; + this.pos++; + } else if (chr === '[') { + if (this.tryScanFieldOrSlot()) { + break; + } + out += chr; this.pos++; } else if (chr === '<') { @@ -115,13 +123,20 @@ export class Tokenizer { return this.str.slice(start, this.pos); } - // does not actually consume the tag - // you should manually `this.pos += tag[0].length;` if you end up consuming it - tryScanTag() { - if (this.str[this.pos] !== '<') { + // does not actually consume the field/slot + // you should manually `this.pos += result.length;` if you end up consuming it + tryScanFieldOrSlot() { + const match = this.str.slice(this.pos).match(fieldOrSlotRegexp); + if (!match) { return; } + return match[0]; + } + + // does not actually consume the tag + // you should manually `this.pos += tag[0].length;` if you end up consuming it + tryScanTag() { const match = this.str.slice(this.pos).match(tagRegexp); if (!match) { return; @@ -297,6 +312,16 @@ export class Tokenizer { } else if (isChars(chr)) { this.enqueue({ name: 'text', contents: this.scanChars() }, start); return; + } else if (chr === '[') { + const fieldOrSlot = this.tryScanFieldOrSlot(); + if (fieldOrSlot) { + this.pos += fieldOrSlot.length; + this.enqueue({ name: 'double-brackets', contents: fieldOrSlot.slice(2, -2) }, start); + return; + } + + // didn't find a valid field/slot, so fall back to text. + this.enqueue({ name: 'text', contents: this.scanChars() }, start); } else if (chr === '<') { if ( this.str[this.pos + 1] === '!' && @@ -432,7 +457,7 @@ function isWhitespace(chr: string) { } function isChars(chr: string) { - return !isFormat(chr) && chr !== '\n' && chr !== ' ' && chr !== '\t'; + return !isFormat(chr) && chr !== '\n' && chr !== ' ' && chr !== '\t' && chr !== '['; } function isFormat(chr: string) { diff --git a/src/visitor.ts b/src/visitor.ts index 0ccca49..9c36179 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -6,6 +6,7 @@ const childKeys = { comment: [], algorithm: ['contents'], text: [], + 'double-brackets': [], star: ['contents'], underscore: [], tick: ['contents'], diff --git a/test/cases/formats-in-text.fragment.ecmarkdown b/test/cases/formats-in-text.fragment.ecmarkdown index 974e424..8d79b54 100644 --- a/test/cases/formats-in-text.fragment.ecmarkdown +++ b/test/cases/formats-in-text.fragment.ecmarkdown @@ -1 +1 @@ -*star*s s*tars* _var_s v_ars_ `tick`s t`icks` |pipe|s p|ipes| ~tilde~s t~ildes~ +*star*s s*tars* _var_s v_ars_ `tick`s t`icks` |pipe|s p|ipes| ~tilde~s t~ildes~ a.[[b]] a.[[B]] a.[b] a.[[<_b_>]] a.[[%b%]] diff --git a/test/cases/formats-in-text.fragment.html b/test/cases/formats-in-text.fragment.html index 6a42d0b..1992ebc 100644 --- a/test/cases/formats-in-text.fragment.html +++ b/test/cases/formats-in-text.fragment.html @@ -1 +1 @@ -<emu-val>star</emu-val>s s*tars* <var>var</var>s v_ars_ <code>tick</code>s t<code>icks</code> <emu-nt>pipe</emu-nt>s p|ipes| <emu-const>tilde</emu-const>s t~ildes~ +<emu-val>star</emu-val>s s*tars* <var>var</var>s v_ars_ <code>tick</code>s t<code>icks</code> <emu-nt>pipe</emu-nt>s p|ipes| <emu-const>tilde</emu-const>s t~ildes~ a.<var class="field">[[b]]</var> a.<var class="field">[[B]]</var> a.[b] a.[[<<var>b</var>>]] a.[[%b%]] diff --git a/test/cases/iterator-close.html b/test/cases/iterator-close.html index 381bfb6..32108d3 100644 --- a/test/cases/iterator-close.html +++ b/test/cases/iterator-close.html @@ -5,7 +5,7 @@ <li>ReturnIfAbrupt(<var>hasReturn</var>).<ol> <li>If <var>hasReturn</var> is <emu-val>true</emu-val>, then<ol> <li>Let <var>innerResult</var> be Invoke(<var>iterator</var>, <code>"return"</code>, ( )).</li> - <li>If <var>completion</var>.[[type]] is not <emu-const>throw</emu-const> and <var>innerResult</var>.[[type]] is <emu-const>throw</emu-const>, then<ol> + <li>If <var>completion</var>.<var class="field">[[type]]</var> is not <emu-const>throw</emu-const> and <var>innerResult</var>.<var class="field">[[type]]</var> is <emu-const>throw</emu-const>, then<ol> <li>Return <var>innerResult</var>.</li> </ol> </li>