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.
- package/dist/splitter.d.ts.map +1 -1
- package/dist/splitter.js +121 -4
- package/package.json +1 -1
package/dist/splitter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["../src/splitter.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,
|
|
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
|
-
|
|
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 (
|
|
22
|
-
return [
|
|
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
|
|
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
|
+
}
|