Skip to content

Commit a40fa1c

Browse files
authored
Merge pull request #134 from sillsdev/fix-image-link-double-bang
Fix two problems with image links caused by regex fix
2 parents f3be228 + 3520700 commit a40fa1c

2 files changed

Lines changed: 153 additions & 3 deletions

File tree

src/plugins/embedTweaks.spec.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,33 @@ import { IPlugin } from "./pluginTypes";
33
import { setLogLevel } from "../log";
44
import { blocksToMarkdown } from "./pluginTestRun";
55
import { gifEmbed, imgurGifEmbed } from "./embedTweaks";
6+
import { standardExternalLinkConversion } from "./externalLinks";
7+
import defaultConfig from "../config/default.docunotion.config";
8+
import { NotionBlock as NB } from "../types";
9+
10+
function paragraph(text: string): NB {
11+
return {
12+
type: "paragraph",
13+
paragraph: {
14+
rich_text: [
15+
{
16+
type: "text",
17+
text: { content: text, link: null },
18+
annotations: {
19+
bold: false,
20+
italic: false,
21+
strikethrough: false,
22+
underline: false,
23+
code: false,
24+
color: "default",
25+
},
26+
plain_text: text,
27+
href: null,
28+
},
29+
],
30+
},
31+
} as unknown as NB;
32+
}
633

