From 3e1c0c5d5960d92344acbf8df7af88c159bda0a5 Mon Sep 17 00:00:00 2001 From: Kevin Gibbons Date: Thu, 6 Jun 2024 19:49:40 -0700 Subject: [PATCH] Make the formatter smarter about mutli-line steps --- src/expr-parser.ts | 2 +- src/formatter/ecmarkdown.ts | 39 +++++++++++++++++++++++++++++++++-- src/formatter/line-builder.ts | 5 ++--- src/formatter/text.ts | 31 +++++++++++++++++++++------- test/formatter.js | 23 +++++++++++++++++++++ 5 files changed, 87 insertions(+), 13 deletions(-) diff --git a/src/expr-parser.ts b/src/expr-parser.ts index ad8dba55..b005e1de 100644 --- a/src/expr-parser.ts +++ b/src/expr-parser.ts @@ -10,7 +10,7 @@ type SimpleLocation = { start: { offset: number }; end: { offset: number } }; type BareText = { name: 'text'; contents: string; - location: { start: { offset: number }; end: { offset: number } }; + location: SimpleLocation; }; // like TextNode, but with less complete location information type ProsePart = FragmentNode | BareText; type List = { diff --git a/src/formatter/ecmarkdown.ts b/src/formatter/ecmarkdown.ts index b82b5ae3..0934abce 100644 --- a/src/formatter/ecmarkdown.ts +++ b/src/formatter/ecmarkdown.ts @@ -34,6 +34,32 @@ async function printListNode( return output; } +function commonIndent(lines: string[]) { + if (lines.length === 0) { + return ''; + } + let common = lines[0].match(/^[ \t]+/)?.[0]; + if (common == null) { + return ''; + } + for (let i = 1; i < lines.length; ++i) { + const line = lines[i]; + let j = 0; + for (; j < line.length && j < common.length; ++j) { + if (common[j] !== line[j]) { + break; + } + } + if (j <= 0) { + return ''; + } + if (j < common.length) { + common = common.slice(0, j); + } + } + return common; +} + async function printStep( source: string, item: OrderedListItemNode | UnorderedListItemNode, @@ -48,7 +74,14 @@ async function printStep( .join(', '); output.appendText(`[${joined}] `); } - const contents = await printFragments(source, item.contents, indent + 1); + + const stepSource = source.substring( + item.location.start.offset, + item.contents[item.contents.length - 1].location.end.offset, + ); + const stepIndent = commonIndent(stepSource.split('\n').slice(1)); + + const contents = await printFragments(source, item.contents, indent + 1, stepIndent); // this is a bit gross, but whatever contents.lines[0] = contents.lines[0].trimStart(); output.append(contents); @@ -69,6 +102,8 @@ export async function printFragments( source: string, contents: FragmentNode[], indent: number, + // for steps which are split over multiple lines, this is the common indent among them + commonIndent: string = '', ): Promise { const output = new LineBuilder(indent); let skipNextElement = false; @@ -82,7 +117,7 @@ export async function printFragments( // so don't even bother const { start, end } = node.location; const originalText = source.substring(start.offset, end.offset); - output.append(printText(originalText, indent)); + output.append(printText(originalText, indent, commonIndent)); break; } case 'comment': { diff --git a/src/formatter/line-builder.ts b/src/formatter/line-builder.ts index 07b6dce0..49140d4b 100644 --- a/src/formatter/line-builder.ts +++ b/src/formatter/line-builder.ts @@ -58,7 +58,7 @@ export class LineBuilder { if (text === '') { return; } - this.last = ' '.repeat(this.indent); + this.last += ' '.repeat(this.indent); } this.last += allowMultiSpace ? text : text.replace(/ +/g, ' '); } @@ -80,7 +80,6 @@ export class LineBuilder { } linebreak(): void { - // console.log('linebreak'); if (this.isEmpty()) { this.firstLineIsPartial = false; return; @@ -121,6 +120,6 @@ export class LineBuilder { // when firstLineIsPartial, we don't indent the first line return false; } - return this.last === ''; + return /^ *$/.test(this.last); } } diff --git a/src/formatter/text.ts b/src/formatter/text.ts index 4ba3bfa2..d27d9e1d 100644 --- a/src/formatter/text.ts +++ b/src/formatter/text.ts @@ -22,7 +22,7 @@ function isBadNumericReference(codePoint: number) { return false; } -export function printText(text: string, indent: number): LineBuilder { +export function printText(text: string, indent: number, commonIndent: string = ''): LineBuilder { const output: LineBuilder = new LineBuilder(indent); if (text === '') { return output; @@ -64,25 +64,42 @@ export function printText(text: string, indent: number): LineBuilder { const leadingSpace = text[0] === ' ' || text[0] === '\t'; const trailingSpace = text[text.length - 1] === ' ' || text[text.length - 1] === '\t'; - const lines = text.split('\n').map(l => l.trim()); + const lines = text.split('\n').map((l, i) => { + if (i === 0 || commonIndent === '' || !l.startsWith(commonIndent)) { + return l.trim(); + } + const withoutIndent = l.substring(commonIndent.length); + const moreIndent = withoutIndent.match(/^ +/)?.[0]; + if (moreIndent) { + return { indent: moreIndent, line: l.trim() }; + } + return l.trim(); + }); if (leadingSpace) { output.appendText(' '); } if (lines.length === 1) { if (lines[0] !== '') { - output.appendText(lines[0]); + output.appendText(lines[0] as string); if (trailingSpace) { output.appendText(' '); } } return output; } - for (let i = 0; i < lines.length - 1; ++i) { - output.appendText(lines[i]); - output.linebreak(); + for (let i = 0; i < lines.length; ++i) { + const line = lines[i]; + if (typeof line === 'string') { + output.appendText(line); + } else { + output.last = line.indent; + output.appendText(line.line); + } + if (i < lines.length - 1) { + output.linebreak(); + } } - output.appendText(lines[lines.length - 1]); if (trailingSpace && output.last !== '') { output.appendText(' '); } diff --git a/test/formatter.js b/test/formatter.js index 6244fae7..2f96b9ff 100644 --- a/test/formatter.js +++ b/test/formatter.js @@ -493,6 +493,29 @@ describe('algorithm formatting', () => { ); }); + it('multiline steps', async () => { + await assertDocFormatsAs( + ` + + 1. Return the Record { + [[Precision]]: *"minute"*, + [[Unit]]: *"minute"*, + [[Increment]]: 1 + }. + + `, + dedentKeepingTrailingNewline` + + 1. Return the Record { + [[Precision]]: *"minute"*, + [[Unit]]: *"minute"*, + [[Increment]]: 1 + }. + + `, + ); + }); + it('nested html', async () => { await assertDocFormatsAs( `