Skip to content

Commit 60141a4

Browse files
committed
Merge branch 'linkConversionFromPlugin' into main
2 parents ac25bea + 5fa61b4 commit 60141a4

File tree

12 files changed

+9032
-7571
lines changed

12 files changed

+9032
-7571
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"callouts",
88
"DOCU_NOTION",
99
"docu-notion",
10+
"docunotion",
1011
"Greenshot",
1112
"imgur",
1213
"kanban",
@@ -17,4 +18,4 @@
1718
"statusBar.noFolderBackground": "#d649ca",
1819
"statussBar.prominentBackground": "#d649ca"
1920
}
20-
}
21+
}

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"dist/**/*"
88
],
99
"scripts": {
10-
"test": "jest",
10+
"test": "vitest",
1111
"build": "yarn test && tsc && cp ./src/css/*.css dist/",
1212
"build-only": "tsc && cp ./src/css/*.css dist/",
1313
"clean": "rimraf ./dist/",
@@ -68,25 +68,25 @@
6868
},
6969
"devDependencies": {
7070
"@types/fs-extra": "^9.0.13",
71-
"@types/jest": "^28.1.6",
7271
"@types/markdown-table": "^2.0.0",
7372
"@types/node": "^12.20.11",
7473
"@typescript-eslint/eslint-plugin": "^4.22.0",
7574
"@typescript-eslint/parser": "^4.22.0",
75+
"@vitest/ui": "^0.30.1",
7676
"cross-var": "^1.1.0",
7777
"cz-conventional-changelog": "^3.3.0",
7878
"eslint": "^7.25.0",
7979
"eslint-config-prettier": "^8.3.0",
8080
"eslint-plugin-node": "^11.1.0",
8181
"eslint-plugin-prettier": "^3.4.0",
82-
"jest": "^28.1.3",
8382
"lint-staged": "^10.5.4",
8483
"prettier": "^2.2.1",
8584
"rimraf": "^4.1.2",
8685
"semantic-release": "^19.0.2",
87-
"ts-jest": "^28.0.7",
8886
"ts-node": "^10.2.1",
89-
"typescript": "^4.6.4"
87+
"typescript": "^4.6.4",
88+
"vite": "^4.2.1",
89+
"vitest": "^0.30.1"
9090
},
9191
"release": {
9292
"branches": [

src/plugins/internalLinks.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,50 @@ Callouts inline [great page](/hello-world).
458458
);
459459
});
460460

