slackmark 0.2.0 → 0.4.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",
@@ -59,12 +68,13 @@ function resolveCapabilities(input, overrides) {
59
68
  if (input === void 0) {
60
69
  base = CAPABILITY_PRESETS.latest;
61
70
  } else if (typeof input === "string") {
62
- const preset = CAPABILITY_PRESETS[input];
71
+ const preset = isPresetName(input) ? CAPABILITY_PRESETS[input] : void 0;
63
72
  if (preset === void 0) {
64
- base = CAPABILITY_PRESETS.latest;
65
- } else {
66
- base = preset;
73
+ throw new ConfigurationError(
74
+ `Unknown capability preset ${JSON.stringify(input)}; use one of: ${Object.keys(CAPABILITY_PRESETS).join(", ")}.`
75
+ );
67
76
  }
77
+ base = preset;
68
78
  } else if (isFullFlags(input)) {
69
79
  base = input;
70
80
  } else {
@@ -75,6 +85,9 @@ function resolveCapabilities(input, overrides) {
75
85
  }
76
86
  return Object.freeze({ ...base, ...overrides });
77
87
  }
88
+ function isPresetName(value) {
89
+ return Object.hasOwn(CAPABILITY_PRESETS, value);
90
+ }
78
91
  function isFullFlags(value) {
79
92
  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";
80
93
  }
@@ -127,48 +140,6 @@ var StandardBudgetLedger = class {
127
140
  this.charts = 0;
128
141
  this.totalChars = 0;
129
142
  }
130
- tryReserveBlocks(count) {
131
- if (this.blocks + count > this.maxBlocks) {
132
- return false;
133
- }
134
- this.blocks += count;
135
- return true;
136
- }
137
- tryReserveTableChars(chars) {
138
- if (this.tableChars + chars > TABLE_CELL_CHARS_MAX) {
139
- return false;
140
- }
141
- this.tableChars += chars;
142
- return true;
143
- }
144
- tryReserveDataTableChars(chars) {
145
- if (this.dataTableChars + chars > DATA_TABLE_CELL_CHARS_MAX) {
146
- return false;
147
- }
148
- this.dataTableChars += chars;
149
- return true;
150
- }
151
- tryReserveMarkdownChars(chars) {
152
- if (this.markdownChars + chars > MARKDOWN_CUMULATIVE_MAX_CHARS) {
153
- return false;
154
- }
155
- this.markdownChars += chars;
156
- return true;
157
- }
158
- tryReserveChart() {
159
- if (this.enforceChartCap && this.charts + 1 > DATA_VISUALIZATION_MAX_PER_MESSAGE) {
160
- return false;
161
- }
162
- this.charts += 1;
163
- return true;
164
- }
165
- tryReserveTotalChars(chars) {
166
- if (this.totalChars + chars > MESSAGE_TEXT_SAFETY_MAX_CHARS) {
167
- return false;
168
- }
169
- this.totalChars += chars;
170
- return true;
171
- }
172
143
  tryReserveClaim(claim) {
173
144
  if (claim === void 0) {
174
145
  return true;
@@ -193,9 +164,6 @@ var StandardBudgetLedger = class {
193
164
  this.totalChars = nextTotal;
194
165
  return true;
195
166
  }
196
- remainingBlocks() {
197
- return this.maxBlocks - this.blocks;
198
- }
199
167
  snapshot() {
200
168
  return {
201
169
  blocks: this.blocks,
@@ -258,30 +226,6 @@ var ConversionContext = class _ConversionContext {
258
226
  this.precedingHeading = deps.precedingHeading;
259
227
  this.path = deps.path;
260
228
  }
261
- withPath(segment) {
262
- return new _ConversionContext({
263
- config: this.config,
264
- budget: this.budget,
265
- degradations: this.degradations,
266
- definitions: this.definitions,
267
- footnotes: this.footnotes,
268
- footnoteOrder: this.footnoteOrder,
269
- precedingHeading: this.precedingHeading,
270
- path: this.path.length === 0 ? segment : `${this.path}/${segment}`
271
- });
272
- }
273
- withPrecedingHeading(heading) {
274
- return new _ConversionContext({
275
- config: this.config,
276
- budget: this.budget,
277
- degradations: this.degradations,
278
- definitions: this.definitions,
279
- footnotes: this.footnotes,
280
- footnoteOrder: this.footnoteOrder,
281
- precedingHeading: heading,
282
- path: this.path
283
- });
284
- }
285
229
  withDegradations(degradations) {
286
230
  return new _ConversionContext({
287
231
  config: this.config,
@@ -367,6 +311,29 @@ function assertNever(value, message) {
367
311
  throw new Error(`${detail}: ${JSON.stringify(value)}`);
368
312
  }
369
313
 
314
+ // src/utf16.ts
315
+ var HIGH_SURROGATE_MIN = 55296;
316
+ var HIGH_SURROGATE_MAX = 56319;
317
+ var LOW_SURROGATE_MIN = 56320;
318
+ var LOW_SURROGATE_MAX = 57343;
319
+ function surrogateSafeEnd(text, end) {
320
+ if (end <= 0) {
321
+ return 0;
322
+ }
323
+ if (end >= text.length) {
324
+ return text.length;
325
+ }
326
+ const before = text.charCodeAt(end - 1);
327
+ const after = text.charCodeAt(end);
328
+ if (before >= HIGH_SURROGATE_MIN && before <= HIGH_SURROGATE_MAX && after >= LOW_SURROGATE_MIN && after <= LOW_SURROGATE_MAX) {
329
+ return end - 1;
330
+ }
331
+ return end;
332
+ }
333
+ function sliceSurrogateSafe(text, max) {
334
+ return text.slice(0, surrogateSafeEnd(text, max));
335
+ }
336
+
370
337
  // src/types.ts
371
338
  var OverflowMode = {
372
339
  Degrade: "degrade",
@@ -467,7 +434,7 @@ var MessageAssembler = class {
467
434
  reason: "Notification text truncated to 40k safety limit",
468
435
  path: ctx.path
469
436
  });
470
- return { blocks, text: raw.slice(0, MESSAGE_TEXT_SAFETY_MAX_CHARS) };
437
+ return { blocks, text: sliceSurrogateSafe(raw, MESSAGE_TEXT_SAFETY_MAX_CHARS) };
471
438
  }
472
439
  };
473
440
  function buildFootnoteBlocks(entries, degradations) {
@@ -486,7 +453,7 @@ function buildFootnoteBlocks(entries, degradations) {
486
453
  reason: "Footnote context text truncated to 3000 chars",
487
454
  path: "footnote"
488
455
  });
489
- elements.push({ type: "mrkdwn", text: note.slice(0, SECTION_TEXT_MAX_CHARS) });
456
+ elements.push({ type: "mrkdwn", text: sliceSurrogateSafe(note, SECTION_TEXT_MAX_CHARS) });
490
457
  } else {
491
458
  elements.push({ type: "mrkdwn", text: note });
492
459
  }
@@ -684,12 +651,116 @@ function inlinesToPlain(elements) {
684
651
  case "channel":
685
652
  parts.push(`#${el.channel_id}`);
686
653
  break;
654
+ case "usergroup":
655
+ parts.push(`@${el.usergroup_id}`);
656
+ break;
657
+ case "broadcast":
658
+ parts.push(`@${el.range}`);
659
+ break;
660
+ case "date":
661
+ parts.push(el.fallback ?? formatDateTokensUtc(el.timestamp, el.format));
662
+ break;
687
663
  default:
688
664
  break;
689
665
  }
690
666
  }
691
667
  return parts.join("");
692
668
  }
669
+ var DATE_TOKEN_RE = /\{([a-z_]+)\}/g;
670
+ var MONTHS = [
671
+ "January",
672
+ "February",
673
+ "March",
674
+ "April",
675
+ "May",
676
+ "June",
677
+ "July",
678
+ "August",
679
+ "September",
680
+ "October",
681
+ "November",
682
+ "December"
683
+ ];
684
+ var WEEKDAYS = [
685
+ "Sunday",
686
+ "Monday",
687
+ "Tuesday",
688
+ "Wednesday",
689
+ "Thursday",
690
+ "Friday",
691
+ "Saturday"
692
+ ];
693
+ function pad2(value) {
694
+ return String(value).padStart(2, "0");
695
+ }
696
+ function ordinal(value) {
697
+ const remainder = value % 100;
698
+ if (remainder >= 11 && remainder <= 13) {
699
+ return `${String(value)}th`;
700
+ }
701
+ switch (value % 10) {
702
+ case 1:
703
+ return `${String(value)}st`;
704
+ case 2:
705
+ return `${String(value)}nd`;
706
+ case 3:
707
+ return `${String(value)}rd`;
708
+ default:
709
+ return `${String(value)}th`;
710
+ }
711
+ }
712
+ function dateToken(token, date) {
713
+ const day = date.getUTCDate();
714
+ const month = MONTHS[date.getUTCMonth()] ?? "";
715
+ const shortMonth = month.slice(0, 3);
716
+ const weekday = WEEKDAYS[date.getUTCDay()] ?? "";
717
+ const year = date.getUTCFullYear();
718
+ switch (token) {
719
+ case "date_num":
720
+ return `${String(year)}-${pad2(date.getUTCMonth() + 1)}-${pad2(day)}`;
721
+ // Slack renders {date_slash} month-first: 02/18/2014 (docs example for
722
+ // timestamp 1392734382).
723
+ case "date_slash":
724
+ return `${pad2(date.getUTCMonth() + 1)}/${pad2(day)}/${String(year)}`;
725
+ case "date_long":
726
+ case "date_long_pretty":
727
+ return `${weekday}, ${month} ${ordinal(day)}, ${String(year)}`;
728
+ case "date_long_full":
729
+ return `${month} ${String(day)}, ${String(year)}`;
730
+ // {date} renders "February 18th, 2014" (ordinal + year), not "February 18".
731
+ case "date":
732
+ case "date_pretty":
733
+ return `${month} ${ordinal(day)}, ${String(year)}`;
734
+ case "date_short":
735
+ case "date_short_pretty":
736
+ return `${shortMonth} ${String(day)}, ${String(year)}`;
737
+ case "time":
738
+ return `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}`;
739
+ case "time_secs":
740
+ return `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`;
741
+ default:
742
+ return void 0;
743
+ }
744
+ }
745
+ function formatDateTokensUtc(timestamp, format) {
746
+ const date = new Date(timestamp * 1e3);
747
+ if (!Number.isFinite(timestamp) || Number.isNaN(date.getTime())) {
748
+ return String(timestamp);
749
+ }
750
+ let unsupported = false;
751
+ const formatted = format.replace(DATE_TOKEN_RE, (_match, token) => {
752
+ const replacement = dateToken(token, date);
753
+ if (replacement === void 0) {
754
+ unsupported = true;
755
+ return "";
756
+ }
757
+ return replacement;
758
+ });
759
+ if (format.length === 0 || unsupported) {
760
+ return date.toISOString().slice(0, 10);
761
+ }
762
+ return formatted;
763
+ }
693
764
  function tableToPlain(rows) {
694
765
  const lines = [];
695
766
  for (const row of rows) {
@@ -737,18 +808,12 @@ var RemarkMarkdownParser = class {
737
808
  // src/mermaid/mermaid-pie-parser.ts
738
809
  var COMMENT_RE = /%%[^\n]{0,500}/g;
739
810
  var TITLE_RE = /^\s*pie\b(?:\s+showData)?(?:\s+title\s+(.+))?\s*$/i;
740
- var SHOW_DATA_RE = /\bshowData\b/i;
741
811
  var ENTRY_RE = /^\s*"([^"]{1,200})"\s*:\s*(-?\d+(?:\.\d+)?)\s*$/;
742
812
  var MermaidPieParser = class {
743
- diagramType;
744
- constructor() {
745
- this.diagramType = "pie";
746
- }
747
813
  parse(source) {
748
814
  const cleaned = source.replace(COMMENT_RE, "");
749
815
  const lines = cleaned.split(/\r?\n/);
750
816
  let title;
751
- let showData = false;
752
817
  const segments = [];
753
818
  let seenHeader = false;
754
819
  for (const raw of lines) {
@@ -761,7 +826,6 @@ var MermaidPieParser = class {
761
826
  return { ok: false, reason: "Not a pie diagram" };
762
827
  }
763
828
  seenHeader = true;
764
- showData = SHOW_DATA_RE.test(line);
765
829
  const tm = TITLE_RE.exec(line);
766
830
  const titlePart = tm?.[1]?.trim();
767
831
  if (titlePart !== void 0 && titlePart.length > 0) {
@@ -794,7 +858,6 @@ var MermaidPieParser = class {
794
858
  const ast = {
795
859
  kind: "pie",
796
860
  title,
797
- showData,
798
861
  segments
799
862
  };
800
863
  return { ok: true, value: ast };
@@ -807,10 +870,6 @@ var NUM = String.raw`-?\d+(?:\.\d+)?`;
807
870
  var NUM_LIST_RE = new RegExp(String.raw`\[\s*((?:${NUM}\s*,\s*)*${NUM})\s*\]`);
808
871
  var STR_LIST_RE = /\[\s*((?:"[^"]{0,200}"\s*,\s*)*"[^"]{0,200}")\s*\]/;
809
872
  var MermaidXyChartParser = class {
810
- diagramType;
811
- constructor() {
812
- this.diagramType = "xychart-beta";
813
- }
814
873
  parse(source) {
815
874
  const cleaned = source.replace(COMMENT_RE2, "");
816
875
  const lines = cleaned.split(/\r?\n/);
@@ -818,8 +877,6 @@ var MermaidXyChartParser = class {
818
877
  let categories = [];
819
878
  let xLabel;
820
879
  let yLabel;
821
- let yMin;
822
- let yMax;
823
880
  const bars = [];
824
881
  const linesSeries = [];
825
882
  let seenHeader = false;
@@ -883,10 +940,6 @@ var MermaidXyChartParser = class {
883
940
  ).exec(line);
884
941
  if (ym !== null) {
885
942
  yLabel = ym[1];
886
- if (ym[2] !== void 0 && ym[3] !== void 0) {
887
- yMin = Number(ym[2]);
888
- yMax = Number(ym[3]);
889
- }
890
943
  continue;
891
944
  }
892
945
  return { ok: false, reason: `Invalid y-axis: ${line}` };
@@ -924,8 +977,6 @@ var MermaidXyChartParser = class {
924
977
  categories,
925
978
  xLabel,
926
979
  yLabel,
927
- yMin,
928
- yMax,
929
980
  bars,
930
981
  lines: linesSeries
931
982
  };
@@ -949,15 +1000,6 @@ function parseNumberList(inner) {
949
1000
  // src/converter/slackmark-converter.ts
950
1001
  import { toString } from "mdast-util-to-string";
951
1002
 
952
- // src/errors.ts
953
- var ConfigurationError = class extends Error {
954
- name;
955
- constructor(message, options) {
956
- super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
957
- this.name = "ConfigurationError";
958
- }
959
- };
960
-
961
1003
  // src/converter/resolve-config.ts
962
1004
  function resolveConfig(defaults, overrides) {
963
1005
  const merged = {
@@ -1047,7 +1089,7 @@ var SlackMarkConverter = class {
1047
1089
  enableMath: config.enableMath,
1048
1090
  enableFrontmatter: config.enableFrontmatter
1049
1091
  });
1050
- const { definitions, footnotes, footnoteOrder } = collectRefs(tree);
1092
+ const { definitions, footnotes, footnoteOrder } = collectRefs(tree, degradations);
1051
1093
  let precedingHeading;
1052
1094
  const emitted = [];
1053
1095
  let index = 0;
@@ -1072,11 +1114,13 @@ var SlackMarkConverter = class {
1072
1114
  emitted.push(...blocks);
1073
1115
  index += 1;
1074
1116
  }
1117
+ const footnoteNumbers = /* @__PURE__ */ new Map();
1118
+ footnoteOrder.forEach((id, i) => footnoteNumbers.set(id, i + 1));
1075
1119
  const footnoteEntries = footnoteOrder.map((id, i) => {
1076
1120
  const def = footnotes.get(id);
1077
1121
  return {
1078
1122
  n: i + 1,
1079
- text: def !== void 0 ? toString(def).trim() : id
1123
+ text: def !== void 0 ? footnoteBodyText(def, footnoteNumbers).trim() : id
1080
1124
  };
1081
1125
  });
1082
1126
  const footnoteBlocks = buildFootnoteBlocks(footnoteEntries, degradations);
@@ -1185,7 +1229,7 @@ function safeNotificationText(plainText, markdown, degradations) {
1185
1229
  reason: "Notification text truncated to 40k safety limit",
1186
1230
  path: "root"
1187
1231
  });
1188
- return raw.slice(0, MESSAGE_TEXT_SAFETY_MAX_CHARS);
1232
+ return sliceSurrogateSafe(raw, MESSAGE_TEXT_SAFETY_MAX_CHARS);
1189
1233
  }
1190
1234
  function clampSectionFallback(text, degradations) {
1191
1235
  if (text.length <= SECTION_TEXT_MAX_CHARS) {
@@ -1198,17 +1242,38 @@ function clampSectionFallback(text, degradations) {
1198
1242
  reason: "Section text truncated to 3000 chars",
1199
1243
  path: "root"
1200
1244
  });
1201
- return text.slice(0, SECTION_TEXT_MAX_CHARS);
1245
+ return sliceSurrogateSafe(text, SECTION_TEXT_MAX_CHARS);
1202
1246
  }
1203
- function collectRefs(tree) {
1247
+ function collectRefs(tree, degradations) {
1204
1248
  const definitions = /* @__PURE__ */ new Map();
1205
1249
  const footnotes = /* @__PURE__ */ new Map();
1206
1250
  for (const node of tree.children) {
1207
1251
  if (node.type === "definition") {
1208
- definitions.set(node.identifier.toLowerCase(), node);
1252
+ const key = node.identifier.toLowerCase();
1253
+ if (definitions.has(key)) {
1254
+ degradations.add({
1255
+ nodeType: "definition",
1256
+ from: node.identifier,
1257
+ to: "ignored",
1258
+ reason: "Duplicate link reference definition; first definition wins",
1259
+ path: "root"
1260
+ });
1261
+ } else {
1262
+ definitions.set(key, node);
1263
+ }
1209
1264
  }
1210
1265
  if (node.type === "footnoteDefinition") {
1211
- footnotes.set(node.identifier, node);
1266
+ if (footnotes.has(node.identifier)) {
1267
+ degradations.add({
1268
+ nodeType: "footnoteDefinition",
1269
+ from: node.identifier,
1270
+ to: "ignored",
1271
+ reason: "Duplicate footnote definition; first definition wins",
1272
+ path: "root"
1273
+ });
1274
+ } else {
1275
+ footnotes.set(node.identifier, node);
1276
+ }
1212
1277
  }
1213
1278
  }
1214
1279
  const footnoteOrder = [];
@@ -1219,8 +1284,46 @@ function collectRefs(tree) {
1219
1284
  footnoteOrder.push(id);
1220
1285
  }
1221
1286
  });
1287
+ for (let i = 0; i < footnoteOrder.length; i += 1) {
1288
+ const def = footnotes.get(footnoteOrder[i] ?? "");
1289
+ if (def === void 0) {
1290
+ continue;
1291
+ }
1292
+ for (const child of def.children) {
1293
+ walkFootnoteReferences(child, (id) => {
1294
+ if (!seen.has(id)) {
1295
+ seen.add(id);
1296
+ footnoteOrder.push(id);
1297
+ }
1298
+ });
1299
+ }
1300
+ }
1222
1301
  return { definitions, footnotes, footnoteOrder };
1223
1302
  }
1303
+ function footnoteBodyText(node, numbers) {
1304
+ if (node === null || typeof node !== "object") {
1305
+ return "";
1306
+ }
1307
+ const record = node;
1308
+ if (record.type === "footnoteReference" && typeof record.identifier === "string") {
1309
+ const n = numbers.get(record.identifier);
1310
+ return n !== void 0 ? `[${String(n)}]` : `[^${record.identifier}]`;
1311
+ }
1312
+ if (typeof record.value === "string") {
1313
+ return record.value;
1314
+ }
1315
+ if (typeof record.alt === "string") {
1316
+ return record.alt;
1317
+ }
1318
+ if (record.children !== void 0) {
1319
+ const parts = [];
1320
+ for (const child of record.children) {
1321
+ parts.push(footnoteBodyText(child, numbers));
1322
+ }
1323
+ return parts.join("");
1324
+ }
1325
+ return "";
1326
+ }
1224
1327
  function walkFootnoteReferences(node, onRef) {
1225
1328
  if (node === null || typeof node !== "object") {
1226
1329
  return;
@@ -1244,8 +1347,11 @@ function chunkMarkdown(text, max) {
1244
1347
  return [text.length > 0 ? text : " "];
1245
1348
  }
1246
1349
  const out = [];
1247
- for (let i = 0; i < text.length; i += max) {
1248
- out.push(text.slice(i, i + max));
1350
+ let i = 0;
1351
+ while (i < text.length) {
1352
+ const end = surrogateSafeEnd(text, i + max);
1353
+ out.push(text.slice(i, end));
1354
+ i = end;
1249
1355
  }
1250
1356
  return out;
1251
1357
  }
@@ -1255,65 +1361,43 @@ import { toString as toString3 } from "mdast-util-to-string";
1255
1361
 
1256
1362
  // src/adapters/inline.ts
1257
1363
  import { toString as toString2 } from "mdast-util-to-string";
1258
- var EMOJI_SHORTCODE_RE = /:([a-z0-9_+-]{1,64}):/g;
1259
- var KNOWN_SLACK_EMOJI = /* @__PURE__ */ new Set([
1260
- "smile",
1261
- "grinning",
1262
- "wink",
1263
- "blush",
1264
- "wave",
1265
- "thumbsup",
1266
- "thumbsdown",
1267
- "tada",
1268
- "rocket",
1269
- "fire",
1270
- "heart",
1271
- "eyes",
1272
- "thinking_face",
1273
- "sweat_smile",
1274
- "joy",
1275
- "sob",
1276
- "clap",
1277
- "100",
1278
- "white_check_mark",
1279
- "x",
1280
- "warning",
1281
- "bulb",
1282
- "memo",
1283
- "mag",
1284
- "link",
1285
- "gear",
1286
- "bug",
1287
- "sparkles",
1288
- "+1",
1289
- "-1",
1290
- "ok_hand",
1291
- "pray",
1292
- "star",
1293
- "zap",
1294
- "check",
1295
- "heavy_check_mark"
1296
- ]);
1364
+ var EMOJI_SHORTCODE_RE = /(?<![A-Za-z0-9]):([a-z0-9_+-]{1,64}):(?![A-Za-z0-9])/g;
1297
1365
  var HTTP_URL_RE = /^(https?:|mailto:)/i;
1298
1366
  var IMAGE_EXT_RE = /\.(png|jpe?g|gif)(?:\?|#|$)/i;
1299
1367
  function emitPhrasing(nodes, ctx, baseStyle = {}) {
1300
1368
  const elements = [];
1301
1369
  const hoistedImages = [];
1370
+ const work = [...nodes];
1302
1371
  let pendingAnchor;
1372
+ const htmlStyles = [];
1373
+ const activeStyle = () => htmlStyles[htmlStyles.length - 1] ?? baseStyle;
1303
1374
  const flushPendingAnchor = () => {
1304
1375
  if (pendingAnchor === void 0) {
1305
1376
  return;
1306
1377
  }
1307
- const label = pendingAnchor.textParts.join("");
1308
- elements.push({
1309
- type: "link",
1310
- url: pendingAnchor.href,
1311
- text: label.length > 0 ? label : pendingAnchor.href,
1312
- style: styleOrUndefined(pendingAnchor.style)
1313
- });
1378
+ elements.push(anchorInline(pendingAnchor, ctx));
1314
1379
  pendingAnchor = void 0;
1315
1380
  };
1316
- for (const node of nodes) {
1381
+ for (let i = 0; i < work.length; i += 1) {
1382
+ const node = work[i];
1383
+ if (pendingAnchor === void 0) {
1384
+ const reassembled = reassembleSlackLabeledLink(work, i, activeStyle());
1385
+ if (reassembled !== void 0) {
1386
+ if (reassembled.leadingText.value.length > 0) {
1387
+ elements.push(
1388
+ ...emitTextWithMentionsAndEmoji(reassembled.leadingText, activeStyle(), ctx)
1389
+ );
1390
+ }
1391
+ elements.push(reassembled.link);
1392
+ if (reassembled.trailingText.value.length > 0) {
1393
+ work.splice(i, 3, reassembled.trailingText);
1394
+ } else {
1395
+ work.splice(i, 3);
1396
+ }
1397
+ i -= 1;
1398
+ continue;
1399
+ }
1400
+ }
1317
1401
  if (pendingAnchor !== void 0) {
1318
1402
  if (node.type === "html" && /^<\/a\s*>$/i.test(node.value.trim())) {
1319
1403
  flushPendingAnchor();
@@ -1332,32 +1416,54 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1332
1416
  }
1333
1417
  if (node.type === "html") {
1334
1418
  const trimmed = node.value.trim();
1419
+ const entityInlines = slackEntityRichTextInlines(trimmed, activeStyle());
1420
+ if (entityInlines !== void 0) {
1421
+ elements.push(...entityInlines);
1422
+ continue;
1423
+ }
1335
1424
  const openOnly = /^<a\b([^>]{0,500})>\s*$/i.exec(trimmed);
1336
1425
  if (openOnly !== null) {
1337
1426
  const hrefMatch = /href\s*=\s*["']([^"']{0,3000})["']/i.exec(openOnly[1] ?? "");
1338
1427
  const href = hrefMatch?.[1];
1339
- if (href !== void 0 && isValidLinkUrl(href)) {
1340
- pendingAnchor = { href, style: baseStyle, textParts: [] };
1428
+ if (href !== void 0) {
1429
+ pendingAnchor = {
1430
+ href,
1431
+ style: activeStyle(),
1432
+ textParts: [],
1433
+ valid: isValidLinkUrl(href)
1434
+ };
1341
1435
  continue;
1342
1436
  }
1343
1437
  }
1344
- elements.push(...emitInlineHtml(node.value, baseStyle, ctx));
1438
+ const openTag = HTML_STYLE_OPEN_RE.exec(trimmed);
1439
+ if (openTag !== null) {
1440
+ htmlStyles.push(htmlTagStyle(activeStyle(), openTag[1] ?? ""));
1441
+ continue;
1442
+ }
1443
+ if (HTML_STYLE_CLOSE_RE.test(trimmed)) {
1444
+ htmlStyles.pop();
1445
+ continue;
1446
+ }
1447
+ elements.push(...emitInlineHtml(node.value, activeStyle(), ctx));
1345
1448
  continue;
1346
1449
  }
1347
1450
  switch (node.type) {
1348
1451
  case "text": {
1349
- elements.push(...emitTextWithMentionsAndEmoji(node, baseStyle, ctx));
1452
+ elements.push(...emitTextWithMentionsAndEmoji(node, activeStyle(), ctx));
1350
1453
  break;
1351
1454
  }
1352
1455
  case "strong": {
1353
- const inner = emitPhrasing(node.children, ctx, { ...baseStyle, bold: true });
1456
+ const inner = emitPhrasing(node.children, ctx, {
1457
+ ...activeStyle(),
1458
+ bold: true
1459
+ });
1354
1460
  elements.push(...inner.elements);
1355
1461
  hoistedImages.push(...inner.hoistedImages);
1356
1462
  break;
1357
1463
  }
1358
1464
  case "emphasis": {
1359
1465
  const inner = emitPhrasing(node.children, ctx, {
1360
- ...baseStyle,
1466
+ ...activeStyle(),
1361
1467
  italic: true
1362
1468
  });
1363
1469
  elements.push(...inner.elements);
@@ -1365,7 +1471,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1365
1471
  break;
1366
1472
  }
1367
1473
  case "delete": {
1368
- const inner = emitPhrasing(node.children, ctx, { ...baseStyle, strike: true });
1474
+ const inner = emitPhrasing(node.children, ctx, { ...activeStyle(), strike: true });
1369
1475
  elements.push(...inner.elements);
1370
1476
  hoistedImages.push(...inner.hoistedImages);
1371
1477
  break;
@@ -1374,21 +1480,21 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1374
1480
  const el = {
1375
1481
  type: "text",
1376
1482
  text: node.value,
1377
- style: styleOrUndefined({ ...baseStyle, code: true })
1483
+ style: styleOrUndefined({ ...activeStyle(), code: true })
1378
1484
  };
1379
1485
  elements.push(el);
1380
1486
  break;
1381
1487
  }
1382
1488
  case "break": {
1383
- elements.push({ type: "text", text: "\n", style: styleOrUndefined(baseStyle) });
1489
+ elements.push({ type: "text", text: "\n", style: styleOrUndefined(activeStyle()) });
1384
1490
  break;
1385
1491
  }
1386
1492
  case "link": {
1387
- elements.push(...emitLink(node, ctx, baseStyle));
1493
+ elements.push(...emitLink(node, ctx, activeStyle()));
1388
1494
  break;
1389
1495
  }
1390
1496
  case "linkReference": {
1391
- elements.push(...emitLinkReference(node, ctx, baseStyle));
1497
+ elements.push(...emitLinkReference(node, ctx, activeStyle()));
1392
1498
  break;
1393
1499
  }
1394
1500
  case "image": {
@@ -1403,22 +1509,33 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1403
1509
  type: "link",
1404
1510
  url,
1405
1511
  text: node.alt && node.alt.length > 0 ? node.alt : url,
1406
- style: styleOrUndefined(baseStyle)
1512
+ style: styleOrUndefined(activeStyle())
1407
1513
  });
1408
1514
  } else {
1515
+ const linkable = isValidLinkUrl(url);
1409
1516
  ctx.degradations.add({
1410
1517
  nodeType: "image",
1411
1518
  from: "image",
1412
- to: "link",
1519
+ to: linkable ? "link" : "text",
1413
1520
  reason: "Image URL is not a public http(s) png/jpg/gif \u22643000 chars",
1414
1521
  path: ctx.path
1415
1522
  });
1416
- elements.push({
1417
- type: "link",
1418
- url: url.length > 0 ? url : "#",
1419
- text: node.alt && node.alt.length > 0 ? node.alt : url,
1420
- style: styleOrUndefined(baseStyle)
1421
- });
1523
+ const label = node.alt && node.alt.length > 0 ? node.alt : url.length > 0 ? url : "image";
1524
+ if (linkable) {
1525
+ elements.push({
1526
+ type: "link",
1527
+ url,
1528
+ text: label,
1529
+ style: styleOrUndefined(activeStyle())
1530
+ });
1531
+ } else {
1532
+ const suffix = url.length > 0 && url !== label ? ` (${url})` : "";
1533
+ elements.push({
1534
+ type: "text",
1535
+ text: `${label}${suffix}`,
1536
+ style: styleOrUndefined(activeStyle())
1537
+ });
1538
+ }
1422
1539
  }
1423
1540
  break;
1424
1541
  }
@@ -1428,7 +1545,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1428
1545
  elements.push({
1429
1546
  type: "text",
1430
1547
  text: node.alt ?? node.identifier,
1431
- style: styleOrUndefined(baseStyle)
1548
+ style: styleOrUndefined(activeStyle())
1432
1549
  });
1433
1550
  break;
1434
1551
  }
@@ -1442,7 +1559,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1442
1559
  }
1443
1560
  ],
1444
1561
  ctx,
1445
- baseStyle
1562
+ activeStyle()
1446
1563
  );
1447
1564
  elements.push(...asImage.elements);
1448
1565
  hoistedImages.push(...asImage.hoistedImages);
@@ -1454,7 +1571,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1454
1571
  elements.push({
1455
1572
  type: "text",
1456
1573
  text: `[${String(n)}]`,
1457
- style: styleOrUndefined(baseStyle)
1574
+ style: styleOrUndefined(activeStyle())
1458
1575
  });
1459
1576
  break;
1460
1577
  }
@@ -1462,7 +1579,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1462
1579
  elements.push({
1463
1580
  type: "text",
1464
1581
  text: node.value,
1465
- style: styleOrUndefined({ ...baseStyle, code: true })
1582
+ style: styleOrUndefined({ ...activeStyle(), code: true })
1466
1583
  });
1467
1584
  ctx.degradations.add({
1468
1585
  nodeType: "inlineMath",
@@ -1476,7 +1593,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
1476
1593
  default: {
1477
1594
  const text = toString2(node);
1478
1595
  if (text.length > 0) {
1479
- elements.push({ type: "text", text, style: styleOrUndefined(baseStyle) });
1596
+ elements.push({ type: "text", text, style: styleOrUndefined(activeStyle()) });
1480
1597
  }
1481
1598
  break;
1482
1599
  }
@@ -1545,7 +1662,7 @@ function truncateLabel(label, max) {
1545
1662
  if (label.length <= max) {
1546
1663
  return { text: label, truncated: false };
1547
1664
  }
1548
- return { text: `${label.slice(0, Math.max(0, max - 1))}${ELLIPSIS}`, truncated: true };
1665
+ return { text: `${sliceSurrogateSafe(label, Math.max(0, max - 1))}${ELLIPSIS}`, truncated: true };
1549
1666
  }
1550
1667
  function hoistedImageBlock(h, ctx) {
1551
1668
  return {
@@ -1566,7 +1683,7 @@ function clampSectionText(text, ctx, nodeType) {
1566
1683
  reason: "Section text truncated to 3000 chars",
1567
1684
  path: ctx.path
1568
1685
  });
1569
- return text.slice(0, SECTION_TEXT_MAX_CHARS);
1686
+ return sliceSurrogateSafe(text, SECTION_TEXT_MAX_CHARS);
1570
1687
  }
1571
1688
  function preformattedRichText(text, language) {
1572
1689
  return {
@@ -1592,7 +1709,7 @@ function clampImageText(value, ctx, field) {
1592
1709
  reason: `Image ${field} truncated to ${String(IMAGE_ALT_MAX_CHARS)} chars`,
1593
1710
  path: ctx.path
1594
1711
  });
1595
- return value.slice(0, IMAGE_ALT_MAX_CHARS);
1712
+ return sliceSurrogateSafe(value, IMAGE_ALT_MAX_CHARS);
1596
1713
  }
1597
1714
  function styleOrUndefined(style) {
1598
1715
  if (style.bold === true || style.italic === true || style.strike === true || style.code === true || style.underline === true) {
@@ -1600,7 +1717,94 @@ function styleOrUndefined(style) {
1600
1717
  }
1601
1718
  return void 0;
1602
1719
  }
1603
- var MENTION_RE = /(?<![A-Za-z0-9._-])@([a-zA-Z0-9._-]{1,64})|(?<![A-Za-z0-9_-])#([a-zA-Z0-9_-]{1,80})/g;
1720
+ var SLACK_ENTITY_CANDIDATE_RE = /<([@#!][^<>]{1,340}|(?:https?:|mailto:)[^<>|]{1,3000}\|[^<>]{1,300})>/gi;
1721
+ var SLACK_USER_RE = /^@([UW][A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
1722
+ var SLACK_CHANNEL_RE = /^#([CDG][A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
1723
+ var SLACK_USERGROUP_RE = /^!subteam\^(S[A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
1724
+ var SLACK_BROADCAST_RE = /^!(here|channel|everyone)(?:\|[^>]{0,300})?$/;
1725
+ var SLACK_DATE_RE = /^!date\^(\d{1,13})\^([^^|]{1,300})(?:\^([^^|]{1,3000}))?(?:\|([^]{0,300}))?$/;
1726
+ var SLACK_LINK_RE = /^((?:https?:|mailto:)[^|]{1,3000})\|([^]{1,300})$/i;
1727
+ function slackEntityElement(inner, style) {
1728
+ const labeledLink = SLACK_LINK_RE.exec(inner);
1729
+ if (labeledLink?.[1] !== void 0 && labeledLink[2] !== void 0) {
1730
+ if (!isValidLinkUrl(labeledLink[1])) {
1731
+ return void 0;
1732
+ }
1733
+ return {
1734
+ type: "link",
1735
+ url: labeledLink[1],
1736
+ text: labeledLink[2],
1737
+ style: styleOrUndefined(style)
1738
+ };
1739
+ }
1740
+ const user = SLACK_USER_RE.exec(inner);
1741
+ if (user?.[1] !== void 0) {
1742
+ return { type: "user", user_id: user[1], style: styleOrUndefined(style) };
1743
+ }
1744
+ const channel = SLACK_CHANNEL_RE.exec(inner);
1745
+ if (channel?.[1] !== void 0) {
1746
+ return { type: "channel", channel_id: channel[1], style: styleOrUndefined(style) };
1747
+ }
1748
+ const usergroup = SLACK_USERGROUP_RE.exec(inner);
1749
+ if (usergroup?.[1] !== void 0) {
1750
+ return { type: "usergroup", usergroup_id: usergroup[1], style: styleOrUndefined(style) };
1751
+ }
1752
+ const broadcast = SLACK_BROADCAST_RE.exec(inner);
1753
+ if (broadcast !== null) {
1754
+ const range = broadcast[1];
1755
+ if (range === "here" || range === "channel" || range === "everyone") {
1756
+ return { type: "broadcast", range, style: styleOrUndefined(style) };
1757
+ }
1758
+ }
1759
+ const date = SLACK_DATE_RE.exec(inner);
1760
+ if (date?.[1] !== void 0 && date[2] !== void 0) {
1761
+ const timestamp = Number(date[1]);
1762
+ if (Number.isSafeInteger(timestamp)) {
1763
+ return {
1764
+ type: "date",
1765
+ timestamp,
1766
+ format: date[2],
1767
+ url: date[3],
1768
+ fallback: date[4],
1769
+ style: styleOrUndefined(style)
1770
+ };
1771
+ }
1772
+ }
1773
+ return void 0;
1774
+ }
1775
+ function slackEntitySplit(value, style, emit) {
1776
+ SLACK_ENTITY_CANDIDATE_RE.lastIndex = 0;
1777
+ let match = SLACK_ENTITY_CANDIDATE_RE.exec(value);
1778
+ if (match === null) {
1779
+ return void 0;
1780
+ }
1781
+ const out = [];
1782
+ let last = 0;
1783
+ let sawEntity = false;
1784
+ while (match !== null) {
1785
+ const element = slackEntityElement(match[1] ?? "", style);
1786
+ if (element !== void 0) {
1787
+ sawEntity = true;
1788
+ if (match.index > last) {
1789
+ out.push(...emit(value.slice(last, match.index)));
1790
+ }
1791
+ out.push(element);
1792
+ last = match.index + match[0].length;
1793
+ }
1794
+ match = SLACK_ENTITY_CANDIDATE_RE.exec(value);
1795
+ }
1796
+ if (!sawEntity) {
1797
+ return void 0;
1798
+ }
1799
+ if (last < value.length) {
1800
+ out.push(...emit(value.slice(last)));
1801
+ }
1802
+ return out;
1803
+ }
1804
+ function slackEntityRichTextInlines(value, style) {
1805
+ return slackEntitySplit(value, style, (slice) => emitTextWithEmoji(slice, style));
1806
+ }
1807
+ var MENTION_RE = /(?<![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})/g;
1604
1808
  function phrasingNeedsMentionResolution(nodes, ctx) {
1605
1809
  const resolvers = ctx.config.mentionResolvers;
1606
1810
  if (resolvers?.resolveUser === void 0 && resolvers?.resolveChannel === void 0) {
@@ -1611,13 +1815,23 @@ function phrasingNeedsMentionResolution(nodes, ctx) {
1611
1815
  return MENTION_RE.test(plain);
1612
1816
  }
1613
1817
  function emitTextWithMentionsAndEmoji(node, style, ctx) {
1818
+ const viaEntities = slackEntitySplit(
1819
+ node.value,
1820
+ style,
1821
+ (slice) => emitMentionsAndEmoji(slice, style, ctx)
1822
+ );
1823
+ if (viaEntities !== void 0) {
1824
+ return viaEntities;
1825
+ }
1826
+ return emitMentionsAndEmoji(node.value, style, ctx);
1827
+ }
1828
+ function emitMentionsAndEmoji(value, style, ctx) {
1614
1829
  const resolvers = ctx.config.mentionResolvers;
1615
1830
  const canUser = resolvers?.resolveUser !== void 0;
1616
1831
  const canChannel = resolvers?.resolveChannel !== void 0;
1617
1832
  if (!canUser && !canChannel) {
1618
- return emitTextWithEmoji(node.value, style);
1833
+ return emitTextWithEmoji(value, style);
1619
1834
  }
1620
- const value = node.value;
1621
1835
  const out = [];
1622
1836
  MENTION_RE.lastIndex = 0;
1623
1837
  let last = 0;
@@ -1667,13 +1881,8 @@ function emitTextWithEmoji(value, style) {
1667
1881
  if (start > last) {
1668
1882
  out.push({ type: "text", text: value.slice(last, start), style: styleOrUndefined(style) });
1669
1883
  }
1670
- const name = match[1] ?? "";
1671
- if (KNOWN_SLACK_EMOJI.has(name)) {
1672
- const emoji = { type: "emoji", name };
1673
- out.push(emoji);
1674
- } else {
1675
- out.push({ type: "text", text: match[0], style: styleOrUndefined(style) });
1676
- }
1884
+ const emoji = { type: "emoji", name: match[1] ?? "" };
1885
+ out.push(emoji);
1677
1886
  last = start + match[0].length;
1678
1887
  match = EMOJI_SHORTCODE_RE.exec(value);
1679
1888
  }
@@ -1685,9 +1894,53 @@ function emitTextWithEmoji(value, style) {
1685
1894
  }
1686
1895
  return out;
1687
1896
  }
1897
+ function reassembleSlackLabeledLink(work, i, style) {
1898
+ const lead = work[i];
1899
+ const linkNode = work[i + 1];
1900
+ const tail = work[i + 2];
1901
+ if (lead?.type !== "text" || !lead.value.endsWith("<")) {
1902
+ return void 0;
1903
+ }
1904
+ if (linkNode?.type !== "link" || tail?.type !== "text") {
1905
+ return void 0;
1906
+ }
1907
+ const url = linkNode.url;
1908
+ const pipeIndex = url.indexOf("|");
1909
+ if (pipeIndex <= 0 || !HTTP_URL_RE.test(url)) {
1910
+ return void 0;
1911
+ }
1912
+ if (phrasingToPlain(linkNode.children) !== url) {
1913
+ return void 0;
1914
+ }
1915
+ const closeIndex = tail.value.indexOf(">");
1916
+ if (closeIndex === -1) {
1917
+ return void 0;
1918
+ }
1919
+ const realUrl = url.slice(0, pipeIndex);
1920
+ if (!isValidLinkUrl(realUrl)) {
1921
+ return void 0;
1922
+ }
1923
+ const label = `${url.slice(pipeIndex + 1)}${tail.value.slice(0, closeIndex)}`;
1924
+ if (label.length === 0) {
1925
+ return void 0;
1926
+ }
1927
+ return {
1928
+ leadingText: { type: "text", value: lead.value.slice(0, -1) },
1929
+ link: { type: "link", url: realUrl, text: label, style: styleOrUndefined(style) },
1930
+ trailingText: { type: "text", value: tail.value.slice(closeIndex + 1) }
1931
+ };
1932
+ }
1688
1933
  function emitLink(node, ctx, style) {
1689
1934
  const url = node.url;
1690
1935
  const text = phrasingToPlain(node.children);
1936
+ const pipeIndex = url.indexOf("|");
1937
+ if (pipeIndex > 0 && HTTP_URL_RE.test(url) && text === url) {
1938
+ const realUrl = url.slice(0, pipeIndex);
1939
+ const label = url.slice(pipeIndex + 1);
1940
+ if (label.length > 0 && isValidLinkUrl(realUrl)) {
1941
+ return [{ type: "link", url: realUrl, text: label, style: styleOrUndefined(style) }];
1942
+ }
1943
+ }
1691
1944
  if (!isValidLinkUrl(url)) {
1692
1945
  ctx.degradations.add({
1693
1946
  nodeType: "link",
@@ -1696,7 +1949,7 @@ function emitLink(node, ctx, style) {
1696
1949
  reason: "Invalid or oversized URL",
1697
1950
  path: ctx.path
1698
1951
  });
1699
- const suffix = url.length > 0 ? ` (${url})` : "";
1952
+ const suffix = url.length > 0 && url !== text ? ` (${url})` : "";
1700
1953
  return [{ type: "text", text: `${text}${suffix}`, style: styleOrUndefined(style) }];
1701
1954
  }
1702
1955
  const link = {
@@ -1733,9 +1986,45 @@ function isValidLinkUrl(url) {
1733
1986
  if (url.length === 0 || url.length > IMAGE_URL_MAX_CHARS) {
1734
1987
  return false;
1735
1988
  }
1736
- return HTTP_URL_RE.test(url) || url.startsWith("#") || url.startsWith("/");
1989
+ return HTTP_URL_RE.test(url);
1990
+ }
1991
+ function anchorInline(anchor, ctx) {
1992
+ const label = anchor.textParts.join("");
1993
+ if (anchor.valid) {
1994
+ return {
1995
+ type: "link",
1996
+ url: anchor.href,
1997
+ text: label.length > 0 ? label : anchor.href,
1998
+ style: styleOrUndefined(anchor.style)
1999
+ };
2000
+ }
2001
+ ctx.degradations.add({
2002
+ nodeType: "link",
2003
+ from: "link",
2004
+ to: "text",
2005
+ reason: "Invalid or oversized URL",
2006
+ path: ctx.path
2007
+ });
2008
+ const base = label.length > 0 ? label : anchor.href;
2009
+ const suffix = label.length > 0 && anchor.href.length > 0 && anchor.href !== label ? ` (${anchor.href})` : "";
2010
+ return { type: "text", text: `${base}${suffix}`, style: styleOrUndefined(anchor.style) };
2011
+ }
2012
+ var HTML_STYLE_OPEN_RE = /^<(b|strong|i|em|s|del|code)(\s+[^>]{0,200})?>$/i;
2013
+ var HTML_STYLE_CLOSE_RE = /^<\/(b|strong|i|em|s|del|code)\s*>$/i;
2014
+ function htmlTagStyle(base, tag) {
2015
+ const t = tag.toLowerCase();
2016
+ if (t === "b" || t === "strong") {
2017
+ return { ...base, bold: true };
2018
+ }
2019
+ if (t === "i" || t === "em") {
2020
+ return { ...base, italic: true };
2021
+ }
2022
+ if (t === "s" || t === "del") {
2023
+ return { ...base, strike: true };
2024
+ }
2025
+ return { ...base, code: true };
1737
2026
  }
1738
- var INLINE_TAG_RE = /<\/?(b|strong|i|em|s|del|code|br|a)(\s+[^>]{0,200})?>|[^<]+/gi;
2027
+ var INLINE_TAG_RE = /<\/?(b|strong|i|em|s|del|code|br|a)(\s+[^>]{0,200})?\s*\/?>|<[^>]{0,200}>|[^<]+|</gi;
1739
2028
  function emitInlineHtml(html, style, ctx) {
1740
2029
  const out = [];
1741
2030
  const stack = [];
@@ -1743,11 +2032,15 @@ function emitInlineHtml(html, style, ctx) {
1743
2032
  let openAnchor;
1744
2033
  INLINE_TAG_RE.lastIndex = 0;
1745
2034
  let m = INLINE_TAG_RE.exec(html);
1746
- let matched = false;
2035
+ let strippedUnknown = false;
1747
2036
  while (m !== null) {
1748
- matched = true;
1749
2037
  const token = m[0];
1750
- if (token.startsWith("<")) {
2038
+ if (token.startsWith("<") && token.length > 1 && m[1] === void 0) {
2039
+ strippedUnknown = true;
2040
+ m = INLINE_TAG_RE.exec(html);
2041
+ continue;
2042
+ }
2043
+ if (token.startsWith("<") && m[1] !== void 0) {
1751
2044
  const tag = (m[1] ?? "").toLowerCase();
1752
2045
  const closing = token.startsWith("</");
1753
2046
  if (tag === "br") {
@@ -1758,13 +2051,7 @@ function emitInlineHtml(html, style, ctx) {
1758
2051
  }
1759
2052
  } else if (closing) {
1760
2053
  if (tag === "a" && openAnchor !== void 0) {
1761
- const label = openAnchor.textParts.join("");
1762
- out.push({
1763
- type: "link",
1764
- url: openAnchor.href,
1765
- text: label.length > 0 ? label : openAnchor.href,
1766
- style: styleOrUndefined(openAnchor.style)
1767
- });
2054
+ out.push(anchorInline(openAnchor, ctx));
1768
2055
  openAnchor = void 0;
1769
2056
  }
1770
2057
  stack.pop();
@@ -1773,8 +2060,11 @@ function emitInlineHtml(html, style, ctx) {
1773
2060
  const hrefMatch = /href\s*=\s*["']([^"']{0,3000})["']/i.exec(token);
1774
2061
  const href = hrefMatch?.[1];
1775
2062
  stack.push({ tag, style: current });
1776
- if (href !== void 0 && isValidLinkUrl(href)) {
1777
- openAnchor = { href, style: current, textParts: [] };
2063
+ if (href !== void 0) {
2064
+ if (openAnchor !== void 0) {
2065
+ out.push(anchorInline(openAnchor, ctx));
2066
+ }
2067
+ openAnchor = { href, style: current, textParts: [], valid: isValidLinkUrl(href) };
1778
2068
  }
1779
2069
  } else {
1780
2070
  const next = { ...current };
@@ -1795,15 +2085,9 @@ function emitInlineHtml(html, style, ctx) {
1795
2085
  m = INLINE_TAG_RE.exec(html);
1796
2086
  }
1797
2087
  if (openAnchor !== void 0) {
1798
- const label = openAnchor.textParts.join("");
1799
- out.push({
1800
- type: "link",
1801
- url: openAnchor.href,
1802
- text: label.length > 0 ? label : openAnchor.href,
1803
- style: styleOrUndefined(openAnchor.style)
1804
- });
2088
+ out.push(anchorInline(openAnchor, ctx));
1805
2089
  }
1806
- if (!matched) {
2090
+ if (strippedUnknown) {
1807
2091
  ctx.degradations.add({
1808
2092
  nodeType: "html",
1809
2093
  from: "html",
@@ -1811,10 +2095,6 @@ function emitInlineHtml(html, style, ctx) {
1811
2095
  reason: "Unrecognized inline HTML stripped to text",
1812
2096
  path: ctx.path
1813
2097
  });
1814
- const stripped = html.replace(/<[^>]{0,200}>/g, "");
1815
- if (stripped.length > 0) {
1816
- out.push({ type: "text", text: stripped, style: styleOrUndefined(style) });
1817
- }
1818
2098
  }
1819
2099
  return out;
1820
2100
  }
@@ -2046,10 +2326,13 @@ function emitPlainHeader(heading, withLevel) {
2046
2326
  const block = {
2047
2327
  type: "header",
2048
2328
  text: { type: "plain_text", text: plain.length > 0 ? plain : " " },
2049
- level: withLevel ? heading.depth : void 0
2329
+ level: withLevel ? headerLevelOf(heading.depth) : void 0
2050
2330
  };
2051
2331
  return [block];
2052
2332
  }
2333
+ function headerLevelOf(depth) {
2334
+ return depth === 1 || depth === 2 || depth === 3 || depth === 4 ? depth : void 0;
2335
+ }
2053
2336
  function emitRichHeading(heading, ctx, hoistImages) {
2054
2337
  const plain = phrasingToPlain(heading.children);
2055
2338
  const depth = heading.depth;
@@ -2158,6 +2441,20 @@ ${body}`, ctx, "html.details.section")
2158
2441
  }
2159
2442
  ];
2160
2443
  }
2444
+ const entityInlines = slackEntityRichTextInlines(html.value.trim(), {});
2445
+ if (entityInlines !== void 0) {
2446
+ return [
2447
+ {
2448
+ budget: { blocks: 1 },
2449
+ emit: () => [
2450
+ {
2451
+ type: "rich_text",
2452
+ elements: [{ type: "rich_text_section", elements: entityInlines }]
2453
+ }
2454
+ ]
2455
+ }
2456
+ ];
2457
+ }
2161
2458
  return [
2162
2459
  {
2163
2460
  budget: { blocks: 1 },
@@ -2198,7 +2495,7 @@ function clampContainerTitle(text, ctx) {
2198
2495
  reason: `Container title truncated to ${String(CONTAINER_TITLE_MAX_CHARS)} chars`,
2199
2496
  path: ctx.path
2200
2497
  });
2201
- return text.slice(0, CONTAINER_TITLE_MAX_CHARS);
2498
+ return sliceSurrogateSafe(text, CONTAINER_TITLE_MAX_CHARS);
2202
2499
  }
2203
2500
 
2204
2501
  // src/adapters/list-adapter.ts
@@ -2278,27 +2575,51 @@ function emitListTree(list, indent, ctx) {
2278
2575
  });
2279
2576
  effectiveIndent = RICH_TEXT_LIST_INDENT_MAX;
2280
2577
  }
2281
- const sections = [];
2282
- const nested = [];
2578
+ const ordered = list.ordered === true;
2579
+ const startOffset = ordered && list.start !== null && list.start !== void 0 && list.start > 1 ? list.start - 1 : 0;
2580
+ const out = [];
2283
2581
  const hoistedImages = [];
2284
- for (const item of list.children) {
2582
+ let pendingSections = [];
2583
+ const flush = (lastIndex) => {
2584
+ if (pendingSections.length === 0) {
2585
+ return;
2586
+ }
2587
+ const chunkStartIndex = lastIndex - pendingSections.length + 1;
2588
+ const offset = startOffset + chunkStartIndex;
2589
+ out.push({
2590
+ type: "rich_text_list",
2591
+ style: ordered ? "ordered" : "bullet",
2592
+ elements: pendingSections,
2593
+ indent: effectiveIndent,
2594
+ offset: ordered && offset > 0 ? offset : void 0
2595
+ });
2596
+ pendingSections = [];
2597
+ };
2598
+ for (const [index, item] of list.children.entries()) {
2285
2599
  const { section, childLists, hoistedImages: itemImages } = emitListItem(item, ctx);
2286
- sections.push(section);
2600
+ pendingSections.push(section);
2287
2601
  hoistedImages.push(...itemImages);
2602
+ if (childLists.length === 0) {
2603
+ continue;
2604
+ }
2605
+ flush(index);
2288
2606
  for (const child of childLists) {
2289
2607
  const nestedResult = emitListTree(child, effectiveIndent + 1, ctx);
2290
- nested.push(...nestedResult.elements);
2608
+ out.push(...nestedResult.elements);
2291
2609
  hoistedImages.push(...nestedResult.hoistedImages);
2292
2610
  }
2293
2611
  }
2294
- const primary = {
2295
- type: "rich_text_list",
2296
- style: list.ordered === true ? "ordered" : "bullet",
2297
- elements: sections.length > 0 ? sections : [{ type: "rich_text_section", elements: [{ type: "text", text: " " }] }],
2298
- indent: effectiveIndent,
2299
- offset: list.ordered === true && list.start !== null && list.start !== void 0 && list.start > 1 ? list.start - 1 : void 0
2300
- };
2301
- return { elements: [primary, ...nested], hoistedImages };
2612
+ flush(list.children.length - 1);
2613
+ if (out.length === 0) {
2614
+ out.push({
2615
+ type: "rich_text_list",
2616
+ style: ordered ? "ordered" : "bullet",
2617
+ elements: [{ type: "rich_text_section", elements: [{ type: "text", text: " " }] }],
2618
+ indent: effectiveIndent,
2619
+ offset: void 0
2620
+ });
2621
+ }
2622
+ return { elements: out, hoistedImages };
2302
2623
  }
2303
2624
  function collectListItemPhrasing(item) {
2304
2625
  const phrasing = [];
@@ -2615,7 +2936,19 @@ async function tryRenderers(source, ctx) {
2615
2936
  if (!renderer.canRender(req)) {
2616
2937
  continue;
2617
2938
  }
2618
- const result = await renderer.render(req);
2939
+ let result;
2940
+ try {
2941
+ result = await renderer.render(req);
2942
+ } catch (err) {
2943
+ ctx.degradations.add({
2944
+ nodeType: "code.mermaid",
2945
+ from: "image.render",
2946
+ to: "skip",
2947
+ reason: `Renderer failed: ${err instanceof Error ? err.message : String(err)}`,
2948
+ path: ctx.path
2949
+ });
2950
+ continue;
2951
+ }
2619
2952
  if (result === null) {
2620
2953
  continue;
2621
2954
  }
@@ -2647,7 +2980,19 @@ async function tryRenderers(source, ctx) {
2647
2980
  });
2648
2981
  continue;
2649
2982
  }
2650
- const uploaded = await uploader.upload(result);
2983
+ let uploaded;
2984
+ try {
2985
+ uploaded = await uploader.upload(result);
2986
+ } catch (err) {
2987
+ ctx.degradations.add({
2988
+ nodeType: "code.mermaid",
2989
+ from: "image.bytes",
2990
+ to: "skip",
2991
+ reason: `Slack upload failed: ${err instanceof Error ? err.message : String(err)}`,
2992
+ path: ctx.path
2993
+ });
2994
+ continue;
2995
+ }
2651
2996
  return {
2652
2997
  type: "image",
2653
2998
  alt_text: clampImageText(result.altText, ctx, "alt_text"),
@@ -2710,11 +3055,11 @@ function emitParagraph(paragraph, emitCtx, loneImage, hoistImages) {
2710
3055
  const img = loneImage;
2711
3056
  if (isPublicImageUrl(img.url)) {
2712
3057
  const altRaw = img.alt ?? "image";
2713
- const alt = clampImageText(altRaw.length > 0 ? altRaw : "image", emitCtx, "alt_text");
3058
+ const alt2 = clampImageText(altRaw.length > 0 ? altRaw : "image", emitCtx, "alt_text");
2714
3059
  const block = {
2715
3060
  type: "image",
2716
3061
  image_url: img.url,
2717
- alt_text: alt,
3062
+ alt_text: alt2,
2718
3063
  title: img.title !== null && img.title !== void 0 && img.title.length > 0 ? {
2719
3064
  type: "plain_text",
2720
3065
  text: clampImageText(img.title, emitCtx, "title")
@@ -2722,20 +3067,23 @@ function emitParagraph(paragraph, emitCtx, loneImage, hoistImages) {
2722
3067
  };
2723
3068
  return [block];
2724
3069
  }
3070
+ const linkable = isValidLinkUrl(img.url);
2725
3071
  emitCtx.degradations.add({
2726
3072
  nodeType: "image",
2727
3073
  from: "image",
2728
- to: "section.link",
3074
+ to: linkable ? "section.link" : "section.text",
2729
3075
  reason: "Non-public or invalid image URL",
2730
3076
  path: emitCtx.path
2731
3077
  });
2732
- const linkText = `<${img.url}|${img.alt ?? img.url}>`;
3078
+ const alt = img.alt ?? "";
3079
+ const label = alt.length > 0 ? alt : img.url.length > 0 ? img.url : "image";
3080
+ const fallbackText = linkable ? `<${img.url}|${label}>` : img.url.length > 0 && img.url !== label ? `${label} (${img.url})` : label;
2733
3081
  return [
2734
3082
  {
2735
3083
  type: "section",
2736
3084
  text: {
2737
3085
  type: "mrkdwn",
2738
- text: clampSectionText(linkText, emitCtx, "section")
3086
+ text: clampSectionText(fallbackText, emitCtx, "section")
2739
3087
  }
2740
3088
  }
2741
3089
  ];
@@ -2795,14 +3143,15 @@ var TableAdapter = class {
2795
3143
  return node.type === "table";
2796
3144
  }
2797
3145
  plan(node, ctx) {
2798
- const table = node;
3146
+ const table = normalizeTableRows(node, ctx);
2799
3147
  const rowCount = table.children.length;
2800
3148
  const colCount = table.children[0]?.children.length ?? 0;
2801
- const cellChars = countEmittedCellChars(table, ctx);
2802
3149
  if (colCount > TABLE_MAX_COLS) {
3150
+ const cellChars = countEmittedCellChars(table, ctx, "table");
2803
3151
  return markdownThenAscii(table, cellChars, "Table exceeds 20 columns");
2804
3152
  }
2805
3153
  if (rowCount <= TABLE_PREFERRED_MAX_ROWS) {
3154
+ const cellChars = countEmittedCellChars(table, ctx, "table");
2806
3155
  return [
2807
3156
  {
2808
3157
  requires: ["tableBlock"],
@@ -2817,11 +3166,12 @@ var TableAdapter = class {
2817
3166
  const dataRows = Math.max(0, rowCount - 1);
2818
3167
  if (dataRows <= DATA_TABLE_MAX_DATA_ROWS) {
2819
3168
  const preferredTable = truncateRows(table, TABLE_PREFERRED_MAX_ROWS);
2820
- const preferredChars = countEmittedCellChars(preferredTable, ctx);
3169
+ const preferredChars = countEmittedCellChars(preferredTable, ctx, "table");
3170
+ const dataTableChars = countEmittedCellChars(table, ctx, "data_table");
2821
3171
  return [
2822
3172
  {
2823
3173
  requires: ["dataTable"],
2824
- budget: { blocks: 1, dataTableChars: cellChars },
3174
+ budget: { blocks: 1, dataTableChars },
2825
3175
  emit: (emitCtx) => {
2826
3176
  emitCtx.degradations.add({
2827
3177
  nodeType: "table",
@@ -2847,11 +3197,11 @@ var TableAdapter = class {
2847
3197
  return [buildTableBlock(preferredTable, emitCtx)];
2848
3198
  }
2849
3199
  },
2850
- ...markdownThenAscii(table, cellChars, "native table unavailable")
3200
+ ...markdownThenAscii(table, dataTableChars, "native table unavailable")
2851
3201
  ];
2852
3202
  }
2853
3203
  const dataChunk = truncateRows(table, DATA_TABLE_MAX_DATA_ROWS + 1);
2854
- const dataChunkChars = countEmittedCellChars(dataChunk, ctx);
3204
+ const dataChunkChars = countEmittedCellChars(dataChunk, ctx, "data_table");
2855
3205
  return [
2856
3206
  {
2857
3207
  requires: ["dataTable"],
@@ -2867,10 +3217,37 @@ var TableAdapter = class {
2867
3217
  return [buildDataTable(dataChunk, emitCtx)];
2868
3218
  }
2869
3219
  },
2870
- ...markdownThenAscii(table, cellChars, "oversized table")
3220
+ ...markdownThenAscii(table, dataChunkChars, "oversized table")
2871
3221
  ];
2872
3222
  }
2873
3223
  };
3224
+ function normalizeTableRows(table, ctx) {
3225
+ const colCount = table.children[0]?.children.length ?? 0;
3226
+ if (colCount === 0 || table.children.every((row) => row.children.length === colCount)) {
3227
+ return table;
3228
+ }
3229
+ const children = table.children.map((row, rowIndex) => {
3230
+ if (row.children.length === colCount) {
3231
+ return row;
3232
+ }
3233
+ if (row.children.length > colCount) {
3234
+ ctx.degradations.add({
3235
+ nodeType: "table",
3236
+ from: `row[${String(rowIndex)}]: ${String(row.children.length)} cells`,
3237
+ to: `${String(colCount)} cells`,
3238
+ reason: "Cells beyond the header column count dropped (GFM ignores the excess)",
3239
+ path: ctx.path
3240
+ });
3241
+ return { ...row, children: row.children.slice(0, colCount) };
3242
+ }
3243
+ const padding = Array.from(
3244
+ { length: colCount - row.children.length },
3245
+ () => ({ type: "tableCell", children: [] })
3246
+ );
3247
+ return { ...row, children: [...row.children, ...padding] };
3248
+ });
3249
+ return { ...table, children };
3250
+ }
2874
3251
  function markdownThenAscii(table, cellChars, reason) {
2875
3252
  const md = toPipeMarkdown(table);
2876
3253
  return [
@@ -2916,7 +3293,7 @@ function buildTableBlock(table, ctx) {
2916
3293
  }
2917
3294
  function buildDataTable(table, ctx) {
2918
3295
  const captionRaw = ctx.precedingHeading !== void 0 && ctx.precedingHeading.length > 0 ? ctx.precedingHeading : "Table";
2919
- const caption = captionRaw.slice(0, 150);
3296
+ const caption = sliceSurrogateSafe(captionRaw, 150);
2920
3297
  if (captionRaw.length > 150) {
2921
3298
  ctx.degradations.add({
2922
3299
  nodeType: "table",
@@ -2927,12 +3304,7 @@ function buildDataTable(table, ctx) {
2927
3304
  });
2928
3305
  }
2929
3306
  const rows = table.children.map(
2930
- (row, rowIndex) => row.children.map((cell) => {
2931
- if (rowIndex === 0) {
2932
- return { type: "raw_text", text: toString5(cell) };
2933
- }
2934
- return cellToSlack(cell, ctx, false);
2935
- })
3307
+ (row, rowIndex) => row.children.map((cell) => cellForDataTable(cell, ctx, rowIndex))
2936
3308
  );
2937
3309
  return {
2938
3310
  type: "data_table",
@@ -2942,25 +3314,31 @@ function buildDataTable(table, ctx) {
2942
3314
  row_header_column_index: 0
2943
3315
  };
2944
3316
  }
3317
+ function cellForDataTable(cell, ctx, rowIndex) {
3318
+ if (rowIndex === 0) {
3319
+ return { type: "raw_text", text: toString5(cell) };
3320
+ }
3321
+ return cellToSlack(cell, ctx, false);
3322
+ }
2945
3323
  function cellToSlack(cell, ctx, header) {
2946
- const plain = toString5(cell);
2947
- const styled = cell.children.some(
2948
- (c) => c.type === "strong" || c.type === "emphasis" || c.type === "delete" || c.type === "link" || c.type === "inlineCode"
2949
- );
2950
- if (!styled) {
2951
- if (!header && /^-?\d+(\.\d+)?$/.test(plain.trim())) {
2952
- const value = Number(plain.trim());
2953
- return { type: "raw_number", value, text: plain.trim() };
3324
+ const inline = emitPhrasing(cell.children, ctx);
3325
+ const single = inline.elements.length === 1 ? inline.elements[0] : void 0;
3326
+ const isEmpty = inline.elements.length === 0;
3327
+ const isSingleUnstyledText = single?.type === "text" && single.style === void 0;
3328
+ const plainText = isEmpty ? toString5(cell) : isSingleUnstyledText ? single.text : void 0;
3329
+ if (plainText !== void 0) {
3330
+ const trimmed = plainText.trim();
3331
+ if (!header && /^-?\d+(\.\d+)?$/.test(trimmed)) {
3332
+ return { type: "raw_number", value: Number(trimmed), text: trimmed };
2954
3333
  }
2955
- return { type: "raw_text", text: plain };
3334
+ return { type: "raw_text", text: plainText };
2956
3335
  }
2957
- const inline = emitPhrasing(cell.children, ctx);
2958
3336
  const rich = {
2959
3337
  type: "rich_text",
2960
3338
  elements: [
2961
3339
  {
2962
3340
  type: "rich_text_section",
2963
- elements: inline.elements.length > 0 ? inline.elements : [{ type: "text", text: plain }]
3341
+ elements: inline.elements
2964
3342
  }
2965
3343
  ]
2966
3344
  };
@@ -2987,12 +3365,13 @@ function columnSettings(table) {
2987
3365
  }
2988
3366
  return settings;
2989
3367
  }
2990
- function countEmittedCellChars(table, ctx) {
3368
+ function countEmittedCellChars(table, ctx, mode) {
2991
3369
  const probe = ctx.withDegradations(new RecordingDegradationCollector());
2992
3370
  let n = 0;
2993
3371
  for (const [rowIndex, row] of table.children.entries()) {
2994
3372
  for (const cell of row.children) {
2995
- n += slackCellCharCount(cellToSlack(cell, probe, rowIndex === 0));
3373
+ const emitted = mode === "data_table" ? cellForDataTable(cell, probe, rowIndex) : cellToSlack(cell, probe, rowIndex === 0);
3374
+ n += slackCellCharCount(emitted);
2996
3375
  }
2997
3376
  }
2998
3377
  return n;
@@ -3115,27 +3494,50 @@ function emitUnicodeTaskTree(list, indent, ctx) {
3115
3494
  });
3116
3495
  effectiveIndent = RICH_TEXT_LIST_INDENT_MAX;
3117
3496
  }
3118
- const sections = [];
3119
- const nested = [];
3497
+ const out = [];
3498
+ let pendingSections = [];
3499
+ const flush = () => {
3500
+ if (pendingSections.length === 0) {
3501
+ return;
3502
+ }
3503
+ out.push({
3504
+ type: "rich_text_list",
3505
+ style: "bullet",
3506
+ elements: pendingSections,
3507
+ indent: effectiveIndent
3508
+ });
3509
+ pendingSections = [];
3510
+ };
3120
3511
  for (const item of list.children) {
3121
3512
  const prefix = item.checked === true ? "\u2611 " : item.checked === false ? "\u2610 " : "";
3122
- sections.push({
3513
+ pendingSections.push({
3123
3514
  type: "rich_text_section",
3124
3515
  elements: [{ type: "text", text: `${prefix}${itemText(item)}` }]
3125
3516
  });
3517
+ const childLists = [];
3126
3518
  for (const child of item.children) {
3127
3519
  if (child.type === "list") {
3128
- nested.push(...emitUnicodeTaskTree(child, effectiveIndent + 1, ctx));
3520
+ childLists.push(child);
3129
3521
  }
3130
3522
  }
3523
+ if (childLists.length === 0) {
3524
+ continue;
3525
+ }
3526
+ flush();
3527
+ for (const child of childLists) {
3528
+ out.push(...emitUnicodeTaskTree(child, effectiveIndent + 1, ctx));
3529
+ }
3131
3530
  }
3132
- const primary = {
3133
- type: "rich_text_list",
3134
- style: "bullet",
3135
- elements: sections.length > 0 ? sections : [{ type: "rich_text_section", elements: [{ type: "text", text: " " }] }],
3136
- indent: effectiveIndent
3137
- };
3138
- return [primary, ...nested];
3531
+ flush();
3532
+ if (out.length === 0) {
3533
+ out.push({
3534
+ type: "rich_text_list",
3535
+ style: "bullet",
3536
+ elements: [{ type: "rich_text_section", elements: [{ type: "text", text: " " }] }],
3537
+ indent: effectiveIndent
3538
+ });
3539
+ }
3540
+ return out;
3139
3541
  }
3140
3542
 
3141
3543
  // src/adapters/thematic-break-adapter.ts
@@ -3383,12 +3785,10 @@ export {
3383
3785
  CapabilityPresetName,
3384
3786
  ConfigurationError,
3385
3787
  ConversionContext,
3386
- ConversionStrategy as ConversionStrategies,
3387
3788
  DeterministicPlainTextRenderer,
3388
3789
  MermaidPieParser,
3389
3790
  MermaidXyChartParser,
3390
3791
  MessageAssembler,
3391
- OverflowMode as OverflowModes,
3392
3792
  RecordingDegradationCollector,
3393
3793
  RemarkMarkdownParser,
3394
3794
  SlackMarkConverter,