markdown-to-slack-blocks 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +361 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/validator.d.ts +3 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +37 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 https://github.com/udivankin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Markdown to Slack Blocks
|
|
2
|
+
|
|
3
|
+
A powerful library to convert Markdown text into Slack's Block Kit JSON format.
|
|
4
|
+
|
|
5
|
+
## Motivation
|
|
6
|
+
|
|
7
|
+
While Slack does offer native [markdown support in blocks](https://api.slack.com/reference/surfaces/formatting#basics), there are significant limitations. The following markdown features are **not supported** by Slack's native markdown:
|
|
8
|
+
|
|
9
|
+
- **Code blocks with syntax highlighting** — Slack renders code blocks but ignores language hints
|
|
10
|
+
- **Horizontal rules** — No native support for `---` or `***` dividers
|
|
11
|
+
- **Tables** — Markdown tables are not rendered
|
|
12
|
+
- **Task lists** — Checkbox-style lists (`- [ ]`, `- [x]`) are not recognized
|
|
13
|
+
|
|
14
|
+
This library is particularly useful for apps that leverage **platform AI features** where you expect a **markdown response from an LLM**. Instead of sending raw markdown that Slack can't fully render, this library converts it to proper Block Kit JSON that displays correctly.
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
This library uses a two-step conversion process:
|
|
19
|
+
|
|
20
|
+
1. **Markdown → AST**: The markdown string is parsed into an Abstract Syntax Tree (AST) using [mdast-util-from-markdown](https://github.com/syntax-tree/mdast-util-from-markdown), with GitHub Flavored Markdown (GFM) support via [mdast-util-gfm](https://github.com/syntax-tree/mdast-util-gfm).
|
|
21
|
+
|
|
22
|
+
2. **AST → Slack Blocks**: The AST is traversed and converted into Slack's Block Kit JSON format, mapping markdown elements to their corresponding Slack block types and rich text elements.
|
|
23
|
+
|
|
24
|
+
### Key Libraries
|
|
25
|
+
|
|
26
|
+
- **[mdast-util-from-markdown](https://github.com/syntax-tree/mdast-util-from-markdown)** — Parses markdown into an AST
|
|
27
|
+
- **[mdast-util-gfm](https://github.com/syntax-tree/mdast-util-gfm)** — Adds GitHub Flavored Markdown support (tables, strikethrough, task lists)
|
|
28
|
+
- **[micromark-extension-gfm](https://github.com/micromark/micromark-extension-gfm)** — Micromark extension for GFM syntax
|
|
29
|
+
- **[mdast-util-to-string](https://github.com/syntax-tree/mdast-util-to-string)** — Extracts plain text from AST nodes
|
|
30
|
+
|
|
31
|
+
## Supported Output
|
|
32
|
+
|
|
33
|
+
### Blocks
|
|
34
|
+
|
|
35
|
+
| Block Type | Description |
|
|
36
|
+
|------------|-------------|
|
|
37
|
+
| `rich_text` | Primary block type for formatted text content |
|
|
38
|
+
| `header` | H1 headings rendered as header blocks |
|
|
39
|
+
| `divider` | Horizontal rules converted to divider blocks |
|
|
40
|
+
| `image` | Standalone images |
|
|
41
|
+
| `section` | Text sections with optional accessories |
|
|
42
|
+
| `context` | Smaller context text and images |
|
|
43
|
+
| `table` | Table data (converted from markdown tables) |
|
|
44
|
+
|
|
45
|
+
### Rich Text Elements
|
|
46
|
+
|
|
47
|
+
| Element Type | Description |
|
|
48
|
+
|--------------|-------------|
|
|
49
|
+
| `rich_text_section` | Container for inline text elements |
|
|
50
|
+
| `rich_text_list` | Ordered and unordered lists (supports nesting) |
|
|
51
|
+
| `rich_text_preformatted` | Code blocks |
|
|
52
|
+
| `rich_text_quote` | Blockquotes |
|
|
53
|
+
|
|
54
|
+
### Rich Text Section Elements
|
|
55
|
+
|
|
56
|
+
| Element Type | Description |
|
|
57
|
+
|--------------|-------------|
|
|
58
|
+
| `text` | Plain text with optional styling (bold, italic, strike, code) |
|
|
59
|
+
| `link` | Hyperlinks |
|
|
60
|
+
| `emoji` | Emoji shortcodes (`:emoji_name:`) |
|
|
61
|
+
| `user` | User mentions (`@username`) |
|
|
62
|
+
| `channel` | Channel mentions (`#channel`) |
|
|
63
|
+
| `usergroup` | User group mentions |
|
|
64
|
+
| `team` | Team mentions |
|
|
65
|
+
| `broadcast` | Broadcast mentions (`@here`, `@channel`, `@everyone`) |
|
|
66
|
+
| `date` | Formatted date objects |
|
|
67
|
+
| `color` | Color values (when `detectColors` is enabled) |
|
|
68
|
+
|
|
69
|
+
### Text Styles
|
|
70
|
+
|
|
71
|
+
| Style | Markdown Syntax |
|
|
72
|
+
|-------|-----------------|
|
|
73
|
+
| **Bold** | `**text**` or `__text__` |
|
|
74
|
+
| *Italic* | `*text*` or `_text_` |
|
|
75
|
+
| ~~Strikethrough~~ | `~~text~~` |
|
|
76
|
+
| `Code` | `` `text` `` |
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
- **Standard Markdown Support**: Converts headings, lists, bold, italic, code blocks, blockquotes, and links.
|
|
81
|
+
- **Slack-Specific Extensions**: Support for user mentions, channel mentions, user groups, and team mentions.
|
|
82
|
+
- **Configurable**: Options to customize behavior, such as color detection.
|
|
83
|
+
- **Type-Safe**: Written in TypeScript with full type definitions.
|
|
84
|
+
|
|
85
|
+
## Installation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm install markdown-to-slack-blocks
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { markdownToBlocks } from 'markdown-to-slack-blocks';
|
|
95
|
+
|
|
96
|
+
const markdown = `
|
|
97
|
+
# Hello World
|
|
98
|
+
This is a **bold** statement.
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
const blocks = markdownToBlocks(markdown);
|
|
102
|
+
console.log(JSON.stringify(blocks, null, 2));
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Options
|
|
106
|
+
|
|
107
|
+
You can pass an options object to `markdownToBlocks`, otherwise the mentions will be rendered as text:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const options = {
|
|
111
|
+
mentions: {
|
|
112
|
+
users: { 'username': 'U123456' },
|
|
113
|
+
channels: { 'general': 'C123456' },
|
|
114
|
+
userGroups: { 'engineers': 'S123456' },
|
|
115
|
+
teams: { 'myteam': 'T123456' }
|
|
116
|
+
},
|
|
117
|
+
detectColors: true
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const blocks = markdownToBlocks(markdown, options);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Validation
|
|
124
|
+
|
|
125
|
+
The library validates that the IDs provided in the `mentions` option adhere to Slack's ID format:
|
|
126
|
+
- **User IDs**: Must start with `U` or `W`.
|
|
127
|
+
- **Channel IDs**: Must start with `C`.
|
|
128
|
+
- **User Group IDs**: Must start with `S`.
|
|
129
|
+
- **Team IDs**: Must start with `T`.
|
|
130
|
+
|
|
131
|
+
All IDs must be alphanumeric.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAIlD,cAAc,SAAS,CAAC;AAExB,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,6BAGnF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.markdownToBlocks = markdownToBlocks;
|
|
18
|
+
const parser_1 = require("./parser");
|
|
19
|
+
const validator_1 = require("./validator");
|
|
20
|
+
// Re-export types for consumers
|
|
21
|
+
__exportStar(require("./types"), exports);
|
|
22
|
+
function markdownToBlocks(markdown, options) {
|
|
23
|
+
(0, validator_1.validateOptions)(options);
|
|
24
|
+
return (0, parser_1.parseMarkdown)(markdown, options);
|
|
25
|
+
}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAIA,OAAO,EACH,KAAK,EAKL,uBAAuB,EAG1B,MAAM,SAAS,CAAC;AAEjB,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,uBAA4B,GAAG,KAAK,EAAE,CAyK9F"}
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseMarkdown = parseMarkdown;
|
|
4
|
+
const mdast_util_from_markdown_1 = require("mdast-util-from-markdown");
|
|
5
|
+
const mdast_util_to_string_1 = require("mdast-util-to-string");
|
|
6
|
+
const micromark_extension_gfm_1 = require("micromark-extension-gfm");
|
|
7
|
+
const mdast_util_gfm_1 = require("mdast-util-gfm");
|
|
8
|
+
function parseMarkdown(markdown, options = {}) {
|
|
9
|
+
const ast = (0, mdast_util_from_markdown_1.fromMarkdown)(markdown, {
|
|
10
|
+
extensions: [(0, micromark_extension_gfm_1.gfm)()],
|
|
11
|
+
mdastExtensions: [(0, mdast_util_gfm_1.gfmFromMarkdown)()],
|
|
12
|
+
});
|
|
13
|
+
const blocks = [];
|
|
14
|
+
let currentRichTextElements = [];
|
|
15
|
+
const flushRichText = () => {
|
|
16
|
+
if (currentRichTextElements.length > 0) {
|
|
17
|
+
blocks.push({
|
|
18
|
+
type: 'rich_text',
|
|
19
|
+
elements: [...currentRichTextElements],
|
|
20
|
+
});
|
|
21
|
+
currentRichTextElements = [];
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
for (const node of ast.children) {
|
|
25
|
+
if (node.type === 'heading') {
|
|
26
|
+
flushRichText();
|
|
27
|
+
const text = (0, mdast_util_to_string_1.toString)(node);
|
|
28
|
+
if (node.depth <= 2) {
|
|
29
|
+
blocks.push({
|
|
30
|
+
type: 'header',
|
|
31
|
+
text: {
|
|
32
|
+
type: 'plain_text',
|
|
33
|
+
text: text,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
blocks.push({
|
|
39
|
+
type: 'rich_text',
|
|
40
|
+
elements: [
|
|
41
|
+
{
|
|
42
|
+
type: 'rich_text_section',
|
|
43
|
+
elements: [{ type: 'text', text: text, style: { bold: true } }],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (node.type === 'paragraph') {
|
|
50
|
+
if (node.children.length === 1 && node.children[0].type === 'image') {
|
|
51
|
+
flushRichText();
|
|
52
|
+
const imageNode = node.children[0];
|
|
53
|
+
blocks.push({
|
|
54
|
+
type: 'image',
|
|
55
|
+
image_url: imageNode.url,
|
|
56
|
+
alt_text: imageNode.alt || 'Image',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
currentRichTextElements.push({
|
|
61
|
+
type: 'rich_text_section',
|
|
62
|
+
elements: node.children.flatMap((child) => mapInlineNode(child, options)),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (node.type === 'list') {
|
|
67
|
+
// Helper function to recursively process lists with proper indentation
|
|
68
|
+
const processList = (listNode, indent) => {
|
|
69
|
+
const results = [];
|
|
70
|
+
const currentListItems = [];
|
|
71
|
+
for (const listItem of listNode.children) {
|
|
72
|
+
// Process the paragraph content of this list item
|
|
73
|
+
const paragraphElements = listItem.children
|
|
74
|
+
.filter((child) => child.type === 'paragraph')
|
|
75
|
+
.flatMap((child) => child.children.flatMap((c) => mapInlineNode(c, options)));
|
|
76
|
+
if (paragraphElements.length > 0) {
|
|
77
|
+
currentListItems.push({
|
|
78
|
+
type: 'rich_text_section',
|
|
79
|
+
elements: paragraphElements,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Process any nested lists (recursively)
|
|
83
|
+
const nestedLists = listItem.children.filter((child) => child.type === 'list');
|
|
84
|
+
if (nestedLists.length > 0) {
|
|
85
|
+
// First, push the current list with items collected so far
|
|
86
|
+
if (currentListItems.length > 0) {
|
|
87
|
+
results.push({
|
|
88
|
+
type: 'rich_text_list',
|
|
89
|
+
style: listNode.ordered ? 'ordered' : 'bullet',
|
|
90
|
+
indent,
|
|
91
|
+
elements: [...currentListItems],
|
|
92
|
+
});
|
|
93
|
+
currentListItems.length = 0; // Clear the array
|
|
94
|
+
}
|
|
95
|
+
// Process each nested list
|
|
96
|
+
for (const nestedList of nestedLists) {
|
|
97
|
+
results.push(...processList(nestedList, indent + 1));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Push any remaining items
|
|
102
|
+
if (currentListItems.length > 0) {
|
|
103
|
+
results.push({
|
|
104
|
+
type: 'rich_text_list',
|
|
105
|
+
style: listNode.ordered ? 'ordered' : 'bullet',
|
|
106
|
+
indent,
|
|
107
|
+
elements: currentListItems,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
};
|
|
112
|
+
const listElements = processList(node, 0);
|
|
113
|
+
for (const listElement of listElements) {
|
|
114
|
+
currentRichTextElements.push(listElement);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (node.type === 'code') {
|
|
118
|
+
currentRichTextElements.push({
|
|
119
|
+
type: 'rich_text_preformatted',
|
|
120
|
+
elements: [{ type: 'text', text: node.value }],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else if (node.type === 'blockquote') {
|
|
124
|
+
currentRichTextElements.push({
|
|
125
|
+
type: 'rich_text_quote',
|
|
126
|
+
elements: node.children
|
|
127
|
+
.flatMap((child) => {
|
|
128
|
+
if (child.type === 'paragraph') {
|
|
129
|
+
return child.children.flatMap((c) => mapInlineNode(c, options));
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else if (node.type === 'thematicBreak') {
|
|
136
|
+
flushRichText();
|
|
137
|
+
blocks.push({ type: 'divider' });
|
|
138
|
+
}
|
|
139
|
+
else if (node.type === 'image') {
|
|
140
|
+
flushRichText();
|
|
141
|
+
blocks.push({
|
|
142
|
+
type: 'image',
|
|
143
|
+
image_url: node.url,
|
|
144
|
+
alt_text: node.alt || 'Image',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else if (node.type === 'table') {
|
|
148
|
+
flushRichText();
|
|
149
|
+
const rows = node.children.map((row) => {
|
|
150
|
+
return row.children.map((cell) => {
|
|
151
|
+
return {
|
|
152
|
+
type: 'rich_text',
|
|
153
|
+
elements: [
|
|
154
|
+
{
|
|
155
|
+
type: 'rich_text_section',
|
|
156
|
+
elements: cell.children.flatMap((c) => mapInlineNode(c, options)),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
blocks.push({
|
|
163
|
+
type: 'table',
|
|
164
|
+
rows: rows,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else if (node.type === 'html') {
|
|
168
|
+
// Handle top-level HTML blocks (e.g. Slack specific tags like <!date...> starting a line)
|
|
169
|
+
currentRichTextElements.push({
|
|
170
|
+
type: 'rich_text_section',
|
|
171
|
+
elements: processTextNode(node.value, {}, options),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
flushRichText();
|
|
176
|
+
return blocks;
|
|
177
|
+
}
|
|
178
|
+
function mapInlineNode(node, options) {
|
|
179
|
+
// console.log('Node:', node.type, node.value || node);
|
|
180
|
+
if (node.type === 'text' || node.type === 'html') {
|
|
181
|
+
// Pass empty object for style, processTextNode will handle it (and not attach if empty)
|
|
182
|
+
return processTextNode(node.value, {}, options);
|
|
183
|
+
}
|
|
184
|
+
else if (node.type === 'emphasis') {
|
|
185
|
+
return flattenStyles(node.children, { italic: true }, options);
|
|
186
|
+
}
|
|
187
|
+
else if (node.type === 'strong') {
|
|
188
|
+
return flattenStyles(node.children, { bold: true }, options);
|
|
189
|
+
}
|
|
190
|
+
else if (node.type === 'delete') {
|
|
191
|
+
return flattenStyles(node.children, { strike: true }, options);
|
|
192
|
+
}
|
|
193
|
+
else if (node.type === 'inlineCode') {
|
|
194
|
+
// inlineCode is text with code style
|
|
195
|
+
return processTextNode(node.value, { code: true }, options);
|
|
196
|
+
}
|
|
197
|
+
else if (node.type === 'link') {
|
|
198
|
+
return [{
|
|
199
|
+
type: 'link',
|
|
200
|
+
url: node.url,
|
|
201
|
+
text: (0, mdast_util_to_string_1.toString)(node),
|
|
202
|
+
}];
|
|
203
|
+
}
|
|
204
|
+
else if (node.type === 'image') {
|
|
205
|
+
return [{
|
|
206
|
+
type: 'link',
|
|
207
|
+
url: node.url,
|
|
208
|
+
text: node.alt || 'Image',
|
|
209
|
+
}];
|
|
210
|
+
}
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
function flattenStyles(children, style, options) {
|
|
214
|
+
const elements = children.flatMap(c => mapInlineNode(c, options));
|
|
215
|
+
return elements.map(el => {
|
|
216
|
+
const mergedStyle = { ...el.style, ...style };
|
|
217
|
+
// If the resulting style object is empty, do not attach it
|
|
218
|
+
if (Object.keys(mergedStyle).length > 0) {
|
|
219
|
+
return { ...el, style: mergedStyle };
|
|
220
|
+
}
|
|
221
|
+
// If it was empty before and we're not adding anything (shouldn't happen here if style has keys), just return
|
|
222
|
+
// But if el.style was undefined and style is {}, we want undefined.
|
|
223
|
+
// Logic: if mergedStyle has keys, use it. Else, if el had style, keep it? No, we want to flatten.
|
|
224
|
+
// Actually, if we merge {bold: true} with {}, we get {bold: true}.
|
|
225
|
+
// If we merge {} with {}, we get {}. We want to avoid {}.
|
|
226
|
+
const { style: _, ...rest } = el;
|
|
227
|
+
return rest;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function processTextNode(text, style, options) {
|
|
231
|
+
// Regex for:
|
|
232
|
+
// 1. Broadcast: <!here> | <!channel> | <!everyone>
|
|
233
|
+
// 2. Mention: <@U...>
|
|
234
|
+
// 3. Color: #123456
|
|
235
|
+
// 4. Channel: <#C...>
|
|
236
|
+
// 5. Team: <!subteam^T...>
|
|
237
|
+
// 6. Date: <!date^timestamp^format|fallback> (simple approx)
|
|
238
|
+
// 7. Emoji: :shortcode:
|
|
239
|
+
// 8. Mapped Mention: @name
|
|
240
|
+
// 9. Mapped Channel: #name
|
|
241
|
+
// Note: JS Regex stateful global matching
|
|
242
|
+
// We need to capture everything carefully.
|
|
243
|
+
// Groups:
|
|
244
|
+
// 1. Broadcast: (<!here>|<!channel>|<!everyone>)
|
|
245
|
+
// 2. Mention: (<@([\w.-]+)>)
|
|
246
|
+
// 3. Color: (#[0-9a-fA-F]{6})
|
|
247
|
+
// 4. Channel: (<#([\w.-]+)>)
|
|
248
|
+
// 5. Team: (<!subteam\^([\w.-]+)>)
|
|
249
|
+
// 6. Date: (<!date\^(\d+)\^([^|]+)\|([^>]+)>)
|
|
250
|
+
// 7. Emoji: (:([\w+-]+):)
|
|
251
|
+
// 8. Mapped Mention: (@([\w.-]+))
|
|
252
|
+
// 9. Mapped Channel: (#([\w.-]+))
|
|
253
|
+
const regex = /(<!here>|<!channel>|<!everyone>)|(<@([\w.-]+)>)|(#[0-9a-fA-F]{6})|(<#([\w.-]+)>)|(<!subteam\^([\w.-]+)>)|(<!date\^(\d+)\^([^|]+)\|([^>]+)>)|(:([\w+-]+):)|(@([\w.-]+))|(#([\w.-]+))/g;
|
|
254
|
+
const elements = [];
|
|
255
|
+
let lastIndex = 0;
|
|
256
|
+
let match;
|
|
257
|
+
const addText = (t) => {
|
|
258
|
+
if (!t)
|
|
259
|
+
return;
|
|
260
|
+
const el = { type: 'text', text: t };
|
|
261
|
+
if (Object.keys(style).length > 0) {
|
|
262
|
+
el.style = style;
|
|
263
|
+
}
|
|
264
|
+
elements.push(el);
|
|
265
|
+
};
|
|
266
|
+
while ((match = regex.exec(text)) !== null) {
|
|
267
|
+
const fullMatch = match[0];
|
|
268
|
+
const index = match.index;
|
|
269
|
+
if (index > lastIndex) {
|
|
270
|
+
addText(text.substring(lastIndex, index));
|
|
271
|
+
}
|
|
272
|
+
// Apply style if it exists
|
|
273
|
+
const withStyle = (obj) => {
|
|
274
|
+
if (Object.keys(style).length > 0) {
|
|
275
|
+
return { ...obj, style };
|
|
276
|
+
}
|
|
277
|
+
return obj;
|
|
278
|
+
};
|
|
279
|
+
if (match[1]) { // Broadcast: <!here>
|
|
280
|
+
const range = match[1].substring(2, match[1].length - 1);
|
|
281
|
+
elements.push(withStyle({ type: 'broadcast', range }));
|
|
282
|
+
}
|
|
283
|
+
else if (match[3]) { // Mention: <@ID>
|
|
284
|
+
const userId = match[3];
|
|
285
|
+
elements.push(withStyle({ type: 'user', user_id: userId }));
|
|
286
|
+
}
|
|
287
|
+
else if (match[4]) { // Color: #Hex
|
|
288
|
+
if (options.detectColors !== false) {
|
|
289
|
+
elements.push(withStyle({ type: 'color', value: match[4] }));
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
addText(fullMatch);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (match[6]) { // Channel: <#ID>
|
|
296
|
+
const channelId = match[6];
|
|
297
|
+
elements.push(withStyle({ type: 'channel', channel_id: channelId }));
|
|
298
|
+
}
|
|
299
|
+
else if (match[8]) { // Team: <!subteam^ID>
|
|
300
|
+
const teamId = match[8];
|
|
301
|
+
elements.push(withStyle({ type: 'team', team_id: teamId }));
|
|
302
|
+
}
|
|
303
|
+
else if (match[10]) { // Date: <!date^...|...>
|
|
304
|
+
const timestamp = parseInt(match[10], 10);
|
|
305
|
+
const format = match[11];
|
|
306
|
+
// match[12] is fallback
|
|
307
|
+
elements.push(withStyle({
|
|
308
|
+
type: 'date',
|
|
309
|
+
timestamp,
|
|
310
|
+
format,
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
else if (match[13]) { // Emoji: :name:
|
|
314
|
+
const name = match[14];
|
|
315
|
+
elements.push(withStyle({ type: 'emoji', name }));
|
|
316
|
+
}
|
|
317
|
+
else if (match[15]) { // Mapped Mention: @name
|
|
318
|
+
const name = match[16];
|
|
319
|
+
let mapped = false;
|
|
320
|
+
// 1. Check Broadcasts from plain text (@here, @channel, @everyone)
|
|
321
|
+
if (['here', 'channel', 'everyone'].includes(name)) {
|
|
322
|
+
elements.push(withStyle({ type: 'broadcast', range: name }));
|
|
323
|
+
mapped = true;
|
|
324
|
+
}
|
|
325
|
+
// 2. Check Users
|
|
326
|
+
else if (options.mentions?.users && options.mentions.users[name]) {
|
|
327
|
+
elements.push(withStyle({ type: 'user', user_id: options.mentions.users[name] }));
|
|
328
|
+
mapped = true;
|
|
329
|
+
}
|
|
330
|
+
// 3. Check User Groups
|
|
331
|
+
else if (options.mentions?.userGroups && options.mentions.userGroups[name]) {
|
|
332
|
+
elements.push(withStyle({ type: 'usergroup', usergroup_id: options.mentions.userGroups[name] }));
|
|
333
|
+
mapped = true;
|
|
334
|
+
}
|
|
335
|
+
// 4. Check Teams
|
|
336
|
+
else if (options.mentions?.teams && options.mentions.teams[name]) {
|
|
337
|
+
elements.push(withStyle({ type: 'team', team_id: options.mentions.teams[name] }));
|
|
338
|
+
mapped = true;
|
|
339
|
+
}
|
|
340
|
+
if (!mapped) {
|
|
341
|
+
addText(fullMatch);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (match[17]) { // Mapped Channel: #name
|
|
345
|
+
const name = match[18];
|
|
346
|
+
let mapped = false;
|
|
347
|
+
if (options.mentions?.channels && options.mentions.channels[name]) {
|
|
348
|
+
elements.push(withStyle({ type: 'channel', channel_id: options.mentions.channels[name] }));
|
|
349
|
+
mapped = true;
|
|
350
|
+
}
|
|
351
|
+
if (!mapped) {
|
|
352
|
+
addText(fullMatch);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
lastIndex = regex.lastIndex;
|
|
356
|
+
}
|
|
357
|
+
if (lastIndex < text.length) {
|
|
358
|
+
addText(text.substring(lastIndex));
|
|
359
|
+
}
|
|
360
|
+
return elements;
|
|
361
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export type Block = SectionBlock | HeaderBlock | ImageBlock | ContextBlock | DividerBlock | RichTextBlock | TableBlock;
|
|
2
|
+
export interface SectionBlock {
|
|
3
|
+
type: 'section';
|
|
4
|
+
text?: TextObject;
|
|
5
|
+
fields?: TextObject[];
|
|
6
|
+
accessory?: any;
|
|
7
|
+
block_id?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HeaderBlock {
|
|
10
|
+
type: 'header';
|
|
11
|
+
text: PlainTextObject;
|
|
12
|
+
block_id?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ImageBlock {
|
|
15
|
+
type: 'image';
|
|
16
|
+
image_url: string;
|
|
17
|
+
alt_text: string;
|
|
18
|
+
title?: PlainTextObject;
|
|
19
|
+
block_id?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ContextBlock {
|
|
22
|
+
type: 'context';
|
|
23
|
+
elements: (ImageElement | TextObject)[];
|
|
24
|
+
block_id?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface DividerBlock {
|
|
27
|
+
type: 'divider';
|
|
28
|
+
block_id?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface RichTextBlock {
|
|
31
|
+
type: 'rich_text';
|
|
32
|
+
elements: RichTextElement[];
|
|
33
|
+
block_id?: string;
|
|
34
|
+
}
|
|
35
|
+
export type RichTextElement = RichTextSection | RichTextList | RichTextPreformatted | RichTextQuote;
|
|
36
|
+
export interface RichTextSection {
|
|
37
|
+
type: 'rich_text_section';
|
|
38
|
+
elements: RichTextSectionElement[];
|
|
39
|
+
}
|
|
40
|
+
export interface RichTextList {
|
|
41
|
+
type: 'rich_text_list';
|
|
42
|
+
style: 'bullet' | 'ordered';
|
|
43
|
+
indent?: number;
|
|
44
|
+
offset?: number;
|
|
45
|
+
border?: number;
|
|
46
|
+
elements: RichTextSection[];
|
|
47
|
+
}
|
|
48
|
+
export interface RichTextPreformatted {
|
|
49
|
+
type: 'rich_text_preformatted';
|
|
50
|
+
elements: RichTextSectionElement[];
|
|
51
|
+
border?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface RichTextQuote {
|
|
54
|
+
type: 'rich_text_quote';
|
|
55
|
+
elements: RichTextSectionElement[];
|
|
56
|
+
border?: number;
|
|
57
|
+
}
|
|
58
|
+
export type RichTextSectionElement = RichTextText | RichTextLink | RichTextEmoji | RichTextDate | RichTextUser | RichTextUserGroup | RichTextTeam | RichTextChannel | RichTextBroadcast | RichTextColor;
|
|
59
|
+
export interface RichTextText {
|
|
60
|
+
type: 'text';
|
|
61
|
+
text: string;
|
|
62
|
+
style?: RichTextStyle;
|
|
63
|
+
}
|
|
64
|
+
export interface RichTextLink {
|
|
65
|
+
type: 'link';
|
|
66
|
+
url: string;
|
|
67
|
+
text?: string;
|
|
68
|
+
unsafe?: boolean;
|
|
69
|
+
style?: RichTextStyle;
|
|
70
|
+
}
|
|
71
|
+
export interface RichTextEmoji {
|
|
72
|
+
type: 'emoji';
|
|
73
|
+
name: string;
|
|
74
|
+
unicode?: string;
|
|
75
|
+
style?: RichTextStyle;
|
|
76
|
+
}
|
|
77
|
+
export interface RichTextDate {
|
|
78
|
+
type: 'date';
|
|
79
|
+
timestamp: number;
|
|
80
|
+
format: string;
|
|
81
|
+
url?: string;
|
|
82
|
+
fallback?: string;
|
|
83
|
+
style?: RichTextStyle;
|
|
84
|
+
}
|
|
85
|
+
export interface RichTextUser {
|
|
86
|
+
type: 'user';
|
|
87
|
+
user_id: string;
|
|
88
|
+
style?: RichTextStyle;
|
|
89
|
+
}
|
|
90
|
+
export interface RichTextUserGroup {
|
|
91
|
+
type: 'usergroup';
|
|
92
|
+
usergroup_id: string;
|
|
93
|
+
style?: RichTextStyle;
|
|
94
|
+
}
|
|
95
|
+
export interface RichTextTeam {
|
|
96
|
+
type: 'team';
|
|
97
|
+
team_id: string;
|
|
98
|
+
style?: RichTextStyle;
|
|
99
|
+
}
|
|
100
|
+
export interface RichTextChannel {
|
|
101
|
+
type: 'channel';
|
|
102
|
+
channel_id: string;
|
|
103
|
+
style?: RichTextStyle;
|
|
104
|
+
}
|
|
105
|
+
export interface RichTextBroadcast {
|
|
106
|
+
type: 'broadcast';
|
|
107
|
+
range: 'here' | 'channel' | 'everyone';
|
|
108
|
+
style?: RichTextStyle;
|
|
109
|
+
}
|
|
110
|
+
export interface RichTextColor {
|
|
111
|
+
type: 'color';
|
|
112
|
+
value: string;
|
|
113
|
+
style?: RichTextStyle;
|
|
114
|
+
}
|
|
115
|
+
export interface RichTextStyle {
|
|
116
|
+
bold?: boolean;
|
|
117
|
+
italic?: boolean;
|
|
118
|
+
strike?: boolean;
|
|
119
|
+
code?: boolean;
|
|
120
|
+
}
|
|
121
|
+
export interface TableBlock {
|
|
122
|
+
type: 'table';
|
|
123
|
+
columns?: {
|
|
124
|
+
width?: number;
|
|
125
|
+
}[];
|
|
126
|
+
rows: RichTextBlock[][];
|
|
127
|
+
block_id?: string;
|
|
128
|
+
}
|
|
129
|
+
export interface TextObject {
|
|
130
|
+
type: 'mrkdwn' | 'plain_text';
|
|
131
|
+
text: string;
|
|
132
|
+
emoji?: boolean;
|
|
133
|
+
verbatim?: boolean;
|
|
134
|
+
}
|
|
135
|
+
export interface PlainTextObject {
|
|
136
|
+
type: 'plain_text';
|
|
137
|
+
text: string;
|
|
138
|
+
emoji?: boolean;
|
|
139
|
+
}
|
|
140
|
+
export interface ImageElement {
|
|
141
|
+
type: 'image';
|
|
142
|
+
image_url: string;
|
|
143
|
+
alt_text: string;
|
|
144
|
+
}
|
|
145
|
+
export interface MarkdownToBlocksOptions {
|
|
146
|
+
mentions?: {
|
|
147
|
+
users?: Record<string, string>;
|
|
148
|
+
channels?: Record<string, string>;
|
|
149
|
+
userGroups?: Record<string, string>;
|
|
150
|
+
teams?: Record<string, string>;
|
|
151
|
+
};
|
|
152
|
+
detectColors?: boolean;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAElD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAsCvE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateOptions = validateOptions;
|
|
4
|
+
function validateOptions(options) {
|
|
5
|
+
if (!options?.mentions) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const { users, channels, userGroups, teams } = options.mentions;
|
|
9
|
+
if (users) {
|
|
10
|
+
for (const [name, id] of Object.entries(users)) {
|
|
11
|
+
if (!/^[UW][A-Z0-9]+$/.test(id)) {
|
|
12
|
+
throw new Error(`Invalid User ID for '${name}': '${id}'. Must start with U or W and contain only alphanumeric characters.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (channels) {
|
|
17
|
+
for (const [name, id] of Object.entries(channels)) {
|
|
18
|
+
if (!/^C[A-Z0-9]+$/.test(id)) {
|
|
19
|
+
throw new Error(`Invalid Channel ID for '${name}': '${id}'. Must start with C and contain only alphanumeric characters.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (userGroups) {
|
|
24
|
+
for (const [name, id] of Object.entries(userGroups)) {
|
|
25
|
+
if (!/^S[A-Z0-9]+$/.test(id)) {
|
|
26
|
+
throw new Error(`Invalid User Group ID for '${name}': '${id}'. Must start with S and contain only alphanumeric characters.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (teams) {
|
|
31
|
+
for (const [name, id] of Object.entries(teams)) {
|
|
32
|
+
if (!/^T[A-Z0-9]+$/.test(id)) {
|
|
33
|
+
throw new Error(`Invalid Team ID for '${name}': '${id}'. Must start with T and contain only alphanumeric characters.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "markdown-to-slack-blocks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Convert Markdown to Slack Block Kit JSON format",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"prepublishOnly": "npm run clean && npm run build && npm test",
|
|
23
|
+
"test": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"slack",
|
|
27
|
+
"markdown",
|
|
28
|
+
"block-kit",
|
|
29
|
+
"converter",
|
|
30
|
+
"rich-text",
|
|
31
|
+
"parser"
|
|
32
|
+
],
|
|
33
|
+
"author": "udi",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/udivankin/markdown-to-slack-blocks.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/udivankin/markdown-to-slack-blocks#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/udivankin/markdown-to-slack-blocks/issues"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"mdast-util-from-markdown": "2.0.2",
|
|
48
|
+
"mdast-util-gfm": "3.1.0",
|
|
49
|
+
"mdast-util-to-string": "4.0.0",
|
|
50
|
+
"micromark-extension-gfm": "3.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "24.10.1",
|
|
54
|
+
"eslint": "9.39.1",
|
|
55
|
+
"typescript": "5.9.3",
|
|
56
|
+
"vitest": "4.0.15"
|
|
57
|
+
}
|
|
58
|
+
}
|