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

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/pull.js CHANGED
@@ -66,16 +66,19 @@ function getOptionsForLogging(options) {
66
66
  const kNotionApiVersion = "2026-03-11";
67
67
  let layoutStrategy;
68
68
  let notionToMarkdown;
69
- const pages = new Array();
70
- const counts = {
71
- output_normally: 0,
72
- skipped_because_empty: 0,
73
- skipped_because_status: 0,
74
- skipped_because_level_cannot_have_content: 0,
75
- error_because_no_slug: 0,
76
- };
77
69
  function notionPull(options) {
78
70
  return __awaiter(this, void 0, void 0, function* () {
71
+ // These are local to each pull so that repeated calls (e.g. in tests or
72
+ // programmatic multi-run scenarios) don't accumulate pages or counts from
73
+ // previous runs.
74
+ const pages = new Array();
75
+ const counts = {
76
+ output_normally: 0,
77
+ skipped_because_empty: 0,
78
+ skipped_because_status: 0,
79
+ skipped_because_level_cannot_have_content: 0,
80
+ error_because_no_slug: 0,
81
+ };
79
82
  // It's helpful when troubleshooting CI secrets and environment variables to see what options actually made it to docu-notion.
80
83
  const optionsForLogging = getOptionsForLogging(options);
81
84
  const config = yield (0, configuration_1.loadConfigAsync)();
@@ -97,12 +100,12 @@ function notionPull(options) {
97
100
  (0, process_1.exit)(1);
98
101
  }
99
102
  (0, log_1.group)("Stage 1: walk children of the page named 'Outline', looking for pages...");
100
- yield getPagesRecursively(options, "", options.rootPage, 0, true);
103
+ yield getPagesRecursively(options, "", options.rootPage, 0, true, pages, counts);
101
104
  (0, log_1.logDebug)("getPagesRecursively", JSON.stringify(pages, null, 2));
102
105
  (0, log_1.info)(`Found ${pages.length} pages`);
103
106
  (0, log_1.endGroup)();
104
107
  (0, log_1.group)(`Stage 2: convert ${pages.length} Notion pages to markdown and save locally...`);
105
- yield outputPages(options, config, pages);
108
+ yield outputPages(options, config, pages, counts);
106
109
  (0, log_1.endGroup)();
107
110
  (0, log_1.group)("Stage 3: clean up old files & images...");
108
111
  yield layoutStrategy.cleanupOldFiles();
@@ -110,7 +113,7 @@ function notionPull(options) {
110
113
  (0, log_1.endGroup)();
111
114
  });
112
115
  }
113
- function outputPages(options, config, pages) {
116
+ function outputPages(options, config, pages, counts) {
114
117
  return __awaiter(this, void 0, void 0, function* () {
115
118
  const context = {
116
119
  getBlockChildren: getBlockChildren,
@@ -124,7 +127,7 @@ function outputPages(options, config, pages) {
124
127
  notionToMarkdown: notionToMarkdown,
125
128
  options: options,
126
129
  pages: pages,
127
- counts: counts, // review will this get copied or pointed to?
130
+ counts: counts,
128
131
  imports: [],
129
132
  convertNotionLinkToLocalDocusaurusLink: (url) => (0, internalLinks_1.convertInternalUrl)(context, url),
130
133
  };
@@ -141,7 +144,7 @@ function outputPages(options, config, pages) {
141
144
  context.options.statusTag != "*" &&
142
145
  page.status !== context.options.statusTag) {
143
146
  (0, log_1.verbose)(`Skipping page because status is not '${context.options.statusTag}': ${page.nameOrTitle}`);
144
- ++context.counts.skipped_because_status;
147
+ ++counts.skipped_because_status;
145
148
  }
146
149
  else {
147
150
  if (options.requireSlugs && !page.hasExplicitSlug) {
@@ -149,7 +152,7 @@ function outputPages(options, config, pages) {
149
152
  ++counts.error_because_no_slug;
150
153
  }
151
154
  const markdown = yield (0, transform_1.getMarkdownForPage)(config, context, page);
152
- writePage(page, markdown);
155
+ writePage(page, markdown, counts);
153
156
  }
154
157
  }
155
158
  if (counts.error_because_no_slug > 0)
@@ -164,7 +167,7 @@ function outputPages(options, config, pages) {
164
167
  // outline that contain content (which we call "Simple" pages). Later, we can
165
168
  // then step through this list creating the files we need, and, crucially, be
166
169
  // able to figure out what the url will be for any links between content pages.
167
- function getPagesRecursively(options, incomingContext, pageIdOfThisParent, orderOfThisParent, rootLevel) {
170
+ function getPagesRecursively(options, incomingContext, pageIdOfThisParent, orderOfThisParent, rootLevel, pages, counts) {
168
171
  return __awaiter(this, void 0, void 0, function* () {
169
172
  var _a;
170
173
  const pageInTheOutline = yield fromPageId(incomingContext, pageIdOfThisParent, orderOfThisParent, true);
@@ -197,7 +200,7 @@ function getPagesRecursively(options, incomingContext, pageIdOfThisParent, order
197
200
  layoutContext = layoutStrategy.newLevel(options.markdownOutputPath, pageInTheOutline.order, incomingContext, pageInTheOutline.nameOrTitle);
198
201
  }
199
202
  for (const childPageInfo of pageInfo.childPageIdsAndOrder) {
200
- yield getPagesRecursively(options, layoutContext, childPageInfo.id, childPageInfo.order, false);
203
+ yield getPagesRecursively(options, layoutContext, childPageInfo.id, childPageInfo.order, false, pages, counts);
201
204
  }
202
205
  for (const linkPageInfo of pageInfo.linksPageIdsAndOrder) {
203
206
  pages.push(yield fromPageId(layoutContext, linkPageInfo.id, linkPageInfo.order, false));
@@ -209,7 +212,7 @@ function getPagesRecursively(options, incomingContext, pageIdOfThisParent, order
209
212
  }
210
213
  });
211
214
  }
212
- function writePage(page, finalMarkdown) {
215
+ function writePage(page, finalMarkdown, counts) {
213
216
  const mdPath = layoutStrategy.getPathForPage(page, ".md");
214
217
  (0, log_1.verbose)(`writing ${mdPath}`);
215
218
  fs.writeFileSync(mdPath, finalMarkdown, {});
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
@@ -54,6 +54,7 @@
54
54
  "lint-staged": "^10.5.4",
55
55
  "prettier": "^2.2.1",
56
56
  "rimraf": "^4.1.2",
57
+ "semantic-release": "25.0.3",
57
58
  "typescript": "^5.9.3",
58
59
  "vite": "^4.2.1",
59
60
  "vitest": "^0.30.1"
@@ -87,8 +88,15 @@
87
88
  "publishConfig": {
88
89
  "access": "public"
89
90
  },
91
+ "packageManager": "npm@11.16.0",
90
92
  "volta": {
91
- "node": "22.21.0"
93
+ "node": "24.16.0",
94
+ "npm": "11.16.0"
92
95
  },
93
- "version": "1.0.0-alpha.2"
96
+ "allowScripts": {
97
+ "core-js@2.6.12": true,
98
+ "esbuild@0.17.17": true,
99
+ "fsevents": false
100
+ },
101
+ "version": "1.0.0-alpha.5"
94
102
  }