@sillsdev/docu-notion 0.14.0-alpha.14 → 0.14.0-alpha.16

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
@@ -6,7 +6,7 @@ Example Site: https://sillsdev.github.io/docu-notion-sample-site/
6
6
 
7
7
  # Instructions
8
8
 
9
- ## 1. Set up your documentation site.
9
+ ## 1. Set up your documentation site
10
10
 
11
11
  First, prepare your markdown-based static file system like [Docusaurus](https://docusaurus.io/). For a shortcut with github actions, search, and deployment to github pages, you can just copy [this template](https://github.com/sillsdev/docu-notion-sample-site).
12
12
 
@@ -27,15 +27,15 @@ Go to the page that will be the root of your site. This page should have, as dir
27
27
 
28
28
  <img width="318" alt="image" src="https://github.com/sillsdev/docu-notion/assets/8448/810c6dca-f9ab-4370-93b4-dc1479332af7">
29
29
 
30
- ## 5. Add your pages under your Outline page.
30
+ ## 5. Add your pages under your Outline page
31
31
 
32
32
  Currently, docu-notion expects that each page has only one of the following: sub-pages, links to other pages, or normal content. Do not mix them. You can add content pages directly here, but then you won't be able to make use of the workflow features. If those matter to you, instead make new pages under the "Database" and then link to them in your outline pages.
33
33
 
34
34
  ## 6. Pull your pages
35
35
 
36
- First, determine the id of your root page by clicking "Share" and looking at the url it gives you. E.g.
37
- https://www.notion.so/hattonjohn/My-Docs-0456aa5842946bdbea3a4f37c97a0e5
38
- means that the id is "0456aa5842946PRETEND4f37c97a0e5".
36
+ First, determine the ID of your root page by clicking "Share" and looking at the url it gives you. E.g.
37
+ `https://www.notion.so/hattonjohn/My-Docs-0456aa5842946PRETEND4f37c97a0e5`
38
+ means that the ID is `0456aa5842946PRETEND4f37c97a0e5`.
39
39
 
40
40
  Try it out:
41
41
 
@@ -114,26 +114,27 @@ NOTE: if you just localize an image, it will not get picked up. You also must lo
114
114
 
115
115
  # Automated builds with Github Actions
116
116
 
117
- Here is a working Github Action script to copy and customize: https://github.com/BloomBooks/bloom-docs/blob/master/.github/workflows/release.yml
117
+ Here is a [working Github Action script to copy and customize](https://github.com/BloomBooks/bloom-docs/blob/master/.github/workflows/release.yml).
118
118
 
119
119
  # Command line
120
120
 
121
- Usage: docu-notion -n <token> -r <root> [options]
121
+ Usage: `docu-notion -n <token> -r <root> [options]`
122
122
 
123
123
  Options:
124
124
 
125
125
  | flag | required? | description |
126
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. |
136
- | -h, --help | | display help for command |
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. |
136
+ | `--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 |
137
138
 
138
139
  # Plugins
139
140
 
@@ -155,8 +156,10 @@ The default admonition type, if no matching icon is found, is "note".
155
156
  # Known Workarounds
156
157
 
157
158
  ### Start a numbered list at a number other than 1
159
+
158
160
  In Notion, make sure the block is "Text," not "Numbered List".
161
+
159
162
  - But make sure the number does NOT have a space in front of it. This can/will cause issues with sub-list items.
160
163
  - One way to get Notion to let you do this:
161
- - Create a numbered list item where the text duplicates the number you want. Convert that numbered list item to "Text."
162
- - i.e. Type "1. 1. Item one." Notion makes the first "1." into a number in a list. When you convert back to "Text," you're left with plain text "1. Item one."
164
+ - Create a numbered list item where the text duplicates the number you want. Convert that numbered list item to "Text."
165
+ - i.e. Type "1. 1. Item one." Notion makes the first "1." into a number in a list. When you convert back to "Text," you're left with plain text "1. Item one."
@@ -1,3 +1,4 @@
1
1
  import { ImageSet } from "./images";
2
- export declare function makeImagePersistencePlan(imageSet: ImageSet, imageOutputRootPath: string, imagePrefix: string): void;
2
+ import { DocuNotionOptions } from "./pull";
3
+ export declare function makeImagePersistencePlan(options: DocuNotionOptions, imageSet: ImageSet, imageBlockId: string, imageOutputRootPath: string, imagePrefix: string): void;
3
4
  export declare function hashOfString(s: string): number;
@@ -22,13 +22,17 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
24
  };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
25
28
  Object.defineProperty(exports, "__esModule", { value: true });
26
29
  exports.hashOfString = exports.makeImagePersistencePlan = void 0;
27
30
  const Path = __importStar(require("path"));
28
31
  const log_1 = require("./log");
29
32
  const process_1 = require("process");
30
- function makeImagePersistencePlan(imageSet, imageOutputRootPath, imagePrefix) {
31
- var _a, _b;
33
+ const crypto_1 = __importDefault(require("crypto"));
34
+ function makeImagePersistencePlan(options, imageSet, imageBlockId, imageOutputRootPath, imagePrefix) {
35
+ var _a, _b, _c;
32
36
  const urlBeforeQuery = imageSet.primaryUrl.split("?")[0];
33
37
  let imageFileExtension = (_a = imageSet.fileType) === null || _a === void 0 ? void 0 : _a.ext;
34
38
  if (!imageFileExtension) {
@@ -39,18 +43,52 @@ function makeImagePersistencePlan(imageSet, imageOutputRootPath, imagePrefix) {
39
43
  (0, process_1.exit)(1);
40
44
  }
41
45
  }
42
- // Since most images come from pasting screenshots, there isn't normally a filename. That's fine, we just make a hash of the url
43
- // Images that are stored by notion come to us with a complex url that changes over time, so we pick out the UUID that doesn't change. Example:
44
- // https://s3.us-west-2.amazonaws.com/secure.notion-static.com/d1058f46-4d2f-4292-8388-4ad393383439/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220516%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220516T233630Z&X-Amz-Expires=3600&X-Amz-Signature=f215704094fcc884d37073b0b108cf6d1c9da9b7d57a898da38bc30c30b4c4b5&X-Amz-SignedHeaders=host&x-id=GetObject
45
- // But around Sept 2023, they changed the url to be something like:
46
- // https://prod-files-secure.s3.us-west-2.amazonaws.com/d9a2b712-cf69-4bd6-9d65-87a4ceeacca2/d1bcdc8c-b065-4e40-9a11-392aabeb220e/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230915%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230915T161258Z&X-Amz-Expires=3600&X-Amz-Signature=28fca48e65fba86d539c3c4b7676fce1fa0857aa194f7b33dd4a468ecca6ab24&X-Amz-SignedHeaders=host&x-id=GetObject
47
- // The thing we want is the last UUID before the ?
48
- const thingToHash = (_b = findLastUuid(urlBeforeQuery)) !== null && _b !== void 0 ? _b : urlBeforeQuery;
49
- const hash = hashOfString(thingToHash);
50
- imageSet.outputFileName = `${hash}.${imageFileExtension}`;
46
+ if (options.imageFileNameFormat === "legacy") {
47
+ // Original behavior and comment:
48
+ // Since most images come from pasting screenshots, there isn't normally a filename. That's fine, we just make a hash of the url
49
+ // Images that are stored by notion come to us with a complex url that changes over time, so we pick out the UUID that doesn't change. Example:
50
+ // https://s3.us-west-2.amazonaws.com/secure.notion-static.com/d1058f46-4d2f-4292-8388-4ad393383439/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220516%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220516T233630Z&X-Amz-Expires=3600&X-Amz-Signature=f215704094fcc884d37073b0b108cf6d1c9da9b7d57a898da38bc30c30b4c4b5&X-Amz-SignedHeaders=host&x-id=GetObject
51
+ // But around Sept 2023, they changed the url to be something like:
52
+ // https://prod-files-secure.s3.us-west-2.amazonaws.com/d9a2b712-cf69-4bd6-9d65-87a4ceeacca2/d1bcdc8c-b065-4e40-9a11-392aabeb220e/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230915%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230915T161258Z&X-Amz-Expires=3600&X-Amz-Signature=28fca48e65fba86d539c3c4b7676fce1fa0857aa194f7b33dd4a468ecca6ab24&X-Amz-SignedHeaders=host&x-id=GetObject
53
+ // The thing we want is the last UUID before the ?
54
+ const thingToHash = (_b = findLastUuid(urlBeforeQuery)) !== null && _b !== void 0 ? _b : urlBeforeQuery;
55
+ const hash = hashOfString(thingToHash);
56
+ imageSet.outputFileName = `${hash}.${imageFileExtension}`;
57
+ }
58
+ else if (options.imageFileNameFormat === "content-hash") {
59
+ // This was requested by a user: https://github.com/sillsdev/docu-notion/issues/76.
60
+ // We chose not to include it in the default file name because we want to maintain
61
+ // as much stability in the file name as feasible for an image localization workflow.
62
+ // However, particularly in a workflow which is not concerned with localization,
63
+ // this could be a good option. One benefit is that the image only needs to exist once
64
+ // in the file system regardless of how many times it is used in the site.
65
+ const imageHash = hashOfBufferContent(imageSet.primaryBuffer);
66
+ imageSet.outputFileName = `${imageHash}.${imageFileExtension}`;
67
+ }
68
+ else {
69
+ // We decided not to do this for the default format because it means
70
+ // instability for the file name in Crowdin, which causes loss of localizations.
71
+ // If we decide to include it in the future, we should add a unit test.
72
+ // const imageFileName = Path.basename(urlBeforeQuery);
73
+ // const imageFileNameWithoutExtension = Path.parse(imageFileName).name;
74
+ // const originalFileNamePart = ["untitled", "unnamed"].includes(
75
+ // imageFileNameWithoutExtension.toLocaleLowerCase()
76
+ // )
77
+ // ? ""
78
+ // : `${imageFileNameWithoutExtension.substring(0, 50)}.`;
79
+ // Format is page slug (if there is one) followed by the image block ID from Notion.
80
+ // The image block ID will remain stable as long as any changes to the image are done
81
+ // using the Replace feature. Also, image blocks can be moved using the Move To feature.
82
+ // We decided to include the page slug for easier workflow during localization, particularly in Crowdin.
83
+ // The block ID is a unique GUID and thus provides a unique file name.
84
+ const pageSlugPart = ((_c = imageSet.pageInfo) === null || _c === void 0 ? void 0 : _c.slug)
85
+ ? `${imageSet.pageInfo.slug.replace(/^\//, "")}.`
86
+ : "";
87
+ imageSet.outputFileName = `${pageSlugPart}${imageBlockId}.${imageFileExtension}`;
88
+ }
51
89
  imageSet.primaryFileOutputPath = Path.posix.join((imageOutputRootPath === null || imageOutputRootPath === void 0 ? void 0 : imageOutputRootPath.length) > 0
52
90
  ? imageOutputRootPath
53
- : imageSet.pathToParentDocument, imageSet.outputFileName);
91
+ : imageSet.pageInfo.directoryContainingMarkdown, decodeURI(imageSet.outputFileName));
54
92
  if (imageOutputRootPath && imageSet.localizedUrls.length) {
55
93
  (0, log_1.error)("imageOutputPath was declared, but one or more localizedUrls were found too. If you are going to localize screenshots, then you can't declare an imageOutputPath.");
56
94
  (0, process_1.exit)(1);
@@ -76,3 +114,7 @@ function hashOfString(s) {
76
114
  return Math.abs(hash);
77
115
  }
78
116
  exports.hashOfString = hashOfString;
117
+ function hashOfBufferContent(buffer) {
118
+ const hash = crypto_1.default.createHash("sha256").update(buffer).digest("hex");
119
+ return hash.slice(0, 20);
120
+ }
package/dist/images.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  import { FileTypeResult } from "file-type";
3
3
  import { ListBlockChildrenResponseResult } from "notion-to-md/build/types";
4
- import { IPlugin } from "./plugins/pluginTypes";
4
+ import { IDocuNotionContext, IDocuNotionContextPageInfo, IPlugin } from "./plugins/pluginTypes";
5
5
  export type ImageSet = {
6
6
  primaryUrl: string;
7
7
  caption?: string;
@@ -9,8 +9,7 @@ export type ImageSet = {
9
9
  iso632Code: string;
10
10
  url: string;
11
11
  }>;
12
- pathToParentDocument?: string;
13
- relativePathToParentDocument?: string;
12
+ pageInfo?: IDocuNotionContextPageInfo;
14
13
  primaryBuffer?: Buffer;
15
14
  fileType?: FileTypeResult;
16
15
  primaryFileOutputPath?: string;
@@ -19,6 +18,6 @@ export type ImageSet = {
19
18
  };
20
19
  export declare function initImageHandling(prefix: string, outputPath: string, incomingLocales: string[]): Promise<void>;
21
20
  export declare const standardImageTransformer: IPlugin;
22
- export declare function markdownToMDImageTransformer(block: ListBlockChildrenResponseResult, fullPathToDirectoryContainingMarkdown: string, relativePathToThisPage: string): Promise<string>;
21
+ export declare function markdownToMDImageTransformer(block: ListBlockChildrenResponseResult, context: IDocuNotionContext): Promise<string>;
23
22
  export declare function parseImageBlock(image: any): ImageSet;
24
23
  export declare function cleanupOldImages(): Promise<void>;
package/dist/images.js CHANGED
@@ -42,7 +42,7 @@ const axios_1 = __importDefault(require("axios"));
42
42
  const Path = __importStar(require("path"));
43
43
  const MakeImagePersistencePlan_1 = require("./MakeImagePersistencePlan");
44
44
  const log_1 = require("./log");
45
- // We several things here:
45
+ // We handle several things here:
46
46
  // 1) copy images locally instead of leaving them in Notion
47
47
  // 2) change the links to point here
48
48
  // 3) read the caption and if there are localized images, get those too
@@ -75,16 +75,16 @@ exports.standardImageTransformer = {
75
75
  type: "image",
76
76
  // we have to set this one up for each page because we need to
77
77
  // give it two extra parameters that are context for each page
78
- getStringFromBlock: (context, block) => markdownToMDImageTransformer(block, context.directoryContainingMarkdown, context.relativeFilePathToFolderContainingPage),
78
+ getStringFromBlock: (context, block) => markdownToMDImageTransformer(block, context),
79
79
  },
80
80
  ],
81
81
  };
82
82
  // This is a "custom transformer" function passed to notion-to-markdown
83
83
  // eslint-disable-next-line @typescript-eslint/require-await
84
- function markdownToMDImageTransformer(block, fullPathToDirectoryContainingMarkdown, relativePathToThisPage) {
84
+ function markdownToMDImageTransformer(block, context) {
85
85
  return __awaiter(this, void 0, void 0, function* () {
86
86
  const image = block.image;
87
- yield processImageBlock(image, fullPathToDirectoryContainingMarkdown, relativePathToThisPage);
87
+ yield processImageBlock(block, context);
88
88
  // just concatenate the caption text parts together
89
89
  const altText = image.caption
90
90
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
@@ -95,17 +95,17 @@ function markdownToMDImageTransformer(block, fullPathToDirectoryContainingMarkdo
95
95
  });
96
96
  }
97
97
  exports.markdownToMDImageTransformer = markdownToMDImageTransformer;
98
- function processImageBlock(imageBlock, pathToParentDocument, relativePathToThisPage) {
98
+ function processImageBlock(block, context) {
99
99
  return __awaiter(this, void 0, void 0, function* () {
100
+ const imageBlock = block.image;
100
101
  (0, log_1.logDebug)("processImageBlock", JSON.stringify(imageBlock));
101
102
  const imageSet = parseImageBlock(imageBlock);
102
- imageSet.pathToParentDocument = pathToParentDocument;
103
- imageSet.relativePathToParentDocument = relativePathToThisPage;
103
+ imageSet.pageInfo = context.pageInfo;
104
104
  // enhance: it would much better if we could split the changes to markdown separately from actual reading/writing,
105
105
  // so that this wasn't part of the markdown-creation loop. It's already almost there; we just need to
106
106
  // save the imageSets somewhere and then do the actual reading/writing later.
107
107
  yield readPrimaryImage(imageSet);
108
- (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(imageSet, imageOutputPath, imagePrefix);
108
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(context.options, imageSet, block.id, imageOutputPath, imagePrefix);
109
109
  yield saveImage(imageSet);
110
110
  // change the src to point to our copy of the image
111
111
  if ("file" in imageBlock) {
@@ -156,7 +156,7 @@ function saveImage(imageSet) {
156
156
  (0, log_1.verbose)(`No localized image specified for ${localizedImage.iso632Code}, will use primary image.`);
157
157
  // otherwise, we're going to fall back to outputting the primary image here
158
158
  }
159
- const directory = `./i18n/${localizedImage.iso632Code}/docusaurus-plugin-content-docs/current/${imageSet.relativePathToParentDocument}`;
159
+ const directory = `./i18n/${localizedImage.iso632Code}/docusaurus-plugin-content-docs/current/${imageSet.pageInfo.relativeFilePathToFolderContainingPage}`;
160
160
  writeImageIfNew((directory + "/" + imageSet.outputFileName).replaceAll("//", "/"), buffer);
161
161
  }
162
162
  });
@@ -33,8 +33,12 @@ test("Latex Rendering", () => __awaiter(void 0, void 0, void 0, function* () {
33
33
  getBlockChildren: (id) => {
34
34
  return new Promise(resolve => resolve(new Array()));
35
35
  },
36
- directoryContainingMarkdown: "",
37
- relativeFilePathToFolderContainingPage: "",
36
+ // this changes with each page
37
+ pageInfo: {
38
+ directoryContainingMarkdown: "",
39
+ relativeFilePathToFolderContainingPage: "",
40
+ slug: "",
41
+ },
38
42
  layoutStrategy: layoutStrategy,
39
43
  notionToMarkdown: new notion_to_md_1.NotionToMarkdown({ notionClient }),
40
44
  options: {
@@ -1,60 +1,79 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const MakeImagePersistencePlan_1 = require("./MakeImagePersistencePlan");
4
+ const optionsUsingDefaultNaming = {
5
+ notionToken: "",
6
+ rootPage: "",
7
+ locales: [],
8
+ markdownOutputPath: "",
9
+ imgOutputPath: "",
10
+ imgPrefixInMarkdown: "",
11
+ statusTag: "",
12
+ };
13
+ const testImageSet = {
14
+ primaryUrl: "https://s3.us-west-2.amazonaws.com/primaryImage?Blah=foo",
15
+ localizedUrls: [],
16
+ pageInfo: {
17
+ directoryContainingMarkdown: "/pathToParentSomewhere/",
18
+ relativeFilePathToFolderContainingPage: "",
19
+ slug: "my-page",
20
+ },
21
+ fileType: { ext: "png", mime: "image/png" },
22
+ primaryBuffer: Buffer.from("some fake image content"),
23
+ };
4
24
  test("primary file with explicit file output path and prefix", () => {
5
- const imageSet = {
6
- primaryUrl: "https://s3.us-west-2.amazonaws.com/primaryImage?Blah=foo",
7
- localizedUrls: [],
8
- pathToParentDocument: "/pathToParentSomewhere/",
9
- fileType: { ext: "png", mime: "image/png" },
10
- };
11
- (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(imageSet, "./static/notion_imgs", "/notion_imgs");
12
- const expectedHash = (0, MakeImagePersistencePlan_1.hashOfString)("https://s3.us-west-2.amazonaws.com/primaryImage");
13
- expect(imageSet.outputFileName).toBe(`${expectedHash}.png`);
14
- expect(imageSet.primaryFileOutputPath).toBe(`static/notion_imgs/${expectedHash}.png`);
15
- expect(imageSet.filePathToUseInMarkdown).toBe(`/notion_imgs/${expectedHash}.png`);
25
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(optionsUsingDefaultNaming, testImageSet, "ABC-123", "./static/notion_imgs", "/notion_imgs");
26
+ const expectedFileName = "my-page.ABC-123.png";
27
+ expect(testImageSet.outputFileName).toBe(`${expectedFileName}`);
28
+ expect(testImageSet.primaryFileOutputPath).toBe(`static/notion_imgs/${expectedFileName}`);
29
+ expect(testImageSet.filePathToUseInMarkdown).toBe(`/notion_imgs/${expectedFileName}`);
16
30
  });
17
31
  test("primary file with defaults for image output path and prefix", () => {
18
- const imageSet = {
19
- primaryUrl: "https://s3.us-west-2.amazonaws.com/primaryImage?Blah=foo",
20
- localizedUrls: [],
21
- pathToParentDocument: "/pathToParentSomewhere/",
22
- fileType: { ext: "png", mime: "image/png" },
23
- };
24
- (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(imageSet, "", "");
25
- const expectedHash = (0, MakeImagePersistencePlan_1.hashOfString)("https://s3.us-west-2.amazonaws.com/primaryImage");
26
- expect(imageSet.outputFileName).toBe(`${expectedHash}.png`);
32
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(optionsUsingDefaultNaming, testImageSet, "ABC-123", "", "");
33
+ const expectedFileName = "my-page.ABC-123.png";
34
+ expect(testImageSet.outputFileName).toBe(`${expectedFileName}`);
27
35
  // the default behavior is to put the image next to the markdown file
28
- expect(imageSet.primaryFileOutputPath).toBe(`/pathToParentSomewhere/${expectedHash}.png`);
29
- expect(imageSet.filePathToUseInMarkdown).toBe(`./${expectedHash}.png`);
36
+ expect(testImageSet.primaryFileOutputPath).toBe(`/pathToParentSomewhere/${expectedFileName}`);
37
+ expect(testImageSet.filePathToUseInMarkdown).toBe(`./${expectedFileName}`);
30
38
  });
31
39
  test("falls back to getting file extension from url if not in fileType", () => {
32
- const imageSet = {
33
- primaryUrl: "https://s3.us-west-2.amazonaws.com/primaryImage.png",
34
- localizedUrls: [],
35
- pathToParentDocument: "/pathToParentSomewhere/",
36
- };
37
- (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(imageSet, "", "");
38
- const expectedHash = (0, MakeImagePersistencePlan_1.hashOfString)("https://s3.us-west-2.amazonaws.com/primaryImage.png");
39
- expect(imageSet.outputFileName).toBe(`${expectedHash}.png`);
40
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(optionsUsingDefaultNaming, testImageSet, "ABC-123", "", "");
41
+ expect(testImageSet.outputFileName).toBe("my-page.ABC-123.png");
42
+ });
43
+ // I'm not sure it is even possible to have encoded characters in the slug, but this proves
44
+ // we are properly encoding them in the markdown file but not in the file system.
45
+ // (This test originally was for including original file names, but we decided not to do that.)
46
+ test("handles encoded characters", () => {
47
+ const imageSet = Object.assign(Object.assign({}, testImageSet), { pageInfo: {
48
+ directoryContainingMarkdown: "/pathToParentSomewhere/",
49
+ relativeFilePathToFolderContainingPage: "",
50
+ slug: "my-page%281%29",
51
+ } });
52
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(optionsUsingDefaultNaming, imageSet, "ABC-123", "", "");
53
+ expect(imageSet.primaryFileOutputPath).toBe(`/pathToParentSomewhere/my-page(1).ABC-123.png`);
54
+ expect(imageSet.filePathToUseInMarkdown).toBe(`./my-page%281%29.ABC-123.png`);
55
+ });
56
+ const optionsUsingHashNaming = Object.assign(Object.assign({}, optionsUsingDefaultNaming), { imageFileNameFormat: "content-hash" });
57
+ test("hash naming", () => {
58
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(optionsUsingHashNaming, testImageSet, "ABC-123", "", "");
59
+ const expectedFileName = "fe3f26fd515b3cf299ac.png";
60
+ expect(testImageSet.outputFileName).toBe(`${expectedFileName}`);
61
+ });
62
+ const optionsUsingLegacyNaming = Object.assign(Object.assign({}, optionsUsingDefaultNaming), { imageFileNameFormat: "legacy" });
63
+ test("Legacy naming", () => {
64
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(optionsUsingLegacyNaming, testImageSet, "ABC-123", "./static/notion_imgs", "/notion_imgs");
65
+ const expectedHash = (0, MakeImagePersistencePlan_1.hashOfString)("https://s3.us-west-2.amazonaws.com/primaryImage");
66
+ expect(testImageSet.outputFileName).toBe(`${expectedHash}.png`);
40
67
  });
41
- test("properly extract UUID from old-style notion image url", () => {
42
- const imageSet = {
43
- primaryUrl: "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/e1058f46-4d2f-4292-8388-4ad393383439/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220516%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220516T233630Z&X-Amz-Expires=3600&X-Amz-Signature=f215704094fcc884d37073b0b108cf6d1c9da9b7d57a898da38bc30c30b4c4b5&X-Amz-SignedHeaders=host&x-id=GetObject",
44
- localizedUrls: [],
45
- fileType: { ext: "png", mime: "image/png" },
46
- };
47
- (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(imageSet, "./static/notion_imgs", "/notion_imgs");
68
+ test("Legacy naming - properly extract UUID from old-style notion image url", () => {
69
+ const imageSet = Object.assign(Object.assign({}, testImageSet), { primaryUrl: "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/e1058f46-4d2f-4292-8388-4ad393383439/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220516%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220516T233630Z&X-Amz-Expires=3600&X-Amz-Signature=f215704094fcc884d37073b0b108cf6d1c9da9b7d57a898da38bc30c30b4c4b5&X-Amz-SignedHeaders=host&x-id=GetObject" });
70
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(optionsUsingLegacyNaming, imageSet, "ABC-123", "./static/notion_imgs", "/notion_imgs");
48
71
  const expectedHash = (0, MakeImagePersistencePlan_1.hashOfString)("e1058f46-4d2f-4292-8388-4ad393383439");
49
72
  expect(imageSet.outputFileName).toBe(`${expectedHash}.png`);
50
73
  });
51
- test("properly extract UUID from new-style (Sept 2023) notion image url", () => {
52
- const imageSet = {
53
- primaryUrl: "https://prod-files-secure.s3.us-west-2.amazonaws.com/d9a2b712-cf69-4bd6-9d65-87a4ceeacca2/d1bcdc8c-b065-4e40-9a11-392aabeb220e/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230915%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230915T161258Z&X-Amz-Expires=3600&X-Amz-Signature=28fca48e65fba86d539c3c4b7676fce1fa0857aa194f7b33dd4a468ecca6ab24&X-Amz-SignedHeaders=host&x-id=GetObject",
54
- localizedUrls: [],
55
- fileType: { ext: "png", mime: "image/png" },
56
- };
57
- (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(imageSet, "./static/notion_imgs", "/notion_imgs");
74
+ test("Legacy naming - properly extract UUID from new-style (Sept 2023) notion image url", () => {
75
+ const imageSet = Object.assign(Object.assign({}, testImageSet), { primaryUrl: "https://prod-files-secure.s3.us-west-2.amazonaws.com/d9a2b712-cf69-4bd6-9d65-87a4ceeacca2/d1bcdc8c-b065-4e40-9a11-392aabeb220e/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230915%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230915T161258Z&X-Amz-Expires=3600&X-Amz-Signature=28fca48e65fba86d539c3c4b7676fce1fa0857aa194f7b33dd4a468ecca6ab24&X-Amz-SignedHeaders=host&x-id=GetObject" });
76
+ (0, MakeImagePersistencePlan_1.makeImagePersistencePlan)(optionsUsingLegacyNaming, imageSet, "ABC-123", "./static/notion_imgs", "/notion_imgs");
58
77
  const expectedHash = (0, MakeImagePersistencePlan_1.hashOfString)("d1bcdc8c-b065-4e40-9a11-392aabeb220e");
59
78
  expect(imageSet.outputFileName).toBe(`${expectedHash}.png`);
60
79
  });
@@ -29,7 +29,8 @@ function convertInternalUrl(context, url) {
29
29
  exports.convertInternalUrl = convertInternalUrl;
30
30
  // handles the whole markdown link, including the label
31
31
  function convertInternalLink(context, markdownLink) {
32
- const linkRegExp = /\[([^\]]+)?\]\(\/?([^),^/]+)\)/g;
32
+ // match both [foo](/123) and [bar](https://www.notion.so/123) <-- the "mention" link style
33
+ const linkRegExp = /\[([^\]]+)?\]\((?:https?:\/\/www\.notion\.so\/|\/)?([^),^/]+)\)/g;
33
34
  const match = linkRegExp.exec(markdownLink);
34
35
  if (match === null) {
35
36
  (0, log_1.warning)(`[standardInternalLinkConversion] Could not parse link ${markdownLink}`);
@@ -96,7 +97,9 @@ exports.standardInternalLinkConversion = {
96
97
  // (has some other text that's been turned into a link) or "raw".
97
98
  // Raw links come in without a leading slash, e.g. [link_to_page](4a6de8c0-b90b-444b-8a7b-d534d6ec71a4)
98
99
  // Inline links come in with a leading slash, e.g. [pointer to the introduction](/4a6de8c0b90b444b8a7bd534d6ec71a4)
99
- match: /\[([^\]]+)?\]\((?!mailto:)(\/?[^),^/]+)\)/,
100
+ // "Mention" links come in as full URLs, e.g. [link_to_page](https://www.notion.so/62f1187010214b0883711a1abb277d31)
101
+ // 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\/[^),^/]+|\/?[^),^/]+)\)/,
100
103
  convert: convertInternalLink,
101
104
  },
102
105
  };
@@ -38,6 +38,43 @@ test("urls that show up as raw text get left that way", () => __awaiter(void 0,
38
38
  });
39
39
  expect(results.trim()).toBe("https://github.com");
40
40
  }));
41
+ // See https://github.com/sillsdev/docu-notion/issues/97
42
+ test("mention-style link to an existing page", () => __awaiter(void 0, void 0, void 0, function* () {
43
+ const targetPageId = "123";
44
+ const targetPage = (0, pluginTestRun_1.makeSamplePageObject)({
45
+ slug: undefined,
46
+ name: "Hello World",
47
+ id: targetPageId,
48
+ });
49
+ const results = yield getMarkdown({
50
+ type: "paragraph",
51
+ paragraph: {
52
+ rich_text: [
53
+ {
54
+ type: "mention",
55
+ mention: {
56
+ type: "page",
57
+ page: {
58
+ id: `${targetPageId}`,
59
+ },
60
+ },
61
+ annotations: {
62
+ bold: false,
63
+ italic: false,
64
+ strikethrough: false,
65
+ underline: false,
66
+ code: false,
67
+ color: "default",
68
+ },
69
+ plain_text: "foo",
70
+ href: `https://www.notion.so/${targetPageId}`,
71
+ },
72
+ ],
73
+ color: "default",
74
+ },
75
+ }, targetPage);
76
+ expect(results.trim()).toBe(`[foo](/${targetPageId})`);
77
+ }));
41
78
  test("link to an existing page on this site that has no slug", () => __awaiter(void 0, void 0, void 0, function* () {
42
79
  const targetPageId = "123";
43
80
  const targetPage = (0, pluginTestRun_1.makeSamplePageObject)({
@@ -7,4 +7,4 @@ export declare function makeSamplePageObject(options: {
7
7
  name?: string;
8
8
  id?: string;
9
9
  }): NotionPage;
10
- export declare function oneBlockToMarkdown(config: IDocuNotionConfig, block: object, targetPage?: NotionPage): Promise<string>;
10
+ export declare function oneBlockToMarkdown(config: IDocuNotionConfig, block: Record<string, unknown>, targetPage?: NotionPage): Promise<string>;
@@ -49,8 +49,11 @@ children, validApiKey) {
49
49
  },
50
50
  imports: [],
51
51
  //TODO might be needed for some tests, e.g. the image transformer...
52
- directoryContainingMarkdown: "not yet",
53
- relativeFilePathToFolderContainingPage: "not yet",
52
+ pageInfo: {
53
+ directoryContainingMarkdown: "not yet",
54
+ relativeFilePathToFolderContainingPage: "not yet",
55
+ slug: "not yet",
56
+ },
54
57
  layoutStrategy: new HierarchicalNamedLayoutStrategy_1.HierarchicalNamedLayoutStrategy(),
55
58
  options: {
56
59
  notionToken: "",
@@ -90,8 +93,8 @@ children, validApiKey) {
90
93
  // },
91
94
  };
92
95
  if (pages && pages.length) {
93
- console.log(pages[0].matchesLinkId);
94
- console.log(docunotionContext.pages[0].matchesLinkId);
96
+ // console.log(pages[0].matchesLinkId);
97
+ // console.log(docunotionContext.pages[0].matchesLinkId);
95
98
  }
96
99
  const r = yield (0, transform_1.getMarkdownFromNotionBlocks)(docunotionContext, config, blocks);
97
100
  //console.log("blocksToMarkdown", r);
@@ -219,7 +222,7 @@ function makeSamplePageObject(options) {
219
222
  metadata: m,
220
223
  foundDirectlyInOutline: false,
221
224
  });
222
- console.log(p.matchesLinkId);
225
+ // console.log(p.matchesLinkId);
223
226
  return p;
224
227
  }
225
228
  exports.makeSamplePageObject = makeSamplePageObject;
@@ -35,11 +35,15 @@ export type IDocuNotionContext = {
35
35
  options: DocuNotionOptions;
36
36
  getBlockChildren: IGetBlockChildrenFn;
37
37
  notionToMarkdown: NotionToMarkdown;
38
- directoryContainingMarkdown: string;
39
- relativeFilePathToFolderContainingPage: string;
38
+ pageInfo: IDocuNotionContextPageInfo;
40
39
  convertNotionLinkToLocalDocusaurusLink: (url: string) => string | undefined;
41
40
  pages: NotionPage[];
42
41
  counts: ICounts;
43
42
  imports: string[];
44
43
  };
44
+ export type IDocuNotionContextPageInfo = {
45
+ directoryContainingMarkdown: string;
46
+ relativeFilePathToFolderContainingPage: string;
47
+ slug: string;
48
+ };
45
49
  export {};
package/dist/pull.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Client } from "@notionhq/client";
2
2
  import { ListBlockChildrenResponseResults } from "notion-to-md/build/types";
3
+ type ImageFileNameFormat = "default" | "content-hash" | "legacy";
3
4
  export type DocuNotionOptions = {
4
5
  notionToken: string;
5
6
  rootPage: string;
@@ -9,8 +10,10 @@ export type DocuNotionOptions = {
9
10
  imgPrefixInMarkdown: string;
10
11
  statusTag: string;
11
12
  requireSlugs?: boolean;
13
+ imageFileNameFormat?: ImageFileNameFormat;
12
14
  };
13
15
  export declare function notionPull(options: DocuNotionOptions): Promise<void>;
14
16
  export declare function executeWithRateLimitAndRetries<T>(label: string, asyncFunction: () => Promise<T>): Promise<T>;
15
17
  export declare function initNotionClient(notionToken: string): Client;
16
18
  export declare function numberChildrenIfNumberedList(blocks: ListBlockChildrenResponseResults): void;
19
+ export {};
package/dist/pull.js CHANGED
@@ -103,8 +103,12 @@ function outputPages(options, config, pages) {
103
103
  return __awaiter(this, void 0, void 0, function* () {
104
104
  const context = {
105
105
  getBlockChildren: getBlockChildren,
106
- directoryContainingMarkdown: "",
107
- relativeFilePathToFolderContainingPage: "",
106
+ // this changes with each page
107
+ pageInfo: {
108
+ directoryContainingMarkdown: "",
109
+ relativeFilePathToFolderContainingPage: "",
110
+ slug: "",
111
+ },
108
112
  layoutStrategy: layoutStrategy,
109
113
  notionToMarkdown: notionToMarkdown,
110
114
  options: options,
@@ -117,10 +121,11 @@ function outputPages(options, config, pages) {
117
121
  layoutStrategy.pageWasSeen(page);
118
122
  const mdPath = layoutStrategy.getPathForPage(page, ".md");
119
123
  // most plugins should not write to disk, but those handling image files need these paths
120
- context.directoryContainingMarkdown = Path.dirname(mdPath);
124
+ context.pageInfo.directoryContainingMarkdown = Path.dirname(mdPath);
121
125
  // TODO: This needs clarifying: getLinkPathForPage() is about urls, but
122
126
  // downstream images.ts is using it as a file system path
123
- context.relativeFilePathToFolderContainingPage = Path.dirname(layoutStrategy.getLinkPathForPage(page));
127
+ context.pageInfo.relativeFilePathToFolderContainingPage = Path.dirname(layoutStrategy.getLinkPathForPage(page));
128
+ context.pageInfo.slug = page.slug;
124
129
  if (page.type === NotionPage_1.PageType.DatabasePage &&
125
130
  context.options.statusTag != "*" &&
126
131
  page.status !== context.options.statusTag) {
@@ -158,7 +163,7 @@ function getPagesRecursively(options, incomingContext, pageIdOfThisParent, order
158
163
  if (!rootLevel &&
159
164
  pageInfo.hasParagraphs &&
160
165
  pageInfo.childPageIdsAndOrder.length) {
161
- (0, log_1.error)(`Skipping "${pageInTheOutline.nameOrTitle}" and its children. docu-notion does not support pages that are both levels and have content at the same time.`);
166
+ (0, log_1.error)(`Skipping "${pageInTheOutline.nameOrTitle}" and its children. docu-notion does not support pages that are both levels and have text content (paragraphs) at the same time. Normally outline pages should just be composed of 1) links to other pages and 2) child pages (other levels of the outline). Note that @-mention style links appear as text paragraphs to docu-notion so must not be used to form the outline.`);
162
167
  ++counts.skipped_because_level_cannot_have_content;
163
168
  return;
164
169
  }
package/dist/run.js CHANGED
@@ -62,7 +62,10 @@ function run() {
62
62
  ]))
63
63
  .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.")
64
64
  .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.")
65
- .option("--require-slugs", "If set, docu-notion will fail if any pages it would otherwise publish are missing a slug in Notion.", false);
65
+ .option("--require-slugs", "If set, docu-notion will fail if any pages it would otherwise publish are missing a slug in Notion.", false)
66
+ .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.")
67
+ .choices(["default", "content-hash", "legacy"])
68
+ .default("default"));
66
69
  commander_1.program.showHelpAfterError();
67
70
  commander_1.program.parse();
68
71
  (0, log_1.setLogLevel)(commander_1.program.opts().logLevel);
package/package.json CHANGED
@@ -17,7 +17,8 @@
17
17
  "pull-sample-site": "npm run ts -- -n $DOCU_NOTION_INTEGRATION_TOKEN -r $DOCU_NOTION_SAMPLE_ROOT_PAGE --log-level debug",
18
18
  "// test with a semi-stable/public site:": "",
19
19
  "pull-sample": "npm run ts -- -n $DOCU_NOTION_INTEGRATION_TOKEN -r $DOCU_NOTION_SAMPLE_ROOT_PAGE -m ./sample --locales en,es,fr,de --log-level verbose",
20
- "pull-sample-with-paths": "npm run ts -- -n $DOCU_NOTION_INTEGRATION_TOKEN -r $DOCU_NOTION_SAMPLE_ROOT_PAGE -m ./sample --img-output-path ./sample_img"
20
+ "pull-sample-with-paths": "npm run ts -- -n $DOCU_NOTION_INTEGRATION_TOKEN -r $DOCU_NOTION_SAMPLE_ROOT_PAGE -m ./sample --img-output-path ./sample_img",
21
+ "lint": "eslint . --ext .ts"
21
22
  },
22
23
  "//file-type": "have to use this version before they switched to ESM, which gives a compile error related to require()",
23
24
  "//chalk@4": "also ESM related problem",
@@ -90,5 +91,5 @@
90
91
  "volta": {
91
92
  "node": "18.16.0"
92
93
  },
93
- "version": "0.14.0-alpha.14"
94
+ "version": "0.14.0-alpha.16"
94
95
  }