Skip to content

Commit 9e04226

Browse files
committed
feat: add setLinkTransformer for adding custom links
1 parent 69642ce commit 9e04226

File tree

3 files changed

+163
-34
lines changed

3 files changed

+163
-34
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,61 @@ const result = n2m.blockToMarkdown(block);
313313
// Result will now only use custom parser if the embed url matches a specific url
314314
```
315315

316+
## Link Transformers
317+
318+
The `linkTransformer` functionality allows you to customize how links are transformed when converting Notion blocks to Markdown. This is particularly useful if you want to modify the appearance or behavior of links in the generated Markdown.
319+
320+
### Usage
321+
322+
To use a link transformer, you can define a custom function that takes a link block and returns a string. This function can be set using the `setLinkTransformer` method.
323+
324+
### Example
325+
326+
```javascript
327+
const { Client } = require("@notionhq/client");
328+
const { NotionToMarkdown } = require("notion-to-md");
329+
330+
const notion = new Client({
331+
auth: "your integration token",
332+
});
333+
334+
const n2m = new NotionToMarkdown({ notionClient: notion });
335+
336+
// Set a custom link transformer
337+
n2m.setLinkTransformer((text, href) => {
338+
// Custom HTML link with additional attributes
339+
return `<a href="${href}" target="_blank" rel="noopener">${text}</a>`;
340+
});
341+
342+
// Or use it to modify the link behavior
343+
n2m.setLinkTransformer((text, href) => {
344+
// Add custom tracking or modify URLs
345+
const trackingUrl = `${href}?utm_source=notion&utm_medium=markdown`;
346+
return `[${text}](${trackingUrl})`;
347+
});
348+
349+
(async () => {
350+
const mdblocks = await n2m.pageToMarkdown("target_page_id");
351+
const mdString = n2m.toMarkdownString(mdblocks);
352+
console.log(mdString.parent);
353+
})();
354+
```
355+
356+
**Default behavior** (without custom transformer):
357+
```
358+
[Link text](https://example.com)
359+
```
360+
361+
**With custom HTML transformer**:
362+
```
363+
<a href="https://example.com" target="_blank" rel="noopener">Link text</a>
364+
```
365+
366+
**With custom tracking transformer**:
367+
```
368+
[Link text](https://example.com?utm_source=notion&utm_medium=markdown)
369+
```
370+
316371
## Contribution
317372

318373
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

src/notion-to-md.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,60 @@ describe("setCustomTransformer", () => {
6565
});
6666
expect(md).toBe("---");
6767
});
68+
69+
test("setLinkTransformer works", async () => {
70+
const n2m = new NotionToMarkdown({ notionClient: {} as any });
71+
n2m.setLinkTransformer(async (text, href) => {
72+
return `<a href="${href}" data-testid="my-link">${text}</a>`;
73+
});
74+
const md = await n2m.blockToMarkdown({
75+
id: "test",
76+
type: "paragraph",
77+
paragraph: {
78+
color: "default",
79+
rich_text: [
80+
{
81+
type: "text",
82+
text: {
83+
content: "Link using at-sign ",
84+
link: null,
85+
},
86+
annotations: {
87+
bold: false,
88+
italic: false,
89+
strikethrough: false,
90+
underline: false,
91+
code: false,
92+
color: "default",
93+
},
94+
plain_text: "Link using at-sign ",
95+
href: null,
96+
},
97+
{
98+
type: "mention",
99+
mention: {
100+
type: "page",
101+
page: {
102+
id: "f1b1910b-caec-8014-aecb-d34ee7f50191",
103+
},
104+
},
105+
annotations: {
106+
bold: false,
107+
italic: false,
108+
strikethrough: false,
109+
underline: false,
110+
code: false,
111+
color: "default",
112+
},
113+
plain_text: "My page",
114+
href: "https://www.notion.so/f1b1910bcaec8014aecbd34ee7f50191",
115+
},
116+
],
117+
},
118+
object: "block",
119+
});
120+
expect(md).toBe(
121+
'Link using at-sign <a href="https://www.notion.so/f1b1910bcaec8014aecbd34ee7f50191" data-testid="my-link">My page</a>'
122+
);
123+
});
68124
});

src/notion-to-md.ts

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
import * as md from "./utils/md";
1717
import { getBlockChildren } from "./utils/notion";
1818

19+
type LinkTransformer = (text: string, href: string) => Promise<string>;
20+
1921
/**
2022
* Converts a Notion page to Markdown.
2123
*/
@@ -37,13 +39,21 @@ export class NotionToMarkdown {
3739

3840
setCustomTransformer(
3941
type: BlockType,
40-
transformer: CustomTransformer,
42+
transformer: CustomTransformer
4143
): NotionToMarkdown {
4244
this.customTransformers[type] = transformer;
4345

4446
return this;
4547
}
4648

49+
setLinkTransformer(fn: LinkTransformer) {
50+
this.linkTransformer = fn;
51+
}
52+
53+
async linkTransformer(text: string, href: string) {
54+
return md.link(text, href);
55+
}
56+
4757
/**
4858
* Converts Markdown Blocks to string
4959
* @param {MdBlock[]} mdBlocks - Array of markdown blocks
@@ -53,7 +63,7 @@ export class NotionToMarkdown {
5363
toMarkdownString(
5464
mdBlocks: MdBlock[] = [],
5565
pageIdentifier: string = "parent",
56-
nestingLevel: number = 0,
66+
nestingLevel: number = 0
5767
): MdStringObject {
5868
let mdOutput: MdStringObject = {};
5969

@@ -80,15 +90,15 @@ export class NotionToMarkdown {
8090
// add extra line breaks non list blocks
8191
mdOutput[pageIdentifier] += `\n${md.addTabSpace(
8292
mdBlocks.parent,
83-
nestingLevel,
93+
nestingLevel
8494
)}\n\n`;
8595
} else {
8696
// initialize if key doesn't exist
8797
mdOutput[pageIdentifier] = mdOutput[pageIdentifier] || "";
8898

8999
mdOutput[pageIdentifier] += `${md.addTabSpace(
90100
mdBlocks.parent,
91-
nestingLevel,
101+
nestingLevel
92102
)}\n`;
93103
}
94104
}
@@ -120,26 +130,27 @@ export class NotionToMarkdown {
120130
mdOutput[pageIdentifier] = mdOutput[pageIdentifier] || "";
121131
if (mdstr[childPageTitle]) {
122132
// child page heading followed by child page content
123-
mdOutput[pageIdentifier] +=
124-
`\n${childPageTitle}\n${mdstr[childPageTitle]}`;
133+
mdOutput[
134+
pageIdentifier
135+
] += `\n${childPageTitle}\n${mdstr[childPageTitle]}`;
125136
}
126137
}
127138
} else if (mdBlocks.type === "toggle") {
128139
// convert children md object to md string
129140
const toggle_children_md_string = this.toMarkdownString(
130-
mdBlocks.children,
141+
mdBlocks.children
131142
);
132143

133144
mdOutput[pageIdentifier] = mdOutput[pageIdentifier] || "";
134145
mdOutput[pageIdentifier] += md.toggle(
135146
mdBlocks.parent,
136-
toggle_children_md_string["parent"],
147+
toggle_children_md_string["parent"]
137148
);
138149
} else if (mdBlocks.type === "quote") {
139150
let mdstr = this.toMarkdownString(
140151
mdBlocks.children,
141152
pageIdentifier,
142-
nestingLevel,
153+
nestingLevel
143154
);
144155

145156
const formattedContent = (mdstr.parent ?? mdstr[pageIdentifier])
@@ -159,12 +170,11 @@ export class NotionToMarkdown {
159170
mdOutput[pageIdentifier] += "\n";
160171
} else if (mdBlocks.type === "callout") {
161172
// do nothing the callout block is already processed
162-
}
163-
else {
173+
} else {
164174
let mdstr = this.toMarkdownString(
165175
mdBlocks.children,
166176
pageIdentifier,
167-
nestingLevel + 1,
177+
nestingLevel + 1
168178
);
169179

170180
mdOutput[pageIdentifier] = mdOutput[pageIdentifier] || "";
@@ -188,11 +198,11 @@ export class NotionToMarkdown {
188198
*/
189199
async pageToMarkdown(
190200
id: string,
191-
totalPage: number | null = null,
201+
totalPage: number | null = null
192202
): Promise<MdBlock[]> {
193203
if (!this.notionClient) {
194204
throw new Error(
195-
"notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md",
205+
"notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md"
196206
);
197207
}
198208
const blocks = await getBlockChildren(this.notionClient, id, totalPage);
@@ -212,11 +222,11 @@ export class NotionToMarkdown {
212222
async blocksToMarkdown(
213223
blocks?: ListBlockChildrenResponseResults,
214224
totalPage: number | null = null,
215-
mdBlocks: MdBlock[] = [],
225+
mdBlocks: MdBlock[] = []
216226
): Promise<MdBlock[]> {
217227
if (!this.notionClient) {
218228
throw new Error(
219-
"notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md",
229+
"notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md"
220230
);
221231
}
222232

@@ -244,7 +254,7 @@ export class NotionToMarkdown {
244254
let child_blocks = await getBlockChildren(
245255
this.notionClient,
246256
block_id,
247-
totalPage,
257+
totalPage
248258
);
249259

250260
// Push this block to mdBlocks.
@@ -265,7 +275,7 @@ export class NotionToMarkdown {
265275
await this.blocksToMarkdown(
266276
child_blocks,
267277
totalPage,
268-
mdBlocks[l - 1].children,
278+
mdBlocks[l - 1].children
269279
);
270280
}
271281

@@ -289,7 +299,9 @@ export class NotionToMarkdown {
289299
* @param {ListBlockChildrenResponseResult} block - single notion block
290300
* @returns {string} corresponding markdown string of the passed block
291301
*/
292-
async blockToMarkdown(block: ListBlockChildrenResponseResult) {
302+
async blockToMarkdown(
303+
block: ListBlockChildrenResponseResult
304+
): Promise<string> {
293305
if (typeof block !== "object" || !("type" in block)) return "";
294306

295307
let parsedData = "";
@@ -332,7 +344,7 @@ export class NotionToMarkdown {
332344
return await md.image(
333345
image_title,
334346
link,
335-
this.config.convertImagesToBase64,
347+
this.config.convertImagesToBase64
336348
);
337349
}
338350
break;
@@ -370,12 +382,12 @@ export class NotionToMarkdown {
370382
title = caption;
371383
} else if (link) {
372384
const matches = link.match(
373-
/[^\/\\&\?]+\.\w{3,4}(?=([\?&].*$|$))/,
385+
/[^\/\\&\?]+\.\w{3,4}(?=([\?&].*$|$))/
374386
);
375387
title = matches ? matches[0] : type;
376388
}
377389

378-
return md.link(title, link);
390+
return await this.linkTransformer(title, link);
379391
}
380392
}
381393
break;
@@ -399,7 +411,8 @@ export class NotionToMarkdown {
399411
};
400412
}
401413

402-
if (blockContent) return md.link(title, blockContent.url);
414+
if (blockContent)
415+
return await this.linkTransformer(title, blockContent.url);
403416
}
404417
break;
405418

@@ -430,7 +443,7 @@ export class NotionToMarkdown {
430443
const tableRows = await getBlockChildren(this.notionClient, id, 100);
431444
let rowsPromise = tableRows?.map(async (row) => {
432445
const { type } = row as any;
433-
if (type !== 'table_row') return
446+
if (type !== "table_row") return;
434447
const cells = (row as any).table_row["cells"];
435448

436449
/**
@@ -443,7 +456,7 @@ export class NotionToMarkdown {
443456
await this.blockToMarkdown({
444457
type: "paragraph",
445458
paragraph: { rich_text: cell },
446-
} as ListBlockChildrenResponseResult),
459+
} as ListBlockChildrenResponseResult)
447460
);
448461

449462
const cellStringArr = await Promise.all(cellStringPromise);
@@ -478,10 +491,10 @@ export class NotionToMarkdown {
478491
// In this case typescript is not able to index the types properly, hence ignoring the error
479492
// @ts-ignore
480493
let blockContent = block[type].text || block[type].rich_text || [];
481-
blockContent.map((content: Text | Equation) => {
494+
495+
for (const content of blockContent) {
482496
if (content.type === "equation") {
483497
parsedData += md.inlineEquation(content.equation.expression);
484-
return;
485498
}
486499

487500
const annotations = content.annotations;
@@ -490,19 +503,24 @@ export class NotionToMarkdown {
490503
plain_text = this.annotatePlainText(plain_text, annotations);
491504

492505
if (content["href"])
493-
plain_text = md.link(plain_text, content["href"]);
506+
plain_text = await this.linkTransformer(
507+
plain_text,
508+
content["href"]
509+
);
494510

495511
parsedData += plain_text;
496-
});
512+
}
497513
}
498514
}
499515

500516
switch (type) {
501517
case "code":
502518
{
503-
const codeContent = block.code.rich_text.map((t: any) => t.plain_text).join("\n");
504-
const language = block.code.language || "plaintext";
505-
parsedData = md.codeBlock(codeContent, language);
519+
const codeContent = block.code.rich_text
520+
.map((t: any) => t.plain_text)
521+
.join("\n");
522+
const language = block.code.language || "plaintext";
523+
parsedData = md.codeBlock(codeContent, language);
506524
}
507525
break;
508526

@@ -542,12 +560,12 @@ export class NotionToMarkdown {
542560
const callout_children_object = await getBlockChildren(
543561
this.notionClient,
544562
id,
545-
100,
563+
100
546564
);
547565

548566
// // parse children blocks to md object
549567
const callout_children = await this.blocksToMarkdown(
550-
callout_children_object,
568+
callout_children_object
551569
);
552570

553571
callout_string += `${parsedData}\n`;

0 commit comments

Comments
 (0)