Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,61 @@ const result = n2m.blockToMarkdown(block);
// Result will now only use custom parser if the embed url matches a specific url
```

## Link Transformers

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.

### Usage

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.

### Example

```javascript
const { Client } = require("@notionhq/client");
const { NotionToMarkdown } = require("notion-to-md");

const notion = new Client({
auth: "your integration token",
});

const n2m = new NotionToMarkdown({ notionClient: notion });

// Set a custom link transformer
n2m.setLinkTransformer((text, href) => {
// Custom HTML link with additional attributes
return `<a href="${href}" target="_blank" rel="noopener">${text}</a>`;
});

// Or use it to modify the link behavior
n2m.setLinkTransformer((text, href) => {
// Add custom tracking or modify URLs
const trackingUrl = `${href}?utm_source=notion&utm_medium=markdown`;
return `[${text}](${trackingUrl})`;
});

(async () => {
const mdblocks = await n2m.pageToMarkdown("target_page_id");
const mdString = n2m.toMarkdownString(mdblocks);
console.log(mdString.parent);
})();
```

**Default behavior** (without custom transformer):
```
[Link text](https://example.com)
```

**With custom HTML transformer**:
```
<a href="https://example.com" target="_blank" rel="noopener">Link text</a>
```

**With custom tracking transformer**:
```
[Link text](https://example.com?utm_source=notion&utm_medium=markdown)
```

## Contribution

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Expand Down
56 changes: 56 additions & 0 deletions src/notion-to-md.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,60 @@ describe("setCustomTransformer", () => {
});
expect(md).toBe("---");
});

test("setLinkTransformer works", async () => {
const n2m = new NotionToMarkdown({ notionClient: {} as any });
n2m.setLinkTransformer(async (text, href) => {
return `<a href="${href}" data-testid="my-link">${text}</a>`;
});
const md = await n2m.blockToMarkdown({
id: "test",
type: "paragraph",
paragraph: {
color: "default",
rich_text: [
{
type: "text",
text: {
content: "Link using at-sign ",
link: null,
},
annotations: {
bold: false,
italic: false,
strikethrough: false,
underline: false,
code: false,
color: "default",
},
plain_text: "Link using at-sign ",
href: null,
},
{
type: "mention",
mention: {
type: "page",
page: {
id: "f1b1910b-caec-8014-aecb-d34ee7f50191",
},
},
annotations: {
bold: false,
italic: false,
strikethrough: false,
underline: false,
code: false,
color: "default",
},
plain_text: "My page",
href: "https://www.notion.so/f1b1910bcaec8014aecbd34ee7f50191",
},
],
},
object: "block",
});
expect(md).toBe(
'Link using at-sign <a href="https://www.notion.so/f1b1910bcaec8014aecbd34ee7f50191" data-testid="my-link">My page</a>'
);
});
});
86 changes: 52 additions & 34 deletions src/notion-to-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
import * as md from "./utils/md";
import { getBlockChildren } from "./utils/notion";

type LinkTransformer = (text: string, href: string) => Promise<string>;

/**
* Converts a Notion page to Markdown.
*/
Expand All @@ -37,13 +39,21 @@ export class NotionToMarkdown {

setCustomTransformer(
type: BlockType,
transformer: CustomTransformer,
transformer: CustomTransformer
): NotionToMarkdown {
this.customTransformers[type] = transformer;

return this;
}

setLinkTransformer(fn: LinkTransformer) {
this.linkTransformer = fn;
}

async linkTransformer(text: string, href: string) {
return md.link(text, href);
}

/**
* Converts Markdown Blocks to string
* @param {MdBlock[]} mdBlocks - Array of markdown blocks
Expand All @@ -53,7 +63,7 @@ export class NotionToMarkdown {
toMarkdownString(
mdBlocks: MdBlock[] = [],
pageIdentifier: string = "parent",
nestingLevel: number = 0,
nestingLevel: number = 0
): MdStringObject {
let mdOutput: MdStringObject = {};

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

mdOutput[pageIdentifier] += `${md.addTabSpace(
mdBlocks.parent,
nestingLevel,
nestingLevel
)}\n`;
}
}
Expand Down Expand Up @@ -120,26 +130,27 @@ export class NotionToMarkdown {
mdOutput[pageIdentifier] = mdOutput[pageIdentifier] || "";
if (mdstr[childPageTitle]) {
// child page heading followed by child page content
mdOutput[pageIdentifier] +=
`\n${childPageTitle}\n${mdstr[childPageTitle]}`;
mdOutput[
pageIdentifier
] += `\n${childPageTitle}\n${mdstr[childPageTitle]}`;
}
}
} else if (mdBlocks.type === "toggle") {
// convert children md object to md string
const toggle_children_md_string = this.toMarkdownString(
mdBlocks.children,
mdBlocks.children
);

mdOutput[pageIdentifier] = mdOutput[pageIdentifier] || "";
mdOutput[pageIdentifier] += md.toggle(
mdBlocks.parent,
toggle_children_md_string["parent"],
toggle_children_md_string["parent"]
);
} else if (mdBlocks.type === "quote") {
let mdstr = this.toMarkdownString(
mdBlocks.children,
pageIdentifier,
nestingLevel,
nestingLevel
);

const formattedContent = (mdstr.parent ?? mdstr[pageIdentifier])
Expand All @@ -159,12 +170,11 @@ export class NotionToMarkdown {
mdOutput[pageIdentifier] += "\n";
} else if (mdBlocks.type === "callout") {
// do nothing the callout block is already processed
}
else {
} else {
let mdstr = this.toMarkdownString(
mdBlocks.children,
pageIdentifier,
nestingLevel + 1,
nestingLevel + 1
);

mdOutput[pageIdentifier] = mdOutput[pageIdentifier] || "";
Expand All @@ -188,11 +198,11 @@ export class NotionToMarkdown {
*/
async pageToMarkdown(
id: string,
totalPage: number | null = null,
totalPage: number | null = null
): Promise<MdBlock[]> {
if (!this.notionClient) {
throw new Error(
"notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md",
"notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md"
);
}
const blocks = await getBlockChildren(this.notionClient, id, totalPage);
Expand All @@ -212,11 +222,11 @@ export class NotionToMarkdown {
async blocksToMarkdown(
blocks?: ListBlockChildrenResponseResults,
totalPage: number | null = null,
mdBlocks: MdBlock[] = [],
mdBlocks: MdBlock[] = []
): Promise<MdBlock[]> {
if (!this.notionClient) {
throw new Error(
"notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md",
"notion client is not provided, for more details check out https://github.com/souvikinator/notion-to-md"
);
}

Expand Down Expand Up @@ -244,7 +254,7 @@ export class NotionToMarkdown {
let child_blocks = await getBlockChildren(
this.notionClient,
block_id,
totalPage,
totalPage
);

// Push this block to mdBlocks.
Expand All @@ -265,7 +275,7 @@ export class NotionToMarkdown {
await this.blocksToMarkdown(
child_blocks,
totalPage,
mdBlocks[l - 1].children,
mdBlocks[l - 1].children
);
}

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

let parsedData = "";
Expand Down Expand Up @@ -332,7 +344,7 @@ export class NotionToMarkdown {
return await md.image(
image_title,
link,
this.config.convertImagesToBase64,
this.config.convertImagesToBase64
);
}
break;
Expand Down Expand Up @@ -370,12 +382,12 @@ export class NotionToMarkdown {
title = caption;
} else if (link) {
const matches = link.match(
/[^\/\\&\?]+\.\w{3,4}(?=([\?&].*$|$))/,
/[^\/\\&\?]+\.\w{3,4}(?=([\?&].*$|$))/
);
title = matches ? matches[0] : type;
}

return md.link(title, link);
return await this.linkTransformer(title, link);
}
}
break;
Expand All @@ -399,7 +411,8 @@ export class NotionToMarkdown {
};
}

if (blockContent) return md.link(title, blockContent.url);
if (blockContent)
return await this.linkTransformer(title, blockContent.url);
}
break;

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

/**
Expand All @@ -443,7 +456,7 @@ export class NotionToMarkdown {
await this.blockToMarkdown({
type: "paragraph",
paragraph: { rich_text: cell },
} as ListBlockChildrenResponseResult),
} as ListBlockChildrenResponseResult)
);

const cellStringArr = await Promise.all(cellStringPromise);
Expand Down Expand Up @@ -478,10 +491,10 @@ export class NotionToMarkdown {
// In this case typescript is not able to index the types properly, hence ignoring the error
// @ts-ignore
let blockContent = block[type].text || block[type].rich_text || [];
blockContent.map((content: Text | Equation) => {

for (const content of blockContent) {
if (content.type === "equation") {
parsedData += md.inlineEquation(content.equation.expression);
return;
}

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

if (content["href"])
plain_text = md.link(plain_text, content["href"]);
plain_text = await this.linkTransformer(
plain_text,
content["href"]
);

parsedData += plain_text;
});
}
}
}

switch (type) {
case "code":
{
const codeContent = block.code.rich_text.map((t: any) => t.plain_text).join("\n");
const language = block.code.language || "plaintext";
parsedData = md.codeBlock(codeContent, language);
const codeContent = block.code.rich_text
.map((t: any) => t.plain_text)
.join("\n");
const language = block.code.language || "plaintext";
parsedData = md.codeBlock(codeContent, language);
}
break;

Expand Down Expand Up @@ -542,12 +560,12 @@ export class NotionToMarkdown {
const callout_children_object = await getBlockChildren(
this.notionClient,
id,
100,
100
);

// // parse children blocks to md object
const callout_children = await this.blocksToMarkdown(
callout_children_object,
callout_children_object
);

callout_string += `${parsedData}\n`;
Expand Down