markdown-to-slack-blocks 1.2.1 → 1.2.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["../src/splitter.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,EAIR,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,kBAAkB;IAC/B,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAKD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,EAAE,EAAE,CAkEpF"}
1
+ {"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["../src/splitter.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,EAQR,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,kBAAkB;IAC/B,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,EAAE,EAAE,CA6EpF"}
package/dist/splitter.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.splitBlocks = splitBlocks;
4
4
  const DEFAULT_MAX_BLOCKS = 40;
5
5
  const DEFAULT_MAX_CHARACTERS = 12000;
6
+ const DEFAULT_MAX_TEXT_SECTION_CHARACTERS = 3000;
6
7
  /**
7
8
  * Splits an array of blocks into multiple arrays that fit within Slack's limits.
8
9
  * Attempts to split at natural boundaries (between top-level blocks, rich_text elements, etc.)
@@ -14,12 +15,24 @@ const DEFAULT_MAX_CHARACTERS = 12000;
14
15
  function splitBlocks(blocks, options) {
15
16
  const maxBlocks = options?.maxBlocks ?? DEFAULT_MAX_BLOCKS;
16
17
  const maxChars = options?.maxCharacters ?? DEFAULT_MAX_CHARACTERS;
17
- if (blocks.length === 0) {
18
+ const normalizedBlocks = [];
19
+ for (const block of blocks) {
20
+ if (block.type === 'section') {
21
+ normalizedBlocks.push(...splitSectionBlock(block, DEFAULT_MAX_TEXT_SECTION_CHARACTERS));
22
+ }
23
+ else if (block.type === 'header') {
24
+ normalizedBlocks.push(...splitHeaderBlock(block, DEFAULT_MAX_TEXT_SECTION_CHARACTERS));
25
+ }
26
+ else {
27
+ normalizedBlocks.push(block);
28
+ }
29
+ }
30
+ if (normalizedBlocks.length === 0) {
18
31
  return [[]];
19
32
  }
20
33
  // Check if everything fits in one message
21
- if (blocks.length <= maxBlocks && JSON.stringify(blocks).length <= maxChars) {
22
- return [blocks];
34
+ if (normalizedBlocks.length <= maxBlocks && JSON.stringify(normalizedBlocks).length <= maxChars) {
35
+ return [normalizedBlocks];
23
36
  }
24
37
  const result = [];
25
38
  let currentBatch = [];
@@ -35,7 +48,7 @@ function splitBlocks(blocks, options) {
35
48
  currentBatch = [];
36
49
  }
37
50
  };
38
- for (const block of blocks) {
51
+ for (const block of normalizedBlocks) {
39
52
  // Try to add block to current batch
40
53
  if (fitsInBatch(currentBatch, block)) {
41
54
  currentBatch.push(block);
@@ -198,3 +211,107 @@ function splitPreformattedElement(element, maxChars) {
198
211
  }
199
212
  return result.length > 0 ? result : [element];
200
213
  }
214
+ /**
215
+ * Splits a large SectionBlock into multiple SectionBlocks if text exceeds limit
216
+ */
217
+ function splitSectionBlock(block, maxChars) {
218
+ if (!block.text || block.text.text.length <= maxChars) {
219
+ return [block];
220
+ }
221
+ const chunks = chunkString(block.text.text, maxChars);
222
+ const result = [];
223
+ // The first block keeps the accessory and fields, subsequent ones are just text
224
+ chunks.forEach((chunk, index) => {
225
+ const newBlock = {
226
+ type: 'section',
227
+ text: {
228
+ ...block.text,
229
+ text: chunk
230
+ },
231
+ ...(block.block_id && index === 0 ? { block_id: block.block_id } : {})
232
+ };
233
+ if (index === 0) {
234
+ if (block.fields)
235
+ newBlock.fields = block.fields;
236
+ if (block.accessory)
237
+ newBlock.accessory = block.accessory;
238
+ // keep block_id only on first? Yes.
239
+ }
240
+ result.push(newBlock);
241
+ });
242
+ return result;
243
+ }
244
+ /**
245
+ * Splits a large HeaderBlock into multiple HeaderBlocks (or Header + Sections) if text exceeds limit
246
+ * Note: Headers are plain_text only.
247
+ */
248
+ function splitHeaderBlock(block, maxChars) {
249
+ if (block.text.text.length <= maxChars) {
250
+ return [block];
251
+ }
252
+ const chunks = chunkString(block.text.text, maxChars);
253
+ const result = [];
254
+ // First chunk remains a header
255
+ result.push({
256
+ type: 'header',
257
+ text: {
258
+ ...block.text,
259
+ text: chunks[0]
260
+ },
261
+ ...(block.block_id ? { block_id: block.block_id } : {})
262
+ });
263
+ // Subsequent chunks become Section blocks
264
+ for (let i = 1; i < chunks.length; i++) {
265
+ result.push({
266
+ type: 'section',
267
+ text: {
268
+ type: 'mrkdwn',
269
+ text: chunks[i]
270
+ }
271
+ });
272
+ }
273
+ return result;
274
+ }
275
+ /**
276
+ * Helper to chunk string by character limit, trying to respect word boundaries
277
+ */
278
+ function chunkString(str, limit) {
279
+ const chunks = [];
280
+ let current = str;
281
+ while (current.length > 0) {
282
+ if (current.length <= limit) {
283
+ chunks.push(current);
284
+ break;
285
+ }
286
+ // Take a slice of 'limit'
287
+ let sliceIndex = limit;
288
+ // Look for last newline within the safety zone (e.g. last 100 chars or just within the limit)
289
+ // We look backwards from limit.
290
+ const newlineIndex = current.lastIndexOf('\n', limit);
291
+ if (newlineIndex !== -1 && newlineIndex > 0) {
292
+ sliceIndex = newlineIndex; // Split AT newline (newline becomes part of first chunk? or consumed?)
293
+ // Usually split at newline means: "Line 1\nLine 2" -> "Line 1", "Line 2".
294
+ // slice(0, index) excludes index.
295
+ // We want to keep the newline structure?
296
+ // If we split "A\nB", chunk 1 "A", chunk 2 "B".
297
+ // So sliceIndex = newlineIndex.
298
+ // And next start = newlineIndex + 1.
299
+ chunks.push(current.slice(0, sliceIndex));
300
+ current = current.slice(sliceIndex + 1);
301
+ continue;
302
+ }
303
+ // Look for last space
304
+ const spaceIndex = current.lastIndexOf(' ', limit);
305
+ if (spaceIndex !== -1 && spaceIndex > limit * 0.8) { // Only split at space if it's somewhat close to the end, to avoid too short lines?
306
+ // Actually, any space is better than mid-word.
307
+ sliceIndex = spaceIndex;
308
+ chunks.push(current.slice(0, sliceIndex));
309
+ current = current.slice(sliceIndex + 1); // Skip the space
310
+ continue;
311
+ }
312
+ // Hard split
313
+ chunks.push(current.slice(0, limit));
314
+ current = current.slice(limit);
315
+ }
316
+ return chunks;
317
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markdown-to-slack-blocks",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Convert Markdown to Slack Block Kit JSON format",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",