@sillsdev/docu-notion 1.0.0-alpha.5 → 1.0.0-alpha.7

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 CHANGED
@@ -58,7 +58,7 @@ means that the ID is `0456aa5842946PRETEND4f37c97a0e5`.
58
58
  Try it out:
59
59
 
60
60
  ```
61
- npx @sillsdev/docu-notion -n secret_PRETEND123456789PRETEND123456789PRETEND6789 -r 0456aa5842946PRETEND4f37c97a0e5"
61
+ npx @sillsdev/docu-notion -n secret_PRETEND123456789PRETEND123456789PRETEND6789 -r 0456aa5842946PRETEND4f37c97a0e5
62
62
  ```
63
63
 
64
64
  Likely, you will want to store these codes in your environment variables and then use them like this:
@@ -6,8 +6,26 @@ exports.gifEmbed = {
6
6
  regexMarkdownModifications: [
7
7
  {
8
8
  // I once saw a gif coming from Notion that wasn't a full
9
- // url, which wouldn't work, hence the "http" requirement
10
- regex: /\[.*?\]\((http.*?(\.(gif|GIF)))\)/,
9
+ // url, which wouldn't work, hence the "http" requirement.
10
+ //
11
+ // We only embed when the link's text is an AUTO-GENERATED label, not
12
+ // something the author typed. By the time this regex runs the label can
13
+ // take three auto shapes:
14
+ // - empty `[]` (an embed)
15
+ // - the literal `[bookmark]` placeholder notion-to-md emits for
16
+ // bookmark blocks (seen when standardExternalLinkConversion isn't in
17
+ // the config, e.g. the unit tests)
18
+ // - the URL repeated as the label, `[http...gif](http...gif)`, which is
19
+ // what standardExternalLinkConversion rewrites `[bookmark]` into in
20
+ // the production config (it runs in an earlier phase, see transform.ts)
21
+ // A link the author gave real text, e.g. `[see animation](...gif)`, is
22
+ // none of these, so it's left as a clickable link rather than turned into
23
+ // an image (which would also throw the text away).
24
+ //
25
+ // The optional leading "!" lets us match (and replace) a link that
26
+ // another mod has already turned into an image, instead of prepending
27
+ // a second "!" and producing "!![](...)".
28
+ regex: /!?\[(?:bookmark|https?:\/\/[^\]]*\.(?:gif|GIF)[^\]]*)?\]\((http.*?(\.(gif|GIF)))\)/,
11
29
  replacementPattern: `![]($1)`,
12
30
  },
13
31
  ],
