@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 +37 -15
- package/dist/latex.spec.js +1 -0
- package/dist/plugins/CalloutTransformer.js +30 -18
- package/dist/plugins/CalloutTransformer.spec.js +39 -2
- package/dist/plugins/HeadingTranformer.spec.js +113 -25
- package/dist/plugins/HeadingTransformer.js +43 -10
- package/dist/plugins/internalLinks.spec.js +1 -1
- package/dist/plugins/pluginTestRun.d.ts +3 -2
- package/dist/plugins/pluginTestRun.js +6 -12
- package/dist/pull.d.ts +4 -0
- package/dist/pull.js +5 -5
- package/dist/run.js +7 -5
- package/dist/transform.js +31 -0
- package/package.json +1 -1
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
|
|
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
|
-
- ⚠️➜
|
|
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
|
|
package/dist/latex.spec.js
CHANGED
|
@@ -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
|
|
14
|
-
// to the
|
|
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(
|
|
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
|
-
"⚠️": "
|
|
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
|
-
(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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(`:::
|
|
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(`:::
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
23
|
-
//
|
|
24
|
-
//
|
|
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
|
-
|
|
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
|
|
79
|
+
getStringFromBlock: (context, block) => headingTransformer(context, block),
|
|
53
80
|
},
|
|
54
81
|
{
|
|
55
82
|
type: "DN_heading_2",
|
|
56
|
-
getStringFromBlock: (context, block) => headingTransformer(context
|
|
83
|
+
getStringFromBlock: (context, block) => headingTransformer(context, block),
|
|
57
84
|
},
|
|
58
85
|
{
|
|
59
86
|
type: "DN_heading_3",
|
|
60
|
-
getStringFromBlock: (context, block) => headingTransformer(context
|
|
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(`:::
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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(
|
|
94
|
-
fs.copyFileSync(pathToCss, path_1.default.join(
|
|
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)(
|
|
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