Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for Notion callout blocks #66

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
258 changes: 255 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Designed to make using the Notion SDK and API easier. Notion API version 1.0.
- All headers (header levels >= 3 are treated as header level 3)
- Code blocks, with language highlighting support
- Block quotes
- Supports GFM alerts (e.g. [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION])
- Supports Notion callouts when blockquote starts with an emoji (optional, enabled with `enableEmojiCallouts`)
- Automatically maps common emojis and alert types to appropriate background colors
- Preserves formatting and nested blocks within callouts
- Tables
- Equations
- Images
Expand Down Expand Up @@ -85,6 +89,10 @@ hello _world_
***
## heading2
* [x] todo

> πŸ“˜ **Note:** Important _information_

> Some other blockquote
`);
```

Expand Down Expand Up @@ -128,6 +136,11 @@ hello _world_
]
}
},
{
"object": "block",
"type": "divider",
"divider": {}
},
{
"object": "block",
"type": "heading_2",
Expand Down Expand Up @@ -172,6 +185,248 @@ hello _world_
],
"checked": true
}
},
{
"type": "callout",
"callout": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Note:"
},
"annotations": {
"bold": true,
"strikethrough": false,
"underline": false,
"italic": false,
"code": false,
"color": "default"
}
},
{
"type": "text",
"text": {
"content": " Important "
}
},
{
"type": "text",
"text": {
"content": "information"
},
"annotations": {
"bold": false,
"strikethrough": false,
"underline": false,
"italic": true,
"code": false,
"color": "default"
}
}
],
"icon": {
"type": "emoji",
"emoji": "πŸ“˜"
},
"color": "blue_background"
}
},
{
"type": "quote",
"quote": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Some other blockquote"
},
"annotations": {
"bold": false,
"strikethrough": false,
"underline": false,
"italic": false,
"code": false,
"color": "default"
}
}
]
}
}
]
</pre>
</details>

### Working with blockquotes

Martian supports three types of blockquotes:

1. Standard blockquotes:

```md
> This is a regular blockquote
> It can span multiple lines
```

2. GFM alerts (based on [GFM Alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts)):

```md
> [!NOTE]
> Important information that users should know

> [!WARNING]
> Critical information that needs attention
```

3. Emoji-style callouts (optional) (based on [ReadMe's markdown callouts](https://docs.readme.com/rdmd/docs/callouts)):

```md
> πŸ“˜ **Note:** This is a callout with a blue background
> It supports all markdown formatting and can span multiple lines

> ❗ **Warning:** This is a callout with a red background
> Perfect for important warnings
```

#### GFM Alerts

GFM alerts are automatically converted to Notion callouts with appropriate icons and colors:

- NOTE (πŸ“˜, blue): Useful information that users should know
- TIP (πŸ’‘, green): Helpful advice for doing things better
- IMPORTANT (☝️, purple): Key information users need to know
- WARNING (⚠️, yellow): Urgent info that needs immediate attention
- CAUTION (❗, red): Advises about risks or negative outcomes

#### Emoji-style Callouts

By default, emoji-style callouts are disabled. You can enable them using the `enableEmojiCallouts` option:

```ts
const options = {
enableEmojiCallouts: true,
};
```

When enabled, callouts are detected when a blockquote starts with an emoji. The emoji determines the callout's background color. The current supported color mappings are:

- πŸ“˜ (blue): Perfect for notes and information
- πŸ‘ (green): Success messages and tips
- ❗ (red): Warnings and important notices
- 🚧 (yellow): Work in progress or caution notices

All other emojis will have a default background color. The supported emoji color mappings can be expanded easily if needed.

If a blockquote doesn't match either GFM alert syntax or emoji-style callout syntax (when enabled), it will be rendered as a Notion quote block.

##### Examples

Standard blockquote:

```ts
markdownToBlocks('> A regular blockquote');
```

<details>
<summary>Result</summary>
<pre>
[
{
"object": "block",
"type": "quote",
"quote": {
"rich_text": [
{
"type": "text",
"text": {
"content": "A regular blockquote"
}
}
]
}
}
]
</pre>
</details>

GFM alert:

```ts
markdownToBlocks('> [!NOTE]\n> Important information');
```

<details>
<summary>Result</summary>
<pre>
[
{
"object": "block",
"type": "callout",
"callout": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Note"
}
}
],
"icon": {
"type": "emoji",
"emoji": "ℹ️"
},
"color": "blue_background",
"children": [
{
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Important information"
}
}
]
}
}
]
}
}
]
</pre>
</details>

