@sillsdev/docu-notion 0.17.0-alpha.3 → 1.0.0-alpha.1

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.
@@ -33,6 +33,7 @@ beforeEach(() => {
33
33
  },
34
34
  has_children: false,
35
35
  archived: false,
36
+ in_trash: false,
36
37
  type: "paragraph",
37
38
  paragraph: {
38
39
  rich_text: [
@@ -51,6 +52,7 @@ beforeEach(() => {
51
52
  href: null,
52
53
  },
53
54
  ],
55
+ icon: null,
54
56
  color: "default",
55
57
  },
56
58
  },
@@ -73,6 +75,7 @@ beforeEach(() => {
73
75
  },
74
76
  has_children: false,
75
77
  archived: false,
78
+ in_trash: false,
76
79
  type: "paragraph",
77
80
  paragraph: {
78
81
  rich_text: [
@@ -91,6 +94,7 @@ beforeEach(() => {
91
94
  href: null,
92
95
  },
93
96
  ],
97
+ icon: null,
94
98
  color: "default",
95
99
  },
96
100
  },
@@ -113,8 +117,9 @@ beforeEach(() => {
113
117
  },
114
118
  has_children: false,
115
119
  archived: false,
120
+ in_trash: false,
116
121
  type: "paragraph",
117
- paragraph: { rich_text: [], color: "default" },
122
+ paragraph: { rich_text: [], icon: null, color: "default" },
118
123
  },
119
124
  ];
120
125
  });
@@ -11,36 +11,116 @@ 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("docusaurus-v2 flag keeps legacy heading id syntax", () => __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, [makeHeadingBlock(headingBlockId, "Heading One")], undefined, undefined, undefined, { docusaurusV2: true });
45
81
  expect(result.trim()).toBe(`# Heading One {#${headingBlockId.replaceAll("-", "")}}`);
46
82
  }));
83
+ test("warns when more than one H1 is generated for a page", () => __awaiter(void 0, void 0, void 0, function* () {
84
+ const consoleLogSpy = vi
85
+ .spyOn(console, "log")
86
+ .mockImplementation(() => undefined);
87
+ try {
88
+ yield (0, pluginTestRun_1.blocksToMarkdown)({ plugins: [HeadingTransformer_1.standardHeadingTransformer] }, [
89
+ makeHeadingBlock("11111111-1111-1111-1111-111111111111", "Heading One"),
90
+ makeHeadingBlock("22222222-2222-2222-2222-222222222222", "Heading Two"),
91
+ ]);
92
+ }
93
+ finally {
94
+ 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);
95
+ consoleLogSpy.mockRestore();
96
+ }
97
+ }));
98
+ test("does not warn when multiple markdown-style H1 lines appear inside a code block", () => __awaiter(void 0, void 0, void 0, function* () {
99
+ const consoleLogSpy = vi
100
+ .spyOn(console, "log")
101
+ .mockImplementation(() => undefined);
102
+ try {
103
+ yield (0, pluginTestRun_1.blocksToMarkdown)({ plugins: [HeadingTransformer_1.standardHeadingTransformer] }, [
104
+ makeCodeBlock("# Not a heading\n# Still not a heading", "markdown"),
105
+ ]);
106
+ }
107
+ finally {
108
+ expect(consoleLogSpy.mock.calls.some(call => String(call[0]).includes("H1 headings"))).toBe(false);
109
+ consoleLogSpy.mockRestore();
110
+ }
111
+ }));
112
+ test("does not count markdown-style H1 lines inside code blocks toward the page H1 warning", () => __awaiter(void 0, void 0, void 0, function* () {
113
+ const consoleLogSpy = vi
114
+ .spyOn(console, "log")
115
+ .mockImplementation(() => undefined);
116
+ try {
117
+ yield (0, pluginTestRun_1.blocksToMarkdown)({ plugins: [HeadingTransformer_1.standardHeadingTransformer] }, [
118
+ makeHeadingBlock("11111111-1111-1111-1111-111111111111", "Heading One"),
119
+ makeCodeBlock("# Not a heading", "markdown"),
120
+ ]);
121
+ }
122
+ finally {
123
+ expect(consoleLogSpy.mock.calls.some(call => String(call[0]).includes("H1 headings"))).toBe(false);
124
+ consoleLogSpy.mockRestore();
125
+ }
126
+ }));
@@ -13,21 +13,23 @@ exports.standardHeadingTransformer = void 0;
13
13
  const log_1 = require("../log");
