use statement tree for code builder instead of strings to avoid conditional interference

pull/1978/head
Chris Reeves 6 years ago
parent 6c89975396
commit 1f77ee3461

@ -1,168 +1,111 @@
import repeat from './repeat'; import repeat from './repeat';
enum ChunkType { const whitespace = /^\s+$/;
Line,
Block interface Chunk {
parent?: BlockChunk;
type: 'root'|'line'|'condition';
children?: Chunk[];
line?: string;
block?: boolean;
condition?: string;
} }
interface Condition { interface BlockChunk extends Chunk {
condition: string; type: 'root'|'condition';
used: boolean; children: Chunk[];
parent: BlockChunk;
} }
export default class CodeBuilder { export default class CodeBuilder {
result: string; root: BlockChunk = { type: 'root', children: [], parent: null };
first: ChunkType; last: Chunk;
last: ChunkType; current: BlockChunk;
lastCondition: string;
conditionStack: Condition[];
indent: string;
constructor(str = '') { constructor(str = '') {
this.result = str; this.current = this.last = this.root;
this.addLine(str);
const initial = str
? /\n/.test(str) ? ChunkType.Block : ChunkType.Line
: null;
this.first = initial;
this.last = initial;
this.lastCondition = null;
this.conditionStack = [];
this.indent = '';
} }
addConditional(condition: string, body: string) { addConditional(condition: string, body: string) {
this.reifyConditions(); if (this.last.type === 'condition' && this.last.condition === condition) {
if (body && !whitespace.test(body)) this.last.children.push({ type: 'line', line: body });
const indent = this.indent + (condition ? '\t' : '');
body = body.replace(/^/gm, indent);
if (condition === this.lastCondition) {
this.result += `\n${body}`;
} else { } else {
if (this.lastCondition) { const next = this.last = { type: 'condition', condition, parent: this.current, children: [] };
this.result += `\n${this.indent}}`; this.current.children.push(next);
} if (body && !whitespace.test(body)) next.children.push({ type: 'line', line: body });
const block = condition
? `if (${condition}) {\n${body}`
: body;
this.result += `${this.last === ChunkType.Block ? '\n\n' : '\n'}${this.indent}${block}`;
this.lastCondition = condition;
} }
this.last = ChunkType.Block;
} }
addLine(line: string) { addLine(line: string) {
this.reifyConditions(); if (line && !whitespace.test(line)) this.current.children.push(this.last = { type: 'line', line });
if (this.lastCondition) {
this.result += `\n${this.indent}}`;
this.lastCondition = null;
}
if (this.last === ChunkType.Block) {
this.result += `\n\n${this.indent}${line}`;
} else if (this.last === ChunkType.Line) {
this.result += `\n${this.indent}${line}`;
} else {
this.result += line;
}
this.last = ChunkType.Line;
if (!this.first) this.first = ChunkType.Line;
} }
addLineAtStart(line: string) { addLineAtStart(line: string) {
this.reifyConditions(); if (line && !whitespace.test(line)) this.root.children.unshift({ type: 'line', line });
if (this.first === ChunkType.Block) {
this.result = `${line}\n\n${this.indent}${this.result}`;
} else if (this.first === ChunkType.Line) {
this.result = `${line}\n${this.indent}${this.result}`;
} else {
this.result += line;
}
this.first = ChunkType.Line;
if (!this.last) this.last = ChunkType.Line;
} }
addBlock(block: string) { addBlock(block: string) {
this.reifyConditions(); if (block && !whitespace.test(block)) this.current.children.push(this.last = { type: 'line', line: block, block: true });
if (this.indent) block = block.replace(/^/gm, `${this.indent}`);
if (this.lastCondition) {
this.result += `\n${this.indent}}`;
this.lastCondition = null;
}
if (this.result) {
this.result += `\n\n${this.indent}${block}`;
} else {
this.result += block;
}
this.last = ChunkType.Block;
if (!this.first) this.first = ChunkType.Block;
} }
addBlockAtStart(block: string) { addBlockAtStart(block: string) {
this.reifyConditions(); if (block && !whitespace.test(block)) this.root.children.unshift({ type: 'line', line: block, block: true });
if (this.result) {
this.result = `${block}\n\n${this.indent}${this.result}`;
} else {
this.result += block;
}
this.first = ChunkType.Block;
if (!this.last) this.last = ChunkType.Block;
} }
isEmpty() { isEmpty() { return !findLine(this.root); }
return this.result === '';
}
pushCondition(condition: string) { pushCondition(condition: string) {
this.conditionStack.push({ condition, used: false }); if (this.last.type === 'condition' && this.last.condition === condition) {
this.current = this.last as BlockChunk;
} else {
const next = this.last = { type: 'condition', condition, parent: this.current, children: [] };
this.current.children.push(next);
this.current = next;
}
} }
popCondition() { popCondition() {
const { used } = this.conditionStack.pop(); if (!this.current.parent) throw new Error(`Popping a condition that maybe wasn't pushed.`);
this.current = this.current.parent;
}
this.indent = repeat('\t', this.conditionStack.length); toString() {
if (used) this.addLine('}'); return chunkToString(this.root);
} }
}
reifyConditions() { function findLine(chunk: BlockChunk) {
for (let i = 0; i < this.conditionStack.length; i += 1) { for (const c of chunk.children) {
const condition = this.conditionStack[i]; if (c.type === 'line' || findLine(c as BlockChunk)) return true;
if (!condition.used) {
const line = `if (${condition.condition}) {`;
if (this.last === ChunkType.Block) {
this.result += `\n\n${this.indent}${line}`;
} else if (this.last === ChunkType.Line) {
this.result += `\n${this.indent}${line}`;
} else {
this.result += line;
}
this.last = ChunkType.Line;
if (!this.first) this.first = ChunkType.Line;
this.indent = repeat('\t', this.conditionStack.length);
condition.used = true;
}
}
} }
return false;
}
toString() { function chunkToString(chunk: Chunk, level: number = 0, lastBlock?: boolean, first?: boolean): string {
return this.result.trim() + (this.lastCondition ? `\n}` : ``); if (chunk.type === 'line') {
return `${lastBlock || (!first && chunk.block) ? '\n' : ''}${chunk.line.replace(/^/gm, repeat('\t', level))}`;
} else if (chunk.type === 'condition') {
let t = false;
const lines = chunk.children.map((c, i) => {
const str = chunkToString(c, level + 1, t, i === 0);
t = c.type !== 'line' || c.block;
return str;
}).filter(l => !!l);
if (!lines.length) return '';
return `${lastBlock || (!first) ? '\n' : ''}${repeat('\t', level)}if (${chunk.condition}) {\n${lines.join('\n')}\n${repeat('\t', level)}}`;
} else if (chunk.type === 'root') {
let t = false;
const lines = chunk.children.map((c, i) => {
const str = chunkToString(c, 0, t, i === 0);
t = c.type !== 'line' || c.block;
return str;
}).filter(l => !!l);
if (!lines.length) return '';
return lines.join('\n');
} }
} }

Loading…
Cancel
Save