@@ -16,7 +34,14 @@ exports.imgurGifEmbed = {
16
34
  name: "imgur",
17
35
  regexMarkdownModifications: [
18
36
  {
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
20
45
  // imgur links to gifs need a .gif at the end, but the url they give you doesn't have one.
21
46
  replacementPattern: `![]($1.gif)`,
22
47
  },
@@ -8,10 +8,38 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  const log_1 = require("../log");
13
16
  const pluginTestRun_1 = require("./pluginTestRun");
14
17
  const embedTweaks_1 = require("./embedTweaks");
18
+ const externalLinks_1 = require("./externalLinks");
19
+ const default_docunotion_config_1 = __importDefault(require("../config/default.docunotion.config"));
20
+ function paragraph(text) {
21
+ return {
22
+ type: "paragraph",
23
+ paragraph: {
24
+ rich_text: [
25
+ {
26
+ type: "text",
27
+ text: { content: text, link: null },
28
+ annotations: {
29
+ bold: false,
30
+ italic: false,
31
+ strikethrough: false,
32
+ underline: false,
33
+ code: false,
34
+ color: "default",
35
+ },
36
+ plain_text: text,
37
+ href: null,
38
+ },
39
+ ],
40
+ },
41
+ };
42
+ }
15
43
  test("imgur", () => __awaiter(void 0, void 0, void 0, function* () {
16
44
  (0, log_1.setLogLevel)("verbose");
17
45
  const config = { plugins: [embedTweaks_1.imgurGifEmbed] };
@@ -25,6 +53,93 @@ test("imgur", () => __awaiter(void 0, void 0, void 0, function* () {
25
53
  ]);
26
54
  expect(result.trim()).toBe(`![](https://imgur.com/gallery/U8TTNuI.gif)`);
27
55
  }));
56
+ // Regression test for the "!![](...)" bug: with both plugins active (the
57
+ // default config order), the imgur mod turns a bare imgur link into an image,
58
+ // then the gif mod sees that ".gif" image and must not prepend a second "!".
59
+ test("imgur + gif together produce a single leading bang", () => __awaiter(void 0, void 0, void 0, function* () {
60
+ (0, log_1.setLogLevel)("verbose");
61
+ const config = { plugins: [embedTweaks_1.imgurGifEmbed, embedTweaks_1.gifEmbed] };
62
+ const result = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [
63
+ {
64
+ object: "block",
65
+ id: "e36710d8-98ad-40dc-b41b-b376ebdd6894",
66
+ type: "bookmark",
67
+ bookmark: { caption: [], url: "https://imgur.com/E83qLj6" },
68
+ },
69
+ ]);
70
+ expect(result.trim()).toBe(`![](https://imgur.com/E83qLj6.gif)`);
71
+ expect(result).not.toContain("!![]");
72
+ }));
73
+ // An inline link the user gave real text to must stay a clickable link: we must
74
+ // not turn it into an image (which also discards the text). Uses the default
75
+ // config order so both mods get a crack at it.
76
+ test("imgur link with author text is left as a clickable link", () => __awaiter(void 0, void 0, void 0, function* () {
77
+ (0, log_1.setLogLevel)("verbose");
78
+ const config = { plugins: [embedTweaks_1.imgurGifEmbed, embedTweaks_1.gifEmbed] };
79
+ const result = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [
80
+ paragraph("all at once ([see animation](https://imgur.com/gcrxl5k))."),
81
+ paragraph("(See an animation of these [new overlay features](https://imgur.com/E83qLj6))"),
82
+ ]);
83
+ expect(result).toContain("[see animation](https://imgur.com/gcrxl5k)");
84
+ expect(result).toContain("[new overlay features](https://imgur.com/E83qLj6)");
85
+ // nothing should have been turned into an image
86
+ expect(result).not.toContain("![]");
87
+ expect(result).not.toContain(".gif");
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", () => __awaiter(void 0, void 0, void 0, function* () {
93
+ (0, log_1.setLogLevel)("verbose");
94
+ const config = {
95
+ plugins: [externalLinks_1.standardExternalLinkConversion, embedTweaks_1.imgurGifEmbed, embedTweaks_1.gifEmbed],
96
+ };
97
+ const result = yield (0, pluginTestRun_1.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
+ },
104
+ ]);
105
+ expect(result.trim()).toBe(`![](https://imgur.com/gallery/U8TTNuI.gif)`);
106
+ expect(result).not.toContain("!![]");
107
+ }));
108
+ // ...but an authored inline link must survive that same pipeline as a link.
109
+ test("authored imgur link survives external-link conversion as a link", () => __awaiter(void 0, void 0, void 0, function* () {
110
+ (0, log_1.setLogLevel)("verbose");
111
+ const config = {
112
+ plugins: [externalLinks_1.standardExternalLinkConversion, embedTweaks_1.imgurGifEmbed, embedTweaks_1.gifEmbed],
113
+ };
114
+ const result = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [
115
+ paragraph("all at once ([see animation](https://imgur.com/gcrxl5k))."),
116
+ ]);
117
+ expect(result).toContain("[see animation](https://imgur.com/gcrxl5k)");
118
+ expect(result).not.toContain("![]");
119
+ expect(result).not.toContain(".gif");
120
+ }));
121
+ // Strongest guard: run the ACTUAL default production config (full plugin set
122
+ // and order) rather than a hand-picked subset, so future config drift (plugin
123
+ // reordering, the external-link converter changing) can't silently reopen the
124
+ // bug. A Notion bookmark to imgur must embed; an authored inline link must stay
125
+ // a clickable link.
126
+ test("default production config: bookmark embeds, authored link stays a link", () => __awaiter(void 0, void 0, void 0, function* () {
127
+ (0, log_1.setLogLevel)("verbose");
128
+ const result = yield (0, pluginTestRun_1.blocksToMarkdown)(default_docunotion_config_1.default, [
129
+ {
130
+ object: "block",
131
+ id: "e36710d8-98ad-40dc-b41b-b376ebdd6894",
132
+ type: "bookmark",
133
+ bookmark: { caption: [], url: "https://imgur.com/gallery/U8TTNuI" },
134
+ },
135
+ paragraph("all at once ([see animation](https://imgur.com/gcrxl5k))."),
136
+ ]);
137
+ // the bookmark became an embedded gif...
138
+ expect(result).toContain("![](https://imgur.com/gallery/U8TTNuI.gif)");
139
+ expect(result).not.toContain("!![]");
140
+ // ...while the authored link is untouched
141
+ expect(result).toContain("[see animation](https://imgur.com/gcrxl5k)");
142
+ }));
28
143
  test("gif", () => __awaiter(void 0, void 0, void 0, function* () {
29
144
  (0, log_1.setLogLevel)("verbose");
30
145
  const config = { plugins: [embedTweaks_1.gifEmbed] };
@@ -29,7 +29,10 @@ function normalizeLinkBaseId(baseLinkId) {
29
29
  }
30
30
  }
31
31
  const withoutLeadingSlash = withoutQuery.replace(/^\/+/, "");
32
- return withoutLeadingSlash;
32
+ // Newer Notion links can also use a relative "/p/<page-id>" form.
33
+ return withoutLeadingSlash.startsWith("p/")
34
+ ? withoutLeadingSlash.substring(2)
35
+ : withoutLeadingSlash;
33
36
  }
34
37
  // converts a url to a local link, if it is a link to a page in the Notion site
35
38
  // only here for plugins, notion won't normally be giving us raw urls (at least not that I've noticed)
@@ -125,7 +128,7 @@ exports.standardInternalLinkConversion = {
125
128
  // "Mention" links come in as full URLs, e.g. [link_to_page](https://www.notion.so/62f1187010214b0883711a1abb277d31)
126
129
  // Newer Notion links can also use app.notion.com, including /p/<page-id> URLs.
127
130
  // YOu can create them either with @+the name of a page, or by pasting a URL and then selecting the "Mention" option.
128
- match: /\[([^\]]+)?\]\((?!mailto:)(https?:\/\/(?:www\.)?notion\.so\/[^),^/]+|https?:\/\/app\.notion\.com\/(?:p\/)?[^),^/]+|\/?[^),^/]+)\)/,
131
+ match: /\[([^\]]+)?\]\((?!mailto:)(https?:\/\/(?:www\.)?notion\.so\/[^),^/]+|https?:\/\/app\.notion\.com\/(?:p\/)?[^),^/]+|\/?(?:p\/)?[^),^/]+)\)/,
129
132
  convert: convertInternalLink,
