slackmark 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/phrasing-to-markdown.d.ts +15 -0
- package/dist/adapters/phrasing-to-markdown.d.ts.map +1 -0
- package/dist/adapters/task-list-adapter.d.ts +6 -1
- package/dist/adapters/task-list-adapter.d.ts.map +1 -1
- package/dist/assemble/text-fallback.d.ts.map +1 -1
- package/dist/index.js +274 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Definition, PhrasingContent } from "mdast";
|
|
2
|
+
/**
|
|
3
|
+
* Inline markdown for content bound for a Slack `markdown` block.
|
|
4
|
+
*
|
|
5
|
+
* Slack renders standard markdown in that block — links, bold, italic,
|
|
6
|
+
* strikethrough, inline code, and images (as link text) — so flattening inlines
|
|
7
|
+
* to bare text discards fidelity the destination would have carried.
|
|
8
|
+
*
|
|
9
|
+
* Text nodes pass through unescaped, exactly as they did when this content was
|
|
10
|
+
* flattened wholesale. This only re-adds syntax around nodes that already
|
|
11
|
+
* carried that meaning in the source, so a document with no links converts
|
|
12
|
+
* byte-identically to before.
|
|
13
|
+
*/
|
|
14
|
+
export declare function phrasingToMarkdown(nodes: ReadonlyArray<PhrasingContent>, definitions: ReadonlyMap<string, Definition>): string;
|
|
15
|
+
//# sourceMappingURL=phrasing-to-markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phrasing-to-markdown.d.ts","sourceRoot":"","sources":["../../src/adapters/phrasing-to-markdown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAGzD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,EACrC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,GAC3C,MAAM,CAMR"}
|
|
@@ -3,9 +3,14 @@ import type { ConversionContext } from "../budget/conversion-context.js";
|
|
|
3
3
|
import type { Candidate, NodeAdapter } from "./node-adapter.js";
|
|
4
4
|
/**
|
|
5
5
|
* Task lists → markdown block (checkboxes); fallback unicode in rich_text_list.
|
|
6
|
+
*
|
|
7
|
+
* Both rungs carry inline links: Slack renders `[label](url)` in a `markdown`
|
|
8
|
+
* block, and a rich_text section holds real `link` elements next to the unicode
|
|
9
|
+
* checkbox. Only a block child of an item (a table, a code fence) still has to
|
|
10
|
+
* be flattened, and only that flattening drops URLs.
|
|
6
11
|
*/
|
|
7
12
|
export declare class TaskListAdapter implements NodeAdapter {
|
|
8
13
|
match(node: RootContent, _ctx: ConversionContext): boolean;
|
|
9
|
-
plan(node: RootContent,
|
|
14
|
+
plan(node: RootContent, ctx: ConversionContext): ReadonlyArray<Candidate>;
|
|
10
15
|
}
|
|
11
16
|
//# sourceMappingURL=task-list-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-list-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/task-list-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"task-list-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/task-list-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA+C,WAAW,EAAE,MAAM,OAAO,CAAC;AAStF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAezE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhE;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,WAAW;IACjD,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAEzD;IAED,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,iBAAiB,GAAG,aAAa,CAAC,SAAS,CAAC,CAwCxE;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-fallback.d.ts","sourceRoot":"","sources":["../../src/assemble/text-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAmC,MAAM,oBAAoB,CAAC;AACjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;GAEG;AACH,qBAAa,8BAA+B,YAAW,iBAAiB;IACtE,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"text-fallback.d.ts","sourceRoot":"","sources":["../../src/assemble/text-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAmC,MAAM,oBAAoB,CAAC;AACjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;GAEG;AACH,qBAAa,8BAA+B,YAAW,iBAAiB;IACtE,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAkC/B;IAED,gBAAgB,CAAC,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,MAAM,CAsDrD;CACF"}
|
package/dist/index.js
CHANGED
|
@@ -958,7 +958,7 @@ var DeterministicPlainTextRenderer = class {
|
|
|
958
958
|
parts.push(src[i] ?? "");
|
|
959
959
|
i += 1;
|
|
960
960
|
}
|
|
961
|
-
const raw = parts.join("").replace(/!\[[^\]]{0,200}\]\([^)]{0,3000}\)/g, " ").replace(/\[([^\]]{0,200})\]\([^)]{0,3000}\)/g, "$1").replace(/[
|
|
961
|
+
const raw = slackEntitiesToPlain(parts.join("")).replace(/!\[[^\]]{0,200}\]\([^)]{0,3000}\)/g, " ").replace(/\[([^\]]{0,200})\]\([^)]{0,3000}\)/g, "$1").replace(/^[ \t]*#{1,6}[ \t]*/gm, "").replace(/^[ \t]*>[ \t]?/gm, "").replace(/[*_`~]/g, "").replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1").replace(/\s+/g, " ").trim();
|
|
962
962
|
return raw.length > 0 ? raw : " ";
|
|
963
963
|
}
|
|
964
964
|
renderFromBlocks(blocks) {
|
|
@@ -1003,7 +1003,37 @@ var DeterministicPlainTextRenderer = class {
|
|
|
1003
1003
|
}
|
|
1004
1004
|
};
|
|
1005
1005
|
function stripMrkdwn(text) {
|
|
1006
|
-
return text
|
|
1006
|
+
return slackEntitiesToPlain(text).replace(/[*_~`]/g, "");
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Slack entity syntax rendered the way {@link inlinesToPlain} renders the
|
|
1010
|
+
* equivalent structured element, so a mention reads the same whether it reached
|
|
1011
|
+
* the output as a `rich_text` element or as literal text in a `markdown` block.
|
|
1012
|
+
*
|
|
1013
|
+
* This has to run before markdown punctuation is stripped. `>` closes every one
|
|
1014
|
+
* of these forms and also marks a blockquote, and `#` opens a channel reference
|
|
1015
|
+
* and also a heading, so stripping those characters first leaves `<@U123` behind.
|
|
1016
|
+
*/
|
|
1017
|
+
function slackEntitiesToPlain(text) {
|
|
1018
|
+
return text.replace(/<([^<>]{1,3000})>/g, (_match, body) => {
|
|
1019
|
+
const [target, label] = splitOnce(body, "|");
|
|
1020
|
+
if (target.startsWith("@")) return `@${target.slice(1)}`;
|
|
1021
|
+
if (target.startsWith("#")) return `#${target.slice(1)}`;
|
|
1022
|
+
if (target.startsWith("!")) return specialToPlain(target.slice(1), label);
|
|
1023
|
+
return label ?? target;
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
/** `<!here>`, `<!subteam^S1|@x>`, `<!date^…|fallback>` and friends. */
|
|
1027
|
+
function specialToPlain(command, label) {
|
|
1028
|
+
const [name, argument] = splitOnce(command, "^");
|
|
1029
|
+
if (name === "subteam") return `@${argument ?? ""}`;
|
|
1030
|
+
if (name === "date") return label ?? command;
|
|
1031
|
+
return `@${name}`;
|
|
1032
|
+
}
|
|
1033
|
+
function splitOnce(value, separator) {
|
|
1034
|
+
const index = value.indexOf(separator);
|
|
1035
|
+
if (index === -1) return [value, void 0];
|
|
1036
|
+
return [value.slice(0, index), value.slice(index + separator.length)];
|
|
1007
1037
|
}
|
|
1008
1038
|
function richTextToPlain(elements) {
|
|
1009
1039
|
const parts = [];
|
|
@@ -3013,7 +3043,7 @@ function flattenQuote(quote, ctx) {
|
|
|
3013
3043
|
reason: "Nested list inside blockquote flattened",
|
|
3014
3044
|
path: ctx.path
|
|
3015
3045
|
});
|
|
3016
|
-
flagFlattenedUrls
|
|
3046
|
+
flagFlattenedUrls(child, ctx);
|
|
3017
3047
|
if (parts.length > 0) parts.push({
|
|
3018
3048
|
type: "text",
|
|
3019
3049
|
text: "\n"
|
|
@@ -3047,7 +3077,7 @@ function flattenQuote(quote, ctx) {
|
|
|
3047
3077
|
reason: "Nested block inside blockquote flattened",
|
|
3048
3078
|
path: ctx.path
|
|
3049
3079
|
});
|
|
3050
|
-
flagFlattenedUrls
|
|
3080
|
+
flagFlattenedUrls(child, ctx);
|
|
3051
3081
|
if (parts.length > 0) parts.push({
|
|
3052
3082
|
type: "text",
|
|
3053
3083
|
text: "\n"
|
|
@@ -3063,7 +3093,7 @@ function flattenQuote(quote, ctx) {
|
|
|
3063
3093
|
}];
|
|
3064
3094
|
}
|
|
3065
3095
|
/** Flattening keeps labels and alt text only; every dropped URL gets a record. */
|
|
3066
|
-
function flagFlattenedUrls
|
|
3096
|
+
function flagFlattenedUrls(node, ctx) {
|
|
3067
3097
|
const dropped = [];
|
|
3068
3098
|
collectFlattenedUrls(node, dropped);
|
|
3069
3099
|
for (const { nodeType, url } of dropped) ctx.degradations.add({
|
|
@@ -4711,83 +4741,266 @@ function asciiTableBlock(table) {
|
|
|
4711
4741
|
return preformattedRichText(toPipeMarkdown(table), void 0);
|
|
4712
4742
|
}
|
|
4713
4743
|
//#endregion
|
|
4744
|
+
//#region src/adapters/phrasing-to-markdown.ts
|
|
4745
|
+
/**
|
|
4746
|
+
* Inline markdown for content bound for a Slack `markdown` block.
|
|
4747
|
+
*
|
|
4748
|
+
* Slack renders standard markdown in that block — links, bold, italic,
|
|
4749
|
+
* strikethrough, inline code, and images (as link text) — so flattening inlines
|
|
4750
|
+
* to bare text discards fidelity the destination would have carried.
|
|
4751
|
+
*
|
|
4752
|
+
* Text nodes pass through unescaped, exactly as they did when this content was
|
|
4753
|
+
* flattened wholesale. This only re-adds syntax around nodes that already
|
|
4754
|
+
* carried that meaning in the source, so a document with no links converts
|
|
4755
|
+
* byte-identically to before.
|
|
4756
|
+
*/
|
|
4757
|
+
function phrasingToMarkdown(nodes, definitions) {
|
|
4758
|
+
let out = "";
|
|
4759
|
+
for (const node of nodes) out += nodeToMarkdown(node, definitions);
|
|
4760
|
+
return out;
|
|
4761
|
+
}
|
|
4762
|
+
function nodeToMarkdown(node, definitions) {
|
|
4763
|
+
if (node.type === "text") return node.value;
|
|
4764
|
+
if (node.type === "inlineCode") return `\`${node.value}\``;
|
|
4765
|
+
if (node.type === "strong") return `**${phrasingToMarkdown(node.children, definitions)}**`;
|
|
4766
|
+
if (node.type === "emphasis") return `_${phrasingToMarkdown(node.children, definitions)}_`;
|
|
4767
|
+
if (node.type === "delete") return `~~${phrasingToMarkdown(node.children, definitions)}~~`;
|
|
4768
|
+
if (node.type === "link") return labelledLink(phrasingToMarkdown(node.children, definitions), node.url);
|
|
4769
|
+
if (node.type === "image") return ``;
|
|
4770
|
+
if (node.type === "linkReference") {
|
|
4771
|
+
const url = definitions.get(node.identifier)?.url;
|
|
4772
|
+
const label = phrasingToMarkdown(node.children, definitions);
|
|
4773
|
+
return url === void 0 ? label : labelledLink(label, url);
|
|
4774
|
+
}
|
|
4775
|
+
if (node.type === "imageReference") {
|
|
4776
|
+
const url = definitions.get(node.identifier)?.url;
|
|
4777
|
+
const alt = node.alt ?? "";
|
|
4778
|
+
return url === void 0 ? alt : ``;
|
|
4779
|
+
}
|
|
4780
|
+
if (node.type === "break") return " ";
|
|
4781
|
+
return toString(node);
|
|
4782
|
+
}
|
|
4783
|
+
/**
|
|
4784
|
+
* An autolink already reads as its own URL; wrapping it as `[url](url)` would be
|
|
4785
|
+
* noise, and bare URLs are what this path emitted before.
|
|
4786
|
+
*/
|
|
4787
|
+
function labelledLink(label, url) {
|
|
4788
|
+
return label === url ? url : `[${label}](${url})`;
|
|
4789
|
+
}
|
|
4790
|
+
//#endregion
|
|
4714
4791
|
//#region src/adapters/task-list-adapter.ts
|
|
4715
4792
|
/**
|
|
4716
4793
|
* Task lists → markdown block (checkboxes); fallback unicode in rich_text_list.
|
|
4794
|
+
*
|
|
4795
|
+
* Both rungs carry inline links: Slack renders `[label](url)` in a `markdown`
|
|
4796
|
+
* block, and a rich_text section holds real `link` elements next to the unicode
|
|
4797
|
+
* checkbox. Only a block child of an item (a table, a code fence) still has to
|
|
4798
|
+
* be flattened, and only that flattening drops URLs.
|
|
4717
4799
|
*/
|
|
4718
4800
|
var TaskListAdapter = class {
|
|
4719
4801
|
match(node, _ctx) {
|
|
4720
4802
|
return node.type === "list" && isTaskList(node);
|
|
4721
4803
|
}
|
|
4722
|
-
plan(node,
|
|
4804
|
+
plan(node, ctx) {
|
|
4723
4805
|
const list = node;
|
|
4724
|
-
const md = serializeTaskList(list);
|
|
4725
|
-
|
|
4806
|
+
const md = serializeTaskList(list, ctx.definitions);
|
|
4807
|
+
const hoistCount = countHoistableImagesInTaskList(list, ctx);
|
|
4808
|
+
const candidates = [{
|
|
4726
4809
|
requires: ["markdownBlock"],
|
|
4727
4810
|
budget: {
|
|
4728
4811
|
blocks: 1,
|
|
4729
4812
|
markdownChars: md.length
|
|
4730
4813
|
},
|
|
4731
|
-
emit: (
|
|
4732
|
-
|
|
4814
|
+
emit: (emitCtx) => {
|
|
4815
|
+
flagBlockChildUrlLoss(list, emitCtx);
|
|
4816
|
+
flagImagesRenderedAsLinks(list, emitCtx);
|
|
4733
4817
|
return [{
|
|
4734
4818
|
type: "markdown",
|
|
4735
4819
|
text: md
|
|
4736
4820
|
}];
|
|
4737
4821
|
}
|
|
4738
4822
|
}, {
|
|
4823
|
+
budget: { blocks: 1 + hoistCount },
|
|
4824
|
+
emit: (emitCtx) => {
|
|
4825
|
+
flagUnicodeCheckboxes(list, emitCtx);
|
|
4826
|
+
return emitUnicodeTaskBlocks(list, emitCtx, HOIST_IMAGES);
|
|
4827
|
+
}
|
|
4828
|
+
}];
|
|
4829
|
+
if (hoistCount > 0) candidates.push({
|
|
4739
4830
|
budget: { blocks: 1 },
|
|
4740
|
-
emit: (
|
|
4741
|
-
|
|
4742
|
-
|
|
4831
|
+
emit: (emitCtx) => {
|
|
4832
|
+
flagUnicodeCheckboxes(list, emitCtx);
|
|
4833
|
+
emitCtx.degradations.add({
|
|
4743
4834
|
nodeType: "list",
|
|
4744
|
-
from: "
|
|
4745
|
-
to: "
|
|
4746
|
-
reason: "
|
|
4747
|
-
path:
|
|
4835
|
+
from: "rich_text+images",
|
|
4836
|
+
to: "rich_text",
|
|
4837
|
+
reason: "Budget exhausted for hoisted task list images; emitting text only",
|
|
4838
|
+
path: emitCtx.path
|
|
4748
4839
|
});
|
|
4749
|
-
return
|
|
4840
|
+
return emitUnicodeTaskBlocks(list, emitCtx, TEXT_ONLY);
|
|
4750
4841
|
}
|
|
4751
|
-
}
|
|
4842
|
+
});
|
|
4843
|
+
return candidates;
|
|
4752
4844
|
}
|
|
4753
4845
|
};
|
|
4754
|
-
function
|
|
4846
|
+
function flagUnicodeCheckboxes(list, ctx) {
|
|
4847
|
+
flagBlockChildUrlLoss(list, ctx);
|
|
4848
|
+
ctx.degradations.add({
|
|
4849
|
+
nodeType: "list",
|
|
4850
|
+
from: "markdown.task_list",
|
|
4851
|
+
to: "rich_text_list.unicode_checkbox",
|
|
4852
|
+
reason: "markdownBlock unavailable or budget exhausted",
|
|
4853
|
+
path: ctx.path
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
/**
|
|
4857
|
+
* URLs inside a block child of an item. Both rungs flatten a table or code fence
|
|
4858
|
+
* to plain text, which keeps labels only, so this loss is real on either path.
|
|
4859
|
+
* Inline links in a paragraph are not flagged: both rungs now carry them.
|
|
4860
|
+
*/
|
|
4861
|
+
function flagBlockChildUrlLoss(list, ctx) {
|
|
4862
|
+
for (const item of list.children) for (const child of item.children) {
|
|
4863
|
+
if (child.type === "list") {
|
|
4864
|
+
flagBlockChildUrlLoss(child, ctx);
|
|
4865
|
+
continue;
|
|
4866
|
+
}
|
|
4867
|
+
if (child.type === "paragraph") continue;
|
|
4868
|
+
const urls = [];
|
|
4869
|
+
collectFlattenedUrls(child, urls);
|
|
4870
|
+
for (const { nodeType, url } of urls) ctx.degradations.add({
|
|
4871
|
+
nodeType,
|
|
4872
|
+
from: url,
|
|
4873
|
+
to: "label_only",
|
|
4874
|
+
reason: "Block child of task list item flattened into text; URL dropped",
|
|
4875
|
+
path: ctx.path
|
|
4876
|
+
});
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
/**
|
|
4880
|
+
* Slack renders `` in a `markdown` block as link text, so on that rung
|
|
4881
|
+
* the URL survives but the image does not. A fidelity note, not a dropped URL.
|
|
4882
|
+
* The rich_text rung hoists images into real image blocks instead.
|
|
4883
|
+
*/
|
|
4884
|
+
function flagImagesRenderedAsLinks(list, ctx) {
|
|
4885
|
+
for (const item of list.children) for (const child of item.children) {
|
|
4886
|
+
if (child.type === "list") {
|
|
4887
|
+
flagImagesRenderedAsLinks(child, ctx);
|
|
4888
|
+
continue;
|
|
4889
|
+
}
|
|
4890
|
+
if (child.type !== "paragraph") continue;
|
|
4891
|
+
const urls = [];
|
|
4892
|
+
collectRenderedImageUrls(child, ctx.definitions, urls);
|
|
4893
|
+
for (const url of urls) ctx.degradations.add({
|
|
4894
|
+
nodeType: "image",
|
|
4895
|
+
from: url,
|
|
4896
|
+
to: "link",
|
|
4897
|
+
reason: "Slack renders images in a markdown block as link text",
|
|
4898
|
+
path: ctx.path
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
/**
|
|
4903
|
+
* Image URLs this rung will render as link text, reference images included.
|
|
4904
|
+
* {@link collectFlattenedUrls} cannot see those: a reference carries an identifier,
|
|
4905
|
+
* not a url. But {@link phrasingToMarkdown} resolves it and emits the same
|
|
4906
|
+
* ``, so it loses the same fidelity and owes the same note. An
|
|
4907
|
+
* unresolvable reference emits no image, and so is correctly skipped here.
|
|
4908
|
+
*/
|
|
4909
|
+
function collectRenderedImageUrls(node, definitions, into) {
|
|
4910
|
+
if (node === null || typeof node !== "object") return;
|
|
4911
|
+
const record = node;
|
|
4912
|
+
if (record.type === "image" && typeof record.url === "string") into.push(record.url);
|
|
4913
|
+
if (record.type === "imageReference" && typeof record.identifier === "string") {
|
|
4914
|
+
const resolved = definitions.get(record.identifier)?.url;
|
|
4915
|
+
if (resolved !== void 0) into.push(resolved);
|
|
4916
|
+
}
|
|
4917
|
+
for (const child of record.children ?? []) collectRenderedImageUrls(child, definitions, into);
|
|
4918
|
+
}
|
|
4919
|
+
function serializeTaskList(list, definitions, indent = 0) {
|
|
4755
4920
|
const pad = " ".repeat(indent);
|
|
4756
4921
|
const lines = [];
|
|
4757
4922
|
for (const item of list.children) {
|
|
4758
|
-
const
|
|
4759
|
-
|
|
4760
|
-
lines.push(
|
|
4761
|
-
for (const child of item.children) if (child.type === "list") lines.push(serializeTaskList(child, indent + 1));
|
|
4923
|
+
const text = itemMarkdown(item, definitions);
|
|
4924
|
+
lines.push(`${pad}${taskMarker(item.checked)}${escapeLeadingCheckbox(item.checked, text)}`);
|
|
4925
|
+
for (const child of item.children) if (child.type === "list") lines.push(serializeTaskList(child, definitions, indent + 1));
|
|
4762
4926
|
}
|
|
4763
4927
|
return lines.join("\n");
|
|
4764
4928
|
}
|
|
4765
4929
|
/**
|
|
4766
|
-
*
|
|
4767
|
-
* so
|
|
4930
|
+
* `checked` has three states, and a plain bullet is not an unchecked task. A
|
|
4931
|
+
* mixed list holds both, so collapsing `undefined` into `false` renders work the
|
|
4932
|
+
* author never wrote as unfinished. The rich_text rung already omits the prefix
|
|
4933
|
+
* for these items; this keeps the two rungs saying the same thing.
|
|
4768
4934
|
*/
|
|
4769
|
-
function
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4935
|
+
function taskMarker(checked) {
|
|
4936
|
+
if (checked === true) return "- [x] ";
|
|
4937
|
+
if (checked === false) return "- [ ] ";
|
|
4938
|
+
return "- ";
|
|
4939
|
+
}
|
|
4940
|
+
/**
|
|
4941
|
+
* A plain item whose text happens to start with `[x]` would be re-parsed as a
|
|
4942
|
+
* checked task once the marker in front of it is a bare bullet, which is the same
|
|
4943
|
+
* meaning change {@link taskMarker} exists to avoid. `- [x] ` with a trailing
|
|
4944
|
+
* space is exactly this: GFM reads it as a plain item whose literal text is
|
|
4945
|
+
* `[x]`. Escaping the bracket keeps it literal.
|
|
4946
|
+
*/
|
|
4947
|
+
function escapeLeadingCheckbox(checked, text) {
|
|
4948
|
+
if (checked !== void 0 && checked !== null) return text;
|
|
4949
|
+
return /^\[[ xX]\]/.test(text) ? `\\${text}` : text;
|
|
4779
4950
|
}
|
|
4780
|
-
|
|
4951
|
+
/**
|
|
4952
|
+
* Item content as inline markdown. Slack renders links, emphasis and code in a
|
|
4953
|
+
* `markdown` block, so flattening those to bare text would discard fidelity the
|
|
4954
|
+
* destination carries natively.
|
|
4955
|
+
*/
|
|
4956
|
+
function itemMarkdown(item, definitions) {
|
|
4781
4957
|
const parts = [];
|
|
4782
|
-
for (const child of item.children) if (child.type === "paragraph") parts.push(
|
|
4783
|
-
else if (child.type !== "list") parts.push(
|
|
4958
|
+
for (const child of item.children) if (child.type === "paragraph") parts.push(phrasingToMarkdown(child.children, definitions));
|
|
4959
|
+
else if (child.type !== "list") parts.push(blockToPlainText(child));
|
|
4784
4960
|
return parts.join(" ").trim();
|
|
4785
4961
|
}
|
|
4786
|
-
|
|
4787
|
-
|
|
4962
|
+
/** Phrasing content of a task item, for hoist counting and for emission. */
|
|
4963
|
+
function taskItemPhrasing(item) {
|
|
4964
|
+
const phrasing = [];
|
|
4965
|
+
for (const child of item.children) {
|
|
4966
|
+
let segment = [];
|
|
4967
|
+
if (child.type === "paragraph") segment = child.children;
|
|
4968
|
+
else if (child.type !== "list") {
|
|
4969
|
+
const text = blockToPlainText(child);
|
|
4970
|
+
segment = text.length > 0 ? [{
|
|
4971
|
+
type: "text",
|
|
4972
|
+
value: text
|
|
4973
|
+
}] : [];
|
|
4974
|
+
}
|
|
4975
|
+
if (segment.length === 0) continue;
|
|
4976
|
+
if (phrasing.length > 0) phrasing.push({
|
|
4977
|
+
type: "text",
|
|
4978
|
+
value: " "
|
|
4979
|
+
});
|
|
4980
|
+
phrasing.push(...segment);
|
|
4981
|
+
}
|
|
4982
|
+
return phrasing;
|
|
4983
|
+
}
|
|
4984
|
+
function countHoistableImagesInTaskList(list, ctx) {
|
|
4985
|
+
let count = 0;
|
|
4986
|
+
for (const item of list.children) {
|
|
4987
|
+
count += countHoistableImages(taskItemPhrasing(item), ctx);
|
|
4988
|
+
for (const child of item.children) if (child.type === "list") count += countHoistableImagesInTaskList(child, ctx);
|
|
4989
|
+
}
|
|
4990
|
+
return count;
|
|
4991
|
+
}
|
|
4992
|
+
/** Named at the call site, because `emitUnicodeTaskBlocks(list, ctx, true)` says nothing. */
|
|
4993
|
+
const HOIST_IMAGES = true;
|
|
4994
|
+
const TEXT_ONLY = false;
|
|
4995
|
+
function emitUnicodeTaskBlocks(list, ctx, hoistImages) {
|
|
4996
|
+
const { elements, hoistedImages } = emitUnicodeTaskTree(list, 0, ctx);
|
|
4997
|
+
const blocks = [{
|
|
4788
4998
|
type: "rich_text",
|
|
4789
|
-
elements
|
|
4790
|
-
};
|
|
4999
|
+
elements
|
|
5000
|
+
}];
|
|
5001
|
+
if (!hoistImages) return blocks;
|
|
5002
|
+
for (const image of hoistedImages) blocks.push(hoistedImageBlock(image, ctx));
|
|
5003
|
+
return blocks;
|
|
4791
5004
|
}
|
|
4792
5005
|
function emitUnicodeTaskTree(list, indent, ctx) {
|
|
4793
5006
|
let effectiveIndent = indent;
|
|
@@ -4802,6 +5015,7 @@ function emitUnicodeTaskTree(list, indent, ctx) {
|
|
|
4802
5015
|
effectiveIndent = 6;
|
|
4803
5016
|
}
|
|
4804
5017
|
const out = [];
|
|
5018
|
+
const hoistedImages = [];
|
|
4805
5019
|
let pendingSections = [];
|
|
4806
5020
|
const flush = () => {
|
|
4807
5021
|
if (pendingSections.length === 0) return;
|
|
@@ -4815,18 +5029,28 @@ function emitUnicodeTaskTree(list, indent, ctx) {
|
|
|
4815
5029
|
};
|
|
4816
5030
|
for (const item of list.children) {
|
|
4817
5031
|
const prefix = item.checked === true ? "☑ " : item.checked === false ? "☐ " : "";
|
|
5032
|
+
const inline = emitPhrasing(taskItemPhrasing(item), ctx);
|
|
5033
|
+
hoistedImages.push(...inline.hoistedImages);
|
|
5034
|
+
const elements = prefix === "" ? [...inline.elements] : [{
|
|
5035
|
+
type: "text",
|
|
5036
|
+
text: prefix
|
|
5037
|
+
}, ...inline.elements];
|
|
4818
5038
|
pendingSections.push({
|
|
4819
5039
|
type: "rich_text_section",
|
|
4820
|
-
elements: [{
|
|
5040
|
+
elements: elements.length > 0 ? elements : [{
|
|
4821
5041
|
type: "text",
|
|
4822
|
-
text:
|
|
5042
|
+
text: " "
|
|
4823
5043
|
}]
|
|
4824
5044
|
});
|
|
4825
5045
|
const childLists = [];
|
|
4826
5046
|
for (const child of item.children) if (child.type === "list") childLists.push(child);
|
|
4827
5047
|
if (childLists.length === 0) continue;
|
|
4828
5048
|
flush();
|
|
4829
|
-
for (const child of childLists)
|
|
5049
|
+
for (const child of childLists) {
|
|
5050
|
+
const nested = emitUnicodeTaskTree(child, effectiveIndent + 1, ctx);
|
|
5051
|
+
out.push(...nested.elements);
|
|
5052
|
+
hoistedImages.push(...nested.hoistedImages);
|
|
5053
|
+
}
|
|
4830
5054
|
}
|
|
4831
5055
|
flush();
|
|
4832
5056
|
if (out.length === 0) out.push({
|
|
@@ -4841,7 +5065,10 @@ function emitUnicodeTaskTree(list, indent, ctx) {
|
|
|
4841
5065
|
}],
|
|
4842
5066
|
indent: effectiveIndent
|
|
4843
5067
|
});
|
|
4844
|
-
return
|
|
5068
|
+
return {
|
|
5069
|
+
elements: out,
|
|
5070
|
+
hoistedImages
|
|
5071
|
+
};
|
|
4845
5072
|
}
|
|
4846
5073
|
//#endregion
|
|
4847
5074
|
//#region src/adapters/thematic-break-adapter.ts
|