Skip to content

Commit

Permalink
Wrap field/slot references in <var class="field"> (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
gibson042 authored Feb 15, 2023
1 parent bcd953b commit a1e7d17
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 15 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 11 additions & 3 deletions src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
TagNode,
UnderscoreNode,
StarNode,
DoubleBracketsNode,
OrderedListItemNode,
UnorderedListItemNode,
OrderedListNode,
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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}>`;
}
}
22 changes: 21 additions & 1 deletion src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,6 +102,7 @@ export type Token =
| ParabreakToken
| LinebreakToken
| WhitespaceToken
| DoubleBracketsToken
| TextToken
| CommentToken
| TagToken
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;

Expand All @@ -211,6 +230,7 @@ export type Node =
| CommentNode
| AlgorithmNode
| TextNode
| DoubleBracketsNode
| StarNode
| UnderscoreNode
| TickNode
Expand Down
16 changes: 13 additions & 3 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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') {
Expand All @@ -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) {
Expand Down
35 changes: 30 additions & 5 deletions src/tokenizer.ts
Original file line number Diff line number Diff line change
@@ -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})*" *)*] /;
Expand Down Expand Up @@ -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 === '<') {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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] === '!' &&
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const childKeys = {
comment: [],
algorithm: ['contents'],
text: [],
'double-brackets': [],
star: ['contents'],
underscore: [],
tick: ['contents'],
Expand Down
2 changes: 1 addition & 1 deletion test/cases/formats-in-text.fragment.ecmarkdown
Original file line number Diff line number Diff line change
@@ -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.[[&lt;_b_>]] a.[[%b%]]
2 changes: 1 addition & 1 deletion test/cases/formats-in-text.fragment.html
Original file line number Diff line number Diff line change
@@ -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.[[&lt;<var>b</var>>]] a.[[%b%]]
2 changes: 1 addition & 1 deletion test/cases/iterator-close.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit a1e7d17

Please sign in to comment.