@sillsdev/docu-notion 1.0.0-alpha.4 → 1.0.0-alpha.6

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:
@@ -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/dist/transform.js CHANGED
@@ -113,13 +113,19 @@ function doTransformsOnMarkdown(context, config, input) {
113
113
  let body = input;
114
114
  //console.log("body before regex: " + body);
115
115
  let match;
116
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
117
116
  for (const mod of regexMods) {
118
- let replacement = undefined;
119
117
  // regex.exec is stateful, so we don't want to mess up the plugin's use of its own regex, so we clone it.
120
118
  // we also add the "g" flag to make sure we get all matches
121
119
  const regex = new RegExp(`${codeBlocks.source}|(${mod.regex.source})`, "g");
122
- while ((match = regex.exec(input)) !== null) {
120
+ // We match against `source` (a snapshot of the current body) and build up a
121
+ // fresh `result`. Mutating the string we're iterating over would invalidate
122
+ // `match.index` for every subsequent match once a replacement changes the
123
+ // length, so instead we copy the spans between matches using indices that
124
+ // always refer to the unchanged `source`.
125
+ const source = body;
126
+ let result = "";
127
+ let lastIndex = 0;
128
+ while ((match = regex.exec(source)) !== null) {
123
129
  if (match[0]) {
124
130
  const original = match[0];
125
131
  if (original.startsWith("```") &&
@@ -127,6 +133,7 @@ function doTransformsOnMarkdown(context, config, input) {
127
133
  !mod.includeCodeBlocks) {
128
134
  continue; // code block, and they didn't say to include them
129
135
  }
136
+ let replacement = undefined;
130
137
  if (mod.getReplacement) {
131
138
  // our match here has an extra group, which is an implementation detail
132
139
  // that shouldn't be made visible to the plugin
@@ -138,11 +145,11 @@ function doTransformsOnMarkdown(context, config, input) {
138
145
  }
139
146
  if (replacement !== undefined) {
140
147
  (0, log_1.verbose)(`[${mod.name}] ${original} --> ${replacement}`);
141
- const precedingPart = body.substring(0, match.index); // ?
142
- const partStartingFromThisMatch = body.substring(match.index); // ?
143
- body =
144
- precedingPart +
145
- partStartingFromThisMatch.replace(original, replacement);
148
+ // copy everything since the last match, then the replacement (the
149
+ // replacement is appended literally, so `$` sequences in it are not
150
+ // given any special meaning)
151
+ result += source.substring(lastIndex, match.index) + replacement;
152
+ lastIndex = match.index + original.length;
146
153
  // add any library imports
147
154
  if (!context.imports)
148
155
  context.imports = [];
@@ -150,6 +157,9 @@ function doTransformsOnMarkdown(context, config, input) {
150
157
  }
151
158
  }
152
159
  }
160
+ // copy whatever follows the last replacement (including any skipped code blocks)
161
+ result += source.substring(lastIndex);
162
+ body = result;
153
163
  }
154
164
  (0, log_1.logDebug)("doTransformsOnMarkdown", "body after regex: " + body);
155
165
  return body;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const log_1 = require("./log");
13
+ const pluginTestRun_1 = require("./plugins/pluginTestRun");
14
+ function paragraph(text) {
15
+ return {
16
+ type: "paragraph",
17
+ paragraph: {
18
+ rich_text: [
19
+ {
20
+ type: "text",
21
+ text: { content: text, link: null },
22
+ annotations: {
23
+ bold: false,
24
+ italic: false,
25
+ strikethrough: false,
26
+ underline: false,
27
+ code: false,
28
+ color: "default",
29
+ },
30
+ plain_text: text,
31
+ href: null,
32
+ },
33
+ ],
34
+ },
35
+ };
36
+ }
37
+ // Regression test for the bug where doTransformsOnMarkdown matched against the
38
+ // original `input` but sliced/replaced the mutating `body`. Once the first
39
+ // replacement changes the string length, every subsequent match.index is wrong.
40
+ // We make the first replacement SHRINK the text so the second match.index lands
41
+ // past the real match, and the inner .replace silently drops the transformation.
42
+ test("applies every match even after a length-changing replacement", () => __awaiter(void 0, void 0, void 0, function* () {
43
+ (0, log_1.setLogLevel)("verbose");
44
+ const p = {
45
+ name: "test",
46
+ regexMarkdownModifications: [
47
+ {
48
+ regex: /REMOVE/,
49
+ replacementPattern: `X`, // shorter than the match, so the body shrinks
50
+ },
51
+ ],
52
+ };
53
+ const config = { plugins: [p] };
54
+ // Produces markdown: "REMOVE\n\nREMOVE keep"
55
+ const result = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [
56
+ paragraph("REMOVE"),
57
+ paragraph("REMOVE keep"),
58
+ ]);
59
+ // Both occurrences must be transformed; none should survive.
60
+ expect(result).not.toContain("REMOVE");
61
+ expect((result.match(/X/g) || []).length).toBe(2);
62
+ expect(result).toContain("keep");
63
+ }));
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.4"
101
+ "version": "1.0.0-alpha.6"
102
102
  }