461+
test("internal link inside codeblock ignored", async () => {
462+
const targetPageId = "123";
463+
const targetPage: NotionPage = makeSamplePageObject({
464+
slug: "hello-world",
465+
name: "Hello World",
466+
id: targetPageId,
467+
});
468+
469+
const results = await getMarkdown(
470+
{
471+
type: "code",
472+
code: {
473+
caption: [],
474+
rich_text: [
475+
{
476+
type: "text",
477+
text: {
478+
content:
479+
"this should not change [link](https://www.notion.so/native/metapages/mypage)",
480+
link: null,
481+
},
482+
annotations: {
483+
bold: false,
484+
italic: false,
485+
strikethrough: false,
486+
underline: false,
487+
code: false,
488+
color: "default",
489+
},
490+
plain_text:
491+
"this should not change [link](https://www.notion.so/native/metapages/mypage)",
492+
href: null,
493+
},
494+
],
495+
language: "javascript", // notion assumed javascript in my test in which I didn't specify a language
496+
},
497+
},
498+
targetPage
499+
);
500+
expect(results.trim()).toContain(
501+
"this should not change [link](https://www.notion.so/native/metapages/mypage)"
502+
);
503+
});
504+
461505
async function getMarkdown(block: object, targetPage?: NotionPage) {
462506
const config = {
463507
plugins: [

src/plugins/internalLinks.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,40 @@ import { IDocuNotionContext, IPlugin } from "./pluginTypes";
22
import { error, warning } from "../log";
33
import { NotionPage } from "../NotionPage";
44

5+
// converts a url to a local link, if it is a link to a page in the Notion site
6+
// only here for plugins, notion won't normally be giving us raw urls (at least not that I've noticed)
7+
// If it finds a URL but can't find the page it points to, it will return undefined.
8+
// If it doesn't find a match at all, it returns undefined.
9+
export function convertInternalUrl(
10+
context: IDocuNotionContext,
11+
url: string
12+
): string | undefined {
13+
const kGetIDFromNotionURL = /https:\/\/www\.notion\.so\S+-([a-z,0-9]+)+.*/;
14+
const match = kGetIDFromNotionURL.exec(url);
15+
if (match === null) {
16+
warning(
17+
`[standardInternalLinkConversion] Could not parse link ${url} as a Notion URL`
18+
);
19+
return undefined;
20+
}
21+
const id = match[1];
22+
const pages = context.pages;
23+
// find the page where pageId matches hrefFromNotion
24+
const targetPage = pages.find(p => {
25+
return p.matchesLinkId(id);
26+
});
27+
28+
if (!targetPage) {
29+
// About this situation. See https://github.com/sillsdev/docu-notion/issues/9
30+
warning(
31+
`[standardInternalLinkConversion] Could not find the target of this link. Note that links to outline sections are not supported. ${url}. https://github.com/sillsdev/docu-notion/issues/9`
32+
);
33+
return undefined;
34+
}
35+
return convertLinkHref(context, targetPage, url);
36+
}
37+
38+
// handles the whole markdown link, including the label
539
function convertInternalLink(
640
context: IDocuNotionContext,
741
markdownLink: string
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { NotionPage } from "../NotionPage";
2+
import { makeSamplePageObject, oneBlockToMarkdown } from "./pluginTestRun";
3+
import { IDocuNotionContext, IPlugin } from "./pluginTypes";
4+
5+
test("raw url inside a mermaid codeblock gets converted to path using slug of that page", async () => {
6+
const targetPageId = "123";
7+
const targetPage: NotionPage = makeSamplePageObject({
8+
slug: "slug-of-target",
9+
name: "My Target Page",
10+
id: targetPageId,
11+
});
12+
13+
const input = {
14+
type: "code",
15+
code: {
16+
caption: [],
17+
rich_text: [
18+
{
19+
type: "text",
20+
text: {
21+
content: `click A "https://www.notion.so/native/metapages/A-Page-${targetPageId}"`,
22+
link: null,
23+
},
24+
annotations: {
25+
bold: false,
26+
italic: false,
27+
strikethrough: false,
28+
underline: false,
29+
code: false,
30+
color: "default",
31+
},
32+
plain_text: `click A "https://www.notion.so/native/metapages/A-Page-${targetPageId}"`,
33+
href: null,
34+
},
35+
],
36+
language: "mermaid", // notion assumed javascript in my test in which I didn't specify a language
37+
},
38+
};
39+
40+
const mermaidLinks: IPlugin = {
41+
name: "mermaidLinks",
42+
regexMarkdownModifications: [
43+
{
44+
regex: /```mermaid\n.*"(https:\/\/www\.notion\.so\S+)"/,
45+
includeCodeBlocks: true,
46+
getReplacement: async (
47+
context: IDocuNotionContext,
48+
match: RegExpExecArray
49+
) => {
50+
const url = match[1];
51+
const docusaurusUrl =
52+
context.convertNotionLinkToLocalDocusaurusLink(url);
53+
// eslint-disable-next-line @typescript-eslint/await-thenable
54+
return await match[0].replace(url, docusaurusUrl);
55+
},
56+
},
57+
],
58+
};
59+
60+
const config = {
61+
plugins: [
62+
// standardInternalLinkConversion,
63+
// standardExternalLinkConversion,
64+
mermaidLinks,
65+
],
66+
};
67+
const results = await oneBlockToMarkdown(config, input, targetPage);
68+
expect(results.trim()).toContain(`click A "/slug-of-target"`);
69+
});

src/plugins/pluginTestRun.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { Client } from "@notionhq/client";
22
import { GetPageResponse } from "@notionhq/client/build/src/api-endpoints";
3-
import { info } from "console";
43
import { NotionToMarkdown } from "notion-to-md";
54
import { IDocuNotionContext } from "./pluginTypes";
65
import { HierarchicalNamedLayoutStrategy } from "../HierarchicalNamedLayoutStrategy";
7-
import { error, logDebug, verbose, warning } from "../log";
86
import { NotionPage } from "../NotionPage";
97
import { getMarkdownFromNotionBlocks } from "../transform";
108
import { IDocuNotionConfig } from "../config/configuration";
119
import { NotionBlock } from "../types";
10+
import { convertInternalUrl } from "./internalLinks";
1211

1312
export async function blocksToMarkdown(
1413
config: IDocuNotionConfig,
@@ -34,6 +33,10 @@ export async function blocksToMarkdown(
3433
resolve([]);
3534
});
3635
},
36+
convertNotionLinkToLocalDocusaurusLink: (url: string) => {
37+
return convertInternalUrl(docunotionContext, url);
38+
},
39+
3740
//TODO might be needed for some tests, e.g. the image transformer...
3841
directoryContainingMarkdown: "not yet",
3942
relativeFilePathToFolderContainingPage: "not yet",

src/plugins/pluginTypes.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ export type IRegexMarkdownModification = {
4646
// Based on that regex, the outputPattern will be used to replace the matched text
4747
replacementPattern?: string;
4848
// Instead of a pattern, you can use this if you have to ask a server somewhere for help in getting the new markdown
49-
getReplacement?(s: string): Promise<string>;
49+
getReplacement?(
50+
context: IDocuNotionContext,
51+
match: RegExpExecArray
52+
): Promise<string>;
53+
// normally, anything in code blocks is will be ignored. If you want to make changes inside of code blocks, set this to true.
54+
includeCodeBlocks?: boolean;
5055

5156
// If the output is creating things like react elements, you can import their definitions here
5257
imports?: string[];
@@ -66,6 +71,7 @@ export type IDocuNotionContext = {
6671
notionToMarkdown: NotionToMarkdown;
6772
directoryContainingMarkdown: string;
6873
relativeFilePathToFolderContainingPage: string;
74+
convertNotionLinkToLocalDocusaurusLink: (url: string) => string;
6975
pages: NotionPage[];
7076
counts: ICounts;
7177
};

src/pull.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Client, isFullBlock } from "@notionhq/client";
2828
import { exit } from "process";
2929
import { IDocuNotionConfig, loadConfigAsync } from "./config/configuration";
3030
import { NotionBlock } from "./types";
31+
import { convertInternalUrl } from "./plugins/internalLinks";
3132

3233
export type DocuNotionOptions = {
3334
notionToken: string;
@@ -110,6 +111,8 @@ async function outputPages(
110111
options: options,
111112
pages: pages,
112113
counts: counts, // review will this get copied or pointed to?
114+
convertNotionLinkToLocalDocusaurusLink: (url: string) =>
115+
convertInternalUrl(context, url),
113116
};
114117
for (const page of pages) {
115118
layoutStrategy.pageWasSeen(page);

src/transform.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ export async function getMarkdownFromNotionBlocks(
5656
//console.log("markdown after link fixes", markdown);
5757

5858
// simple regex-based tweaks. These are usually related to docusaurus
59-
const { imports, body } = await doTransformsOnMarkdown(config, markdown);
59+
const { imports, body } = await doTransformsOnMarkdown(
60+
context,
61+
config,
62+
markdown
63+
);
6064

6165
// console.log("markdown after regex fixes", markdown);
6266
// console.log("body after regex", body);
@@ -82,6 +86,7 @@ function doNotionBlockTransforms(
8286
}
8387

8488
async function doTransformsOnMarkdown(
89+
context: IDocuNotionContext,
8590
config: IDocuNotionConfig,
8691
input: string
8792
) {
@@ -109,15 +114,24 @@ async function doTransformsOnMarkdown(
109114
// regex.exec is stateful, so we don't want to mess up the plugin's use of its own regex, so we clone it.
110115
// we also add the "g" flag to make sure we get all matches
111116
const regex = new RegExp(`${codeBlocks.source}|(${mod.regex.source})`, "g");
112-
let count = 0;
113117
while ((match = regex.exec(input)) !== null) {
114118
if (match[0]) {
115119
const original = match[0];
116-
if (original.startsWith("```") && original.endsWith("```")) {
117-
continue; // code block
120+
if (
121+
original.startsWith("```") &&
122+
original.endsWith("```") &&
123+
!mod.includeCodeBlocks
124+
) {
125+
continue; // code block, and they didn't say to include them
118126
}
119127
if (mod.getReplacement) {
120-
replacement = await mod.getReplacement(original);
128+
// our match here has an extra group, which is an implementation detail
129+
// that shouldn't be made visible to the plugin
130+
const matchAsThePluginWouldExpectIt = mod.regex.exec(match[0])!;
131+
replacement = await mod.getReplacement(
132+
context,
133+
matchAsThePluginWouldExpectIt
134+
);
121135
} else if (mod.replacementPattern) {
122136
console.log(`mod.replacementPattern.replace("$1", ${match[2]}`);
123137
replacement = mod.replacementPattern.replace("$1", match[2]);
@@ -149,7 +163,8 @@ async function doNotionToMarkdown(
149163
blocks
150164
);
151165

152-
let markdown = docunotionContext.notionToMarkdown.toMarkdownString(mdBlocks);
166+
const markdown =
167+
docunotionContext.notionToMarkdown.toMarkdownString(mdBlocks);
153168
return markdown;
154169
}
155170

@@ -187,6 +202,7 @@ function doLinkFixes(
187202
config.plugins.some(plugin => {
188203
if (!plugin.linkModifier) return false;
189204
if (plugin.linkModifier.match.exec(originalLinkMarkdown) === null) {
205+
verbose(`plugin "${plugin.name}" did not match this url`);
190206
return false;
191207
}
192208
const newMarkdown = plugin.linkModifier.convert(

tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
4747
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
4848
// "typeRoots": [], /* List of folders to include type definitions from. */
49-
// "types": [], /* Type declaration files to be included in compilation. */
49+
"types": [
50+
"vitest/globals"
51+
],
5052
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
5153
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
5254
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */

0 commit comments

Comments
 (0)