slackmark 0.3.0 → 0.5.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/dist/index.js CHANGED
@@ -1,3 +1,12 @@
1
+ // src/errors.ts
2
+ var ConfigurationError = class extends Error {
3
+ name;
4
+ constructor(message, options) {
5
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
6
+ this.name = "ConfigurationError";
7
+ }
8
+ };
9
+
1
10
  // src/capabilities/capabilities.ts
2
11
  var CapabilityPresetName = {
3
12
  Latest: "latest",
@@ -9,7 +18,6 @@ var CapabilityPresetName = {
9
18
  D2026_06: "2026-06"
10
19
  };
11
20
  var ALL_OFF = {
12
- richText: false,
13
21
  markdownBlock: false,
14
22
  tableBlock: false,
15
23
  preformattedLanguage: false,
@@ -18,10 +26,7 @@ var ALL_OFF = {
18
26
  dataVisualization: false,
19
27
  containerBlock: false
20
28
  };
21
- var CONSERVATIVE = {
22
- ...ALL_OFF,
23
- richText: true
24
- };
29
+ var CONSERVATIVE = { ...ALL_OFF };
25
30
  var P_2025_02 = {
26
31
  ...CONSERVATIVE,
27
32
  markdownBlock: true
@@ -59,12 +64,13 @@ function resolveCapabilities(input, overrides) {
59
64
  if (input === void 0) {
60
65
  base = CAPABILITY_PRESETS.latest;
61
66
  } else if (typeof input === "string") {
62
- const preset = CAPABILITY_PRESETS[input];
67
+ const preset = isPresetName(input) ? CAPABILITY_PRESETS[input] : void 0;
63
68
  if (preset === void 0) {
64
- base = CAPABILITY_PRESETS.latest;
65
- } else {
66
- base = preset;
69
+ throw new ConfigurationError(
70
+ `Unknown capability preset ${JSON.stringify(input)}; use one of: ${Object.keys(CAPABILITY_PRESETS).join(", ")}.`
71
+ );
67
72
  }
73
+ base = preset;
68
74
  } else if (isFullFlags(input)) {
69
75
  base = input;
70
76
  } else {
@@ -75,8 +81,11 @@ function resolveCapabilities(input, overrides) {
75
81
  }
76
82
  return Object.freeze({ ...base, ...overrides });
77
83
  }
84
+ function isPresetName(value) {
85
+ return Object.hasOwn(CAPABILITY_PRESETS, value);
86
+ }
78
87
  function isFullFlags(value) {
79
- return typeof value.richText === "boolean" && typeof value.markdownBlock === "boolean" && typeof value.tableBlock === "boolean" && typeof value.preformattedLanguage === "boolean" && typeof value.headerLevel === "boolean" && typeof value.dataTable === "boolean" && typeof value.dataVisualization === "boolean" && typeof value.containerBlock === "boolean";
88
+ return typeof value.markdownBlock === "boolean" && typeof value.tableBlock === "boolean" && typeof value.preformattedLanguage === "boolean" && typeof value.headerLevel === "boolean" && typeof value.dataTable === "boolean" && typeof value.dataVisualization === "boolean" && typeof value.containerBlock === "boolean";
80
89
  }
81
90
 
82
91
  // src/constants.ts
@@ -298,6 +307,29 @@ function assertNever(value, message) {
298
307
  throw new Error(`${detail}: ${JSON.stringify(value)}`);
299
308
  }
300
309
 
310
+ // src/utf16.ts
311
+ var HIGH_SURROGATE_MIN = 55296;
312
+ var HIGH_SURROGATE_MAX = 56319;
313
+ var LOW_SURROGATE_MIN = 56320;
314
+ var LOW_SURROGATE_MAX = 57343;
315
+ function surrogateSafeEnd(text, end) {
316
+ if (end <= 0) {
317
+ return 0;
318
+ }
319
+ if (end >= text.length) {
320
+ return text.length;
321
+ }
322
+ const before = text.charCodeAt(end - 1);
323
+ const after = text.charCodeAt(end);
324
+ if (before >= HIGH_SURROGATE_MIN && before <= HIGH_SURROGATE_MAX && after >= LOW_SURROGATE_MIN && after <= LOW_SURROGATE_MAX) {
325
+ return end - 1;
326
+ }
327
+ return end;
328
+ }
329
+ function sliceSurrogateSafe(text, max) {
330
+ return text.slice(0, surrogateSafeEnd(text, max));
331
+ }
332
+
301
333
  // src/types.ts
302
334
  var OverflowMode = {
303
335
  Degrade: "degrade",
@@ -398,7 +430,7 @@ var MessageAssembler = class {
398
430
  reason: "Notification text truncated to 40k safety limit",
399
431
  path: ctx.path
400
432
  });
401
- return { blocks, text: raw.slice(0, MESSAGE_TEXT_SAFETY_MAX_CHARS) };
433
+ return { blocks, text: sliceSurrogateSafe(raw, MESSAGE_TEXT_SAFETY_MAX_CHARS) };
402
434
  }
403
435
  };
404
436
  function buildFootnoteBlocks(entries, degradations) {
@@ -417,7 +449,7 @@ function buildFootnoteBlocks(entries, degradations) {
417
449
  reason: "Footnote context text truncated to 3000 chars",
418
450
  path: "footnote"
419
451
  });
420
- elements.push({ type: "mrkdwn", text: note.slice(0, SECTION_TEXT_MAX_CHARS) });
452
+ elements.push({ type: "mrkdwn", text: sliceSurrogateSafe(note, SECTION_TEXT_MAX_CHARS) });
421
453
  } else {
422
454
  elements.push({ type: "mrkdwn", text: note });
423
455
  }
@@ -622,7 +654,7 @@ function inlinesToPlain(elements) {
622
654
  parts.push(`@${el.range}`);
623
655
  break;
624
656
  case "date":
625
- parts.push(el.fallback ?? el.format);
657
+ parts.push(el.fallback ?? formatDateTokensUtc(el.timestamp, el.format));
626
658
  break;
627
659
  default:
628
660
  break;
@@ -630,6 +662,101 @@ function inlinesToPlain(elements) {
630
662
  }
631
663
  return parts.join("");
632
664
  }
665
+ var DATE_TOKEN_RE = /\{([a-z_]+)\}/g;
666
+ var MONTHS = [
667
+ "January",
668
+ "February",
669
+ "March",
670
+ "April",
671
+ "May",
672
+ "June",
673
+ "July",
674
+ "August",
675
+ "September",
676
+ "October",
677
+ "November",
678
+ "December"
679
+ ];
680
+ var WEEKDAYS = [
681
+ "Sunday",
682
+ "Monday",
683
+ "Tuesday",
684
+ "Wednesday",
685
+ "Thursday",
686
+ "Friday",
687
+ "Saturday"
688
+ ];
689
+ function pad2(value) {
690
+ return String(value).padStart(2, "0");
691
+ }
692
+ function ordinal(value) {
693
+ const remainder = value % 100;
694
+ if (remainder >= 11 && remainder <= 13) {
695
+ return `${String(value)}th`;
696
+ }
697
+ switch (value % 10) {
698
+ case 1:
699
+ return `${String(value)}st`;
700
+ case 2:
701
+ return `${String(value)}nd`;
702
+ case 3:
703
+ return `${String(value)}rd`;
704
+ default:
705
+ return `${String(value)}th`;
706
+ }
707
+ }
708
+ function dateToken(token, date) {
709
+ const day = date.getUTCDate();
710
+ const month = MONTHS[date.getUTCMonth()] ?? "";
711
+ const shortMonth = month.slice(0, 3);
712
+ const weekday = WEEKDAYS[date.getUTCDay()] ?? "";
713
+ const year = date.getUTCFullYear();
714
+ switch (token) {
715
+ case "date_num":
716
+ return `${String(year)}-${pad2(date.getUTCMonth() + 1)}-${pad2(day)}`;
717
+ // Slack renders {date_slash} month-first: 02/18/2014 (docs example for
718
+ // timestamp 1392734382).
719
+ case "date_slash":
720
+ return `${pad2(date.getUTCMonth() + 1)}/${pad2(day)}/${String(year)}`;
721
+ case "date_long":
722
+ case "date_long_pretty":
723
+ return `${weekday}, ${month} ${ordinal(day)}, ${String(year)}`;
724
+ case "date_long_full":
725
+ return `${month} ${String(day)}, ${String(year)}`;
726
+ // {date} renders "February 18th, 2014" (ordinal + year), not "February 18".
727
+ case "date":
728
+ case "date_pretty":
729
+ return `${month} ${ordinal(day)}, ${String(year)}`;
730
+ case "date_short":
731
+ case "date_short_pretty":
732
+ return `${shortMonth} ${String(day)}, ${String(year)}`;
733
+ case "time":
734
+ return `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}`;
735
+ case "time_secs":
736
+ return `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`;
737
+ default:
738
+ return void 0;
739
+ }
740
+ }
741
+ function formatDateTokensUtc(timestamp, format) {
742
+ const date = new Date(timestamp * 1e3);
743
+ if (!Number.isFinite(timestamp) || Number.isNaN(date.getTime())) {
744
+ return String(timestamp);
745
+ }
746
+ let unsupported = false;
747
+ const formatted = format.replace(DATE_TOKEN_RE, (_match, token) => {
748
+ const replacement = dateToken(token, date);
749
+ if (replacement === void 0) {
750
+ unsupported = true;
751
+ return "";
752
+ }
753
+ return replacement;
754
+ });
755
+ if (format.length === 0 || unsupported) {
756
+ return date.toISOString().slice(0, 10);
757
+ }
758
+ return formatted;
759
+ }
633
760
  function tableToPlain(rows) {
634
761
  const lines = [];
635
762
  for (const row of rows) {
@@ -869,15 +996,6 @@ function parseNumberList(inner) {
869
996
  // src/converter/slackmark-converter.ts
870
997
  import { toString } from "mdast-util-to-string";
871
998
 
872
- // src/errors.ts
873
- var ConfigurationError = class extends Error {
874
- name;
875
- constructor(message, options) {
876
- super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
877
- this.name = "ConfigurationError";
878
- }
879
- };
880
-
881
999
  // src/converter/resolve-config.ts
882
1000
  function resolveConfig(defaults, overrides) {
883
1001
  const merged = {
@@ -967,7 +1085,7 @@ var SlackMarkConverter = class {
967
1085
  enableMath: config.enableMath,
968
1086
  enableFrontmatter: config.enableFrontmatter
969
1087
  });
970
- const { definitions, footnotes, footnoteOrder } = collectRefs(tree);
1088
+ const { definitions, footnotes, footnoteOrder } = collectRefs(tree, degradations);
971
1089
  let precedingHeading;
972
1090
  const emitted = [];
973
1091
  let index = 0;
@@ -992,11 +1110,13 @@ var SlackMarkConverter = class {
992
1110
  emitted.push(...blocks);
993
1111
  index += 1;
994
1112
  }
1113
+ const footnoteNumbers = /* @__PURE__ */ new Map();
1114
+ footnoteOrder.forEach((id, i) => footnoteNumbers.set(id, i + 1));
995
1115
  const footnoteEntries = footnoteOrder.map((id, i) => {
996
1116
  const def = footnotes.get(id);
997
1117
  return {
998
1118
  n: i + 1,
999
- text: def !== void 0 ? toString(def).trim() : id
1119
+ text: def !== void 0 ? footnoteBodyText(def, footnoteNumbers).trim() : id
1000
1120
  };
1001
1121
  });
1002
1122
  const footnoteBlocks = buildFootnoteBlocks(footnoteEntries, degradations);
@@ -1105,7 +1225,7 @@ function safeNotificationText(plainText, markdown, degradations) {
1105
1225
  reason: "Notification text truncated to 40k safety limit",
1106
1226
  path: "root"
1107
1227
  });
1108
- return raw.slice(0, MESSAGE_TEXT_SAFETY_MAX_CHARS);
1228
+ return sliceSurrogateSafe(raw, MESSAGE_TEXT_SAFETY_MAX_CHARS);
1109
1229
  }
1110
1230
  function clampSectionFallback(text, degradations) {
1111
1231
  if (text.length <= SECTION_TEXT_MAX_CHARS) {
@@ -1118,17 +1238,38 @@ function clampSectionFallback(text, degradations) {
1118
1238
  reason: "Section text truncated to 3000 chars",
1119
1239
  path: "root"
1120
1240
  });
1121
- return text.slice(0, SECTION_TEXT_MAX_CHARS);
1241
+ return sliceSurrogateSafe(text, SECTION_TEXT_MAX_CHARS);
1122
1242
  }
1123
- function collectRefs(tree) {
1243
+ function collectRefs(tree, degradations) {
1124
1244
  const definitions = /* @__PURE__ */ new Map();
1125
1245
  const footnotes = /* @__PURE__ */ new Map();
1126
1246
  for (const node of tree.children) {
1127
1247
  if (node.type === "definition") {
1128
- definitions.set(node.identifier.toLowerCase(), node);
1248
+ const key = node.identifier.toLowerCase();
1249
+ if (definitions.has(key)) {
1250
+ degradations.add({
1251
+ nodeType: "definition",
1252
+ from: node.identifier,
1253
+ to: "ignored",
1254
+ reason: "Duplicate link reference definition; first definition wins",
1255
+ path: "root"
1256
+ });
1257
+ } else {
1258
+ definitions.set(key, node);
1259
+ }
1129
1260
  }
1130
1261
  if (node.type === "footnoteDefinition") {
1131
- footnotes.set(node.identifier, node);
1262
+ if (footnotes.has(node.identifier)) {
1263
+ degradations.add({
1264
+ nodeType: "footnoteDefinition",
1265
+ from: node.identifier,
1266
+ to: "ignored",
1267
+ reason: "Duplicate footnote definition; first definition wins",
1268
+ path: "root"
1269
+ });
1270
+ } else {
1271
+ footnotes.set(node.identifier, node);
1272
+ }
1132
1273
  }
1133
1274
  }
1134
1275
  const footnoteOrder = [];
@@ -1139,8 +1280,46 @@ function collectRefs(tree) {
1139
1280
  footnoteOrder.push(id);
1140
1281
  }
1141
1282
  });
1283
+ for (let i = 0; i < footnoteOrder.length; i += 1) {
1284
+ const def = footnotes.get(footnoteOrder[i] ?? "");
1285
+ if (def === void 0) {
1286
+ continue;
1287
+ }
1288
+ for (const child of def.children) {
1289
+ walkFootnoteReferences(child, (id) => {
1290
+ if (!seen.has(id)) {
1291
+ seen.add(id);
1292
+ footnoteOrder.push(id);
1293
+ }
1294
+ });
1295
+ }
1296
+ }
1142
1297
  return { definitions, footnotes, footnoteOrder };
1143
1298
  }
1299
+ function footnoteBodyText(node, numbers) {
1300
+ if (node === null || typeof node !== "object") {
1301
+ return "";
1302
+ }
1303
+ const record = node;
1304
+ if (record.type === "footnoteReference" && typeof record.identifier === "string") {
1305
+ const n = numbers.get(record.identifier);
1306
+ return n !== void 0 ? `[${String(n)}]` : `[^${record.identifier}]`;
1307
+ }
1308
+ if (typeof record.value === "string") {
1309
+ return record.value;
1310
+ }
1311
+ if (typeof record.alt === "string") {
1312
+ return record.alt;
1313
+ }
1314
+ if (record.children !== void 0) {
1315
+ const parts = [];
1316
+ for (const child of record.children) {
1317
+ parts.push(footnoteBodyText(child, numbers));
1318
+ }
1319
+ return parts.join("");
1320
+ }
1321
+ return "";
1322
+ }
1144
1323
  function walkFootnoteReferences(node, onRef) {
1145
1324
  if (node === null || typeof node !== "object") {
1146
1325
  return;
@@ -1164,8 +1343,11 @@ function chunkMarkdown(text, max) {
1164
1343
  return [text.length > 0 ? text : " "];
1165
1344
  }
1166
1345
  const out = [];
1167
- for (let i = 0; i < text.length; i += max) {
1168
- out.push(text.slice(i, i + max));
1346
+ let i = 0;
1347
+ while (i < text.length) {
1348
+ const end = surrogateSafeEnd(text, i + max);
1349
+ out.push(text.slice(i, end));
1350
+ i = end;
1169
1351
  }
1170
1352
  return out;
1171
1353
  }
@@ -1175,45 +1357,6 @@ import { toString as toString3 } from "mdast-util-to-string";
1175
1357
 
1176
1358
  // src/adapters/inline.ts
1177
1359
  import { toString as toString2 } from "mdast-util-to-string";
1178
- var EMOJI_SHORTCODE_RE = /:([a-z0-9_+-]{1,64}):/g;
1179
- var KNOWN_SLACK_EMOJI = /* @__PURE__ */ new Set([
1180
- "smile",
1181
- "grinning",
1182
- "wink",
1183
- "blush",
1184
- "wave",
1185
- "thumbsup",
1186
- "thumbsdown",
1187
- "tada",
1188
- "rocket",
1189
- "fire",
1190
- "heart",
1191
- "eyes",
1192
- "thinking_face",
1193
- "sweat_smile",
1194
- "joy",
1195
- "sob",
1196
- "clap",
1197
- "100",
1198
- "white_check_mark",
1199
- "x",
1200
- "warning",
1201
- "bulb",
1202
- "memo",
1203
- "mag",
1204
- "link",
1205
- "gear",
1206
- "bug",
1207
- "sparkles",
1208
- "+1",
1209
- "-1",
1210
- "ok_hand",
1211
- "pray",
1212
- "star",
1213
- "zap",
1214
- "check",
1215
- "heavy_check_mark"
1216
- ]);
1217
1360
  var HTTP_URL_RE = /^(https?:|mailto:)/i;
1218
1361
  var IMAGE_EXT_RE = /\.(png|jpe?g|gif)(?:\?|#|$)/i;
1219
1362
  function emitPhrasing(nodes, ctx, baseStyle = {}) {
@@ -1221,26 +1364,24 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1221
1364
  const hoistedImages = [];
1222
1365
  const work = [...nodes];
1223
1366
  let pendingAnchor;
1367
+ const htmlStyles = [];
1368
+ const activeStyle = () => htmlStyles[htmlStyles.length - 1] ?? baseStyle;
1224
1369
  const flushPendingAnchor = () => {
1225
1370
  if (pendingAnchor === void 0) {
1226
1371
  return;
1227
1372
  }
1228
- const label = pendingAnchor.textParts.join("");
1229
- elements.push({
1230
- type: "link",
1231
- url: pendingAnchor.href,
1232
- text: label.length > 0 ? label : pendingAnchor.href,
1233
- style: styleOrUndefined(pendingAnchor.style)
1234
- });
1373
+ elements.push(anchorInline(pendingAnchor, ctx));
1235
1374
  pendingAnchor = void 0;
1236
1375
  };
1237
1376
  for (let i = 0; i < work.length; i += 1) {
1238
1377
  const node = work[i];
1239
1378
  if (pendingAnchor === void 0) {
1240
- const reassembled = reassembleSlackLabeledLink(work, i, baseStyle);
1379
+ const reassembled = reassembleSlackLabeledLink(work, i, activeStyle());
1241
1380
  if (reassembled !== void 0) {
1242
1381
  if (reassembled.leadingText.value.length > 0) {
1243
- elements.push(...emitTextWithMentionsAndEmoji(reassembled.leadingText, baseStyle, ctx));
1382
+ elements.push(
1383
+ ...emitTextWithMentionsAndEmoji(reassembled.leadingText, activeStyle(), ctx)
1384
+ );
1244
1385
  }
1245
1386
  elements.push(reassembled.link);
1246
1387
  if (reassembled.trailingText.value.length > 0) {
@@ -1270,7 +1411,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1270
1411
  }
1271
1412
  if (node.type === "html") {
1272
1413
  const trimmed = node.value.trim();
1273
- const entityInlines = slackEntityRichTextInlines(trimmed, baseStyle);
1414
+ const entityInlines = slackEntityRichTextInlines(trimmed, activeStyle());
1274
1415
  if (entityInlines !== void 0) {
1275
1416
  elements.push(...entityInlines);
1276
1417
  continue;
@@ -1279,28 +1420,45 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1279
1420
  if (openOnly !== null) {
1280
1421
  const hrefMatch = /href\s*=\s*["']([^"']{0,3000})["']/i.exec(openOnly[1] ?? "");
1281
1422
  const href = hrefMatch?.[1];
1282
- if (href !== void 0 && isValidLinkUrl(href)) {
1283
- pendingAnchor = { href, style: baseStyle, textParts: [] };
1423
+ if (href !== void 0) {
1424
+ pendingAnchor = {
1425
+ href,
1426
+ style: activeStyle(),
1427
+ textParts: [],
1428
+ valid: isValidLinkUrl(href)
1429
+ };
1284
1430
  continue;
1285
1431
  }
1286
1432
  }
1287
- elements.push(...emitInlineHtml(node.value, baseStyle, ctx));
1433
+ const openTag = HTML_STYLE_OPEN_RE.exec(trimmed);
1434
+ if (openTag !== null) {
1435
+ htmlStyles.push(htmlTagStyle(activeStyle(), openTag[1] ?? ""));
1436
+ continue;
1437
+ }
1438
+ if (HTML_STYLE_CLOSE_RE.test(trimmed)) {
1439
+ htmlStyles.pop();
1440
+ continue;
1441
+ }
1442
+ elements.push(...emitInlineHtml(node.value, activeStyle(), ctx));
1288
1443
  continue;
1289
1444
  }
1290
1445
  switch (node.type) {
1291
1446
  case "text": {
1292
- elements.push(...emitTextWithMentionsAndEmoji(node, baseStyle, ctx));
1447
+ elements.push(...emitTextWithMentionsAndEmoji(node, activeStyle(), ctx));
1293
1448
  break;
1294
1449
  }
1295
1450
  case "strong": {
1296
- const inner = emitPhrasing(node.children, ctx, { ...baseStyle, bold: true });
1451
+ const inner = emitPhrasing(node.children, ctx, {
1452
+ ...activeStyle(),
1453
+ bold: true
1454
+ });
1297
1455
  elements.push(...inner.elements);
1298
1456
  hoistedImages.push(...inner.hoistedImages);
1299
1457
  break;
1300
1458
  }
1301
1459
  case "emphasis": {
1302
1460
  const inner = emitPhrasing(node.children, ctx, {
1303
- ...baseStyle,
1461
+ ...activeStyle(),
1304
1462
  italic: true
1305
1463
  });
1306
1464
  elements.push(...inner.elements);
@@ -1308,7 +1466,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1308
1466
  break;
1309
1467
  }
1310
1468
  case "delete": {
1311
- const inner = emitPhrasing(node.children, ctx, { ...baseStyle, strike: true });
1469
+ const inner = emitPhrasing(node.children, ctx, { ...activeStyle(), strike: true });
1312
1470
  elements.push(...inner.elements);
1313
1471
  hoistedImages.push(...inner.hoistedImages);
1314
1472
  break;
@@ -1317,21 +1475,21 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1317
1475
  const el = {
1318
1476
  type: "text",
1319
1477
  text: node.value,
1320
- style: styleOrUndefined({ ...baseStyle, code: true })
1478
+ style: styleOrUndefined({ ...activeStyle(), code: true })
1321
1479
  };
1322
1480
  elements.push(el);
1323
1481
  break;
1324
1482
  }
1325
1483
  case "break": {
1326
- elements.push({ type: "text", text: "\n", style: styleOrUndefined(baseStyle) });
1484
+ elements.push({ type: "text", text: "\n", style: styleOrUndefined(activeStyle()) });
1327
1485
  break;
1328
1486
  }
1329
1487
  case "link": {
1330
- elements.push(...emitLink(node, ctx, baseStyle));
1488
+ elements.push(...emitLink(node, ctx, activeStyle()));
1331
1489
  break;
1332
1490
  }
1333
1491
  case "linkReference": {
1334
- elements.push(...emitLinkReference(node, ctx, baseStyle));
1492
+ elements.push(...emitLinkReference(node, ctx, activeStyle()));
1335
1493
  break;
1336
1494
  }
1337
1495
  case "image": {
@@ -1346,22 +1504,33 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1346
1504
  type: "link",
1347
1505
  url,
1348
1506
  text: node.alt && node.alt.length > 0 ? node.alt : url,
1349
- style: styleOrUndefined(baseStyle)
1507
+ style: styleOrUndefined(activeStyle())
1350
1508
  });
1351
1509
  } else {
1510
+ const linkable = isValidLinkUrl(url);
1352
1511
  ctx.degradations.add({
1353
1512
  nodeType: "image",
1354
1513
  from: "image",
1355
- to: "link",
1514
+ to: linkable ? "link" : "text",
1356
1515
  reason: "Image URL is not a public http(s) png/jpg/gif \u22643000 chars",
1357
1516
  path: ctx.path
1358
1517
  });
1359
- elements.push({
1360
- type: "link",
1361
- url: url.length > 0 ? url : "#",
1362
- text: node.alt && node.alt.length > 0 ? node.alt : url,
1363
- style: styleOrUndefined(baseStyle)
1364
- });
1518
+ const label = node.alt && node.alt.length > 0 ? node.alt : url.length > 0 ? url : "image";
1519
+ if (linkable) {
1520
+ elements.push({
1521
+ type: "link",
1522
+ url,
1523
+ text: label,
1524
+ style: styleOrUndefined(activeStyle())
1525
+ });
1526
+ } else {
1527
+ const suffix = url.length > 0 && url !== label ? ` (${url})` : "";
1528
+ elements.push({
1529
+ type: "text",
1530
+ text: `${label}${suffix}`,
1531
+ style: styleOrUndefined(activeStyle())
1532
+ });
1533
+ }
1365
1534
  }
1366
1535
  break;
1367
1536
  }
@@ -1371,7 +1540,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1371
1540
  elements.push({
1372
1541
  type: "text",
1373
1542
  text: node.alt ?? node.identifier,
1374
- style: styleOrUndefined(baseStyle)
1543
+ style: styleOrUndefined(activeStyle())
1375
1544
  });
1376
1545
  break;
1377
1546
  }
@@ -1385,7 +1554,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1385
1554
  }
1386
1555
  ],
1387
1556
  ctx,
1388
- baseStyle
1557
+ activeStyle()
1389
1558
  );
1390
1559
  elements.push(...asImage.elements);
1391
1560
  hoistedImages.push(...asImage.hoistedImages);
@@ -1397,7 +1566,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1397
1566
  elements.push({
1398
1567
  type: "text",
1399
1568
  text: `[${String(n)}]`,
1400
- style: styleOrUndefined(baseStyle)
1569
+ style: styleOrUndefined(activeStyle())
1401
1570
  });
1402
1571
  break;
1403
1572
  }
@@ -1405,13 +1574,13 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1405
1574
  elements.push({
1406
1575
  type: "text",
1407
1576
  text: node.value,
1408
- style: styleOrUndefined({ ...baseStyle, code: true })
1577
+ style: styleOrUndefined({ ...activeStyle(), code: true })
1409
1578
  });
1410
1579
  ctx.degradations.add({
1411
1580
  nodeType: "inlineMath",
1412
1581
  from: "inlineMath",
1413
1582
  to: "style.code",
1414
- reason: "Math rendering not configured; emitted as inline code",
1583
+ reason: "Math is emitted as preformatted/code text",
1415
1584
  path: ctx.path
1416
1585
  });
1417
1586
  break;
@@ -1419,7 +1588,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1419
1588
  default: {
1420
1589
  const text = toString2(node);
1421
1590
  if (text.length > 0) {
1422
- elements.push({ type: "text", text, style: styleOrUndefined(baseStyle) });
1591
+ elements.push({ type: "text", text, style: styleOrUndefined(activeStyle()) });
1423
1592
  }
1424
1593
  break;
1425
1594
  }
@@ -1488,7 +1657,7 @@ function truncateLabel(label, max) {
1488
1657
  if (label.length <= max) {
1489
1658
  return { text: label, truncated: false };
1490
1659
  }
1491
- return { text: `${label.slice(0, Math.max(0, max - 1))}${ELLIPSIS}`, truncated: true };
1660
+ return { text: `${sliceSurrogateSafe(label, Math.max(0, max - 1))}${ELLIPSIS}`, truncated: true };
1492
1661
  }
1493
1662
  function hoistedImageBlock(h, ctx) {
1494
1663
  return {
@@ -1509,7 +1678,7 @@ function clampSectionText(text, ctx, nodeType) {
1509
1678
  reason: "Section text truncated to 3000 chars",
1510
1679
  path: ctx.path
1511
1680
  });
1512
- return text.slice(0, SECTION_TEXT_MAX_CHARS);
1681
+ return sliceSurrogateSafe(text, SECTION_TEXT_MAX_CHARS);
1513
1682
  }
1514
1683
  function preformattedRichText(text, language) {
1515
1684
  return {
@@ -1535,7 +1704,7 @@ function clampImageText(value, ctx, field) {
1535
1704
  reason: `Image ${field} truncated to ${String(IMAGE_ALT_MAX_CHARS)} chars`,
1536
1705
  path: ctx.path
1537
1706
  });
1538
- return value.slice(0, IMAGE_ALT_MAX_CHARS);
1707
+ return sliceSurrogateSafe(value, IMAGE_ALT_MAX_CHARS);
1539
1708
  }
1540
1709
  function styleOrUndefined(style) {
1541
1710
  if (style.bold === true || style.italic === true || style.strike === true || style.code === true || style.underline === true) {
@@ -1543,13 +1712,21 @@ function styleOrUndefined(style) {
1543
1712
  }
1544
1713
  return void 0;
1545
1714
  }
1546
- var SLACK_ENTITY_CANDIDATE_RE = /<([@#!][^<>]{1,340}|(?:https?:|mailto:)[^<>|]{1,3000}\|[^<>]{1,300})>/gi;
1715
+ var SLACK_ENTITY_AT_RE = /<([@#!][^<>]{1,340}|(?:https?:|mailto:)[^<>|]{1,3000}\|[^<>]{1,300})>/iy;
1716
+ var CHAR_LT = 60;
1717
+ var CHAR_HASH = 35;
1718
+ var CHAR_AT = 64;
1719
+ var CHAR_COLON = 58;
1547
1720
  var SLACK_USER_RE = /^@([UW][A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
1548
1721
  var SLACK_CHANNEL_RE = /^#([CDG][A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
1549
1722
  var SLACK_USERGROUP_RE = /^!subteam\^(S[A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
1550
1723
  var SLACK_BROADCAST_RE = /^!(here|channel|everyone)(?:\|[^>]{0,300})?$/;
1551
1724
  var SLACK_DATE_RE = /^!date\^(\d{1,13})\^([^^|]{1,300})(?:\^([^^|]{1,3000}))?(?:\|([^]{0,300}))?$/;
1552
1725
  var SLACK_LINK_RE = /^((?:https?:|mailto:)[^|]{1,3000})\|([^]{1,300})$/i;
1726
+ var MENTION_PATTERN = String.raw`(?<![A-Za-z0-9._-])@([a-zA-Z0-9_-](?:[a-zA-Z0-9._-]{0,62}[a-zA-Z0-9_-])?)|(?<![A-Za-z0-9_-])#([a-zA-Z0-9_-]{1,80})`;
1727
+ var MENTION_RE = new RegExp(MENTION_PATTERN, "g");
1728
+ var MENTION_AT_RE = new RegExp(MENTION_PATTERN, "y");
1729
+ var EMOJI_AT_RE = /(?<![A-Za-z0-9]):([a-z0-9_+-]{1,64}):(?![A-Za-z0-9])/y;
1553
1730
  function slackEntityElement(inner, style) {
1554
1731
  const labeledLink = SLACK_LINK_RE.exec(inner);
1555
1732
  if (labeledLink?.[1] !== void 0 && labeledLink[2] !== void 0) {
@@ -1598,39 +1775,79 @@ function slackEntityElement(inner, style) {
1598
1775
  }
1599
1776
  return void 0;
1600
1777
  }
1601
- function slackEntitySplit(value, style, emit) {
1602
- SLACK_ENTITY_CANDIDATE_RE.lastIndex = 0;
1603
- let match = SLACK_ENTITY_CANDIDATE_RE.exec(value);
1604
- if (match === null) {
1605
- return void 0;
1606
- }
1778
+ function scanTextRun(value, style, resolveUser, resolveChannel) {
1779
+ const scanMentions = resolveUser !== void 0 || resolveChannel !== void 0;
1780
+ const styleOpt = styleOrUndefined(style);
1607
1781
  const out = [];
1608
- let last = 0;
1609
1782
  let sawEntity = false;
1610
- while (match !== null) {
1611
- const element = slackEntityElement(match[1] ?? "", style);
1783
+ let i = 0;
1784
+ let textStart = 0;
1785
+ while (i < value.length) {
1786
+ const ch = value.charCodeAt(i);
1787
+ let element;
1788
+ let tokenEnd = 0;
1789
+ if (ch === CHAR_LT) {
1790
+ SLACK_ENTITY_AT_RE.lastIndex = i;
1791
+ const match = SLACK_ENTITY_AT_RE.exec(value);
1792
+ if (match !== null) {
1793
+ element = slackEntityElement(match[1] ?? "", style);
1794
+ if (element !== void 0) {
1795
+ tokenEnd = SLACK_ENTITY_AT_RE.lastIndex;
1796
+ sawEntity = true;
1797
+ }
1798
+ }
1799
+ } else if (scanMentions && (ch === CHAR_AT || ch === CHAR_HASH)) {
1800
+ MENTION_AT_RE.lastIndex = i;
1801
+ const match = MENTION_AT_RE.exec(value);
1802
+ if (match !== null) {
1803
+ const userHandle = match[1];
1804
+ const channelName = match[2];
1805
+ if (userHandle !== void 0 && resolveUser !== void 0) {
1806
+ const userId = resolveUser(userHandle);
1807
+ if (userId !== void 0 && userId.length > 0) {
1808
+ element = { type: "user", user_id: userId, style: styleOpt };
1809
+ tokenEnd = MENTION_AT_RE.lastIndex;
1810
+ }
1811
+ } else if (channelName !== void 0 && resolveChannel !== void 0) {
1812
+ const channelId = resolveChannel(channelName);
1813
+ if (channelId !== void 0 && channelId.length > 0) {
1814
+ element = { type: "channel", channel_id: channelId, style: styleOpt };
1815
+ tokenEnd = MENTION_AT_RE.lastIndex;
1816
+ }
1817
+ }
1818
+ }
1819
+ } else if (ch === CHAR_COLON) {
1820
+ EMOJI_AT_RE.lastIndex = i;
1821
+ const match = EMOJI_AT_RE.exec(value);
1822
+ if (match !== null) {
1823
+ const emoji = { type: "emoji", name: match[1] ?? "" };
1824
+ element = emoji;
1825
+ tokenEnd = EMOJI_AT_RE.lastIndex;
1826
+ }
1827
+ }
1612
1828
  if (element !== void 0) {
1613
- sawEntity = true;
1614
- if (match.index > last) {
1615
- out.push(...emit(value.slice(last, match.index)));
1829
+ if (i > textStart) {
1830
+ out.push({ type: "text", text: value.slice(textStart, i), style: styleOpt });
1616
1831
  }
1617
1832
  out.push(element);
1618
- last = match.index + match[0].length;
1833
+ i = tokenEnd;
1834
+ textStart = i;
1835
+ continue;
1619
1836
  }
1620
- match = SLACK_ENTITY_CANDIDATE_RE.exec(value);
1837
+ i += 1;
1621
1838
  }
1622
- if (!sawEntity) {
1623
- return void 0;
1839
+ if (textStart < value.length) {
1840
+ out.push({ type: "text", text: value.slice(textStart), style: styleOpt });
1624
1841
  }
1625
- if (last < value.length) {
1626
- out.push(...emit(value.slice(last)));
1842
+ if (out.length === 0) {
1843
+ out.push({ type: "text", text: value, style: styleOpt });
1627
1844
  }
1628
- return out;
1845
+ return { elements: out, sawEntity };
1629
1846
  }
1630
1847
  function slackEntityRichTextInlines(value, style) {
1631
- return slackEntitySplit(value, style, (slice) => emitTextWithEmoji(slice, style));
1848
+ const { elements, sawEntity } = scanTextRun(value, style);
1849
+ return sawEntity ? elements : void 0;
1632
1850
  }
1633
- var MENTION_RE = /(?<![A-Za-z0-9._-])@([a-zA-Z0-9._-]{1,64})|(?<![A-Za-z0-9_-])#([a-zA-Z0-9_-]{1,80})/g;
1634
1851
  function phrasingNeedsMentionResolution(nodes, ctx) {
1635
1852
  const resolvers = ctx.config.mentionResolvers;
1636
1853
  if (resolvers?.resolveUser === void 0 && resolvers?.resolveChannel === void 0) {
@@ -1641,89 +1858,8 @@ function phrasingNeedsMentionResolution(nodes, ctx) {
1641
1858
  return MENTION_RE.test(plain);
1642
1859
  }
1643
1860
  function emitTextWithMentionsAndEmoji(node, style, ctx) {
1644
- const viaEntities = slackEntitySplit(
1645
- node.value,
1646
- style,
1647
- (slice) => emitMentionsAndEmoji(slice, style, ctx)
1648
- );
1649
- if (viaEntities !== void 0) {
1650
- return viaEntities;
1651
- }
1652
- return emitMentionsAndEmoji(node.value, style, ctx);
1653
- }
1654
- function emitMentionsAndEmoji(value, style, ctx) {
1655
1861
  const resolvers = ctx.config.mentionResolvers;
1656
- const canUser = resolvers?.resolveUser !== void 0;
1657
- const canChannel = resolvers?.resolveChannel !== void 0;
1658
- if (!canUser && !canChannel) {
1659
- return emitTextWithEmoji(value, style);
1660
- }
1661
- const out = [];
1662
- MENTION_RE.lastIndex = 0;
1663
- let last = 0;
1664
- let match = MENTION_RE.exec(value);
1665
- while (match !== null) {
1666
- const start = match.index;
1667
- if (start > last) {
1668
- out.push(...emitTextWithEmoji(value.slice(last, start), style));
1669
- }
1670
- const userHandle = match[1];
1671
- const channelName = match[2];
1672
- if (userHandle !== void 0 && canUser) {
1673
- const userId = resolvers.resolveUser?.(userHandle);
1674
- if (userId !== void 0 && userId.length > 0) {
1675
- out.push({ type: "user", user_id: userId, style: styleOrUndefined(style) });
1676
- } else {
1677
- out.push(...emitTextWithEmoji(match[0], style));
1678
- }
1679
- } else if (channelName !== void 0 && canChannel) {
1680
- const channelId = resolvers.resolveChannel?.(channelName);
1681
- if (channelId !== void 0 && channelId.length > 0) {
1682
- out.push({ type: "channel", channel_id: channelId, style: styleOrUndefined(style) });
1683
- } else {
1684
- out.push(...emitTextWithEmoji(match[0], style));
1685
- }
1686
- } else {
1687
- out.push(...emitTextWithEmoji(match[0], style));
1688
- }
1689
- last = start + match[0].length;
1690
- match = MENTION_RE.exec(value);
1691
- }
1692
- if (last < value.length) {
1693
- out.push(...emitTextWithEmoji(value.slice(last), style));
1694
- }
1695
- if (out.length === 0) {
1696
- out.push(...emitTextWithEmoji(value, style));
1697
- }
1698
- return out;
1699
- }
1700
- function emitTextWithEmoji(value, style) {
1701
- const out = [];
1702
- EMOJI_SHORTCODE_RE.lastIndex = 0;
1703
- let last = 0;
1704
- let match = EMOJI_SHORTCODE_RE.exec(value);
1705
- while (match !== null) {
1706
- const start = match.index;
1707
- if (start > last) {
1708
- out.push({ type: "text", text: value.slice(last, start), style: styleOrUndefined(style) });
1709
- }
1710
- const name = match[1] ?? "";
1711
- if (KNOWN_SLACK_EMOJI.has(name)) {
1712
- const emoji = { type: "emoji", name };
1713
- out.push(emoji);
1714
- } else {
1715
- out.push({ type: "text", text: match[0], style: styleOrUndefined(style) });
1716
- }
1717
- last = start + match[0].length;
1718
- match = EMOJI_SHORTCODE_RE.exec(value);
1719
- }
1720
- if (last < value.length) {
1721
- out.push({ type: "text", text: value.slice(last), style: styleOrUndefined(style) });
1722
- }
1723
- if (out.length === 0) {
1724
- out.push({ type: "text", text: value, style: styleOrUndefined(style) });
1725
- }
1726
- return out;
1862
+ return scanTextRun(node.value, style, resolvers?.resolveUser, resolvers?.resolveChannel).elements;
1727
1863
  }
1728
1864
  function reassembleSlackLabeledLink(work, i, style) {
1729
1865
  const lead = work[i];
@@ -1780,7 +1916,7 @@ function emitLink(node, ctx, style) {
1780
1916
  reason: "Invalid or oversized URL",
1781
1917
  path: ctx.path
1782
1918
  });
1783
- const suffix = url.length > 0 ? ` (${url})` : "";
1919
+ const suffix = url.length > 0 && url !== text ? ` (${url})` : "";
1784
1920
  return [{ type: "text", text: `${text}${suffix}`, style: styleOrUndefined(style) }];
1785
1921
  }
1786
1922
  const link = {
@@ -1817,9 +1953,45 @@ function isValidLinkUrl(url) {
1817
1953
  if (url.length === 0 || url.length > IMAGE_URL_MAX_CHARS) {
1818
1954
  return false;
1819
1955
  }
1820
- return HTTP_URL_RE.test(url) || url.startsWith("#") || url.startsWith("/");
1956
+ return HTTP_URL_RE.test(url);
1957
+ }
1958
+ function anchorInline(anchor, ctx) {
1959
+ const label = anchor.textParts.join("");
1960
+ if (anchor.valid) {
1961
+ return {
1962
+ type: "link",
1963
+ url: anchor.href,
1964
+ text: label.length > 0 ? label : anchor.href,
1965
+ style: styleOrUndefined(anchor.style)
1966
+ };
1967
+ }
1968
+ ctx.degradations.add({
1969
+ nodeType: "link",
1970
+ from: "link",
1971
+ to: "text",
1972
+ reason: "Invalid or oversized URL",
1973
+ path: ctx.path
1974
+ });
1975
+ const base = label.length > 0 ? label : anchor.href;
1976
+ const suffix = label.length > 0 && anchor.href.length > 0 && anchor.href !== label ? ` (${anchor.href})` : "";
1977
+ return { type: "text", text: `${base}${suffix}`, style: styleOrUndefined(anchor.style) };
1978
+ }
1979
+ var HTML_STYLE_OPEN_RE = /^<(b|strong|i|em|s|del|code)(\s+[^>]{0,200})?>$/i;
1980
+ var HTML_STYLE_CLOSE_RE = /^<\/(b|strong|i|em|s|del|code)\s*>$/i;
1981
+ function htmlTagStyle(base, tag) {
1982
+ const t = tag.toLowerCase();
1983
+ if (t === "b" || t === "strong") {
1984
+ return { ...base, bold: true };
1985
+ }
1986
+ if (t === "i" || t === "em") {
1987
+ return { ...base, italic: true };
1988
+ }
1989
+ if (t === "s" || t === "del") {
1990
+ return { ...base, strike: true };
1991
+ }
1992
+ return { ...base, code: true };
1821
1993
  }
1822
- var INLINE_TAG_RE = /<\/?(b|strong|i|em|s|del|code|br|a)(\s+[^>]{0,200})?>|[^<]+/gi;
1994
+ var INLINE_TAG_RE = /<\/?(b|strong|i|em|s|del|code|br|a)(\s+[^>]{0,200})?\s*\/?>|<[^>]{0,200}>|[^<]+|</gi;
1823
1995
  function emitInlineHtml(html, style, ctx) {
1824
1996
  const out = [];
1825
1997
  const stack = [];
@@ -1827,11 +1999,15 @@ function emitInlineHtml(html, style, ctx) {
1827
1999
  let openAnchor;
1828
2000
  INLINE_TAG_RE.lastIndex = 0;
1829
2001
  let m = INLINE_TAG_RE.exec(html);
1830
- let matched = false;
2002
+ let strippedUnknown = false;
1831
2003
  while (m !== null) {
1832
- matched = true;
1833
2004
  const token = m[0];
1834
- if (token.startsWith("<")) {
2005
+ if (token.startsWith("<") && token.length > 1 && m[1] === void 0) {
2006
+ strippedUnknown = true;
2007
+ m = INLINE_TAG_RE.exec(html);
2008
+ continue;
2009
+ }
2010
+ if (token.startsWith("<") && m[1] !== void 0) {
1835
2011
  const tag = (m[1] ?? "").toLowerCase();
1836
2012
  const closing = token.startsWith("</");
1837
2013
  if (tag === "br") {
@@ -1842,13 +2018,7 @@ function emitInlineHtml(html, style, ctx) {
1842
2018
  }
1843
2019
  } else if (closing) {
1844
2020
  if (tag === "a" && openAnchor !== void 0) {
1845
- const label = openAnchor.textParts.join("");
1846
- out.push({
1847
- type: "link",
1848
- url: openAnchor.href,
1849
- text: label.length > 0 ? label : openAnchor.href,
1850
- style: styleOrUndefined(openAnchor.style)
1851
- });
2021
+ out.push(anchorInline(openAnchor, ctx));
1852
2022
  openAnchor = void 0;
1853
2023
  }
1854
2024
  stack.pop();
@@ -1857,8 +2027,11 @@ function emitInlineHtml(html, style, ctx) {
1857
2027
  const hrefMatch = /href\s*=\s*["']([^"']{0,3000})["']/i.exec(token);
1858
2028
  const href = hrefMatch?.[1];
1859
2029
  stack.push({ tag, style: current });
1860
- if (href !== void 0 && isValidLinkUrl(href)) {
1861
- openAnchor = { href, style: current, textParts: [] };
2030
+ if (href !== void 0) {
2031
+ if (openAnchor !== void 0) {
2032
+ out.push(anchorInline(openAnchor, ctx));
2033
+ }
2034
+ openAnchor = { href, style: current, textParts: [], valid: isValidLinkUrl(href) };
1862
2035
  }
1863
2036
  } else {
1864
2037
  const next = { ...current };
@@ -1879,15 +2052,9 @@ function emitInlineHtml(html, style, ctx) {
1879
2052
  m = INLINE_TAG_RE.exec(html);
1880
2053
  }
1881
2054
  if (openAnchor !== void 0) {
1882
- const label = openAnchor.textParts.join("");
1883
- out.push({
1884
- type: "link",
1885
- url: openAnchor.href,
1886
- text: label.length > 0 ? label : openAnchor.href,
1887
- style: styleOrUndefined(openAnchor.style)
1888
- });
2055
+ out.push(anchorInline(openAnchor, ctx));
1889
2056
  }
1890
- if (!matched) {
2057
+ if (strippedUnknown) {
1891
2058
  ctx.degradations.add({
1892
2059
  nodeType: "html",
1893
2060
  from: "html",
@@ -1895,10 +2062,6 @@ function emitInlineHtml(html, style, ctx) {
1895
2062
  reason: "Unrecognized inline HTML stripped to text",
1896
2063
  path: ctx.path
1897
2064
  });
1898
- const stripped = html.replace(/<[^>]{0,200}>/g, "");
1899
- if (stripped.length > 0) {
1900
- out.push({ type: "text", text: stripped, style: styleOrUndefined(style) });
1901
- }
1902
2065
  }
1903
2066
  return out;
1904
2067
  }
@@ -2130,10 +2293,13 @@ function emitPlainHeader(heading, withLevel) {
2130
2293
  const block = {
2131
2294
  type: "header",
2132
2295
  text: { type: "plain_text", text: plain.length > 0 ? plain : " " },
2133
- level: withLevel ? heading.depth : void 0
2296
+ level: withLevel ? headerLevelOf(heading.depth) : void 0
2134
2297
  };
2135
2298
  return [block];
2136
2299
  }
2300
+ function headerLevelOf(depth) {
2301
+ return depth === 1 || depth === 2 || depth === 3 || depth === 4 ? depth : void 0;
2302
+ }
2137
2303
  function emitRichHeading(heading, ctx, hoistImages) {
2138
2304
  const plain = phrasingToPlain(heading.children);
2139
2305
  const depth = heading.depth;
@@ -2296,7 +2462,7 @@ function clampContainerTitle(text, ctx) {
2296
2462
  reason: `Container title truncated to ${String(CONTAINER_TITLE_MAX_CHARS)} chars`,
2297
2463
  path: ctx.path
2298
2464
  });
2299
- return text.slice(0, CONTAINER_TITLE_MAX_CHARS);
2465
+ return sliceSurrogateSafe(text, CONTAINER_TITLE_MAX_CHARS);
2300
2466
  }
2301
2467
 
2302
2468
  // src/adapters/list-adapter.ts
@@ -2376,27 +2542,51 @@ function emitListTree(list, indent, ctx) {
2376
2542
  });
2377
2543
  effectiveIndent = RICH_TEXT_LIST_INDENT_MAX;
2378
2544
  }
2379
- const sections = [];
2380
- const nested = [];
2545
+ const ordered = list.ordered === true;
2546
+ const startOffset = ordered && list.start !== null && list.start !== void 0 && list.start > 1 ? list.start - 1 : 0;
2547
+ const out = [];
2381
2548
  const hoistedImages = [];
2382
- for (const item of list.children) {
2549
+ let pendingSections = [];
2550
+ const flush = (lastIndex) => {
2551
+ if (pendingSections.length === 0) {
2552
+ return;
2553
+ }
2554
+ const chunkStartIndex = lastIndex - pendingSections.length + 1;
2555
+ const offset = startOffset + chunkStartIndex;
2556
+ out.push({
2557
+ type: "rich_text_list",
2558
+ style: ordered ? "ordered" : "bullet",
2559
+ elements: pendingSections,
2560
+ indent: effectiveIndent,
2561
+ offset: ordered && offset > 0 ? offset : void 0
2562
+ });
2563
+ pendingSections = [];
2564
+ };
2565
+ for (const [index, item] of list.children.entries()) {
2383
2566
  const { section, childLists, hoistedImages: itemImages } = emitListItem(item, ctx);
2384
- sections.push(section);
2567
+ pendingSections.push(section);
2385
2568
  hoistedImages.push(...itemImages);
2569
+ if (childLists.length === 0) {
2570
+ continue;
2571
+ }
2572
+ flush(index);
2386
2573
  for (const child of childLists) {
2387
2574
  const nestedResult = emitListTree(child, effectiveIndent + 1, ctx);
2388
- nested.push(...nestedResult.elements);
2575
+ out.push(...nestedResult.elements);
2389
2576
  hoistedImages.push(...nestedResult.hoistedImages);
2390
2577
  }
2391
2578
  }
2392
- const primary = {
2393
- type: "rich_text_list",
2394
- style: list.ordered === true ? "ordered" : "bullet",
2395
- elements: sections.length > 0 ? sections : [{ type: "rich_text_section", elements: [{ type: "text", text: " " }] }],
2396
- indent: effectiveIndent,
2397
- offset: list.ordered === true && list.start !== null && list.start !== void 0 && list.start > 1 ? list.start - 1 : void 0
2398
- };
2399
- return { elements: [primary, ...nested], hoistedImages };
2579
+ flush(list.children.length - 1);
2580
+ if (out.length === 0) {
2581
+ out.push({
2582
+ type: "rich_text_list",
2583
+ style: ordered ? "ordered" : "bullet",
2584
+ elements: [{ type: "rich_text_section", elements: [{ type: "text", text: " " }] }],
2585
+ indent: effectiveIndent,
2586
+ offset: void 0
2587
+ });
2588
+ }
2589
+ return { elements: out, hoistedImages };
2400
2590
  }
2401
2591
  function collectListItemPhrasing(item) {
2402
2592
  const phrasing = [];
@@ -2489,7 +2679,7 @@ function preformattedMath(value, withLang, ctx) {
2489
2679
  nodeType: "math",
2490
2680
  from: "math",
2491
2681
  to: "rich_text_preformatted",
2492
- reason: "No math renderer configured; emitting TeX source",
2682
+ reason: "Math is emitted as preformatted/code text",
2493
2683
  path: ctx.path
2494
2684
  });
2495
2685
  return preformattedRichText(value, withLang ? "latex" : void 0);
@@ -2713,7 +2903,19 @@ async function tryRenderers(source, ctx) {
2713
2903
  if (!renderer.canRender(req)) {
2714
2904
  continue;
2715
2905
  }
2716
- const result = await renderer.render(req);
2906
+ let result;
2907
+ try {
2908
+ result = await renderer.render(req);
2909
+ } catch (err) {
2910
+ ctx.degradations.add({
2911
+ nodeType: "code.mermaid",
2912
+ from: "image.render",
2913
+ to: "skip",
2914
+ reason: `Renderer failed: ${err instanceof Error ? err.message : String(err)}`,
2915
+ path: ctx.path
2916
+ });
2917
+ continue;
2918
+ }
2717
2919
  if (result === null) {
2718
2920
  continue;
2719
2921
  }
@@ -2745,7 +2947,19 @@ async function tryRenderers(source, ctx) {
2745
2947
  });
2746
2948
  continue;
2747
2949
  }
2748
- const uploaded = await uploader.upload(result);
2950
+ let uploaded;
2951
+ try {
2952
+ uploaded = await uploader.upload(result);
2953
+ } catch (err) {
2954
+ ctx.degradations.add({
2955
+ nodeType: "code.mermaid",
2956
+ from: "image.bytes",
2957
+ to: "skip",
2958
+ reason: `Slack upload failed: ${err instanceof Error ? err.message : String(err)}`,
2959
+ path: ctx.path
2960
+ });
2961
+ continue;
2962
+ }
2749
2963
  return {
2750
2964
  type: "image",
2751
2965
  alt_text: clampImageText(result.altText, ctx, "alt_text"),
@@ -2808,11 +3022,11 @@ function emitParagraph(paragraph, emitCtx, loneImage, hoistImages) {
2808
3022
  const img = loneImage;
2809
3023
  if (isPublicImageUrl(img.url)) {
2810
3024
  const altRaw = img.alt ?? "image";
2811
- const alt = clampImageText(altRaw.length > 0 ? altRaw : "image", emitCtx, "alt_text");
3025
+ const alt2 = clampImageText(altRaw.length > 0 ? altRaw : "image", emitCtx, "alt_text");
2812
3026
  const block = {
2813
3027
  type: "image",
2814
3028
  image_url: img.url,
2815
- alt_text: alt,
3029
+ alt_text: alt2,
2816
3030
  title: img.title !== null && img.title !== void 0 && img.title.length > 0 ? {
2817
3031
  type: "plain_text",
2818
3032
  text: clampImageText(img.title, emitCtx, "title")
@@ -2820,20 +3034,23 @@ function emitParagraph(paragraph, emitCtx, loneImage, hoistImages) {
2820
3034
  };
2821
3035
  return [block];
2822
3036
  }
3037
+ const linkable = isValidLinkUrl(img.url);
2823
3038
  emitCtx.degradations.add({
2824
3039
  nodeType: "image",
2825
3040
  from: "image",
2826
- to: "section.link",
3041
+ to: linkable ? "section.link" : "section.text",
2827
3042
  reason: "Non-public or invalid image URL",
2828
3043
  path: emitCtx.path
2829
3044
  });
2830
- const linkText = `<${img.url}|${img.alt ?? img.url}>`;
3045
+ const alt = img.alt ?? "";
3046
+ const label = alt.length > 0 ? alt : img.url.length > 0 ? img.url : "image";
3047
+ const fallbackText = linkable ? `<${img.url}|${label}>` : img.url.length > 0 && img.url !== label ? `${label} (${img.url})` : label;
2831
3048
  return [
2832
3049
  {
2833
3050
  type: "section",
2834
3051
  text: {
2835
3052
  type: "mrkdwn",
2836
- text: clampSectionText(linkText, emitCtx, "section")
3053
+ text: clampSectionText(fallbackText, emitCtx, "section")
2837
3054
  }
2838
3055
  }
2839
3056
  ];
@@ -2893,14 +3110,15 @@ var TableAdapter = class {
2893
3110
  return node.type === "table";
2894
3111
  }
2895
3112
  plan(node, ctx) {
2896
- const table = node;
3113
+ const table = normalizeTableRows(node, ctx);
2897
3114
  const rowCount = table.children.length;
2898
3115
  const colCount = table.children[0]?.children.length ?? 0;
2899
- const cellChars = countEmittedCellChars(table, ctx);
2900
3116
  if (colCount > TABLE_MAX_COLS) {
3117
+ const cellChars = countEmittedCellChars(table, ctx, "table");
2901
3118
  return markdownThenAscii(table, cellChars, "Table exceeds 20 columns");
2902
3119
  }
2903
3120
  if (rowCount <= TABLE_PREFERRED_MAX_ROWS) {
3121
+ const cellChars = countEmittedCellChars(table, ctx, "table");
2904
3122
  return [
2905
3123
  {
2906
3124
  requires: ["tableBlock"],
@@ -2915,11 +3133,12 @@ var TableAdapter = class {
2915
3133
  const dataRows = Math.max(0, rowCount - 1);
2916
3134
  if (dataRows <= DATA_TABLE_MAX_DATA_ROWS) {
2917
3135
  const preferredTable = truncateRows(table, TABLE_PREFERRED_MAX_ROWS);
2918
- const preferredChars = countEmittedCellChars(preferredTable, ctx);
3136
+ const preferredChars = countEmittedCellChars(preferredTable, ctx, "table");
3137
+ const dataTableChars = countEmittedCellChars(table, ctx, "data_table");
2919
3138
  return [
2920
3139
  {
2921
3140
  requires: ["dataTable"],
2922
- budget: { blocks: 1, dataTableChars: cellChars },
3141
+ budget: { blocks: 1, dataTableChars },
2923
3142
  emit: (emitCtx) => {
2924
3143
  emitCtx.degradations.add({
2925
3144
  nodeType: "table",
@@ -2945,11 +3164,11 @@ var TableAdapter = class {
2945
3164
  return [buildTableBlock(preferredTable, emitCtx)];
2946
3165
  }
2947
3166
  },
2948
- ...markdownThenAscii(table, cellChars, "native table unavailable")
3167
+ ...markdownThenAscii(table, dataTableChars, "native table unavailable")
2949
3168
  ];
2950
3169
  }
2951
3170
  const dataChunk = truncateRows(table, DATA_TABLE_MAX_DATA_ROWS + 1);
2952
- const dataChunkChars = countEmittedCellChars(dataChunk, ctx);
3171
+ const dataChunkChars = countEmittedCellChars(dataChunk, ctx, "data_table");
2953
3172
  return [
2954
3173
  {
2955
3174
  requires: ["dataTable"],
@@ -2965,10 +3184,37 @@ var TableAdapter = class {
2965
3184
  return [buildDataTable(dataChunk, emitCtx)];
2966
3185
  }
2967
3186
  },
2968
- ...markdownThenAscii(table, cellChars, "oversized table")
3187
+ ...markdownThenAscii(table, dataChunkChars, "oversized table")
2969
3188
  ];
2970
3189
  }
2971
3190
  };
3191
+ function normalizeTableRows(table, ctx) {
3192
+ const colCount = table.children[0]?.children.length ?? 0;
3193
+ if (colCount === 0 || table.children.every((row) => row.children.length === colCount)) {
3194
+ return table;
3195
+ }
3196
+ const children = table.children.map((row, rowIndex) => {
3197
+ if (row.children.length === colCount) {
3198
+ return row;
3199
+ }
3200
+ if (row.children.length > colCount) {
3201
+ ctx.degradations.add({
3202
+ nodeType: "table",
3203
+ from: `row[${String(rowIndex)}]: ${String(row.children.length)} cells`,
3204
+ to: `${String(colCount)} cells`,
3205
+ reason: "Cells beyond the header column count dropped (GFM ignores the excess)",
3206
+ path: ctx.path
3207
+ });
3208
+ return { ...row, children: row.children.slice(0, colCount) };
3209
+ }
3210
+ const padding = Array.from(
3211
+ { length: colCount - row.children.length },
3212
+ () => ({ type: "tableCell", children: [] })
3213
+ );
3214
+ return { ...row, children: [...row.children, ...padding] };
3215
+ });
3216
+ return { ...table, children };
3217
+ }
2972
3218
  function markdownThenAscii(table, cellChars, reason) {
2973
3219
  const md = toPipeMarkdown(table);
2974
3220
  return [
@@ -3014,7 +3260,7 @@ function buildTableBlock(table, ctx) {
3014
3260
  }
3015
3261
  function buildDataTable(table, ctx) {
3016
3262
  const captionRaw = ctx.precedingHeading !== void 0 && ctx.precedingHeading.length > 0 ? ctx.precedingHeading : "Table";
3017
- const caption = captionRaw.slice(0, 150);
3263
+ const caption = sliceSurrogateSafe(captionRaw, 150);
3018
3264
  if (captionRaw.length > 150) {
3019
3265
  ctx.degradations.add({
3020
3266
  nodeType: "table",
@@ -3025,12 +3271,7 @@ function buildDataTable(table, ctx) {
3025
3271
  });
3026
3272
  }
3027
3273
  const rows = table.children.map(
3028
- (row, rowIndex) => row.children.map((cell) => {
3029
- if (rowIndex === 0) {
3030
- return { type: "raw_text", text: toString5(cell) };
3031
- }
3032
- return cellToSlack(cell, ctx, false);
3033
- })
3274
+ (row, rowIndex) => row.children.map((cell) => cellForDataTable(cell, ctx, rowIndex))
3034
3275
  );
3035
3276
  return {
3036
3277
  type: "data_table",
@@ -3040,25 +3281,31 @@ function buildDataTable(table, ctx) {
3040
3281
  row_header_column_index: 0
3041
3282
  };
3042
3283
  }
3284
+ function cellForDataTable(cell, ctx, rowIndex) {
3285
+ if (rowIndex === 0) {
3286
+ return { type: "raw_text", text: toString5(cell) };
3287
+ }
3288
+ return cellToSlack(cell, ctx, false);
3289
+ }
3043
3290
  function cellToSlack(cell, ctx, header) {
3044
- const plain = toString5(cell);
3045
- const styled = cell.children.some(
3046
- (c) => c.type === "strong" || c.type === "emphasis" || c.type === "delete" || c.type === "link" || c.type === "inlineCode"
3047
- );
3048
- if (!styled) {
3049
- if (!header && /^-?\d+(\.\d+)?$/.test(plain.trim())) {
3050
- const value = Number(plain.trim());
3051
- return { type: "raw_number", value, text: plain.trim() };
3291
+ const inline = emitPhrasing(cell.children, ctx);
3292
+ const single = inline.elements.length === 1 ? inline.elements[0] : void 0;
3293
+ const isEmpty = inline.elements.length === 0;
3294
+ const isSingleUnstyledText = single?.type === "text" && single.style === void 0;
3295
+ const plainText = isEmpty ? toString5(cell) : isSingleUnstyledText ? single.text : void 0;
3296
+ if (plainText !== void 0) {
3297
+ const trimmed = plainText.trim();
3298
+ if (!header && /^-?\d+(\.\d+)?$/.test(trimmed)) {
3299
+ return { type: "raw_number", value: Number(trimmed), text: trimmed };
3052
3300
  }
3053
- return { type: "raw_text", text: plain };
3301
+ return { type: "raw_text", text: plainText };
3054
3302
  }
3055
- const inline = emitPhrasing(cell.children, ctx);
3056
3303
  const rich = {
3057
3304
  type: "rich_text",
3058
3305
  elements: [
3059
3306
  {
3060
3307
  type: "rich_text_section",
3061
- elements: inline.elements.length > 0 ? inline.elements : [{ type: "text", text: plain }]
3308
+ elements: inline.elements
3062
3309
  }
3063
3310
  ]
3064
3311
  };
@@ -3085,12 +3332,13 @@ function columnSettings(table) {
3085
3332
  }
3086
3333
  return settings;
3087
3334
  }
3088
- function countEmittedCellChars(table, ctx) {
3335
+ function countEmittedCellChars(table, ctx, mode) {
3089
3336
  const probe = ctx.withDegradations(new RecordingDegradationCollector());
3090
3337
  let n = 0;
3091
3338
  for (const [rowIndex, row] of table.children.entries()) {
3092
3339
  for (const cell of row.children) {
3093
- n += slackCellCharCount(cellToSlack(cell, probe, rowIndex === 0));
3340
+ const emitted = mode === "data_table" ? cellForDataTable(cell, probe, rowIndex) : cellToSlack(cell, probe, rowIndex === 0);
3341
+ n += slackCellCharCount(emitted);
3094
3342
  }
3095
3343
  }
3096
3344
  return n;
@@ -3213,27 +3461,50 @@ function emitUnicodeTaskTree(list, indent, ctx) {
3213
3461
  });
3214
3462
  effectiveIndent = RICH_TEXT_LIST_INDENT_MAX;
3215
3463
  }
3216
- const sections = [];
3217
- const nested = [];
3464
+ const out = [];
3465
+ let pendingSections = [];
3466
+ const flush = () => {
3467
+ if (pendingSections.length === 0) {
3468
+ return;
3469
+ }
3470
+ out.push({
3471
+ type: "rich_text_list",
3472
+ style: "bullet",
3473
+ elements: pendingSections,
3474
+ indent: effectiveIndent
3475
+ });
3476
+ pendingSections = [];
3477
+ };
3218
3478
  for (const item of list.children) {
3219
3479
  const prefix = item.checked === true ? "\u2611 " : item.checked === false ? "\u2610 " : "";
3220
- sections.push({
3480
+ pendingSections.push({
3221
3481
  type: "rich_text_section",
3222
3482
  elements: [{ type: "text", text: `${prefix}${itemText(item)}` }]
3223
3483
  });
3484
+ const childLists = [];
3224
3485
  for (const child of item.children) {
3225
3486
  if (child.type === "list") {
3226
- nested.push(...emitUnicodeTaskTree(child, effectiveIndent + 1, ctx));
3487
+ childLists.push(child);
3227
3488
  }
3228
3489
  }
3490
+ if (childLists.length === 0) {
3491
+ continue;
3492
+ }
3493
+ flush();
3494
+ for (const child of childLists) {
3495
+ out.push(...emitUnicodeTaskTree(child, effectiveIndent + 1, ctx));
3496
+ }
3229
3497
  }
3230
- const primary = {
3231
- type: "rich_text_list",
3232
- style: "bullet",
3233
- elements: sections.length > 0 ? sections : [{ type: "rich_text_section", elements: [{ type: "text", text: " " }] }],
3234
- indent: effectiveIndent
3235
- };
3236
- return [primary, ...nested];
3498
+ flush();
3499
+ if (out.length === 0) {
3500
+ out.push({
3501
+ type: "rich_text_list",
3502
+ style: "bullet",
3503
+ elements: [{ type: "rich_text_section", elements: [{ type: "text", text: " " }] }],
3504
+ indent: effectiveIndent
3505
+ });
3506
+ }
3507
+ return out;
3237
3508
  }
3238
3509
 
3239
3510
  // src/adapters/thematic-break-adapter.ts
@@ -3308,117 +3579,37 @@ function validateBlocks(blocks) {
3308
3579
  message: `Too many blocks: ${String(blocks.length)} > ${String(MAX_BLOCKS_PER_MESSAGE)}`
3309
3580
  });
3310
3581
  }
3311
- let markdownChars = 0;
3312
- let tableChars = 0;
3313
- let dataTableChars = 0;
3314
- let charts = 0;
3582
+ const counters = {
3583
+ markdownChars: 0,
3584
+ tableChars: 0,
3585
+ dataTableChars: 0,
3586
+ charts: 0
3587
+ };
3315
3588
  blocks.forEach((block, i) => {
3316
- const path = `blocks[${String(i)}]`;
3317
- switch (block.type) {
3318
- case "header":
3319
- if (block.text.text.length > HEADER_TEXT_MAX_CHARS) {
3320
- issues.push({ path, message: "header text > 150" });
3321
- }
3322
- if (block.level !== void 0 && (block.level < 1 || block.level > 4)) {
3323
- issues.push({ path, message: "header level out of 1\u20134" });
3324
- }
3325
- break;
3326
- case "section":
3327
- if (block.text !== void 0 && block.text.text.length > SECTION_TEXT_MAX_CHARS) {
3328
- issues.push({ path, message: "section text > 3000" });
3329
- }
3330
- break;
3331
- case "image":
3332
- if (block.alt_text.length > IMAGE_ALT_MAX_CHARS) {
3333
- issues.push({ path, message: "image alt > 2000" });
3334
- }
3335
- if (block.image_url !== void 0 && block.image_url.length > IMAGE_URL_MAX_CHARS) {
3336
- issues.push({ path, message: "image_url > 3000" });
3337
- }
3338
- break;
3339
- case "context":
3340
- if (block.elements.length > CONTEXT_ELEMENTS_MAX) {
3341
- issues.push({ path, message: "context elements > 10" });
3342
- }
3343
- break;
3344
- case "markdown":
3345
- markdownChars += block.text.length;
3346
- break;
3347
- case "table": {
3348
- if (block.rows.length > TABLE_MAX_ROWS) {
3349
- issues.push({ path, message: "table rows > 100" });
3350
- }
3351
- for (const row of block.rows) {
3352
- if (row.length > TABLE_MAX_COLS) {
3353
- issues.push({ path, message: "table cols > 20" });
3354
- }
3355
- for (const cell of row) {
3356
- tableChars += cellCharCount(cell);
3357
- }
3358
- }
3359
- break;
3360
- }
3361
- case "data_table": {
3362
- if (block.rows.length > DATA_TABLE_MAX_DATA_ROWS + 1) {
3363
- issues.push({ path, message: "data_table rows > 201" });
3364
- }
3365
- for (const row of block.rows) {
3366
- if (row.length > TABLE_MAX_COLS) {
3367
- issues.push({ path, message: "data_table cols > 20" });
3368
- }
3369
- for (const cell of row) {
3370
- dataTableChars += cellCharCount(cell);
3371
- }
3372
- }
3373
- break;
3374
- }
3375
- case "data_visualization":
3376
- charts += 1;
3377
- if (block.title.length > CHART_TITLE_MAX_CHARS) {
3378
- issues.push({ path, message: "chart title > 50" });
3379
- }
3380
- validateChart(block, path, issues);
3381
- break;
3382
- case "container":
3383
- if (block.child_blocks.length > CONTAINER_CHILDREN_MAX) {
3384
- issues.push({ path, message: "container children > 10" });
3385
- }
3386
- if (block.title !== void 0 && block.title.text.length > CONTAINER_TITLE_MAX_CHARS) {
3387
- issues.push({ path, message: "container title > 150" });
3388
- }
3389
- break;
3390
- case "divider":
3391
- case "rich_text":
3392
- break;
3393
- default: {
3394
- const exhaustive = block;
3395
- void exhaustive;
3396
- break;
3397
- }
3398
- }
3589
+ validateBlock(block, `blocks[${String(i)}]`, issues, counters, 0);
3399
3590
  });
3400
- if (markdownChars > MARKDOWN_CUMULATIVE_MAX_CHARS) {
3591
+ if (counters.markdownChars > MARKDOWN_CUMULATIVE_MAX_CHARS) {
3401
3592
  issues.push({
3402
3593
  path: "$",
3403
- message: `markdown cumulative ${String(markdownChars)} > 12000`
3594
+ message: `markdown cumulative ${String(counters.markdownChars)} > 12000`
3404
3595
  });
3405
3596
  }
3406
- if (tableChars > TABLE_CELL_CHARS_MAX) {
3597
+ if (counters.tableChars > TABLE_CELL_CHARS_MAX) {
3407
3598
  issues.push({
3408
3599
  path: "$",
3409
- message: `table chars ${String(tableChars)} > 10000`
3600
+ message: `table chars ${String(counters.tableChars)} > 10000`
3410
3601
  });
3411
3602
  }
3412
- if (dataTableChars > DATA_TABLE_CELL_CHARS_MAX) {
3603
+ if (counters.dataTableChars > DATA_TABLE_CELL_CHARS_MAX) {
3413
3604
  issues.push({
3414
3605
  path: "$",
3415
- message: `data_table chars ${String(dataTableChars)} > 20000`
3606
+ message: `data_table chars ${String(counters.dataTableChars)} > 20000`
3416
3607
  });
3417
3608
  }
3418
- if (charts > DATA_VISUALIZATION_MAX_PER_MESSAGE) {
3609
+ if (counters.charts > DATA_VISUALIZATION_MAX_PER_MESSAGE) {
3419
3610
  issues.push({
3420
3611
  path: "$",
3421
- message: `charts ${String(charts)} > ${String(DATA_VISUALIZATION_MAX_PER_MESSAGE)}`
3612
+ message: `charts ${String(counters.charts)} > ${String(DATA_VISUALIZATION_MAX_PER_MESSAGE)}`
3422
3613
  });
3423
3614
  }
3424
3615
  if (issues.length > 0) {
@@ -3427,6 +3618,158 @@ function validateBlocks(blocks) {
3427
3618
  }
3428
3619
  return blocks;
3429
3620
  }
3621
+ function validateBlock(block, path, issues, counters, depth) {
3622
+ if (depth > MAX_BLOCKS_PER_MESSAGE) {
3623
+ issues.push({ path, message: "container nesting too deep" });
3624
+ return;
3625
+ }
3626
+ switch (block.type) {
3627
+ case "header":
3628
+ if (block.text.text.length > HEADER_TEXT_MAX_CHARS) {
3629
+ issues.push({ path, message: "header text > 150" });
3630
+ }
3631
+ if (block.level !== void 0 && (block.level < 1 || block.level > 4)) {
3632
+ issues.push({ path, message: "header level out of 1\u20134" });
3633
+ }
3634
+ break;
3635
+ case "section":
3636
+ if (block.text !== void 0 && block.text.text.length > SECTION_TEXT_MAX_CHARS) {
3637
+ issues.push({ path, message: "section text > 3000" });
3638
+ }
3639
+ break;
3640
+ case "image":
3641
+ if (block.alt_text.length > IMAGE_ALT_MAX_CHARS) {
3642
+ issues.push({ path, message: "image alt > 2000" });
3643
+ }
3644
+ if (block.image_url !== void 0 && block.image_url.length > IMAGE_URL_MAX_CHARS) {
3645
+ issues.push({ path, message: "image_url > 3000" });
3646
+ }
3647
+ break;
3648
+ case "context":
3649
+ if (block.elements.length === 0) {
3650
+ issues.push({ path, message: "context elements empty" });
3651
+ } else if (block.elements.length > CONTEXT_ELEMENTS_MAX) {
3652
+ issues.push({ path, message: "context elements > 10" });
3653
+ }
3654
+ break;
3655
+ case "markdown":
3656
+ counters.markdownChars += block.text.length;
3657
+ break;
3658
+ case "table":
3659
+ if (block.rows.length > TABLE_MAX_ROWS) {
3660
+ issues.push({ path, message: "table rows > 100" });
3661
+ }
3662
+ counters.tableChars += validateTableCells(block.rows, path, "table cols > 20", issues);
3663
+ break;
3664
+ case "data_table":
3665
+ if (block.rows.length > DATA_TABLE_MAX_DATA_ROWS + 1) {
3666
+ issues.push({ path, message: "data_table rows > 201" });
3667
+ }
3668
+ counters.dataTableChars += validateTableCells(
3669
+ block.rows,
3670
+ path,
3671
+ "data_table cols > 20",
3672
+ issues
3673
+ );
3674
+ break;
3675
+ case "data_visualization":
3676
+ counters.charts += 1;
3677
+ if (block.title.length > CHART_TITLE_MAX_CHARS) {
3678
+ issues.push({ path, message: "chart title > 50" });
3679
+ }
3680
+ validateChart(block, path, issues);
3681
+ break;
3682
+ case "container":
3683
+ if (block.child_blocks.length > CONTAINER_CHILDREN_MAX) {
3684
+ issues.push({ path, message: "container children > 10" });
3685
+ }
3686
+ if (block.title !== void 0 && block.title.text.length > CONTAINER_TITLE_MAX_CHARS) {
3687
+ issues.push({ path, message: "container title > 150" });
3688
+ }
3689
+ if (block.rich_text_title !== void 0) {
3690
+ validateRichTextBlock(block.rich_text_title, `${path}.rich_text_title`, issues);
3691
+ }
3692
+ for (const [i, child] of block.child_blocks.entries()) {
3693
+ validateBlock(child, `${path}.child_blocks[${String(i)}]`, issues, counters, depth + 1);
3694
+ }
3695
+ break;
3696
+ case "divider":
3697
+ break;
3698
+ case "rich_text":
3699
+ validateRichTextBlock(block, path, issues);
3700
+ break;
3701
+ default: {
3702
+ const exhaustive = block;
3703
+ void exhaustive;
3704
+ break;
3705
+ }
3706
+ }
3707
+ }
3708
+ function validateRichTextBlock(block, path, issues) {
3709
+ if (block.elements.length === 0) {
3710
+ issues.push({ path, message: "rich_text elements empty" });
3711
+ return;
3712
+ }
3713
+ for (const [i, element] of block.elements.entries()) {
3714
+ validateRichTextElement(element, `${path}.elements[${String(i)}]`, issues);
3715
+ }
3716
+ }
3717
+ function validateRichTextElement(element, path, issues) {
3718
+ switch (element.type) {
3719
+ case "rich_text_section":
3720
+ case "rich_text_quote":
3721
+ case "rich_text_preformatted":
3722
+ if (element.elements.length === 0) {
3723
+ issues.push({ path, message: `${element.type} elements empty` });
3724
+ return;
3725
+ }
3726
+ for (const [i, inline] of element.elements.entries()) {
3727
+ if (inline.type === "link" && !isValidRichTextLinkUrl(inline.url)) {
3728
+ issues.push({
3729
+ path: `${path}.elements[${String(i)}]`,
3730
+ message: "invalid rich text link URL"
3731
+ });
3732
+ }
3733
+ }
3734
+ break;
3735
+ case "rich_text_list":
3736
+ if (element.elements.length === 0) {
3737
+ issues.push({ path, message: "rich_text_list elements empty" });
3738
+ return;
3739
+ }
3740
+ for (const [i, section] of element.elements.entries()) {
3741
+ validateRichTextElement(section, `${path}.elements[${String(i)}]`, issues);
3742
+ }
3743
+ break;
3744
+ default: {
3745
+ const exhaustive = element;
3746
+ void exhaustive;
3747
+ break;
3748
+ }
3749
+ }
3750
+ }
3751
+ function validateTableCells(rows, path, colsMessage, issues) {
3752
+ let chars = 0;
3753
+ for (const [r, row] of rows.entries()) {
3754
+ if (row.length > TABLE_MAX_COLS) {
3755
+ issues.push({ path, message: colsMessage });
3756
+ }
3757
+ for (const [c, cell] of row.entries()) {
3758
+ chars += cellCharCount(cell);
3759
+ if (cell.type === "rich_text") {
3760
+ validateRichTextBlock(cell, `${path}.rows[${String(r)}][${String(c)}]`, issues);
3761
+ }
3762
+ }
3763
+ }
3764
+ return chars;
3765
+ }
3766
+ var HTTP_OR_MAILTO_URL_RE = /^(https?:|mailto:)/i;
3767
+ function isValidRichTextLinkUrl(url) {
3768
+ if (url.length === 0 || url.length > IMAGE_URL_MAX_CHARS) {
3769
+ return false;
3770
+ }
3771
+ return HTTP_OR_MAILTO_URL_RE.test(url);
3772
+ }
3430
3773
  function cellCharCount(cell) {
3431
3774
  if (cell.type === "raw_text") {
3432
3775
  return cell.text.length;