From c1c31de69f876c010eb52e299a218d57b58711ae Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 23 Jan 2023 16:39:19 -0500 Subject: [PATCH 1/2] Deindent rendered list item contents --- src/emitter.ts | 29 ++++++++++++++++++- src/node-types.ts | 2 ++ src/parser.ts | 17 +++++++---- .../list-item-deindenting.raw.ecmarkdown | 22 ++++++++++++++ test/cases/list-item-deindenting.raw.html | 12 ++++++++ test/parser.js | 1 + test/run-cases.js | 2 +- 7 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 test/cases/list-item-deindenting.raw.ecmarkdown create mode 100644 test/cases/list-item-deindenting.raw.html diff --git a/src/emitter.ts b/src/emitter.ts index 0301f34..44d30e3 100644 --- a/src/emitter.ts +++ b/src/emitter.ts @@ -16,11 +16,18 @@ import type { CommentNode, } from './node-types'; +const getIndentMatcher = unaryMemoize( + (length: number) => new RegExp(String.raw`\n[\p{Space_Separator}\t]{1,${length}}`, 'gu'), + Array.from({ length: 20 }, (_, i) => i + 1) +); + export class Emitter { str: string; + indent: number; constructor() { this.str = ''; + this.indent = 0; } emit(node: Node | Node[]) { @@ -106,7 +113,10 @@ export class Emitter { emitListItem(li: OrderedListItemNode | UnorderedListItemNode) { const attrs = li.attrs.map(a => ` ${a.key}=${JSON.stringify(a.value)}`).join(''); this.str += ``; + const oldIndent = this.indent; + this.indent = li.contentsIndent; this.emitFragment(li.contents); + this.indent = oldIndent; if (li.sublist !== null) { if (li.sublist.name === 'ol') { this.emitOrderedList(li.sublist); @@ -130,7 +140,12 @@ export class Emitter { } emitText(text: TextNode) { - this.str += text.contents; + let contents = text.contents; + if (this.indent) { + const indentMatcher = getIndentMatcher(this.indent); + contents = contents.replace(indentMatcher, '\n'); + } + this.str += contents; } emitTick(node: TickNode) { @@ -165,3 +180,15 @@ export class Emitter { this.str += ``; } } + +function unaryMemoize(fn: (arg: K) => V, prepopulate: K[] = []) { + const cache = new Map(prepopulate.map(arg => [arg, fn(arg)])); + return (arg: K) => { + let value = cache.get(arg); + if (!value) { + value = fn(arg); + cache.set(arg, value); + } + return value; + }; +} diff --git a/src/node-types.ts b/src/node-types.ts index cf2000a..9393aaf 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -188,6 +188,7 @@ export type OrderedListNode = { export type UnorderedListItemNode = { name: 'unordered-list-item'; contents: FragmentNode[]; + contentsIndent: number; sublist: ListNode | null; attrs: { key: string; value: string; location: LocationRange }[]; location: LocationRange; @@ -196,6 +197,7 @@ export type UnorderedListItemNode = { export type OrderedListItemNode = { name: 'ordered-list-item'; contents: FragmentNode[]; + contentsIndent: number; sublist: ListNode | null; attrs: { key: string; value: string; location: LocationRange }[]; location: LocationRange; diff --git a/src/parser.ts b/src/parser.ts index 5ba5065..861ada8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -79,12 +79,15 @@ export class Parser { const startTok = this._t.peek() as OrderedListToken | UnorderedListToken; let node: Unlocated; + let contentsIndent: number; if (startTok.name === 'ul') { const match = startTok.contents.match(/(\s*)\* /); node = { name: 'ul', indent: match![1].length, contents: [] }; + contentsIndent = match![0].length; } else { const match = startTok.contents.match(/(\s*)([^.]+)\. /); node = { name: 'ol', indent: match![1].length, start: Number(match![2]), contents: [] }; + contentsIndent = match![0].length; } while (true) { @@ -101,15 +104,19 @@ export class Parser { } // @ts-ignore typescript is not smart enough to figure out that the types line up - node.contents.push(this.parseListItem(node.name, node.indent)); + node.contents.push(this.parseListItem(node.name, node.indent, contentsIndent)); } return this.finish(node); } - parseListItem(kind: 'ol', indent: number): OrderedListItemNode; - parseListItem(kind: 'ul', indent: number): UnorderedListItemNode; - parseListItem(kind: 'ol' | 'ul', indent: number): OrderedListItemNode | UnorderedListItemNode { + parseListItem(kind: 'ol', indent: number, contentsIndent: number): OrderedListItemNode; + parseListItem(kind: 'ul', indent: number, contentsIndent: number): UnorderedListItemNode; + parseListItem( + kind: 'ol' | 'ul', + indent: number, + contentsIndent: number + ): OrderedListItemNode | UnorderedListItemNode { this.pushPos(); // consume list token this._t.next(); @@ -132,7 +139,7 @@ export class Parser { let name: 'ordered-list-item' | 'unordered-list-item' = kind === 'ol' ? 'ordered-list-item' : 'unordered-list-item'; - return this.finish({ name, contents, sublist, attrs }); + return this.finish({ name, contents, contentsIndent, sublist, attrs }); } parseFragment(opts: ParseFragmentOpts): FragmentNode[]; diff --git a/test/cases/list-item-deindenting.raw.ecmarkdown b/test/cases/list-item-deindenting.raw.ecmarkdown new file mode 100644 index 0000000..f88c0dd --- /dev/null +++ b/test/cases/list-item-deindenting.raw.ecmarkdown @@ -0,0 +1,22 @@ +1. Item 1 + 1. Item 1a + (multi-line) + 1. Item 1b + (also multi-line) + ...and extra-indented +1. Item 2 + 1. Item 2a + (multi-line) + 1. Item 2b + (also multi-line) + ...and extra-indented +1. Item 3 + (multi-line but under-indented) +1. Item 4 + * unordered item + (multi-line) + * unordered item + (multi-line and extra-indented) + ...partially + * unordered item + (multi-line and under-indented) diff --git a/test/cases/list-item-deindenting.raw.html b/test/cases/list-item-deindenting.raw.html new file mode 100644 index 0000000..9d224db --- /dev/null +++ b/test/cases/list-item-deindenting.raw.html @@ -0,0 +1,12 @@ +
  1. Item 1
    1. Item 1a +(multi-line)
    2. Item 1b +(also multi-line) + ...and extra-indented
  2. Item 2
    1. Item 2a +(multi-line)
    2. Item 2b +(also multi-line) + ...and extra-indented
  3. Item 3 +(multi-line but under-indented)
  4. Item 4
    • unordered item +(multi-line)
    • unordered item + (multi-line and extra-indented) +...partially
    • unordered item +(multi-line and under-indented)
diff --git a/test/parser.js b/test/parser.js index 0d8d5f3..5eced82 100644 --- a/test/parser.js +++ b/test/parser.js @@ -59,6 +59,7 @@ describe('Parser', function () { { name: 'ordered-list-item', attrs: [], + contentsIndent: 3, contents: [ { contents: 'Foo. ', diff --git a/test/run-cases.js b/test/run-cases.js index d9a5be6..737b5d5 100644 --- a/test/run-cases.js +++ b/test/run-cases.js @@ -22,7 +22,7 @@ describe('baselines', () => { ? ecmarkdown.fragment : ecmarkdown.algorithm; let rawOutput = processor(input); - let output = beautify(rawOutput); + let output = file.endsWith('.raw.ecmarkdown') ? rawOutput + '\n' : beautify(rawOutput); let existing = fs.existsSync(snapshotFile) ? fs.readFileSync(snapshotFile, 'utf8') : null; if (shouldUpdate) { if (existing !== output) { From 80ab7d6a8b9008b4135b2e461f8cd65f35ad0faa Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 12 Jul 2023 12:34:13 -0400 Subject: [PATCH 2/2] Generalize unaryMemoize to efficiently support `undefined` results --- src/emitter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emitter.ts b/src/emitter.ts index 44d30e3..9a3002c 100644 --- a/src/emitter.ts +++ b/src/emitter.ts @@ -185,10 +185,10 @@ function unaryMemoize(fn: (arg: K) => V, prepopulate: K[] = []) { const cache = new Map(prepopulate.map(arg => [arg, fn(arg)])); return (arg: K) => { let value = cache.get(arg); - if (!value) { + if (value === undefined && !cache.has(arg)) { value = fn(arg); cache.set(arg, value); } - return value; + return value as V; }; }