Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,78 @@ describe('markdown-parser.util verification & check lines', () => {
expect(html).toContain('status is ok');
});
});

describe('renderMarkdownToHtml inline code', () => {
for (const literal of ['**literal**', '__name__', 'a*b*c', '_literal_', '***literal***', '___literal___', '~~literal~~']) {
it(`should preserve ${literal} inside inline code`, () => {
expect(renderMarkdownToHtml('`' + literal + '`'))
.toBe(`<div><code class="inline-code">${literal}</code></div>`);
});
}

it('should format surrounding text while preserving multiple code spans', () => {
expect(renderMarkdownToHtml('**bold** `**first**` _italic_ `__second__` ~~removed~~'))
.toBe('<div><strong>bold</strong> <code class="inline-code">**first**</code> <em>italic</em> <code class="inline-code">__second__</code> <del>removed</del></div>');
});

for (const [delimiter, open, close] of [
['***', '<strong><em>', '</em></strong>'],
['___', '<strong><em>', '</em></strong>'],
['**', '<strong>', '</strong>'],
['__', '<strong>', '</strong>'],
['*', '<em>', '</em>'],
['_', '<em>', '</em>'],
['~~', '<del>', '</del>'],
]) {
it(`should preserve ${delimiter} formatting around inline code`, () => {
expect(renderMarkdownToHtml(delimiter + 'before `a*b_c~d` after' + delimiter))
.toBe(`<div>${open}before <code class="inline-code">a*b_c~d</code> after${close}</div>`);
});
}

it('should not pair formatting delimiters across code spans', () => {
expect(renderMarkdownToHtml('`a*` plain `*b` and `a_` plain `_b` and `a~~` plain `~~b`'))
.toBe('<div><code class="inline-code">a*</code> plain <code class="inline-code">*b</code> and <code class="inline-code">a_</code> plain <code class="inline-code">_b</code> and <code class="inline-code">a~~</code> plain <code class="inline-code">~~b</code></div>');
});

it('should handle adjacent code spans', () => {
expect(renderMarkdownToHtml('`**first**``__second__`'))
.toBe('<div><code class="inline-code">**first**</code><code class="inline-code">__second__</code></div>');
});

it('should keep HTML escaped inside and outside inline code', () => {
expect(renderMarkdownToHtml('<b>outside</b> `<img src=x onerror="alert(1)"> & </code> **literal**`'))
.toBe('<div>&lt;b&gt;outside&lt;/b&gt; <code class="inline-code">&lt;img src=x onerror="alert(1)"&gt; &amp; &lt;/code&gt; **literal**</code></div>');
});

it('should leave empty and unmatched backticks unchanged', () => {
expect(renderMarkdownToHtml('``')).toBe('<div>``</div>');
expect(renderMarkdownToHtml('`unfinished **bold**'))
.toBe('<div>`unfinished <strong>bold</strong></div>');
});

it('should preserve underscore word boundaries next to code', () => {
expect(renderMarkdownToHtml('`code`_after_ and _before_`code` and _`code`_'))
.toBe('<div><code class="inline-code">code</code><em>after</em> and <em>before</em><code class="inline-code">code</code> and <em><code class="inline-code">code</code></em></div>');
});

it('should not pair outside delimiters with code contents', () => {
expect(renderMarkdownToHtml('*before `x*` and `*x` after*'))
.toBe('<div><em>before <code class="inline-code">x*</code> and <code class="inline-code">*x</code> after</em></div>');
});

it('should preserve placeholder-like text and replacement patterns literally', () => {
expect(renderMarkdownToHtml('<inline-code-0> `<inline-code-1> $& $1 $$ **literal**`'))
.toBe('<div>&lt;inline-code-0&gt; <code class="inline-code">&lt;inline-code-1&gt; $&amp; $1 $$ **literal**</code></div>');
});

it('should protect code in headings, lists, and blockquotes', () => {
expect(renderMarkdownToHtml('# `**heading**`\n- `__item__`\n> `~~quote~~`'))
.toBe('<h1><code class="inline-code">**heading**</code></h1>\n<ul>\n<li><code class="inline-code">__item__</code></li>\n</ul>\n<blockquote><code class="inline-code">~~quote~~</code></blockquote>');
});

it('should leave fenced code formatting unchanged', () => {
expect(renderMarkdownToHtml('```text\n`**literal**` <b>&</b>\n```'))
.toBe('<pre class="code-block"><code class="lang-text">`**literal**` &lt;b&gt;&amp;&lt;/b&gt;</code></pre>');
});
});
14 changes: 11 additions & 3 deletions apps/showcase_ui/src/app/utils/markdown-parser.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,14 @@ export function extractCheckerResult(text: string): CheckerResult | null {
*/
function formatInlineMarkdown(text: string): string {
if (!text) return '';
return text
// Inline code first (so asterisks inside code aren't formatted)
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
const codeSpans: string[] = [];
const formatted = text
// Input is already HTML-escaped, so these placeholders cannot collide with user text.
// Restore code after formatting so its contents remain literal.
.replace(/`([^`]+)`/g, (_, code: string) => {
codeSpans.push(code);
return `<inline-code-${codeSpans.length - 1}>`;
})
// Bold + Italic
.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>')
.replace(/___([^_]+)___/g, '<strong><em>$1</em></strong>')
Expand All @@ -227,6 +232,9 @@ function formatInlineMarkdown(text: string): string {
.replace(/\b_([^_]+)_\b/g, '<em>$1</em>')
// Strikethrough
.replace(/~~([^~]+)~~/g, '<del>$1</del>');

return formatted.replace(/<inline-code-(\d+)>/g, (_, index: string) =>
`<code class="inline-code">${codeSpans[Number(index)]}</code>`);
}

/**
Expand Down
Loading