Emoji-style callout (with `enableEmojiCallouts: true`):

```ts
markdownToBlocks('> πŸ“˜ Note: Important information', {
enableEmojiCallouts: true,
});
```

<details>
<summary>Result</summary>
<pre>
[
{
"object": "block",
"type": "callout",
"callout": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Note: Important information"
}
}
],
"icon": {
"type": "emoji",
"emoji": "πŸ“˜"
},
"color": "blue_background"
}
}
]
</pre>
Expand Down Expand Up @@ -338,11 +593,8 @@ Error: Unsupported markdown element: {"type":"heading","depth":1,"children":[{"t
</pre>
</details>



---

Built with πŸ’™ by the team behind [Fabric](https://tryfabric.com).

<img src="https://static.scarf.sh/a.png?x-pxid=79ae4e0a-7e48-4965-8a83-808c009aa47a" />

27 changes: 26 additions & 1 deletion src/notion/blocks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {richText, supportedCodeLang} from './common';
import {richText, supportedCodeLang, supportedCalloutColor} from './common';
import {AppendBlockChildrenParameters} from '@notionhq/client/build/src/api-endpoints';

export type Block = AppendBlockChildrenParameters['children'][number];
Expand All @@ -11,6 +11,10 @@ export type BlockWithoutChildren = Exclude<
export type RichText = (Block & {
type: 'paragraph';
})['paragraph']['rich_text'][number];
export type EmojiRequest = ((Block & {
object: 'block';
type: 'callout';
})['callout']['icon'] & {type: 'emoji'})['emoji'];

export function divider(): Block {
return {
Expand Down Expand Up @@ -188,3 +192,24 @@ export function equation(value: string): Block {
},
};
}

export function callout(
text: RichText[] = [],
emoji: EmojiRequest = 'πŸ‘',
color: supportedCalloutColor = 'default',
children: Block[] = []
): Block {
return {
object: 'block',
type: 'callout',
callout: {
rich_text: text.length ? text : [richText('')],
icon: {
type: 'emoji',
emoji,
},
color,
children: children.length ? children : undefined,
},
};
}
58 changes: 58 additions & 0 deletions src/notion/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,61 @@ export type supportedCodeLang = typeof SUPPORTED_CODE_BLOCK_LANGUAGES[number];
export function isSupportedCodeLang(lang: string): lang is supportedCodeLang {
return (SUPPORTED_CODE_BLOCK_LANGUAGES as readonly string[]).includes(lang);
}

export const SUPPORTED_CALLOUT_BLOCK_COLORS = [
'default',
'gray_background',
'brown_background',
'orange_background',
'yellow_background',
'green_background',
'blue_background',
'purple_background',
'pink_background',
'red_background',
] as const;

export type supportedCalloutColor =
typeof SUPPORTED_CALLOUT_BLOCK_COLORS[number];

export function isSupportedCalloutColor(
color: string
): color is supportedCalloutColor {
return (SUPPORTED_CALLOUT_BLOCK_COLORS as readonly string[]).includes(color);
}

export const SUPPORTED_GFM_ALERT_TYPES = [
'NOTE',
'TIP',
'IMPORTANT',
'WARNING',
'CAUTION',
] as const;

export type GfmAlertType = typeof SUPPORTED_GFM_ALERT_TYPES[number];

export function isGfmAlertType(type: string): type is GfmAlertType {
return (SUPPORTED_GFM_ALERT_TYPES as readonly string[]).includes(type);
}

export const GFM_ALERT_MAP: Record<
GfmAlertType,
{
emoji: string;
color: supportedCalloutColor;
}
> = {
NOTE: {emoji: 'πŸ“˜', color: 'blue_background'},
TIP: {emoji: 'πŸ’‘', color: 'green_background'},
IMPORTANT: {emoji: '☝️', color: 'purple_background'},
WARNING: {emoji: '⚠️', color: 'yellow_background'},
CAUTION: {emoji: '❗', color: 'red_background'},
} as const;

export const SUPPORTED_EMOJI_COLOR_MAP: Record<string, supportedCalloutColor> =
{
'πŸ‘': 'green_background',
'πŸ“˜': 'blue_background',
'🚧': 'yellow_background',
'❗': 'red_background',
};
Loading