@sillsdev/docu-notion 0.17.0-alpha.4 → 1.0.0-alpha.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/README.md CHANGED
@@ -4,6 +4,24 @@ docu-notion lets you use Notion as your editor for [Docusaurus](https://docusaur
4
4
 
5
5
  Example Site: https://sillsdev.github.io/docu-notion-sample-site/
6
6
 
7
+ # Docusaurus 3 Output
8
+
9
+ docu-notion emits Docusaurus v3-compatible markdown by default.
10
+
11
+ The default output changed in these ways:
12
+
13
+ - Heading anchors are emitted with Docusaurus v3's MDX-comment syntax, for example `# Heading {/* #my-explicit-id */}`, instead of Docusaurus v2's `{#...}` syntax.
14
+ - `⚠️` callouts now emit `:::warning[Caution]` instead of the deprecated `:::caution` syntax.
15
+ - Callouts with unrecognized emojis now emit `:::note[emoji]` instead of using the emoji itself as a custom admonition keyword.
16
+ - Named Notion callout icons that are not returned by the API as emoji are ignored and fall back to a plain `:::note` admonition.
17
+ - docu-notion now warns when a generated page contains more than one Markdown H1, and the warning lists the H1 headings it found.
18
+
19
+ If you need the previous output, pass `--docusaurus-v2`. This restores the legacy heading ID syntax, the `:::caution` output, and the raw-emoji admonition fallback.
20
+
21
+ If you use `--docusaurus-v2` on Docusaurus v3, keep the `markdown.mdx1Compat.headingIds` and `markdown.mdx1Compat.admonitions` compatibility options enabled. They are on by default in Docusaurus v3, but some sites turn them off during migration.
22
+
23
+ When upgrading an existing Docusaurus site to v3, it is also worth running `npx docusaurus-mdx-checker` on the site to catch MDX v3 issues in any hand-written docs or custom plugin output.
24
+
7
25
  # Instructions
8
26
 
9
27
  ## 1. Set up your documentation site
@@ -122,19 +140,20 @@ Usage: `docu-notion -n <token> -r <root> [options]`
122
140
 
123
141
  Options:
124
142
 
125
- | flag | required? | description |
126
- | ------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
127
- | `-n, --notion-token <string>` | required | notion api token, which looks like `secret_3bc1b50XFYb15123RHF243x43450XFY33250XFYa343` |
128
- | `-r, --root-page <string>` | required | The 31 character ID of the page which is the root of your docs page in notion. The code will look like `9120ec9960244ead80fa2ef4bc1bba25`. This page must have a child page named 'Outline' |
129
- | `-m, --markdown-output-path <string>` | | Root of the hierarchy for md files. WARNING: node-pull-mdx will delete files from this directory. Note also that if it finds localized images, it will create an i18n/ directory as a sibling. (default: `./docs`) |
130
- | `-t, --status-tag <string>` | | Database pages without a Notion page property 'status' matching this will be ignored. Use '\*' to ignore status altogether. (default: `Publish`) |
131
- | `--locales <codes>` | | Comma-separated list of iso 639-2 codes, the same list as in docusaurus.config.js, minus the primary (i.e. 'en'). This is needed for image localization. (default: `[]`) |
132
- | `-l, --log-level <level>` | | Log level (choices: `info`, `verbose`, `debug`) |
133
- | `-i, --img-output-path <string>` | | Path to directory where images will be stored. If this is not included, images will be placed in the same directory as the document that uses them, which then allows for localization of screenshots. |
134
- | `-p, --img-prefix-in-markdown <string>` | | When referencing an image from markdown, prefix with this path instead of the full img-output-path. Should be used only in conjunction with --img-output-path. |
135
- | `--require-slugs` | | If set, docu-notion will fail if any pages it would otherwise publish are missing a slug in Notion. |
143
+ | flag | required? | description |
144
+ | --------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
+ | `-n, --notion-token <string>` | required | notion api token, which looks like `secret_3bc1b50XFYb15123RHF243x43450XFY33250XFYa343` |
146
+ | `-r, --root-page <string>` | required | The 31 character ID of the page which is the root of your docs page in notion. The code will look like `9120ec9960244ead80fa2ef4bc1bba25`. This page must have a child page named 'Outline' |
147
+ | `-m, --markdown-output-path <string>` | | Root of the hierarchy for md files. WARNING: node-pull-mdx will delete files from this directory. Note also that if it finds localized images, it will create an i18n/ directory as a sibling. (default: `./docs`) |
148
+ | `-t, --status-tag <string>` | | Database pages without a Notion page property 'status' matching this will be ignored. Use '\*' to ignore status altogether. (default: `Publish`) |
149
+ | `--locales <codes>` | | Comma-separated list of iso 639-2 codes, the same list as in docusaurus.config.js, minus the primary (i.e. 'en'). This is needed for image localization. (default: `[]`) |
150
+ | `-l, --log-level <level>` | | Log level (choices: `info`, `verbose`, `debug`) |
151
+ | `-i, --img-output-path <string>` | | Path to directory where images will be stored. If this is not included, images will be placed in the same directory as the document that uses them, which then allows for localization of screenshots. |
152
+ | `-p, --img-prefix-in-markdown <string>` | | When referencing an image from markdown, prefix with this path instead of the full img-output-path. Should be used only in conjunction with --img-output-path. |
153
+ | `--require-slugs` | | If set, docu-notion will fail if any pages it would otherwise publish are missing a slug in Notion. |
154
+ | `--docusaurus-v2` | | Emit Docusaurus v2-compatible markdown instead of the default Docusaurus v3-compatible output. This preserves legacy heading IDs, `:::caution`, and raw-emoji admonition keywords. |
136
155
  | `--image-file-name-format <format>` | | choices:<ul><li>`default`: {page slug (if any)}.{image block ID}</li><li>`content-hash`: Use a hash of the image content.</li><li>`legacy`: Use the legacy (before v0.16) method of determining file names. Set this to maintain backward compatibility.</li></ul>All formats will use the original file extension. |
137
- | `-h, --help` | | display help for command |
156
+ | `-h, --help` | | display help for command |
138
157
 
139
158
  # Plugins
140
159
 
@@ -142,16 +161,19 @@ If your project needs some processing that docu-notion doesn't already provide,
142
161
 
143
162
  # Callouts ➜ Admonitions
144
163
 
145
- To map Notion callouts to Docusaurus admonitions, ensure the icon is for the type you want.
164
+ To map Notion callouts to Docusaurus admonitions, ensure the icon is an emoji for the type you want.
146
165
 
147
166
  - ℹ️ ➜ note
148
167
  - 📝➜ note
149
168
  - 💡➜ tip
150
169
  - ❗➜ info
151
- - ⚠️➜ caution
170
+ - ⚠️➜ warning[Caution]
152
171
  - 🔥➜ danger
172
+ - unknown emoji ➜ note[emoji]
173
+
174
+ If a Notion callout uses a named Notion icon instead of an emoji, docu-notion does not try to synthesize an equivalent symbol from the icon name or color. Those callouts fall back to a plain `:::note` admonition.
153
175
 
154
- The default admonition type, if no matching icon is found, is "note".
176
+ The default admonition type, if no matching icon is found, is "note". Use `--docusaurus-v2` to keep the legacy `⚠️ ➜ caution` behavior and the old raw-emoji fallback.
155
177
 
156
178
  # Known Workarounds
157
179
 
@@ -49,6 +49,7 @@ test("Latex Rendering", () => __awaiter(void 0, void 0, void 0, function* () {
49
49
  imgOutputPath: "",
50
50
  imgPrefixInMarkdown: "",
51
51
  statusTag: "",
52
+ docusaurusV2: false,
52
53
  },
53
54
  pages: pages,
54
55
  counts: counts, // review will this get copied or pointed to?
@@ -10,14 +10,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.standardCalloutTransformer = void 0;
13
- // In Notion, you can make a callout and change its emoji. We map 5 of these
14
- // to the 5 Docusaurus admonition styles.
13
+ // In Notion, you can make a callout and change its emoji. We map common callout
14
+ // emojis to the built-in Docusaurus admonition styles.
15
15
  // This is mostly a copy of the callout code from notion-to-md. The change is to output docusaurus
16
16
  // admonitions instead of emulating a callout with markdown > syntax.
17
17
  // Note: I haven't yet tested this with any emoji except "💡"/"tip", nor the case where the
18
18
  // callout has-children. Not even sure what that would mean, since the document I was testing
19
19
  // with has quite complex markup inside the callout, but still takes the no-children branch.
20
- function notionCalloutToAdmonition(notionToMarkdown, getBlockChildren, block) {
20
+ function notionCalloutToAdmonition(context, block) {
21
21
  return __awaiter(this, void 0, void 0, function* () {
22
22
  // In this case typescript is not able to index the types properly, hence ignoring the error
23
23
  // @ts-ignore
@@ -28,7 +28,7 @@ function notionCalloutToAdmonition(notionToMarkdown, getBlockChildren, block) {
28
28
  blockContent.map((content) => {
29
29
  const annotations = content.annotations;
30
30
  let plain_text = content.plain_text;
31
- plain_text = notionToMarkdown.annotatePlainText(plain_text, annotations);
31
+ plain_text = context.notionToMarkdown.annotatePlainText(plain_text, annotations);
32
32
  if (content["href"])
33
33
  plain_text = `[${plain_text}](${content["href"]})`;
34
34
  parsedData += plain_text;
@@ -36,17 +36,17 @@ function notionCalloutToAdmonition(notionToMarkdown, getBlockChildren, block) {
36
36
  let callout_string = "";
37
37
  const { id, has_children } = block;
38
38
  if (!has_children) {
39
- const result1 = callout(parsedData, icon);
39
+ const result1 = callout(parsedData, icon, context.options);
40
40
  return result1;
41
41
  }
42
- const callout_children_object = yield getBlockChildren(id);
42
+ const callout_children_object = yield context.getBlockChildren(id);
43
43
  // // parse children blocks to md object
44
- const callout_children = yield notionToMarkdown.blocksToMarkdown(callout_children_object);
44
+ const callout_children = yield context.notionToMarkdown.blocksToMarkdown(callout_children_object);
45
45
  callout_string += `${parsedData}\n`;
46
46
  callout_children.map(child => {
47
47
  callout_string += `${child.parent}\n\n`;
48
48
  });
49
- const result = callout(callout_string.trim(), icon);
49
+ const result = callout(callout_string.trim(), icon, context.options);
50
50
  return result;
51
51
  });
52
52
  }
@@ -55,34 +55,46 @@ const calloutsToAdmonitions = {
55
55
  "📝": "note",
56
56
  "💡": "tip",
57
57
  "❗": "info",
58
- "⚠️": "caution",
58
+ "⚠️": "warning",
59
59
  "🔥": "danger",
60
60
  };
61
61
  // This is the main change from the notion-to-md code.
62
- function callout(text, icon) {
63
- var _a;
62
+ function callout(text, icon, options) {
63
+ var _a, _b, _c;
64
64
  let emoji;
65
65
  if ((icon === null || icon === void 0 ? void 0 : icon.type) === "emoji") {
66
66
  emoji = icon.emoji;
67
67
  }
68
+ const docusaurusV2 = (_a = options.docusaurusV2) !== null && _a !== void 0 ? _a : false;
68
69
  let docusaurusAdmonition = "note";
70
+ let admonitionTitle = "";
69
71
  if (emoji) {
72
+ if (docusaurusV2) {
73
+ docusaurusAdmonition =
74
+ (_b = calloutsToAdmonitions[emoji]) !== null && _b !== void 0 ? _b : emoji;
75
+ if (docusaurusAdmonition === "warning") {
76
+ docusaurusAdmonition = "caution";
77
+ }
78
+ return `:::${docusaurusAdmonition}\n\n${text}\n\n:::\n\n`;
79
+ }
70
80
  // the keyof typeof magic persuades typescript that it really is OK to use emoji as a key into calloutsToAdmonitions
71
81
  docusaurusAdmonition =
72
- (_a = calloutsToAdmonitions[emoji]) !== null && _a !== void 0 ? _a :
73
- // For Notion callouts with other emojis, pass them through using hte emoji as the name.
74
- // For this to work on a Docusaurus site, it will need to define that time on the remark-admonitions options in the docusaurus.config.js.
75
- // See https://github.com/elviswolcott/remark-admonitions and https://docusaurus.io/docs/using-plugins#using-presets.
76
- emoji;
82
+ (_c = calloutsToAdmonitions[emoji]) !== null && _c !== void 0 ? _c : "note";
83
+ if (emoji === "⚠️") {
84
+ admonitionTitle = "[Caution]";
85
+ }
86
+ else if (!(emoji in calloutsToAdmonitions)) {
87
+ admonitionTitle = `[${emoji}]`;
88
+ }
77
89
  }
78
- return `:::${docusaurusAdmonition}\n\n${text}\n\n:::\n\n`;
90
+ return `:::${docusaurusAdmonition}${admonitionTitle}\n\n${text}\n\n:::\n\n`;
79
91
  }
80
92
  exports.standardCalloutTransformer = {
81
93
  name: "standardCalloutTransformer",
82
94
  notionToMarkdownTransforms: [
83
95
  {
84
96
  type: "callout",
85
- getStringFromBlock: (context, block) => notionCalloutToAdmonition(context.notionToMarkdown, context.getBlockChildren, block),
97
+ getStringFromBlock: (context, block) => notionCalloutToAdmonition(context, block),
86
98
  },
87
99
  ],
88
100
  };
@@ -116,7 +116,7 @@ test("external link inside callout, bold preserved", () => __awaiter(void 0, voi
116
116
  },
117
117
  },
118
118
  ]);
119
- expect(results.trim()).toBe(`:::caution
119
+ expect(results.trim()).toBe(`:::warning[Caution]
120
120
 
121
121
  Callouts inline [**great page**](https://github.com).
122
122
 
@@ -191,9 +191,46 @@ test("internal link inside callout, bold preserved", () => __awaiter(void 0, voi
191
191
  },
192
192
  },
193
193
  ], [slugTargetPage]);
194
- expect(results.trim()).toBe(`:::caution
194
+ expect(results.trim()).toBe(`:::warning[Caution]
195
195
 
196
196
  Callouts inline [**great page**](/hello-world#456) the end.
197
197
 
198
198
  :::`);
199
199
  }));
200
+ test("unknown emoji callout falls back to note with title in docusaurus v3 mode", () => __awaiter(void 0, void 0, void 0, function* () {
201
+ const config = { plugins: [CalloutTransformer_1.standardCalloutTransformer] };
202
+ block.callout.icon.emoji = "🧪";
203
+ const results = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [
204
+ block,
205
+ ]);
206
+ expect(results.trim()).toBe(`:::note[🧪]
207
+
208
+ This is the callout
209
+
210
+ :::`);
211
+ }));
212
+ test("named notion icon callout falls back to plain note in docusaurus v3 mode", () => __awaiter(void 0, void 0, void 0, function* () {
213
+ const config = { plugins: [CalloutTransformer_1.standardCalloutTransformer] };
214
+ block.callout.icon = {
215
+ type: "icon",
216
+ icon: { name: "airplane", color: "purple" },
217
+ };
218
+ const results = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [
219
+ block,
220
+ ]);
221
+ expect(results.trim()).toBe(`:::note
222
+
223
+ This is the callout
224
+
225
+ :::`);
226
+ }));
227
+ test("docusaurus-v2 flag keeps legacy callout syntax", () => __awaiter(void 0, void 0, void 0, function* () {
228
+ const config = { plugins: [CalloutTransformer_1.standardCalloutTransformer] };
229
+ block.callout.icon.emoji = "⚠️";
230
+ const results = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [block], undefined, undefined, undefined, { docusaurusV2: true });
231
+ expect(results.trim()).toBe(`:::caution
232
+
233
+ This is the callout
234
+
235
+ :::`);
236
+ }));
@@ -11,36 +11,124 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const pluginTestRun_1 = require("./pluginTestRun");
13
13
  const HeadingTransformer_1 = require("./HeadingTransformer");
14
+ function makeHeadingBlock(headingBlockId, text, type = "heading_1") {
15
+ return {
16
+ object: "block",
17
+ id: headingBlockId,
18
+ type,
19
+ [type]: {
20
+ rich_text: [
21
+ {
22
+ type: "text",
23
+ text: { content: text, link: null },
24
+ annotations: {
25
+ bold: false,
26
+ italic: false,
27
+ strikethrough: false,
28
+ underline: false,
29
+ code: false,
30
+ color: "default",
31
+ },
32
+ plain_text: text,
33
+ href: null,
34
+ },
35
+ ],
36
+ is_toggleable: false,
37
+ color: "default",
38
+ },
39
+ };
40
+ }
41
+ function makeCodeBlock(text, language = "") {
42
+ return {
43
+ object: "block",
44
+ id: "33333333-3333-3333-3333-333333333333",
45
+ type: "code",
46
+ code: {
47
+ caption: [],
48
+ rich_text: [
49
+ {
50
+ type: "text",
51
+ text: { content: text, link: null },
52
+ annotations: {
53
+ bold: false,
54
+ italic: false,
55
+ strikethrough: false,
56
+ underline: false,
57
+ code: false,
58
+ color: "default",
59
+ },
60
+ plain_text: text,
61
+ href: null,
62
+ },
63
+ ],
64
+ language,
65
+ },
66
+ };
67
+ }
14
68
  test("Adds anchor to headings", () => __awaiter(void 0, void 0, void 0, function* () {
15
69
  //setLogLevel("verbose");
16
70
  const headingBlockId = "86f746f4-1c79-4ba1-a2f6-a1d59c2f9d23";
17
71
  const config = { plugins: [HeadingTransformer_1.standardHeadingTransformer] };
18
72
  const result = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [
19
- {
20
- object: "block",
21
- id: headingBlockId,
22
- type: "heading_1",
23
- heading_1: {
24
- rich_text: [
25
- {
26
- type: "text",
27
- text: { content: "Heading One", 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: "Heading One",
37
- href: null,
38
- },
39
- ],
40
- is_toggleable: false,
41
- color: "default",
42
- },
43
- },
73
+ makeHeadingBlock(headingBlockId, "Heading One"),
44
74
  ]);
75
+ expect(result.trim()).toBe(`# Heading One {/* #${headingBlockId.replaceAll("-", "")} */}`);
76
+ }));
77
+ test("Adds anchor to H4 headings", () => __awaiter(void 0, void 0, void 0, function* () {
78
+ const headingBlockId = "86f746f4-1c79-4ba1-a2f6-a1d59c2f9d23";
79
+ const config = { plugins: [HeadingTransformer_1.standardHeadingTransformer] };
80
+ const result = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [
81
+ makeHeadingBlock(headingBlockId, "Heading Four", "heading_4"),
82
+ ]);
83
+ expect(result.trim()).toBe(`#### Heading Four {/* #${headingBlockId.replaceAll("-", "")} */}`);
84
+ }));
85
+ test("docusaurus-v2 flag keeps legacy heading id syntax", () => __awaiter(void 0, void 0, void 0, function* () {
86
+ const headingBlockId = "86f746f4-1c79-4ba1-a2f6-a1d59c2f9d23";
87
+ const config = { plugins: [HeadingTransformer_1.standardHeadingTransformer] };
88
+ const result = yield (0, pluginTestRun_1.blocksToMarkdown)(config, [makeHeadingBlock(headingBlockId, "Heading One")], undefined, undefined, undefined, { docusaurusV2: true });
45
89
  expect(result.trim()).toBe(`# Heading One {#${headingBlockId.replaceAll("-", "")}}`);
46
90
  }));
91
+ test("warns when more than one H1 is generated for a page", () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const consoleLogSpy = vi
93
+ .spyOn(console, "log")
94
+ .mockImplementation(() => undefined);
95
+ try {
96
+ yield (0, pluginTestRun_1.blocksToMarkdown)({ plugins: [HeadingTransformer_1.standardHeadingTransformer] }, [
97
+ makeHeadingBlock("11111111-1111-1111-1111-111111111111", "Heading One"),
98
+ makeHeadingBlock("22222222-2222-2222-2222-222222222222", "Heading Two"),
99
+ ]);
100
+ }
101
+ finally {
102
+ expect(consoleLogSpy.mock.calls.some(call => String(call[0]).includes('contains 2 H1 headings. Docusaurus pages should have at most one H1. H1 headings: "Heading One", "Heading Two".'))).toBe(true);
103
+ consoleLogSpy.mockRestore();
104
+ }
105
+ }));
106
+ test("does not warn when multiple markdown-style H1 lines appear inside a code block", () => __awaiter(void 0, void 0, void 0, function* () {
107
+ const consoleLogSpy = vi
108
+ .spyOn(console, "log")
109
+ .mockImplementation(() => undefined);
110
+ try {
111
+ yield (0, pluginTestRun_1.blocksToMarkdown)({ plugins: [HeadingTransformer_1.standardHeadingTransformer] }, [
112
+ makeCodeBlock("# Not a heading\n# Still not a heading", "markdown"),
113
+ ]);
114
+ }
115
+ finally {
116
+ expect(consoleLogSpy.mock.calls.some(call => String(call[0]).includes("H1 headings"))).toBe(false);
117
+ consoleLogSpy.mockRestore();
118
+ }
119
+ }));
120
+ test("does not count markdown-style H1 lines inside code blocks toward the page H1 warning", () => __awaiter(void 0, void 0, void 0, function* () {
121
+ const consoleLogSpy = vi
122
+ .spyOn(console, "log")
123
+ .mockImplementation(() => undefined);
124
+ try {
125
+ yield (0, pluginTestRun_1.blocksToMarkdown)({ plugins: [HeadingTransformer_1.standardHeadingTransformer] }, [
126
+ makeHeadingBlock("11111111-1111-1111-1111-111111111111", "Heading One"),
127
+ makeCodeBlock("# Not a heading", "markdown"),
128
+ ]);
129
+ }
130
+ finally {
131
+ expect(consoleLogSpy.mock.calls.some(call => String(call[0]).includes("H1 headings"))).toBe(false);
132
+ consoleLogSpy.mockRestore();
133
+ }
134
+ }));
@@ -11,23 +11,50 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.standardHeadingTransformer = void 0;
13
13
  const log_1 = require("../log");
14
+ function renderHeadingText(context, block, type) {
15
+ // Work around notion-to-md only shipping built-in heading renderers for
16
+ // heading_1 through heading_3. For heading_4 and above we still reuse its
17
+ // inline annotation logic, but we assemble the heading markdown ourselves.
18
+ const headingBlock = block[type];
19
+ const blockContent = (headingBlock === null || headingBlock === void 0 ? void 0 : headingBlock.text) || (headingBlock === null || headingBlock === void 0 ? void 0 : headingBlock.rich_text) || [];
20
+ return blockContent
21
+ .map((content) => {
22
+ if (content.type === "equation" && content.equation) {
23
+ return `$${content.equation.expression}$`;
24
+ }
25
+ let plainText = context.notionToMarkdown.annotatePlainText(content.plain_text, content.annotations);
26
+ if (content.href) {
27
+ plainText = `[${plainText}](${content.href})`;
28
+ }
29
+ return plainText;
30
+ })
31
+ .join("");
32
+ }
14
33
  // Makes links to headings work in docusaurus
15
34
  // https://github.com/sillsdev/docu-notion/issues/20
16
- function headingTransformer(notionToMarkdown, block) {
35
+ function headingTransformer(context, block) {
17
36
  return __awaiter(this, void 0, void 0, function* () {
18
37
  // First, remove the prefix we added to the heading type
19
- block.type = block.type.replace("DN_", "");
20
- const markdown = yield notionToMarkdown.blockToMarkdown(block);
38
+ const type = block.type.replace("DN_", "");
39
+ block.type = type;
40
+ const headingLevel = Number(type.replace("heading_", ""));
41
+ const markdown = headingLevel <= 3
42
+ ? yield context.notionToMarkdown.blockToMarkdown(block)
43
+ // notion-to-md 3.1.1 falls through on heading_4+ and crashes trying to
44
+ // read block[type].text, so render those levels locally.
45
+ : `${"#".repeat(headingLevel)} ${renderHeadingText(context, block, type)}`;
21
46
  (0, log_1.logDebug)("headingTransformer, markdown of a heading before adding id", markdown);
22
- // To make heading links work in docusaurus, we append an id. E.g.
23
- // ### Hello World {#my-explicit-id}
24
- // See https://docusaurus.io/docs/markdown-features/toc#heading-ids.
47
+ // To make heading links work in Docusaurus, we add a stable block-id anchor.
48
+ // Docusaurus v2 uses explicit heading IDs, while the v3 default can use the
49
+ // MDX comment syntax at the end of the heading.
25
50
  // For some reason, inline links come in without the dashes, so we have to strip
26
51
  // dashes here to match them.
27
52
  //console.log("block.id", block.id)
28
53
  const blockIdWithoutDashes = block.id.replaceAll("-", "");
29
54
  // Finally, append the block id so that it can be the target of a link.
30
- return `${markdown} {#${blockIdWithoutDashes}}`;
55
+ if (context.options.docusaurusV2)
56
+ return `${markdown} {#${blockIdWithoutDashes}}`;
57
+ return `${markdown} {/* #${blockIdWithoutDashes} */}`;
31
58
  });
32
59
  }
33
60
  exports.standardHeadingTransformer = {
@@ -49,15 +76,21 @@ exports.standardHeadingTransformer = {
49
76
  notionToMarkdownTransforms: [
50
77
  {
51
78
  type: "DN_heading_1",
52
- getStringFromBlock: (context, block) => headingTransformer(context.notionToMarkdown, block),
79
+ getStringFromBlock: (context, block) => headingTransformer(context, block),
53
80
  },
54
81
  {
55
82
  type: "DN_heading_2",
56
- getStringFromBlock: (context, block) => headingTransformer(context.notionToMarkdown, block),
83
+ getStringFromBlock: (context, block) => headingTransformer(context, block),
57
84
  },
58
85
  {
59
86
  type: "DN_heading_3",
60
- getStringFromBlock: (context, block) => headingTransformer(context.notionToMarkdown, block),
87
+ getStringFromBlock: (context, block) => headingTransformer(context, block),
88
+ },
89
+ {
90
+ type: "DN_heading_4",
91
+ // Keep this explicit so H4 blocks take the local workaround path instead
92
+ // of being handed back to notion-to-md's unsupported default branch.
93
+ getStringFromBlock: (context, block) => headingTransformer(context, block),
61
94
  },
62
95
  ],
63
96
  };
@@ -559,7 +559,7 @@ test("internal link inside callout", () => __awaiter(void 0, void 0, void 0, fun
559
559
  color: "gray_background",
560
560
  },
561
561
  }, targetPage);
562
- expect(results.trim()).toBe(`:::caution
562
+ expect(results.trim()).toBe(`:::warning[Caution]
563
563
 
564
564
  Callouts inline [great page](/hello-world).
565
565
 
@@ -1,11 +1,12 @@
1
1
  import { NotionPage } from "../NotionPage";
2
2
  import { IDocuNotionConfig } from "../config/configuration";
3
+ import { DocuNotionOptions } from "../pull";
3
4
  import { NotionBlock } from "../types";
4
5
  export declare const kTemporaryTestDirectory = "tempTestFileDir";
5
- export declare function blocksToMarkdown(config: IDocuNotionConfig, blocks: NotionBlock[], pages?: NotionPage[], children?: NotionBlock[], validApiKey?: string): Promise<string>;
6
+ export declare function blocksToMarkdown(config: IDocuNotionConfig, blocks: NotionBlock[], pages?: NotionPage[], children?: NotionBlock[], validApiKey?: string, optionsOverrides?: Partial<DocuNotionOptions>): Promise<string>;
6
7
  export declare function makeSamplePageObject(options: {
7
8
  slug?: string;
8
9
  name?: string;
9
10
  id?: string;
10
11
  }): NotionPage;
11
- export declare function oneBlockToMarkdown(config: IDocuNotionConfig, block: Record<string, unknown>, targetPage?: NotionPage, targetPage2?: NotionPage): Promise<string>;
12
+ export declare function oneBlockToMarkdown(config: IDocuNotionConfig, block: Record<string, unknown>, targetPage?: NotionPage, targetPage2?: NotionPage, optionsOverrides?: Partial<DocuNotionOptions>): Promise<string>;
@@ -26,7 +26,7 @@ function blocksToMarkdown(config, blocks, pages,
26
26
  // - These children will apply to each block in blocks. (could enhance but not needed yet)
27
27
  // - If you are passing in children, it is probably because your parent block has has_children=true.
28
28
  // In that case, notion-to-md will make an API call... you'll need to set any validApiKey.
29
- children, validApiKey) {
29
+ children, validApiKey, optionsOverrides) {
30
30
  return __awaiter(this, void 0, void 0, function* () {
31
31
  const notionClient = new client_1.Client({
32
32
  auth: validApiKey || "unused",
@@ -59,15 +59,7 @@ children, validApiKey) {
59
59
  slug: "not yet",
60
60
  },
61
61
  layoutStrategy: new HierarchicalNamedLayoutStrategy_1.HierarchicalNamedLayoutStrategy(),
62
- options: {
63
- notionToken: "",
64
- rootPage: "",
65
- locales: [],
66
- markdownOutputPath: "",
67
- imgOutputPath: "",
68
- imgPrefixInMarkdown: "",
69
- statusTag: "",
70
- },
62
+ options: Object.assign({ notionToken: "", rootPage: "", locales: [], markdownOutputPath: "", imgOutputPath: "", imgPrefixInMarkdown: "", statusTag: "", docusaurusV2: false }, optionsOverrides),
71
63
  pages: pages !== null && pages !== void 0 ? pages : [],
72
64
  counts: {
73
65
  output_normally: 0,
@@ -228,7 +220,7 @@ function makeSamplePageObject(options) {
228
220
  // console.log(p.matchesLinkId);
229
221
  return p;
230
222
  }
231
- function oneBlockToMarkdown(config, block, targetPage, targetPage2) {
223
+ function oneBlockToMarkdown(config, block, targetPage, targetPage2, optionsOverrides) {
232
224
  return __awaiter(this, void 0, void 0, function* () {
233
225
  // just in case someone expects these other properties that aren't normally relevant,
234
226
  // we merge the given block properties into an actual, full block
@@ -260,6 +252,8 @@ function oneBlockToMarkdown(config, block, targetPage, targetPage2) {
260
252
  slug: "dummy2",
261
253
  name: "Dummy2",
262
254
  });
263
- return yield blocksToMarkdown(config, [fullBlock], targetPage ? [dummyPage1, targetPage, targetPage2 !== null && targetPage2 !== void 0 ? targetPage2 : dummyPage2] : undefined);
255
+ return yield blocksToMarkdown(config, [fullBlock], targetPage
256
+ ? [dummyPage1, targetPage, targetPage2 !== null && targetPage2 !== void 0 ? targetPage2 : dummyPage2]
257
+ : undefined, undefined, undefined, optionsOverrides);
264
258
  });
265
259
  }
package/dist/pull.d.ts CHANGED
@@ -11,7 +11,11 @@ export type DocuNotionOptions = {
11
11
  statusTag: string;
12
12
  requireSlugs?: boolean;
13
13
  imageFileNameFormat?: ImageFileNameFormat;
14
+ docusaurusV2?: boolean;
14
15
  };
16
+ export declare function getOptionsForLogging<T extends {
17
+ notionToken: string;
18
+ }>(options: T): T;
15
19
  export declare function notionPull(options: DocuNotionOptions): Promise<void>;
16
20
  export declare function executeWithRateLimitAndRetries<T>(label: string, asyncFunction: () => Promise<T>): Promise<T>;
17
21
  export declare function initNotionClient(notionToken: string): Client;
package/dist/pull.js CHANGED
@@ -42,6 +42,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.getOptionsForLogging = getOptionsForLogging;
45
46
  exports.notionPull = notionPull;
46
47
  exports.executeWithRateLimitAndRetries = executeWithRateLimitAndRetries;
47
48
  exports.initNotionClient = initNotionClient;
@@ -59,6 +60,9 @@ const limiter_1 = require("limiter");
59
60
  const process_1 = require("process");
60
61
  const configuration_1 = require("./config/configuration");
61
62
  const internalLinks_1 = require("./plugins/internalLinks");
63
+ function getOptionsForLogging(options) {
64
+ return Object.assign(Object.assign({}, options), { notionToken: options.notionToken.substring(0, 10) + "..." });
65
+ }
62
66
  const kNotionApiVersion = "2026-03-11";
63
67
  let layoutStrategy;
64
68
  let notionToMarkdown;
@@ -73,11 +77,7 @@ const counts = {
73
77
  function notionPull(options) {
74
78
  return __awaiter(this, void 0, void 0, function* () {
75
79
  // It's helpful when troubleshooting CI secrets and environment variables to see what options actually made it to docu-notion.
76
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
77
- const optionsForLogging = Object.assign({}, options);
78
- // Just show the first few letters of the notion token, which start with "secret" anyhow.
79
- optionsForLogging.notionToken =
80
- optionsForLogging.notionToken.substring(0, 10) + "...";
80
+ const optionsForLogging = getOptionsForLogging(options);
81
81
  const config = yield (0, configuration_1.loadConfigAsync)();
82
82
  (0, log_1.verbose)(`Options:${JSON.stringify(optionsForLogging, null, 2)}`);
83
83
  yield (0, images_1.initImageHandling)(options.imgPrefixInMarkdown || options.imgOutputPath || "", options.imgOutputPath || "", options.locales);
package/dist/run.js CHANGED
@@ -73,13 +73,15 @@ function run() {
73
73
  .option("-i, --img-output-path <string>", "Path to directory where images will be stored. If this is not included, images will be placed in the same directory as the document that uses them, which then allows for localization of screenshots.")
74
74
  .option("-p, --img-prefix-in-markdown <string>", "When referencing an image from markdown, prefix with this path instead of the full img-output-path. Should be used only in conjunction with --img-output-path.")
75
75
  .option("--require-slugs", "If set, docu-notion will fail if any pages it would otherwise publish are missing a slug in Notion.", false)
76
+ .option("--docusaurus-v2", "Emit Docusaurus v2-compatible markdown. By default docu-notion emits Docusaurus v3-compatible output.", false)
76
77
  .addOption(new commander_1.Option("--image-file-name-format <format>", "format:\n- default: {page slug (if any)}.{image block ID}\n- content-hash: Use a hash of the image content.\n- legacy: Use the legacy (before v0.16) method of determining file names. Set this to maintain backward compatibility.\nAll formats will use the original file extension.")
77
78
  .choices(["default", "content-hash", "legacy"])
78
79
  .default("default"));
79
80
  commander_1.program.showHelpAfterError();
80
81
  commander_1.program.parse();
81
- (0, log_1.setLogLevel)(commander_1.program.opts().logLevel);
82
- console.log(JSON.stringify(commander_1.program.opts()));
82
+ const parsedOptions = commander_1.program.opts();
83
+ (0, log_1.setLogLevel)(parsedOptions.logLevel);
84
+ console.log(JSON.stringify((0, pull_1.getOptionsForLogging)(parsedOptions)));
83
85
  // copy in the this version of the css needed to make columns (and maybe other things?) work
84
86
  let pathToCss = "";
85
87
  try {
@@ -90,10 +92,10 @@ function run() {
90
92
  pathToCss = "./src/css/docu-notion-styles.css";
91
93
  }
92
94
  // make any missing parts of the path exist
93
- fs.ensureDirSync(commander_1.program.opts().cssOutputDirectory);
94
- fs.copyFileSync(pathToCss, path_1.default.join(commander_1.program.opts().cssOutputDirectory, "docu-notion-styles.css"));
95
+ fs.ensureDirSync(parsedOptions.cssOutputDirectory);
96
+ fs.copyFileSync(pathToCss, path_1.default.join(parsedOptions.cssOutputDirectory, "docu-notion-styles.css"));
95
97
  // pull and convert
96
- yield (0, pull_1.notionPull)(commander_1.program.opts()).then(() => console.log("docu-notion Finished."));
98
+ yield (0, pull_1.notionPull)(parsedOptions).then(() => console.log("docu-notion Finished."));
97
99
  });
98
100
  }
99
101
  function parseLocales(value) {
package/dist/transform.js CHANGED
@@ -45,6 +45,13 @@ function getMarkdownFromNotionBlocks(context, config, blocks) {
45
45
  //console.log("markdown after link fixes", markdown);
46
46
  // simple regex-based tweaks. These are usually related to docusaurus
47
47
  const body = yield doTransformsOnMarkdown(context, config, markdown);
48
+ const h1Headings = getMarkdownH1Headings(body);
49
+ if (h1Headings.length > 1) {
50
+ const pageLabel = context.pageInfo.slug || "(unknown page)";
51
+ (0, log_1.warning)(`[docu-notion] Generated page "${pageLabel}" contains ${h1Headings.length} H1 headings. Docusaurus pages should have at most one H1. H1 headings: ${h1Headings
52
+ .map(heading => `"${heading}"`)
53
+ .join(", ")}.`);
54
+ }
48
55
  // console.log("markdown after regex fixes", markdown);
49
56
  // console.log("body after regex", body);
50
57
  const uniqueImports = [...new Set(context.imports)];
@@ -53,6 +60,30 @@ function getMarkdownFromNotionBlocks(context, config, blocks) {
53
60
  return `${imports}\n${body}`;
54
61
  });
55
62
  }
63
+ function getMarkdownH1Headings(body) {
64
+ const headings = [];
65
+ let activeFenceMarker;
66
+ for (const line of body.split(/\r?\n/)) {
67
+ const trimmedLine = line.trimStart();
68
+ if (trimmedLine.startsWith("```")) {
69
+ activeFenceMarker = activeFenceMarker === "```" ? undefined : "```";
70
+ continue;
71
+ }
72
+ if (trimmedLine.startsWith("~~~")) {
73
+ activeFenceMarker = activeFenceMarker === "~~~" ? undefined : "~~~";
74
+ continue;
75
+ }
76
+ if (activeFenceMarker || !/^#\s+/.test(line)) {
77
+ continue;
78
+ }
79
+ headings.push(line
80
+ .replace(/^#\s+/, "")
81
+ .replace(/\s+\{\/\*\s*#.*?\*\/\}\s*$/, "")
82
+ .replace(/\s+\{#.*?\}\s*$/, "")
83
+ .trim());
84
+ }
85
+ return headings;
86
+ }
56
87
  // operations on notion blocks before they are converted to markdown
57
88
  function doNotionBlockTransforms(blocks, config) {
58
89
  for (const block of blocks) {
package/package.json CHANGED
@@ -90,5 +90,5 @@
90
90
  "volta": {
91
91
  "node": "22.21.0"
92
92
  },
93
- "version": "0.17.0-alpha.4"
93
+ "version": "1.0.0-alpha.2"
94
94
  }