slackmark 0.4.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/README.md +22 -17
- package/dist/adapters/inline.d.ts.map +1 -1
- package/dist/adapters/math-block-adapter.d.ts +1 -1
- package/dist/capabilities/capabilities.d.ts +0 -1
- package/dist/capabilities/capabilities.d.ts.map +1 -1
- package/dist/index.js +242 -203
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validate/validate-blocks.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ renders any Block Kit JSON and pairs perfectly with `slackmark`.
|
|
|
14
14
|
- Capability profiles (`latest`, `conservative`, dated presets)
|
|
15
15
|
- Ordered fallback chains with `Degradation` records (never silent)
|
|
16
16
|
- Message budgets (50 blocks, table/markdown/chart caps)
|
|
17
|
-
- Mermaid `pie` / `xychart-beta` → native charts; other diagrams
|
|
17
|
+
- Mermaid `pie` / `xychart-beta` → native charts; other mermaid diagrams → configured `renderers` or preformatted fallback
|
|
18
|
+
- Math and non-mermaid diagram fences → preformatted/code text (renderers are not consulted)
|
|
18
19
|
- OOP + constructor DI; `convert()` convenience for simple use
|
|
19
20
|
|
|
20
21
|
## Quickstart
|
|
@@ -60,7 +61,7 @@ Result shape: `{ blocks, text, degradations }` — total on user markdown (never
|
|
|
60
61
|
| Preset | Flags |
|
|
61
62
|
| --- | --- |
|
|
62
63
|
| `latest` (default) | all |
|
|
63
|
-
| `conservative` |
|
|
64
|
+
| `conservative` | all flags off |
|
|
64
65
|
| `2025-02` | + `markdownBlock` |
|
|
65
66
|
| `2025-08` | + `tableBlock` |
|
|
66
67
|
| `2026-03` | + `preformattedLanguage`, `headerLevel` |
|
|
@@ -73,36 +74,32 @@ Pass a preset name, a flags object, and/or `capabilityOverrides`.
|
|
|
73
74
|
|
|
74
75
|
Built-in adapters cover the mapping matrix. Prepend custom `NodeAdapter`s via `createConverter({ adapters })`.
|
|
75
76
|
|
|
76
|
-
No renderer or uploader is enabled by default, so default conversion performs no network I/O.
|
|
77
|
+
No renderer or uploader is enabled by default, so default conversion performs no network I/O. **Only the mermaid fence adapter reads `config.renderers`** — and only after native `pie` / `xychart-beta` charts are unavailable. Math always becomes preformatted/code text; non-mermaid diagram fences (PlantUML, D2, Chart.js, …) always become code blocks. Image-rendered math and non-mermaid diagrams are planned as future Node-only packages.
|
|
78
|
+
|
|
79
|
+
Hosted URL renderers compose durable public PNG URLs (no network I/O in core); they return `null` when Slack's 3,000-character `image_url` limit would be exceeded. An absent or declining renderer falls back to preformatted mermaid source and records a degradation.
|
|
77
80
|
|
|
78
81
|
Import first-party URL adapters from the dedicated subpath:
|
|
79
82
|
|
|
80
83
|
```ts
|
|
81
84
|
import { createConverter } from "slackmark";
|
|
82
|
-
import {
|
|
83
|
-
CodeCogsRenderer,
|
|
84
|
-
KrokiRenderer,
|
|
85
|
-
MermaidInkRenderer,
|
|
86
|
-
QuickChartRenderer,
|
|
87
|
-
} from "slackmark/renderers";
|
|
85
|
+
import { KrokiRenderer, MermaidInkRenderer } from "slackmark/renderers";
|
|
88
86
|
|
|
89
87
|
const converter = createConverter({
|
|
90
|
-
renderers: [new
|
|
88
|
+
renderers: [new MermaidInkRenderer()],
|
|
89
|
+
// or: renderers: [new KrokiRenderer()], // consulted for lang=mermaid only
|
|
91
90
|
});
|
|
92
91
|
```
|
|
93
92
|
|
|
94
|
-
- `
|
|
95
|
-
- `
|
|
96
|
-
- `CodeCogsRenderer`
|
|
97
|
-
- `QuickChartRenderer` emits full Chart.js GET URLs and never uses expiring `/chart/create` short URLs.
|
|
93
|
+
- `MermaidInkRenderer` emits pako-state PNG URLs for mermaid fences with a non-transparent background by default.
|
|
94
|
+
- `KrokiRenderer` can render mermaid fences the same way (its other diagram-type aliases are unused by conversion today).
|
|
95
|
+
- `CodeCogsRenderer` and `QuickChartRenderer` are also exported from `slackmark/renderers` as `ImageRenderer` implementations, but no adapter calls them during conversion.
|
|
98
96
|
|
|
99
|
-
Hosted services receive the diagram
|
|
97
|
+
Hosted services receive the diagram source encoded in the URL when Slack fetches the image. Treat them as explicit data-egress opt-ins. Each adapter accepts `baseUrl` for a self-hosted deployment:
|
|
100
98
|
|
|
101
99
|
```ts
|
|
102
100
|
const privateRenderers = [
|
|
103
101
|
new KrokiRenderer({ baseUrl: "https://kroki.internal.example" }),
|
|
104
102
|
new MermaidInkRenderer({ baseUrl: "https://mermaid.internal.example" }),
|
|
105
|
-
new QuickChartRenderer({ baseUrl: "https://charts.internal.example" }),
|
|
106
103
|
];
|
|
107
104
|
```
|
|
108
105
|
|
|
@@ -125,9 +122,17 @@ const converter = createConverter({
|
|
|
125
122
|
|
|
126
123
|
Inject `fetch`, sleep, and clock functions for deterministic tests. The Slack token needs `files:write` and `files:read`; supplying `channelId` shares the upload to the target channel for reliable `slack_file` rendering.
|
|
127
124
|
|
|
125
|
+
## Raw Slack entities
|
|
126
|
+
|
|
127
|
+
Markdown that already contains Slack mrkdwn tokens — `<@U…>`, `<#C…>`, `<!here>`,
|
|
128
|
+
`<!subteam^S…>`, `<!date^…>` — converts to native rich text elements (no resolvers
|
|
129
|
+
required). HTML-entity-escaped forms such as `<@U123>` convert the same way:
|
|
130
|
+
remark decodes character references before slackmark sees the text. To keep the
|
|
131
|
+
syntax literal, wrap it in a code span (`` `<@U123>` ``).
|
|
132
|
+
|
|
128
133
|
## Optional remark plugins
|
|
129
134
|
|
|
130
|
-
`remark-math` and `remark-frontmatter` are dependencies; enable with `enableMath` / `enableFrontmatter`.
|
|
135
|
+
`remark-math` and `remark-frontmatter` are dependencies; enable with `enableMath` / `enableFrontmatter`. Math is parsed when enabled, then emitted as preformatted/code text.
|
|
131
136
|
|
|
132
137
|
## License
|
|
133
138
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inline.d.ts","sourceRoot":"","sources":["../../src/adapters/inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiC,eAAe,EAAgB,MAAM,OAAO,CAAC;AAG1F,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAEb,cAAc,EAEd,aAAa,EAEd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"inline.d.ts","sourceRoot":"","sources":["../../src/adapters/inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiC,eAAe,EAAgB,MAAM,OAAO,CAAC;AAG1F,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAEb,cAAc,EAEd,aAAa,EAEd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAYzE,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACjD,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CACrD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,EACrC,GAAG,EAAE,iBAAiB,EACtB,SAAS,GAAE,aAAkB,GAC5B,gBAAgB,CAiQlB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,GAAG,MAAM,CAE7E;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAerD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,EACrC,GAAG,EAAE,iBAAiB,GACrB,MAAM,CAuCR;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAK9F;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,iBAAiB,GAAG,UAAU,CAUrF;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY/F;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CAY9F;AAED,mFAAmF;AACnF,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,UAAU,GAAG,OAAO,GAC1B,MAAM,CAYR;AA0LD;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,aAAa,GACnB,aAAa,CAAC,cAAc,CAAC,GAAG,SAAS,CAI3C;AAED,gGAAgG;AAChG,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,EACrC,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAQT;AA4HD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAKnD"}
|
|
@@ -2,7 +2,7 @@ import type { RootContent } from "mdast";
|
|
|
2
2
|
import type { ConversionContext } from "../budget/conversion-context.js";
|
|
3
3
|
import type { Candidate, NodeAdapter } from "./node-adapter.js";
|
|
4
4
|
/**
|
|
5
|
-
* Display math → preformatted TeX (
|
|
5
|
+
* Display math → preformatted TeX (`config.renderers` is not consulted).
|
|
6
6
|
*/
|
|
7
7
|
export declare class MathBlockAdapter implements NodeAdapter {
|
|
8
8
|
match(node: RootContent, _ctx: ConversionContext): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/capabilities/capabilities.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/capabilities/capabilities.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC;AAEnD,eAAO,MAAM,oBAAoB;aAC/B,MAAM,EAAE,QAAQ;aAChB,YAAY,EAAE,cAAc;aAC5B,QAAQ,EAAE,SAAS;aACnB,QAAQ,EAAE,SAAS;aACnB,QAAQ,EAAE,SAAS;aACnB,QAAQ,EAAE,SAAS;aACnB,QAAQ,EAAE,SAAS;CACX,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,OAAO,oBAAoB,CAAC,CAAC;AA4CpG,eAAO,MAAM,kBAAkB,EAAE;IAC/B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;CASrC,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,CAAC,IAAI,cAAc,CAAC,CAAC,EAAE,OAAO;CACzC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,oBAAoB,GAAG,eAAe,GAAG,sBAAsB,GAAG,SAAS,EAClF,SAAS,CAAC,EAAE,sBAAsB,GACjC,eAAe,CAqBjB"}
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,6 @@ var CapabilityPresetName = {
|
|
|
18
18
|
D2026_06: "2026-06"
|
|
19
19
|
};
|
|
20
20
|
var ALL_OFF = {
|
|
21
|
-
richText: false,
|
|
22
21
|
markdownBlock: false,
|
|
23
22
|
tableBlock: false,
|
|
24
23
|
preformattedLanguage: false,
|
|
@@ -27,10 +26,7 @@ var ALL_OFF = {
|
|
|
27
26
|
dataVisualization: false,
|
|
28
27
|
containerBlock: false
|
|
29
28
|
};
|
|
30
|
-
var CONSERVATIVE = {
|
|
31
|
-
...ALL_OFF,
|
|
32
|
-
richText: true
|
|
33
|
-
};
|
|
29
|
+
var CONSERVATIVE = { ...ALL_OFF };
|
|
34
30
|
var P_2025_02 = {
|
|
35
31
|
...CONSERVATIVE,
|
|
36
32
|
markdownBlock: true
|
|
@@ -89,7 +85,7 @@ function isPresetName(value) {
|
|
|
89
85
|
return Object.hasOwn(CAPABILITY_PRESETS, value);
|
|
90
86
|
}
|
|
91
87
|
function isFullFlags(value) {
|
|
92
|
-
return typeof value.
|
|
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";
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
// src/constants.ts
|
|
@@ -1361,7 +1357,6 @@ import { toString as toString3 } from "mdast-util-to-string";
|
|
|
1361
1357
|
|
|
1362
1358
|
// src/adapters/inline.ts
|
|
1363
1359
|
import { toString as toString2 } from "mdast-util-to-string";
|
|
1364
|
-
var EMOJI_SHORTCODE_RE = /(?<![A-Za-z0-9]):([a-z0-9_+-]{1,64}):(?![A-Za-z0-9])/g;
|
|
1365
1360
|
var HTTP_URL_RE = /^(https?:|mailto:)/i;
|
|
1366
1361
|
var IMAGE_EXT_RE = /\.(png|jpe?g|gif)(?:\?|#|$)/i;
|
|
1367
1362
|
function emitPhrasing(nodes, ctx, baseStyle = {}) {
|
|
@@ -1585,7 +1580,7 @@ function emitPhrasing(nodes, ctx, baseStyle = {}) {
|
|
|
1585
1580
|
nodeType: "inlineMath",
|
|
1586
1581
|
from: "inlineMath",
|
|
1587
1582
|
to: "style.code",
|
|
1588
|
-
reason: "Math
|
|
1583
|
+
reason: "Math is emitted as preformatted/code text",
|
|
1589
1584
|
path: ctx.path
|
|
1590
1585
|
});
|
|
1591
1586
|
break;
|
|
@@ -1717,13 +1712,21 @@ function styleOrUndefined(style) {
|
|
|
1717
1712
|
}
|
|
1718
1713
|
return void 0;
|
|
1719
1714
|
}
|
|
1720
|
-
var
|
|
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;
|
|
1721
1720
|
var SLACK_USER_RE = /^@([UW][A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
|
|
1722
1721
|
var SLACK_CHANNEL_RE = /^#([CDG][A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
|
|
1723
1722
|
var SLACK_USERGROUP_RE = /^!subteam\^(S[A-Z0-9]{1,60})(?:\|[^>]{0,300})?$/;
|
|
1724
1723
|
var SLACK_BROADCAST_RE = /^!(here|channel|everyone)(?:\|[^>]{0,300})?$/;
|
|
1725
1724
|
var SLACK_DATE_RE = /^!date\^(\d{1,13})\^([^^|]{1,300})(?:\^([^^|]{1,3000}))?(?:\|([^]{0,300}))?$/;
|
|
1726
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;
|
|
1727
1730
|
function slackEntityElement(inner, style) {
|
|
1728
1731
|
const labeledLink = SLACK_LINK_RE.exec(inner);
|
|
1729
1732
|
if (labeledLink?.[1] !== void 0 && labeledLink[2] !== void 0) {
|
|
@@ -1772,39 +1775,79 @@ function slackEntityElement(inner, style) {
|
|
|
1772
1775
|
}
|
|
1773
1776
|
return void 0;
|
|
1774
1777
|
}
|
|
1775
|
-
function
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
if (match === null) {
|
|
1779
|
-
return void 0;
|
|
1780
|
-
}
|
|
1778
|
+
function scanTextRun(value, style, resolveUser, resolveChannel) {
|
|
1779
|
+
const scanMentions = resolveUser !== void 0 || resolveChannel !== void 0;
|
|
1780
|
+
const styleOpt = styleOrUndefined(style);
|
|
1781
1781
|
const out = [];
|
|
1782
|
-
let last = 0;
|
|
1783
1782
|
let sawEntity = false;
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
+
}
|
|
1786
1828
|
if (element !== void 0) {
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
out.push(...emit(value.slice(last, match.index)));
|
|
1829
|
+
if (i > textStart) {
|
|
1830
|
+
out.push({ type: "text", text: value.slice(textStart, i), style: styleOpt });
|
|
1790
1831
|
}
|
|
1791
1832
|
out.push(element);
|
|
1792
|
-
|
|
1833
|
+
i = tokenEnd;
|
|
1834
|
+
textStart = i;
|
|
1835
|
+
continue;
|
|
1793
1836
|
}
|
|
1794
|
-
|
|
1837
|
+
i += 1;
|
|
1795
1838
|
}
|
|
1796
|
-
if (
|
|
1797
|
-
|
|
1839
|
+
if (textStart < value.length) {
|
|
1840
|
+
out.push({ type: "text", text: value.slice(textStart), style: styleOpt });
|
|
1798
1841
|
}
|
|
1799
|
-
if (
|
|
1800
|
-
out.push(
|
|
1842
|
+
if (out.length === 0) {
|
|
1843
|
+
out.push({ type: "text", text: value, style: styleOpt });
|
|
1801
1844
|
}
|
|
1802
|
-
return out;
|
|
1845
|
+
return { elements: out, sawEntity };
|
|
1803
1846
|
}
|
|
1804
1847
|
function slackEntityRichTextInlines(value, style) {
|
|
1805
|
-
|
|
1848
|
+
const { elements, sawEntity } = scanTextRun(value, style);
|
|
1849
|
+
return sawEntity ? elements : void 0;
|
|
1806
1850
|
}
|
|
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;
|
|
1808
1851
|
function phrasingNeedsMentionResolution(nodes, ctx) {
|
|
1809
1852
|
const resolvers = ctx.config.mentionResolvers;
|
|
1810
1853
|
if (resolvers?.resolveUser === void 0 && resolvers?.resolveChannel === void 0) {
|
|
@@ -1815,84 +1858,8 @@ function phrasingNeedsMentionResolution(nodes, ctx) {
|
|
|
1815
1858
|
return MENTION_RE.test(plain);
|
|
1816
1859
|
}
|
|
1817
1860
|
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) {
|
|
1829
1861
|
const resolvers = ctx.config.mentionResolvers;
|
|
1830
|
-
|
|
1831
|
-
const canChannel = resolvers?.resolveChannel !== void 0;
|
|
1832
|
-
if (!canUser && !canChannel) {
|
|
1833
|
-
return emitTextWithEmoji(value, style);
|
|
1834
|
-
}
|
|
1835
|
-
const out = [];
|
|
1836
|
-
MENTION_RE.lastIndex = 0;
|
|
1837
|
-
let last = 0;
|
|
1838
|
-
let match = MENTION_RE.exec(value);
|
|
1839
|
-
while (match !== null) {
|
|
1840
|
-
const start = match.index;
|
|
1841
|
-
if (start > last) {
|
|
1842
|
-
out.push(...emitTextWithEmoji(value.slice(last, start), style));
|
|
1843
|
-
}
|
|
1844
|
-
const userHandle = match[1];
|
|
1845
|
-
const channelName = match[2];
|
|
1846
|
-
if (userHandle !== void 0 && canUser) {
|
|
1847
|
-
const userId = resolvers.resolveUser?.(userHandle);
|
|
1848
|
-
if (userId !== void 0 && userId.length > 0) {
|
|
1849
|
-
out.push({ type: "user", user_id: userId, style: styleOrUndefined(style) });
|
|
1850
|
-
} else {
|
|
1851
|
-
out.push(...emitTextWithEmoji(match[0], style));
|
|
1852
|
-
}
|
|
1853
|
-
} else if (channelName !== void 0 && canChannel) {
|
|
1854
|
-
const channelId = resolvers.resolveChannel?.(channelName);
|
|
1855
|
-
if (channelId !== void 0 && channelId.length > 0) {
|
|
1856
|
-
out.push({ type: "channel", channel_id: channelId, style: styleOrUndefined(style) });
|
|
1857
|
-
} else {
|
|
1858
|
-
out.push(...emitTextWithEmoji(match[0], style));
|
|
1859
|
-
}
|
|
1860
|
-
} else {
|
|
1861
|
-
out.push(...emitTextWithEmoji(match[0], style));
|
|
1862
|
-
}
|
|
1863
|
-
last = start + match[0].length;
|
|
1864
|
-
match = MENTION_RE.exec(value);
|
|
1865
|
-
}
|
|
1866
|
-
if (last < value.length) {
|
|
1867
|
-
out.push(...emitTextWithEmoji(value.slice(last), style));
|
|
1868
|
-
}
|
|
1869
|
-
if (out.length === 0) {
|
|
1870
|
-
out.push(...emitTextWithEmoji(value, style));
|
|
1871
|
-
}
|
|
1872
|
-
return out;
|
|
1873
|
-
}
|
|
1874
|
-
function emitTextWithEmoji(value, style) {
|
|
1875
|
-
const out = [];
|
|
1876
|
-
EMOJI_SHORTCODE_RE.lastIndex = 0;
|
|
1877
|
-
let last = 0;
|
|
1878
|
-
let match = EMOJI_SHORTCODE_RE.exec(value);
|
|
1879
|
-
while (match !== null) {
|
|
1880
|
-
const start = match.index;
|
|
1881
|
-
if (start > last) {
|
|
1882
|
-
out.push({ type: "text", text: value.slice(last, start), style: styleOrUndefined(style) });
|
|
1883
|
-
}
|
|
1884
|
-
const emoji = { type: "emoji", name: match[1] ?? "" };
|
|
1885
|
-
out.push(emoji);
|
|
1886
|
-
last = start + match[0].length;
|
|
1887
|
-
match = EMOJI_SHORTCODE_RE.exec(value);
|
|
1888
|
-
}
|
|
1889
|
-
if (last < value.length) {
|
|
1890
|
-
out.push({ type: "text", text: value.slice(last), style: styleOrUndefined(style) });
|
|
1891
|
-
}
|
|
1892
|
-
if (out.length === 0) {
|
|
1893
|
-
out.push({ type: "text", text: value, style: styleOrUndefined(style) });
|
|
1894
|
-
}
|
|
1895
|
-
return out;
|
|
1862
|
+
return scanTextRun(node.value, style, resolvers?.resolveUser, resolvers?.resolveChannel).elements;
|
|
1896
1863
|
}
|
|
1897
1864
|
function reassembleSlackLabeledLink(work, i, style) {
|
|
1898
1865
|
const lead = work[i];
|
|
@@ -2712,7 +2679,7 @@ function preformattedMath(value, withLang, ctx) {
|
|
|
2712
2679
|
nodeType: "math",
|
|
2713
2680
|
from: "math",
|
|
2714
2681
|
to: "rich_text_preformatted",
|
|
2715
|
-
reason: "
|
|
2682
|
+
reason: "Math is emitted as preformatted/code text",
|
|
2716
2683
|
path: ctx.path
|
|
2717
2684
|
});
|
|
2718
2685
|
return preformattedRichText(value, withLang ? "latex" : void 0);
|
|
@@ -3612,117 +3579,37 @@ function validateBlocks(blocks) {
|
|
|
3612
3579
|
message: `Too many blocks: ${String(blocks.length)} > ${String(MAX_BLOCKS_PER_MESSAGE)}`
|
|
3613
3580
|
});
|
|
3614
3581
|
}
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3582
|
+
const counters = {
|
|
3583
|
+
markdownChars: 0,
|
|
3584
|
+
tableChars: 0,
|
|
3585
|
+
dataTableChars: 0,
|
|
3586
|
+
charts: 0
|
|
3587
|
+
};
|
|
3619
3588
|
blocks.forEach((block, i) => {
|
|
3620
|
-
|
|
3621
|
-
switch (block.type) {
|
|
3622
|
-
case "header":
|
|
3623
|
-
if (block.text.text.length > HEADER_TEXT_MAX_CHARS) {
|
|
3624
|
-
issues.push({ path, message: "header text > 150" });
|
|
3625
|
-
}
|
|
3626
|
-
if (block.level !== void 0 && (block.level < 1 || block.level > 4)) {
|
|
3627
|
-
issues.push({ path, message: "header level out of 1\u20134" });
|
|
3628
|
-
}
|
|
3629
|
-
break;
|
|
3630
|
-
case "section":
|
|
3631
|
-
if (block.text !== void 0 && block.text.text.length > SECTION_TEXT_MAX_CHARS) {
|
|
3632
|
-
issues.push({ path, message: "section text > 3000" });
|
|
3633
|
-
}
|
|
3634
|
-
break;
|
|
3635
|
-
case "image":
|
|
3636
|
-
if (block.alt_text.length > IMAGE_ALT_MAX_CHARS) {
|
|
3637
|
-
issues.push({ path, message: "image alt > 2000" });
|
|
3638
|
-
}
|
|
3639
|
-
if (block.image_url !== void 0 && block.image_url.length > IMAGE_URL_MAX_CHARS) {
|
|
3640
|
-
issues.push({ path, message: "image_url > 3000" });
|
|
3641
|
-
}
|
|
3642
|
-
break;
|
|
3643
|
-
case "context":
|
|
3644
|
-
if (block.elements.length > CONTEXT_ELEMENTS_MAX) {
|
|
3645
|
-
issues.push({ path, message: "context elements > 10" });
|
|
3646
|
-
}
|
|
3647
|
-
break;
|
|
3648
|
-
case "markdown":
|
|
3649
|
-
markdownChars += block.text.length;
|
|
3650
|
-
break;
|
|
3651
|
-
case "table": {
|
|
3652
|
-
if (block.rows.length > TABLE_MAX_ROWS) {
|
|
3653
|
-
issues.push({ path, message: "table rows > 100" });
|
|
3654
|
-
}
|
|
3655
|
-
for (const row of block.rows) {
|
|
3656
|
-
if (row.length > TABLE_MAX_COLS) {
|
|
3657
|
-
issues.push({ path, message: "table cols > 20" });
|
|
3658
|
-
}
|
|
3659
|
-
for (const cell of row) {
|
|
3660
|
-
tableChars += cellCharCount(cell);
|
|
3661
|
-
}
|
|
3662
|
-
}
|
|
3663
|
-
break;
|
|
3664
|
-
}
|
|
3665
|
-
case "data_table": {
|
|
3666
|
-
if (block.rows.length > DATA_TABLE_MAX_DATA_ROWS + 1) {
|
|
3667
|
-
issues.push({ path, message: "data_table rows > 201" });
|
|
3668
|
-
}
|
|
3669
|
-
for (const row of block.rows) {
|
|
3670
|
-
if (row.length > TABLE_MAX_COLS) {
|
|
3671
|
-
issues.push({ path, message: "data_table cols > 20" });
|
|
3672
|
-
}
|
|
3673
|
-
for (const cell of row) {
|
|
3674
|
-
dataTableChars += cellCharCount(cell);
|
|
3675
|
-
}
|
|
3676
|
-
}
|
|
3677
|
-
break;
|
|
3678
|
-
}
|
|
3679
|
-
case "data_visualization":
|
|
3680
|
-
charts += 1;
|
|
3681
|
-
if (block.title.length > CHART_TITLE_MAX_CHARS) {
|
|
3682
|
-
issues.push({ path, message: "chart title > 50" });
|
|
3683
|
-
}
|
|
3684
|
-
validateChart(block, path, issues);
|
|
3685
|
-
break;
|
|
3686
|
-
case "container":
|
|
3687
|
-
if (block.child_blocks.length > CONTAINER_CHILDREN_MAX) {
|
|
3688
|
-
issues.push({ path, message: "container children > 10" });
|
|
3689
|
-
}
|
|
3690
|
-
if (block.title !== void 0 && block.title.text.length > CONTAINER_TITLE_MAX_CHARS) {
|
|
3691
|
-
issues.push({ path, message: "container title > 150" });
|
|
3692
|
-
}
|
|
3693
|
-
break;
|
|
3694
|
-
case "divider":
|
|
3695
|
-
case "rich_text":
|
|
3696
|
-
break;
|
|
3697
|
-
default: {
|
|
3698
|
-
const exhaustive = block;
|
|
3699
|
-
void exhaustive;
|
|
3700
|
-
break;
|
|
3701
|
-
}
|
|
3702
|
-
}
|
|
3589
|
+
validateBlock(block, `blocks[${String(i)}]`, issues, counters, 0);
|
|
3703
3590
|
});
|
|
3704
|
-
if (markdownChars > MARKDOWN_CUMULATIVE_MAX_CHARS) {
|
|
3591
|
+
if (counters.markdownChars > MARKDOWN_CUMULATIVE_MAX_CHARS) {
|
|
3705
3592
|
issues.push({
|
|
3706
3593
|
path: "$",
|
|
3707
|
-
message: `markdown cumulative ${String(markdownChars)} > 12000`
|
|
3594
|
+
message: `markdown cumulative ${String(counters.markdownChars)} > 12000`
|
|
3708
3595
|
});
|
|
3709
3596
|
}
|
|
3710
|
-
if (tableChars > TABLE_CELL_CHARS_MAX) {
|
|
3597
|
+
if (counters.tableChars > TABLE_CELL_CHARS_MAX) {
|
|
3711
3598
|
issues.push({
|
|
3712
3599
|
path: "$",
|
|
3713
|
-
message: `table chars ${String(tableChars)} > 10000`
|
|
3600
|
+
message: `table chars ${String(counters.tableChars)} > 10000`
|
|
3714
3601
|
});
|
|
3715
3602
|
}
|
|
3716
|
-
if (dataTableChars > DATA_TABLE_CELL_CHARS_MAX) {
|
|
3603
|
+
if (counters.dataTableChars > DATA_TABLE_CELL_CHARS_MAX) {
|
|
3717
3604
|
issues.push({
|
|
3718
3605
|
path: "$",
|
|
3719
|
-
message: `data_table chars ${String(dataTableChars)} > 20000`
|
|
3606
|
+
message: `data_table chars ${String(counters.dataTableChars)} > 20000`
|
|
3720
3607
|
});
|
|
3721
3608
|
}
|
|
3722
|
-
if (charts > DATA_VISUALIZATION_MAX_PER_MESSAGE) {
|
|
3609
|
+
if (counters.charts > DATA_VISUALIZATION_MAX_PER_MESSAGE) {
|
|
3723
3610
|
issues.push({
|
|
3724
3611
|
path: "$",
|
|
3725
|
-
message: `charts ${String(charts)} > ${String(DATA_VISUALIZATION_MAX_PER_MESSAGE)}`
|
|
3612
|
+
message: `charts ${String(counters.charts)} > ${String(DATA_VISUALIZATION_MAX_PER_MESSAGE)}`
|
|
3726
3613
|
});
|
|
3727
3614
|
}
|
|
3728
3615
|
if (issues.length > 0) {
|
|
@@ -3731,6 +3618,158 @@ function validateBlocks(blocks) {
|
|
|
3731
3618
|
}
|
|
3732
3619
|
return blocks;
|
|
3733
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
|
+
}
|
|
3734
3773
|
function cellCharCount(cell) {
|
|
3735
3774
|
if (cell.type === "raw_text") {
|
|
3736
3775
|
return cell.text.length;
|