14
14
  // Makes links to headings work in docusaurus
15
15
  // https://github.com/sillsdev/docu-notion/issues/20
16
- function headingTransformer(notionToMarkdown, block) {
16
+ function headingTransformer(context, block) {
17
17
  return __awaiter(this, void 0, void 0, function* () {
18
18
  // First, remove the prefix we added to the heading type
19
19
  block.type = block.type.replace("DN_", "");
20
- const markdown = yield notionToMarkdown.blockToMarkdown(block);
20
+ const markdown = yield context.notionToMarkdown.blockToMarkdown(block);
21
21
  (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.
22
+ // To make heading links work in Docusaurus, we add a stable block-id anchor.
23
+ // Docusaurus v2 uses explicit heading IDs, while the v3 default can use the
24
+ // MDX comment syntax at the end of the heading.
25
25
  // For some reason, inline links come in without the dashes, so we have to strip
26
26
  // dashes here to match them.
27
27
  //console.log("block.id", block.id)
28
28
  const blockIdWithoutDashes = block.id.replaceAll("-", "");
29
29
  // Finally, append the block id so that it can be the target of a link.
30
- return `${markdown} {#${blockIdWithoutDashes}}`;
30
+ if (context.options.docusaurusV2)
31
+ return `${markdown} {#${blockIdWithoutDashes}}`;
32
+ return `${markdown} {/* #${blockIdWithoutDashes} */}`;
31
33
  });
32
34
  }
33
35
  exports.standardHeadingTransformer = {
@@ -49,15 +51,15 @@ exports.standardHeadingTransformer = {
49
51
  notionToMarkdownTransforms: [
50
52
  {
51
53
  type: "DN_heading_1",
52
- getStringFromBlock: (context, block) => headingTransformer(context.notionToMarkdown, block),
54
+ getStringFromBlock: (context, block) => headingTransformer(context, block),
53
55
  },
54
56
  {
55
57
  type: "DN_heading_2",
56
- getStringFromBlock: (context, block) => headingTransformer(context.notionToMarkdown, block),
58
+ getStringFromBlock: (context, block) => headingTransformer(context, block),
57
59
  },
58
60
  {
59
61
  type: "DN_heading_3",
60
- getStringFromBlock: (context, block) => headingTransformer(context.notionToMarkdown, block),
62
+ getStringFromBlock: (context, block) => headingTransformer(context, block),
61
63
  },
62
64
  ],
63
65
  };
@@ -4,22 +4,47 @@ exports.standardInternalLinkConversion = void 0;
4
4
  exports.convertInternalUrl = convertInternalUrl;
5
5
  exports.parseLinkId = parseLinkId;
6
6
  const log_1 = require("../log");
7
+ const kNotionUrlRegExp = /^https?:\/\/(?:www\.)?notion\.so\/|^https?:\/\/app\.notion\.com\//;
8
+ function getLegacyTrailingSegment(pathOrId) {
9
+ const trimmedPath = pathOrId.replace(/^\/+|\/+$/g, "");
10
+ const lastPathSegment = trimmedPath.split("/").at(-1);
11
+ if (!lastPathSegment)
12
+ return undefined;
13
+ const trailingDashSegment = lastPathSegment.split("-").at(-1);
14
+ return trailingDashSegment || lastPathSegment;
15
+ }
16
+ function normalizeLinkBaseId(baseLinkId) {
17
+ const withoutQuery = baseLinkId.split("?")[0];
18
+ if (kNotionUrlRegExp.test(withoutQuery)) {
19
+ try {
20
+ const url = new URL(withoutQuery);
21
+ const trimmedPath = url.pathname.replace(/^\/+|\/+$/g, "");
22
+ const appPathWithoutPrefix = trimmedPath.startsWith("p/")
23
+ ? trimmedPath.substring(2)
24
+ : trimmedPath;
25
+ return (getLegacyTrailingSegment(appPathWithoutPrefix) || appPathWithoutPrefix);
26
+ }
27
+ catch (_a) {
28
+ return withoutQuery;
29
+ }
30
+ }
31
+ const withoutLeadingSlash = withoutQuery.replace(/^\/+/, "");
32
+ return withoutLeadingSlash;
33
+ }
7
34
  // converts a url to a local link, if it is a link to a page in the Notion site
8
35
  // only here for plugins, notion won't normally be giving us raw urls (at least not that I've noticed)
9
36
  // If it finds a URL but can't find the page it points to, it will return undefined.
10
37
  // If it doesn't find a match at all, it returns undefined.
11
38
  function convertInternalUrl(context, url) {
12
- const kGetIDFromNotionURL = /https:\/\/www\.notion\.so\S+-([a-z,0-9]+)+.*/;
13
- const match = kGetIDFromNotionURL.exec(url);
14
- if (match === null) {
39
+ const { baseLinkId } = parseLinkId(url);
40
+ if (baseLinkId === url) {
15
41
  (0, log_1.warning)(`[standardInternalLinkConversion] Could not parse link ${url} as a Notion URL`);
16
42
  return undefined;
17
43
  }
18
- const id = match[1];
19
44
  const pages = context.pages;
20
45
  // find the page where pageId matches hrefFromNotion
21
46
  const targetPage = pages.find(p => {
22
- return p.matchesLinkId(id);
47
+ return p.matchesLinkId(baseLinkId);
23
48
  });
24
49
  if (!targetPage) {
25
50
  // About this situation. See https://github.com/sillsdev/docu-notion/issues/9
@@ -30,8 +55,8 @@ function convertInternalUrl(context, url) {
30
55
  }
31
56
  // handles the whole markdown link, including the label
32
57
  function convertInternalLink(context, markdownLink) {
33
- // match both [foo](/123) and [bar](https://www.notion.so/123) <-- the "mention" link style
34
- const linkRegExp = /\[([^\]]+)?\]\((?:https?:\/\/www\.notion\.so\/|\/)?([^),^/]+)\)/g;
58
+ // match both [foo](/123) and [bar](https://app.notion.com/p/123) mention-style links
59
+ const linkRegExp = /\[([^\]]+)?\]\(([^)]+)\)/;
35
60
  const match = linkRegExp.exec(markdownLink);
36
61
  if (match === null) {
37
62
  (0, log_1.warning)(`[standardInternalLinkConversion] Could not parse link ${markdownLink}`);
@@ -84,11 +109,11 @@ function parseLinkId(fullLinkId) {
84
109
  const iHash = fullLinkId.indexOf("#");
85
110
  if (iHash >= 0) {
86
111
  return {
87
- baseLinkId: fullLinkId.substring(0, iHash),
112
+ baseLinkId: normalizeLinkBaseId(fullLinkId.substring(0, iHash)),
88
113
  fragmentId: fullLinkId.substring(iHash),
89
114
  };
90
115
  }
91
- return { baseLinkId: fullLinkId, fragmentId: "" };
116
+ return { baseLinkId: normalizeLinkBaseId(fullLinkId), fragmentId: "" };
92
117
  }
93
118
  exports.standardInternalLinkConversion = {
94
119
  name: "standard internal link conversion",
@@ -98,8 +123,9 @@ exports.standardInternalLinkConversion = {
98
123
  // Raw links come in without a leading slash, e.g. [link_to_page](4a6de8c0-b90b-444b-8a7b-d534d6ec71a4)
99
124
  // Inline links come in with a leading slash, e.g. [pointer to the introduction](/4a6de8c0b90b444b8a7bd534d6ec71a4)
100
125
  // "Mention" links come in as full URLs, e.g. [link_to_page](https://www.notion.so/62f1187010214b0883711a1abb277d31)
126
+ // Newer Notion links can also use app.notion.com, including /p/<page-id> URLs.
101
127
  // YOu can create them either with @+the name of a page, or by pasting a URL and then selecting the "Mention" option.
102
- match: /\[([^\]]+)?\]\((?!mailto:)(https:\/\/www\.notion\.so\/[^),^/]+|\/?[^),^/]+)\)/,
128
+ match: /\[([^\]]+)?\]\((?!mailto:)(https?:\/\/(?:www\.)?notion\.so\/[^),^/]+|https?:\/\/app\.notion\.com\/(?:p\/)?[^),^/]+|\/?[^),^/]+)\)/,
103
129
  convert: convertInternalLink,