130
133
  },
131
134
  };
@@ -121,6 +121,16 @@ test("parseLinkId extracts the page id from app.notion.com URLs", () => {
121
121
  fragmentId: "",
122
122
  });
123
123
  });
124
+ test("parseLinkId extracts the page id from relative /p/ links", () => {
125
+ expect((0, internalLinks_1.parseLinkId)("/p/123456781234123412341234567890ab")).toEqual({
126
+ baseLinkId: "123456781234123412341234567890ab",
127
+ fragmentId: "",
128
+ });
129
+ expect((0, internalLinks_1.parseLinkId)("/p/123456781234123412341234567890ab#heading")).toEqual({
130
+ baseLinkId: "123456781234123412341234567890ab",
131
+ fragmentId: "#heading",
132
+ });
133
+ });
124
134
  test("link to an existing page on this site that has no slug", () => __awaiter(void 0, void 0, void 0, function* () {
125
135
  const targetPageId = "123";
126
136
  const targetPage = (0, pluginTestRun_1.makeSamplePageObject)({
@@ -183,6 +193,68 @@ test("link to an existing page on this site that has no slug", () => __awaiter(v
183
193
  }, targetPage);
184
194
  expect(results.trim()).toBe(`Inline [great page](/${targetPageId}) the end.`);
185
195
  }));
196
+ test("inline link using the newer relative /p/ form", () => __awaiter(void 0, void 0, void 0, function* () {
197
+ const targetPageId = "123";
198
+ const targetPage = (0, pluginTestRun_1.makeSamplePageObject)({
199
+ slug: undefined,
200
+ name: "Hello World",
201
+ id: targetPageId,
202
+ });
203
+ const results = yield getMarkdown({
204
+ type: "paragraph",
205
+ paragraph: {
206
+ rich_text: [
207
+ {
208
+ type: "text",
209
+ text: { content: "Inline ", link: null },
210
+ annotations: {
211
+ bold: false,
212
+ italic: false,
213
+ strikethrough: false,
214
+ underline: false,
215
+ code: false,
216
+ color: "default",
217
+ },
218
+ plain_text: "Inline ",
219
+ href: null,
220
+ },
221
+ {
222
+ type: "text",
223
+ text: {
224
+ content: "great page",
225
+ link: { url: `/p/${targetPageId}` },
226
+ },
227
+ annotations: {
228
+ bold: false,
229
+ italic: false,
230
+ strikethrough: false,
231
+ underline: false,
232
+ code: false,
233
+ color: "default",
234
+ },
235
+ plain_text: "great page",
236
+ href: `/p/${targetPageId}`,
237
+ },
238
+ {
239
+ type: "text",
240
+ text: { content: " the end.", link: null },
241
+ annotations: {
242
+ bold: false,
243
+ italic: false,
244
+ strikethrough: false,
245
+ underline: false,
246
+ code: false,
247
+ color: "default",
248
+ },
249
+ plain_text: " the end.",
250
+ href: null,
251
+ },
252
+ ],
253
+ color: "default",
254
+ },
255
+ }, targetPage);
256
+ expect(results.trim()).toBe(`Inline [great page](/${targetPageId}) the end.`);
257
+ }));
186
258
  test("link to a heading block on a page", () => __awaiter(void 0, void 0, void 0, function* () {
187
259
  const targetPageId = "123";
188
260
  const blocks = {
package/package.json CHANGED
@@ -98,5 +98,5 @@
98
98
  "esbuild@0.17.17": true,
99
99
  "fsevents": false
100
100
  },
101
- "version": "1.0.0-alpha.5"
101
+ "version": "1.0.0-alpha.7"
102
102
  }