Skip to content
Open
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
23 changes: 22 additions & 1 deletion crates/atuin-desktop-runtime/src/blocks/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ impl BlockBehavior for Script {
.filter(|line| line.is_stdout)
.map(|line| line.text.clone())
.collect::<Vec<String>>()
.join("\n");
.join("");

let _ = context
.update_active_context(self.id, move |ctx| {
Expand Down Expand Up @@ -1139,6 +1139,27 @@ echo "Successfully wrote to $ATUIN_OUTPUT_VARS"
// 3. The fs_var integration is working
}

#[test]
fn test_stdout_preserves_markdown_table_formatting() {
let output = ScriptExecutionOutput {
exit_code: Some(0),
output: vec![
OutputLine::stdout("# Script output\n".to_string()),
OutputLine::stdout("\n".to_string()),
OutputLine::stdout("| Column 1 | Column 2 |\n".to_string()),
OutputLine::stdout("| --- | --- |\n".to_string()),
OutputLine::stdout("| Value 1 | Value 2 |\n".to_string()),
],
};

assert_eq!(
output.stdout().as_deref(),
Some(
"# Script output\n\n| Column 1 | Column 2 |\n| --- | --- |\n| Value 1 | Value 2 |\n"
)
);
}

#[tokio::test]
async fn test_ssh_host_parsing() {
assert_eq!(
Expand Down
19 changes: 12 additions & 7 deletions docs/docs/blocks/executable/markdown-render.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Open the rendered markdown in a fullscreen modal for easier reading of long cont

All rendered content is fully selectable and copyable, making it easy to extract information from the output.

### GitHub-like Presentation

Rendered content uses GitHub-like markdown styling in both light and dark mode, including tables, task lists, blockquotes, and syntax-highlighted fenced code blocks.

## Example Workflow

A common pattern is using a Script block to generate markdown content, then displaying it with a Markdown Render block:
Expand All @@ -48,16 +52,17 @@ Save the output to a variable (e.g., `release_notes`), then reference it in your

## Supported Markdown

The block supports GitHub Flavored Markdown (GFM), including:
The block supports GitHub Flavored Markdown (GFM) with GitHub-like styling, including:

- Headers and paragraphs
- **Bold**, *italic*, and ~~strikethrough~~ text
- Ordered and unordered lists
- Code blocks with syntax highlighting
- Tables
- Headings, paragraphs, and horizontal rules
- **Bold**, *italic*, `inline code`, and ~~strikethrough~~ text
- Ordered, unordered, and task lists
- Fenced and indented code blocks
- Syntax highlighting for common fenced code block languages
- Tables, including column alignment
- Links and images
- Blockquotes
- Task lists
- Footnotes

## View Mode vs Edit Mode

Expand Down
230 changes: 230 additions & 0 deletions markdown-render-fixture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Markdown Render Fixture

Use this file to test the `markdown_render` block against the Markdown + GFM syntax that the current renderer should support.

## Headings

# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6

## Paragraphs

This is a normal paragraph with enough text to check spacing, wrapping, and line height in the rendered output.

This paragraph includes a
soft line break inside the same paragraph.

This line ends with two spaces
so the next line should render as a hard break.

## Emphasis

This text includes *italic*, **bold**, ***bold italic***, ~~strikethrough~~, and `inline code`.

You can also mix them together: **bold with `inline code` inside**, *italic with a [link](https://example.com)*, and ~~strikethrough with **bold**~~.

## Links

Inline link: [Atuin](https://atuin.sh)

Autolink: <https://example.com/docs/getting-started>

Bare URL literal: https://example.com/releases/latest

Email autolink literal: support@example.com

Reference-style link: [Renderer docs][renderer-docs]

[renderer-docs]: https://example.com/markdown-render

## Blockquotes

> This is a simple blockquote.

> A blockquote can contain multiple paragraphs.
>
> It can also contain other Markdown:
> - a list item
> - another list item
>
> And a nested quote:
> > Nested quote content

## Lists

Unordered list:

- First item
- Second item
- Third item

Ordered list:

1. First ordered item
2. Second ordered item
3. Third ordered item

Nested lists:

- Parent item
- Nested child item
- Another nested child item
- Second parent item

1. Ordered parent
1. Nested ordered child
2. Another nested ordered child
2. Second ordered parent

## Task Lists

- [x] Completed task
- [ ] Incomplete task
- [x] Completed task with `inline code`
- [ ] Incomplete task with a [link](https://example.com)

## Code

Inline code example: `const answer = 42;`

Fenced code block with language:

```ts
type User = {
id: string;
name: string;
active: boolean;
};

const user: User = {
id: "u_123",
name: "Taylor",
active: true,
};

console.log(user.name);
```

Fenced code block without language:

```
Plain fenced code block
with multiple lines
and no language hint.
```

Indented code block:

SELECT id, name
FROM users
WHERE active = true
ORDER BY name ASC;

## Tables

Simple table:

| Column 1 | Column 2 |
| --- | --- |
| Value 1 | Value 2 |
| Value 3 | Value 4 |

Alignment table:

| Left | Right | Center |
| --- | ---: | :---: |
| left text | 123 | centered |
| more left text | 4567 | more centered |

Table with inline formatting:

| Syntax | Example | Notes |
| --- | --- | --- |
| Bold | **strong** | Should keep emphasis |
| Italic | *emphasis* | Should keep italics |
| Code | `npm test` | Should keep inline code |
| Link | [Example](https://example.com) | Should stay clickable |

## Horizontal Rules

---

Content after the first rule.

***

Content after the second rule.

___

## Images

Image from the app public directory:

![Vite Logo](/vite.svg)

Linked image:

[![Tauri Logo](/tauri.svg)](https://tauri.app)

## Footnotes

Here is a statement with a footnote.[^note-one]

Here is another footnote reference.[^note-two]

[^note-one]: This is the first footnote.
[^note-two]: This footnote includes **formatting**, `inline code`, and a [link](https://example.com).

## Escaping

\*This should not be italic\*

\[This should not become a link\](https://example.com)

\# This should not become a heading

## Raw HTML

<details>
<summary>Expandable HTML block</summary>
<p>This content is written in raw HTML inside the Markdown document.</p>
</details>

<kbd>Ctrl</kbd> + <kbd>K</kbd>

## Mixed Content Stress Test

> ### Quoted heading
>
> - [x] Task inside quote
> - [ ] Another task inside quote
>
> | Name | Status | Score |
> | --- | ---: | :---: |
> | Alpha | done | 10 |
> | Beta | pending | 7 |
>
> ```bash
> echo "quoted code block"
> ```

1. Ordered item with a paragraph.

Additional paragraph text inside the same list item.

2. Ordered item with a nested unordered list:
- child item one
- child item two

3. Ordered item with a nested blockquote:

> Nested quote inside a list item

## End

If all of the above renders correctly, the current `markdown_render` styling is covering the main Markdown and GFM cases we expect to support.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ vi.mock("@/tracking", () => ({
default: vi.fn(),
}));

vi.mock("@/state/store", () => ({
useStore: (selector: (state: { functionalColorMode: "light" | "dark" }) => unknown) =>
selector({ functionalColorMode: "light" }),
}));

vi.mock("@tauri-apps/plugin-shell", () => ({
open: vi.fn(),
}));

import { insertMarkdownRender } from "./index";

describe("MarkdownRender", () => {
Expand Down
59 changes: 59 additions & 0 deletions src/components/runbooks/editor/components/Markdown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @vitest-environment jsdom
*/
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, test, vi } from "vitest";

vi.mock("@/state/store", () => ({
useStore: (selector: (state: { functionalColorMode: "light" | "dark" }) => unknown) =>
selector({ functionalColorMode: "light" }),
}));

vi.mock("@tauri-apps/plugin-shell", () => ({
open: vi.fn(),
}));

import Markdown, { normalizeMarkdownCodeLanguage } from "./Markdown";

describe("Markdown", () => {
test("normalizes common fenced-code language aliases", () => {
expect(normalizeMarkdownCodeLanguage("ts")).toBe("typescript");
expect(normalizeMarkdownCodeLanguage("sh")).toBe("bash");
expect(normalizeMarkdownCodeLanguage("yml")).toBe("yaml");
expect(normalizeMarkdownCodeLanguage("")).toBe("");
});

test("renders fenced code blocks with syntax token markup", () => {
const markup = renderToStaticMarkup(
<Markdown content={"```ts\nconst answer: number = 42;\n```"} />,
);

expect(markup).toContain("markdown-code-block");
expect(markup).toContain("token keyword");
expect(markup).toContain("token operator");
});

test("preserves non-code GFM structures when rendering to React elements", () => {
const markup = renderToStaticMarkup(
<Markdown
content={[
"| Left | Right |",
"| --- | ---: |",
"| a | 1 |",
"",
"- [x] done",
"",
"Footnote ref[^1]",
"",
"[^1]: note",
].join("\n")}
/>,
);

expect(markup).toContain("<table>");
expect(markup).toContain('align="right"');
expect(markup).toContain('type="checkbox"');
expect(markup).toContain('data-footnotes=""');
expect(markup).toContain('data-footnote-ref=""');
});
});
Loading
Loading