734
test("imgur", async () => {
835
setLogLevel("verbose");
@@ -18,6 +45,104 @@ test("imgur", async () => {
1845
expect(result.trim()).toBe(`![](https://imgur.com/gallery/U8TTNuI.gif)`);
1946
});
2047

48+
// Regression test for the "!![](...)" bug: with both plugins active (the
49+
// default config order), the imgur mod turns a bare imgur link into an image,
50+
// then the gif mod sees that ".gif" image and must not prepend a second "!".
51+
test("imgur + gif together produce a single leading bang", async () => {
52+
setLogLevel("verbose");
53+
const config = { plugins: [imgurGifEmbed, gifEmbed] };
54+
const result = await blocksToMarkdown(config, [
55+
{
56+
object: "block",
57+
id: "e36710d8-98ad-40dc-b41b-b376ebdd6894",
58+
type: "bookmark",
59+
bookmark: { caption: [], url: "https://imgur.com/E83qLj6" },
60+
} as unknown as NotionBlock,
61+
]);
62+
expect(result.trim()).toBe(`![](https://imgur.com/E83qLj6.gif)`);
63+
expect(result).not.toContain("!![]");
64+
});
65+
66+
// An inline link the user gave real text to must stay a clickable link: we must
67+
// not turn it into an image (which also discards the text). Uses the default
68+
// config order so both mods get a crack at it.
69+
test("imgur link with author text is left as a clickable link", async () => {
70+
setLogLevel("verbose");
71+
const config = { plugins: [imgurGifEmbed, gifEmbed] };
72+
const result = await blocksToMarkdown(config, [
73+
paragraph(
74+
"all at once ([see animation](https://imgur.com/gcrxl5k))."
75+
),
76+
paragraph(
77+
"(See an animation of these [new overlay features](https://imgur.com/E83qLj6))"
78+
),
79+
]);
80+
expect(result).toContain("[see animation](https://imgur.com/gcrxl5k)");
81+
expect(result).toContain(
82+
"[new overlay features](https://imgur.com/E83qLj6)"
83+
);
84+
// nothing should have been turned into an image
85+
expect(result).not.toContain("![]");
86+
expect(result).not.toContain(".gif");
87+
});
88+
89+
// Production-realistic: standardExternalLinkConversion runs in an earlier phase
90+
// and rewrites a Notion `[bookmark](url)` into `[url](url)` before the embed
91+
// regexes run. The embed must still recognize that as an auto-label and embed it.
92+
test("imgur bookmark still embeds after external-link conversion", async () => {
93+
setLogLevel("verbose");
94+
const config = {
95+
plugins: [standardExternalLinkConversion, imgurGifEmbed, gifEmbed],
96+
};
97+
const result = await blocksToMarkdown(config, [
98+
{
99+
object: "block",
100+
id: "e36710d8-98ad-40dc-b41b-b376ebdd6894",
101+
type: "bookmark",
102+
bookmark: { caption: [], url: "https://imgur.com/gallery/U8TTNuI" },
103+
} as unknown as NotionBlock,
104+
]);
105+
expect(result.trim()).toBe(`![](https://imgur.com/gallery/U8TTNuI.gif)`);
106+
expect(result).not.toContain("!![]");
107+
});
108+
109+
// ...but an authored inline link must survive that same pipeline as a link.
110+
test("authored imgur link survives external-link conversion as a link", async () => {
111+
setLogLevel("verbose");
112+
const config = {
113+
plugins: [standardExternalLinkConversion, imgurGifEmbed, gifEmbed],
114+
};
115+
const result = await blocksToMarkdown(config, [
116+
paragraph("all at once ([see animation](https://imgur.com/gcrxl5k))."),
117+
]);
118+
expect(result).toContain("[see animation](https://imgur.com/gcrxl5k)");
119+
expect(result).not.toContain("![]");
120+
expect(result).not.toContain(".gif");
121+
});
122+
123+
// Strongest guard: run the ACTUAL default production config (full plugin set
124+
// and order) rather than a hand-picked subset, so future config drift (plugin
125+
// reordering, the external-link converter changing) can't silently reopen the
126+
// bug. A Notion bookmark to imgur must embed; an authored inline link must stay
127+
// a clickable link.
128+
test("default production config: bookmark embeds, authored link stays a link", async () => {
129+
setLogLevel("verbose");
130+
const result = await blocksToMarkdown(defaultConfig, [
131+
{
132+
object: "block",
133+
id: "e36710d8-98ad-40dc-b41b-b376ebdd6894",
134+
type: "bookmark",
135+
bookmark: { caption: [], url: "https://imgur.com/gallery/U8TTNuI" },
136+
} as unknown as NotionBlock,
137+
paragraph("all at once ([see animation](https://imgur.com/gcrxl5k))."),
138+
]);
139+
// the bookmark became an embedded gif...
140+
expect(result).toContain("![](https://imgur.com/gallery/U8TTNuI.gif)");
141+
expect(result).not.toContain("!![]");
142+
// ...while the authored link is untouched
143+
expect(result).toContain("[see animation](https://imgur.com/gcrxl5k)");
144+
});
145+
21146
test("gif", async () => {
22147
setLogLevel("verbose");
23148
const config = { plugins: [gifEmbed] };

src/plugins/embedTweaks.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,26 @@ export const gifEmbed: IPlugin = {
55
regexMarkdownModifications: [
66
{
77
// I once saw a gif coming from Notion that wasn't a full
8-
// url, which wouldn't work, hence the "http" requirement
9-
regex: /\[.*?\]\((http.*?(\.(gif|GIF)))\)/,
8+
// url, which wouldn't work, hence the "http" requirement.
9+
//
10+
// We only embed when the link's text is an AUTO-GENERATED label, not
11+
// something the author typed. By the time this regex runs the label can
12+
// take three auto shapes:
13+
// - empty `[]` (an embed)
14+
// - the literal `[bookmark]` placeholder notion-to-md emits for
15+
// bookmark blocks (seen when standardExternalLinkConversion isn't in
16+
// the config, e.g. the unit tests)
17+
// - the URL repeated as the label, `[http...gif](http...gif)`, which is
18+
// what standardExternalLinkConversion rewrites `[bookmark]` into in
19+
// the production config (it runs in an earlier phase, see transform.ts)
20+
// A link the author gave real text, e.g. `[see animation](...gif)`, is
21+
// none of these, so it's left as a clickable link rather than turned into
22+
// an image (which would also throw the text away).
23+
//
24+
// The optional leading "!" lets us match (and replace) a link that
25+
// another mod has already turned into an image, instead of prepending
26+
// a second "!" and producing "!![](...)".
27+
regex: /!?\[(?:bookmark|https?:\/\/[^\]]*\.(?:gif|GIF)[^\]]*)?\]\((http.*?(\.(gif|GIF)))\)/,
1028
replacementPattern: `![]($1)`,
1129
},
1230
],
@@ -16,7 +34,14 @@ export const imgurGifEmbed: IPlugin = {
1634
name: "imgur",
1735
regexMarkdownModifications: [
1836
{
19-
regex: /\[.*?\]\((.*?imgur\.com\/.*?)\)/, // imgur.com
37+
// Only embed links whose text is an auto-generated label (empty `[]`,
38+
// `[bookmark]`, or the URL repeated as the label `[http...imgur.com/...]`),
39+
// so that a link the author gave real text to is kept as a clickable link
40+
// rather than converted to an image. See the longer note in gifEmbed above
41+
// for why all three label shapes can occur here.
42+
// The optional leading "!" lets us match a link that's already an image
43+
// without prepending a second "!".
44+
regex: /!?\[(?:bookmark|https?:\/\/[^\]]*imgur\.com\/[^\]]*)?\]\((.*?imgur\.com\/.*?)\)/, // imgur.com
2045
// imgur links to gifs need a .gif at the end, but the url they give you doesn't have one.
2146
replacementPattern: `![]($1.gif)`,
2247
},

0 commit comments

Comments
 (0)