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 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.
@@ -0,0 +1,4 @@
1
+ import { MarkdownToBlocksOptions } from './types';
2
+ export * from './types';
3
+ export declare function markdownToBlocks(markdown: string, options?: MarkdownToBlocksOptions): import("./types").Block[];
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { Block, MarkdownToBlocksOptions } from './types';
2
+ export declare function parseMarkdown(markdown: string, options?: MarkdownToBlocksOptions): Block[];
3
+ //# sourceMappingURL=parser.d.ts.map
@@ -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
+ }
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import { MarkdownToBlocksOptions } from './types';
2
+ export declare function validateOptions(options?: MarkdownToBlocksOptions): void;
3
+ //# sourceMappingURL=validator.d.ts.map
@@ -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
+ }