104
130
  },
105
131
  };
@@ -75,6 +75,52 @@ test("mention-style link to an existing page", () => __awaiter(void 0, void 0, v
75
75
  }, targetPage);
76
76
  expect(results.trim()).toBe(`[foo](/${targetPageId})`);
77
77
  }));
78
+ test("mention-style link using app.notion.com/p resolves to a local page", () => __awaiter(void 0, void 0, void 0, function* () {
79
+ const targetPageId = "123456781234123412341234567890ab";
80
+ const targetPage = (0, pluginTestRun_1.makeSamplePageObject)({
81
+ slug: undefined,
82
+ name: "Hello World",
83
+ id: "12345678-1234-1234-1234-1234567890ab",
84
+ });
85
+ const results = yield getMarkdown({
86
+ type: "paragraph",
87
+ paragraph: {
88
+ rich_text: [
89
+ {
90
+ type: "mention",
91
+ mention: {
92
+ type: "page",
93
+ page: {
94
+ id: targetPage.pageId,
95
+ },
96
+ },
97
+ annotations: {
98
+ bold: false,
99
+ italic: false,
100
+ strikethrough: false,
101
+ underline: false,
102
+ code: false,
103
+ color: "default",
104
+ },
105
+ plain_text: "foo",
106
+ href: `https://app.notion.com/p/${targetPageId}`,
107
+ },
108
+ ],
109
+ color: "default",
110
+ },
111
+ }, targetPage);
112
+ expect(results.trim()).toBe(`[foo](/12345678-1234-1234-1234-1234567890ab)`);
113
+ }));
114
+ test("parseLinkId extracts the page id from app.notion.com URLs", () => {
115
+ expect((0, internalLinks_1.parseLinkId)("https://app.notion.com/p/123456781234123412341234567890ab#heading")).toEqual({
116
+ baseLinkId: "123456781234123412341234567890ab",
117
+ fragmentId: "#heading",
118
+ });
119
+ expect((0, internalLinks_1.parseLinkId)("https://app.notion.com/Interesting-page-123456781234123412341234567890ab")).toEqual({
120
+ baseLinkId: "123456781234123412341234567890ab",
121
+ fragmentId: "",
122
+ });
123
+ });
78
124
  test("link to an existing page on this site that has no slug", () => __awaiter(void 0, void 0, void 0, function* () {
79
125
  const targetPageId = "123";
80
126
  const targetPage = (0, pluginTestRun_1.makeSamplePageObject)({
@@ -513,7 +559,7 @@ test("internal link inside callout", () => __awaiter(void 0, void 0, void 0, fun
513
559
  color: "gray_background",
514
560
  },
515
561
  }, targetPage);
516
- expect(results.trim()).toBe(`:::caution
562
+ expect(results.trim()).toBe(`:::warning[Caution]
517
563
 
518
564
  Callouts inline [great page](/hello-world).
519
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;
@@ -54,11 +55,15 @@ const images_1 = require("./images");
54
55
  const Path = __importStar(require("path"));
55
56
  const log_1 = require("./log");
56
57
  const transform_1 = require("./transform");
57
- const limiter_1 = require("limiter");
58
58
  const client_1 = require("@notionhq/client");
59
+ 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
+ }
66
+ const kNotionApiVersion = "2026-03-11";
62
67
  let layoutStrategy;
63
68
  let notionToMarkdown;
64
69
  const pages = new Array();
@@ -72,11 +77,7 @@ const counts = {
72
77
  function notionPull(options) {
73
78
  return __awaiter(this, void 0, void 0, function* () {
74
79
  // It's helpful when troubleshooting CI secrets and environment variables to see what options actually made it to docu-notion.
75
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
76
- const optionsForLogging = Object.assign({}, options);
77
- // Just show the first few letters of the notion token, which start with "secret" anyhow.
78
- optionsForLogging.notionToken =
79
- optionsForLogging.notionToken.substring(0, 10) + "...";
80
+ const optionsForLogging = getOptionsForLogging(options);
80
81
  const config = yield (0, configuration_1.loadConfigAsync)();
81
82
  (0, log_1.verbose)(`Options:${JSON.stringify(optionsForLogging, null, 2)}`);
82
83
  yield (0, images_1.initImageHandling)(options.imgPrefixInMarkdown || options.imgOutputPath || "", options.imgOutputPath || "", options.locales);
@@ -281,9 +282,12 @@ function rateLimit() {
281
282
  yield notionLimiter.removeTokens(1);
282
283
  });
283
284
  }
285
+ function isFullBlockFromChildrenList(block) {
286
+ return "type" in block;
287
+ }
284
288
  function getBlockChildren(id) {
285
289
  return __awaiter(this, void 0, void 0, function* () {
286
- var _a, _b;
290
+ var _a;
287
291
  // we can only get so many responses per call, so we set this to
288
292
  // the first response we get, then keep adding to its array of blocks
289
293
  // with each subsequent response
@@ -293,7 +297,7 @@ function getBlockChildren(id) {
293
297
  // we could switch to using that (I don't know if it does rate limiting?)
294
298
  do {
295
299
  const response = yield notionClient.blocks.children.list({
296
- start_cursor: start_cursor,
300
+ start_cursor,
297
301
  block_id: id,
298
302
  });
299
303
  if (!overallResult) {
@@ -304,18 +308,29 @@ function getBlockChildren(id) {
304
308
  }
305
309
  start_cursor = response === null || response === void 0 ? void 0 : response.next_cursor;
306
310
  } while (start_cursor != null);
307
- if ((_a = overallResult === null || overallResult === void 0 ? void 0 : overallResult.results) === null || _a === void 0 ? void 0 : _a.some(b => !(0, client_1.isFullBlock)(b))) {
311
+ if ((_a = overallResult === null || overallResult === void 0 ? void 0 : overallResult.results) === null || _a === void 0 ? void 0 : _a.some(b => !isFullBlockFromChildrenList(b))) {
308
312
  (0, log_1.error)(`The Notion API returned some blocks that were not full blocks. docu-notion does not handle this yet. Please report it.`);
309
313
  (0, process_1.exit)(1);
310
314
  }
311
- const result = (_b = overallResult === null || overallResult === void 0 ? void 0 : overallResult.results) !== null && _b !== void 0 ? _b : [];
315
+ const result = [];
316
+ if (overallResult) {
317
+ const blocks = overallResult.results;
318
+ for (const block of blocks) {
319
+ if (isFullBlockFromChildrenList(block)) {
320
+ result.push(block);
321
+ }
322
+ }
323
+ }
312
324
  numberChildrenIfNumberedList(result);
325
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
313
326
  return result;
314
327
  });
315
328
  }
316
329
  function initNotionClient(notionToken) {
317
330
  notionClient = new client_1.Client({
318
331
  auth: notionToken,
332
+ // width_ratio on column blocks is available in newer Notion API versions.
333
+ notionVersion: kNotionApiVersion,
319
334
  });
320
335
  const originalRequest = notionClient.request.bind(notionClient);
321
336
  notionClient.request = (args) => __awaiter(this, void 0, void 0, function* () {
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/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BlockObjectResponse } from "@notionhq/client/build/src/api-endpoints";
1
+ import { BlockObjectResponse } from "@notionhq/client";
2
2
  export type NotionBlock = BlockObjectResponse;
3
3
  export type ICounts = {
4
4
  output_normally: number;