function disambiguateAsteriskRuns(line: string): string { const SENTINEL = '\u200C'; const ASTERISK_RUN = /\*{1,3}/g; const stack: ('*' | '**')[] = []; let result = ''; let lastIndex = 0; let match: RegExpExecArray | null; console.log(`DISAMBIGUATE: '${line}'`); while ((match = ASTERISK_RUN.exec(line)) !== null) { result += line.slice(lastIndex, match.index); const run = match[0]; console.log(`DISAMBIGUATE: run == '${run}', stack == ${stack}`); if (run.length === 3 && stack.length === 2) { const innerCloser = stack.pop()!; const outerCloser = stack.pop()!; result += innerCloser + SENTINEL + outerCloser; } else if (run.length === 3 && stack.length === 0) { if (match.index == 0) { result += '***'; } else { result += '*' + SENTINEL + '**'; stack.push('**'); stack.push('*'); } } else if (run.length === 3) { result += run; } else if (stack.length > 0 && stack[stack.length - 1] === run) { stack.pop(); result += run; } else { stack.push(run as '*' | '**'); result += run; } lastIndex = match.index + run.length; } result += line.slice(lastIndex); console.log(`DISAMBIGUATE: '${result}'`); return result; } function assert(condition, message) { if (!condition) { throw new Error(message); } } function rewrite(line: string): string { const SENTINEL = '|' //'\u200C'; const ASTERISK_RUN = /\*{1,3}/g; const stack: ('*' | '**' | '***')[] = []; let result = ''; let lastIndex = 0; let match: RegExpExecArray | null; const bold = '**'; const italic = '*'; let lastSequence = ''; while ((match = ASTERISK_RUN.exec(line)) !== null) { result += line.slice(lastIndex, match.index); const sequence = match[0]; if (sequence.length === 3) { // split *** into opening ** and * if (stack.length === 0) { result += sequence; stack.push('***'); // closing ***, close the stack } else if (stack.length === 1 && stack[0] === sequence) { result = result.replace('***', bold + SENTINEL + italic); result += italic + SENTINEL + bold; stack.pop(); } else if (stack.length === 2) { const inner = stack.pop(); const outer = stack.pop(); result += inner + SENTINEL + outer; } else if (stack.length === 1) { console.warn(`Cannot parsed line '${line}': invalid sequence ${sequence} with stack ${stack}!`); } else { console.warn(`UNHANDLED '${sequence}', last sequence '${lastSequence}'`); } } else if (stack.length) { if (stack[stack.length - 1] === sequence) { result += stack.pop(); } else if (stack.length === 1 && stack[0] === '***') { const opener = stack[0].substring(0, stack[0].length - sequence.length); result = result.replace('***', opener + SENTINEL + sequence); result += sequence; stack[0] = opener as '*' | '**'; } else { result += sequence; stack.push(sequence as '*' | '**'); } } else { stack.push(sequence as '*' | '**'); result += sequence; } lastIndex = match.index + sequence.length; } result += line.slice(lastIndex); return result; } function test_it() { console.log(rewrite('*italic **bold***')); let cases = [ { 'input': '***bold-italic***', 'expected': '**|*bold-italic*|**' }, { 'input': '***bold** italic*', 'expected': '*|**bold** italic*', }, { 'input': '***italic* bold**', 'expected': '**|*italic* bold**', }, { 'input': '**bold, *italic***', 'expected': '**bold, *italic*|**' }, { 'input': '*italic, **bold***', 'expected': '*italic, **bold**|*' }, ]; for (const testCase of cases) { let result = rewrite(testCase.input); assert(result == testCase.expected, `'${result}' (${result.length}) != '${testCase.expected}' (${testCase.expected.length})`); } }; test_it();