@@ -258,91 +258,104 @@ describe('Messages', () => {
258258 expect ( frame ) . not . toContain ( '\n ## Usage' ) ;
259259 } ) ;
260260
261- it ( 'renders the markdown sample assistant message without leaking fenced block delimiters ' , ( ) => {
262- const markdownSamplesMessage : { role : Role ; content : string } = {
261+ it ( 'falls back to raw text for ambiguous nested fences inside markdown examples ' , ( ) => {
262+ const nestedFenceMessage : { role : Role ; content : string } = {
263263 role : ROLE . ASSISTANT ,
264- content :
265- "Based on the search results, Markdown is a lightweight markup language used to format plain text. It's designed to be easy to read and write, and it gets converted into HTML for display.\n\n" +
266- "Here are some common markdown samples covering the basic syntax. I'll show you the **Markdown Input** and what the **Rendered Output** should look like.\n\n" +
267- '### ✏️ Basic Structure & Formatting\n\n' +
268- '| Feature | Markdown Input | Rendered Output |\n' +
269- '| :--- | :--- | :--- |\n' +
270- '| **Heading 1** | `# Main Title` | **<h1>Main Title</h1>** |\n' +
271- '| **Heading 2** | `## Section Header` | **<h2>Section Header</h2>** |\n' +
272- '| **Heading 3** | `### Subsection` | **<h3>Subsection</h3>** |\n' +
273- '| **Bold Text** | `**This text is bold**` or `__This text is bold__` | **This text is bold** |\n' +
274- '| **Italics Text** | `*This text is italic*` or `_This text is italic_` | *This text is italic* |\n' +
275- '| **Strikethrough** | `~~This text is crossed out~~` | ~~This text is crossed out~~ |\n' +
276- '| **Blockquote** | `> This is a quote.` | *This is a quote.* |\n\n' +
277- '### 📝 Lists\n\n' +
278- 'Markdown supports ordered (numbered) and unordered (bulleted) lists.\n\n' +
279- '**Unordered List (Bullets)**\n' +
280- '```markdown\n' +
281- '* Item one\n' +
282- '* Item two\n' +
283- ' * Sub-item A\n' +
284- ' * Sub-item B\n' +
285- '* Item three\n' +
286- '```\n' +
287- '*Rendered Output:*\n' +
288- '* Item one\n' +
289- '* Item two\n' +
290- ' * Sub-item A\n' +
291- ' * Sub-item B\n' +
292- '* Item three\n\n' +
293- '**Ordered List (Numbered)**\n' +
294- '```markdown\n' +
295- '1. First step\n' +
296- '2. Second step\n' +
297- '3. Third step\n' +
298- '```\n' +
299- '*Rendered Output:*\n' +
300- '1. First step\n' +
301- '2. Second step\n' +
302- '3. Third step\n\n' +
303- '### 🔗 Links and Images\n\n' +
304- '| Element | Markdown Input | Rendered Output |\n' +
305- '| :--- | :--- | :--- |\n' +
306- '| **Link** | `[Google Links](https://www.google.com)` | [Google Links](https://www.google.com) |\n' +
307- '| **Image** | `` | *(Displays an image)* |\n\n' +
308- '### 💻 Code Blocks\n\n' +
309- 'Code blocks are essential for showing snippets of code. There are two main types:\n\n' +
310- '1. **Inline Code** (for short snippets within a sentence): Use single backticks (\\`).\n' +
311- " *Input:* `The function is called \\`calculateSum()\\`.'`\n" +
312- ' *Output:* The function is called `calculateSum()`.\n\n' +
313- '2. **Code Block** (for multi-line code): Use triple backticks (```) and optionally specify the language for syntax highlighting.\n' +
314- ' *Input:*\n' +
315- ' ```typescript\n' +
316- ' function greet(name: string): void {\n' +
317- ' console.log(`Hello, ${name}!`);\n' +
318- ' }\n' +
319- ' ```\n' +
320- ' *Output:* (Formatted as a code block, typically with syntax highlighting)\n\n' +
321- '### 📊 Tables\n\n' +
322- 'Tables are structured using pipes (`|`) and hyphens (`-`).\n\n' +
323- '```markdown\n' +
324- '| Header 1 | Header 2 | Header 3 |\n' +
325- '| :--- | :---: | ---: |\n' +
326- '| Left Aligned | Center Aligned | Right Aligned |\n' +
327- '| Data A | Data B | Data C |\n' +
328- '```\n' +
329- '*Rendered Output:* (A clean table structure)\n\n' +
330- '***\n\n' +
331- 'Do you need samples for a more specific feature, such as **Tables**, **Footnotes**, or perhaps how to integrate this with **TypeScript/Code Snippets**?' ,
264+ content : [
265+ '**Current:**' ,
266+ '```markdown' ,
267+ '## Usage' ,
268+ '' ,
269+ '```sh' ,
270+ 'code-ollama' ,
271+ '```' ,
272+ '```' ,
273+ '' ,
274+ 'After example.' ,
275+ ] . join ( '\n' ) ,
332276 } ;
333277
334278 const { lastFrame } = render (
335- < Messages messages = { [ markdownSamplesMessage ] } isLoading = { false } /> ,
279+ < Messages messages = { [ nestedFenceMessage ] } isLoading = { false } /> ,
336280 ) ;
337281 const frame = lastFrame ( ) ?? '' ;
338282
339- expect ( frame ) . toContain ( 'Basic Structure & Formatting' ) ;
340- expect ( frame ) . toContain ( 'Unordered List (Bullets)' ) ;
341- expect ( frame ) . toContain ( 'function greet(name: string): void {' ) ;
342- expect ( frame ) . toContain ( 'console.log(`Hello, ${name}!`);' ) ;
343- expect ( frame ) . toContain ( 'Do you need samples for a more specific feature' ) ;
283+ expect ( frame ) . toContain ( 'Current:' ) ;
284+ expect ( frame ) . toContain ( '```sh' ) ;
285+ expect ( frame ) . toContain ( 'code-ollama' ) ;
344286 expect ( frame ) . not . toContain ( '```markdown' ) ;
345- expect ( frame ) . not . toContain ( '```typescript' ) ;
287+ expect ( frame ) . toContain ( 'After example.' ) ;
288+ } ) ;
289+
290+ it ( 'keeps non-markdown ambiguous raw fences literal inside a code block' , ( ) => {
291+ const nestedShellFenceMessage : { role : Role ; content : string } = {
292+ role : ROLE . ASSISTANT ,
293+ content : [
294+ 'Shell example:' ,
295+ '```sh' ,
296+ 'echo start' ,
297+ '```ts' ,
298+ 'const x = 1;' ,
299+ '```' ,
300+ '```' ,
301+ ] . join ( '\n' ) ,
302+ } ;
303+
304+ const { lastFrame } = render (
305+ < Messages messages = { [ nestedShellFenceMessage ] } isLoading = { false } /> ,
306+ ) ;
307+ const frame = lastFrame ( ) ?? '' ;
308+
309+ expect ( frame ) . toContain ( 'Shell example:' ) ;
310+ expect ( frame ) . toContain ( '```sh' ) ;
311+ expect ( frame ) . toContain ( '```ts' ) ;
312+ expect ( frame ) . toContain ( 'const x = 1;' ) ;
313+ } ) ;
314+
315+ it ( 'does not swallow following markdown headings into the previous code block' , ( ) => {
316+ const messageWithFollowingHeading : { role : Role ; content : string } = {
317+ role : ROLE . ASSISTANT ,
318+ content : [
319+ 'View the help documentation:' ,
320+ '' ,
321+ '```sh' ,
322+ 'code-ollama --help' ,
323+ '```' ,
324+ '' ,
325+ '### ⭐ 3. Adding a "Prerequisites" Section' ,
326+ '' ,
327+ '**Goal:** Ensure users know what they need installed *before* they run the CLI.' ,
328+ ] . join ( '\n' ) ,
329+ } ;
330+
331+ const { lastFrame } = render (
332+ < Messages messages = { [ messageWithFollowingHeading ] } isLoading = { false } /> ,
333+ ) ;
334+ const frame = lastFrame ( ) ?? '' ;
335+ const lines = frame . split ( '\n' ) ;
336+ const codeLineIndex = lines . findIndex ( ( line ) =>
337+ line . includes ( 'code-ollama --help' ) ,
338+ ) ;
339+ const headingLineIndex = lines . findIndex ( ( line ) =>
340+ line . includes ( '3. Adding a "Prerequisites" Section' ) ,
341+ ) ;
342+ const borderAfterCode = lines . findIndex (
343+ ( line , index ) =>
344+ index > codeLineIndex &&
345+ ( line . includes ( '┘' ) ||
346+ line . includes ( '┛' ) ||
347+ line . includes ( '└' ) ||
348+ line . includes ( '┗' ) ) ,
349+ ) ;
350+
351+ expect ( frame ) . toContain ( 'View the help documentation:' ) ;
352+ expect ( frame ) . toContain ( 'code-ollama --help' ) ;
353+ expect ( frame ) . toContain ( '3. Adding a "Prerequisites" Section' ) ;
354+ expect ( frame ) . toContain ( 'Ensure users know what they need installed' ) ;
355+ expect ( codeLineIndex ) . toBeGreaterThan ( - 1 ) ;
356+ expect ( headingLineIndex ) . toBeGreaterThan ( - 1 ) ;
357+ expect ( borderAfterCode ) . toBeGreaterThan ( - 1 ) ;
358+ expect ( borderAfterCode ) . toBeLessThan ( headingLineIndex ) ;
346359 } ) ;
347360
348361 it ( 'renders system code blocks as plain text (no syntax highlighting)' , ( ) => {
@@ -383,4 +396,116 @@ describe('Messages', () => {
383396 expect ( frame ) . toContain ( 'const x = 1;' ) ;
384397 expect ( frame ) . toContain ( UI . PROMPT_PREFIX ) ;
385398 } ) ;
399+
400+ it ( 'handles ambiguous nested fences with language identifiers' , ( ) => {
401+ const ambiguousNestedMessage : { role : Role ; content : string } = {
402+ role : ROLE . ASSISTANT ,
403+ content : [
404+ 'Example:' ,
405+ '```markdown' ,
406+ '## Title' ,
407+ '```js' ,
408+ 'console.log("hello");' ,
409+ '```' ,
410+ '```js' ,
411+ 'const x = 2;' ,
412+ '```' ,
413+ '```' ,
414+ 'Done.' ,
415+ ] . join ( '\n' ) ,
416+ } ;
417+
418+ const { lastFrame } = render (
419+ < Messages messages = { [ ambiguousNestedMessage ] } isLoading = { false } /> ,
420+ ) ;
421+ const frame = lastFrame ( ) ?? '' ;
422+
423+ expect ( frame ) . toContain ( 'Example:' ) ;
424+ expect ( frame ) . toContain ( '## Title' ) ;
425+ expect ( frame ) . toContain ( 'console.log("hello");' ) ;
426+ expect ( frame ) . toContain ( 'Done.' ) ;
427+ } ) ;
428+
429+ it ( 'treats unclosed fences as plain text' , ( ) => {
430+ const unclosedMessage : { role : Role ; content : string } = {
431+ role : ROLE . ASSISTANT ,
432+ content : [
433+ 'Start' ,
434+ '```typescript' ,
435+ 'const x = 1;' ,
436+ 'console.log(x);' ,
437+ ] . join ( '\n' ) ,
438+ } ;
439+
440+ const { lastFrame } = render (
441+ < Messages messages = { [ unclosedMessage ] } isLoading = { false } /> ,
442+ ) ;
443+ const frame = lastFrame ( ) ?? '' ;
444+
445+ expect ( frame ) . toContain ( 'Start' ) ;
446+ expect ( frame ) . toContain ( 'const x = 1;' ) ;
447+ expect ( frame ) . toContain ( 'console.log(x);' ) ;
448+ } ) ;
449+
450+ it ( 'handles empty code blocks' , ( ) => {
451+ const emptyCodeMessage : { role : Role ; content : string } = {
452+ role : ROLE . ASSISTANT ,
453+ content : 'Example:\n```typescript\n \n```\nDone.' ,
454+ } ;
455+
456+ const { lastFrame } = render (
457+ < Messages messages = { [ emptyCodeMessage ] } isLoading = { false } /> ,
458+ ) ;
459+ const frame = lastFrame ( ) ?? '' ;
460+
461+ expect ( frame ) . toContain ( 'Example:' ) ;
462+ expect ( frame ) . toContain ( 'Done.' ) ;
463+ } ) ;
464+
465+ it ( 'handles mismatched fence markers with same indent' , ( ) => {
466+ const mismatchedFenceMessage : { role : Role ; content : string } = {
467+ role : ROLE . ASSISTANT ,
468+ content : [
469+ 'Example:' ,
470+ '```typescript' ,
471+ 'const x = 1;' ,
472+ '~~~~' ,
473+ 'four ticks' ,
474+ '~~~~' ,
475+ '```' ,
476+ ] . join ( '\n' ) ,
477+ } ;
478+
479+ const { lastFrame } = render (
480+ < Messages messages = { [ mismatchedFenceMessage ] } isLoading = { false } /> ,
481+ ) ;
482+ const frame = lastFrame ( ) ?? '' ;
483+
484+ expect ( frame ) . toContain ( 'Example:' ) ;
485+ expect ( frame ) . toContain ( 'const x = 1;' ) ;
486+ expect ( frame ) . toContain ( 'four ticks' ) ;
487+ } ) ;
488+
489+ it ( 'handles different indent with same fence chars' , ( ) => {
490+ const differentIndentMessage : { role : Role ; content : string } = {
491+ role : ROLE . ASSISTANT ,
492+ content : [
493+ 'Example:' ,
494+ '```typescript' ,
495+ 'const x = 1;' ,
496+ ' ```' ,
497+ 'indented close' ,
498+ '```' ,
499+ ] . join ( '\n' ) ,
500+ } ;
501+
502+ const { lastFrame } = render (
503+ < Messages messages = { [ differentIndentMessage ] } isLoading = { false } /> ,
504+ ) ;
505+ const frame = lastFrame ( ) ?? '' ;
506+
507+ expect ( frame ) . toContain ( 'Example:' ) ;
508+ expect ( frame ) . toContain ( 'const x = 1;' ) ;
509+ expect ( frame ) . toContain ( 'indented close' ) ;
510+ } ) ;
386511} ) ;
0 commit comments