Skip to content

Commit f477672

Browse files
committed
feat: provide a notion to markdown parser
1 parent 5db89f4 commit f477672

File tree

3 files changed

+1808
-0
lines changed

3 files changed

+1808
-0
lines changed

source/markdown.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* *** MIT LICENSE ***
3+
* -------------------------------------------------------------------------
4+
* This code may be modified and distributed under the MIT license.
5+
* See the LICENSE file for details.
6+
* -------------------------------------------------------------------------
7+
*
8+
* @summary Collection of block to markdown convertors
9+
*
10+
* @author Alvis HT Tang <alvis@hilbert.space>
11+
* @license MIT
12+
* @copyright Copyright (c) 2021 - All Rights Reserved.
13+
* -------------------------------------------------------------------------
14+
*/
15+
16+
import type { FullBlock, RichText } from '#types';
17+
18+
/* eslint-disable @typescript-eslint/naming-convention */
19+
20+
/**
21+
* annotate a text as bold
22+
* @param block a RichText block to be annotated
23+
* @returns an annotated RichText block
24+
*/
25+
export function bold(block: RichText): RichText {
26+
return block.annotations.bold
27+
? {
28+
...block,
29+
annotations: { ...block.annotations, bold: false },
30+
plain_text: `**${block.plain_text}**`,
31+
}
32+
: block;
33+
}
34+
35+
/**
36+
* annotate a text as italic
37+
* @param block a RichText block to be annotated
38+
* @returns an annotated RichText block
39+
*/
40+
export function italic(block: RichText): RichText {
41+
return block.annotations.italic
42+
? {
43+
...block,
44+
annotations: { ...block.annotations, italic: false },
45+
plain_text: `_${block.plain_text}_`,
46+
}
47+
: block;
48+
}
49+
50+
/**
51+
* annotate a text as strike-through
52+
* @param block a RichText block to be annotated
53+
* @returns an annotated RichText block
54+
*/
55+
export function strikethrough(block: RichText): RichText {
56+
return block.annotations.strikethrough
57+
? {
58+
...block,
59+
annotations: { ...block.annotations, strikethrough: false },
60+
plain_text: `~~${block.plain_text}~~`,
61+
}
62+
: block;
63+
}
64+
65+
/**
66+
* annotate a text as an inline code
67+
* @param block a RichText block to be annotated
68+
* @returns an annotated RichText block
69+
*/
70+
export function code(block: RichText): RichText {
71+
return block.annotations.code
72+
? {
73+
...block,
74+
annotations: { ...block.annotations, code: false },
75+
plain_text: `\`${block.plain_text}\``,
76+
}
77+
: block;
78+
}
79+
80+
/**
81+
* annotate a text as an inline code
82+
* @param block a RichText block to be annotated
83+
* @returns an annotated RichText block
84+
*/
85+
export function math(block: RichText): RichText {
86+
return block.type === 'equation'
87+
? {
88+
...block,
89+
type: 'text',
90+
plain_text: `$${block.equation.expression}$`,
91+
text: { content: `$${block.equation.expression}$`, link: null },
92+
}
93+
: block;
94+
}
95+
96+
/* eslint-enable */
97+
98+
/**
99+
* convert a RichText block to markdown format
100+
* @param block a RichText block to be parsed
101+
* @returns text in markdown format
102+
*/
103+
export function text(block: RichText): string {
104+
const plain = strikethrough(italic(bold(code(math(block))))).plain_text;
105+
106+
return block.href ? `[${plain}](${block.href})` : plain;
107+
}
108+
109+
/**
110+
* convert RichText blocks to markdown format
111+
* @param blocks RichText blocks to be parsed
112+
* @param indent space to be prefixed to the content per line
113+
* @returns text in markdown format
114+
*/
115+
export function texts(blocks: RichText[], indent = ''): string {
116+
return `${indent}${blocks.map(text).join('')}`;
117+
}
118+
119+
/**
120+
* add children content to the parent text if present
121+
* @param parent first part of the content
122+
* @param block the content block which may contain children
123+
* @param indent space to be prefixed to the content per line
124+
* @returns content with children content if present
125+
*/
126+
function appendChildren(
127+
parent: string,
128+
block: FullBlock,
129+
indent: string,
130+
): string {
131+
const supportedChildren = block.has_children
132+
? block.children.filter((child) => child.type !== 'unsupported')
133+
: [];
134+
135+
if (supportedChildren.length) {
136+
const content = markdown(supportedChildren, indent);
137+
138+
// no extra line for list-like items
139+
const glue = [
140+
'bulleted_list_item',
141+
'numbered_list_item',
142+
'to_do',
143+
undefined,
144+
].includes(supportedChildren[0].type)
145+
? ''
146+
: '\n';
147+
148+
// the ending \n will be attached to the parent block
149+
// so removing it from the children content to prevent extra lines
150+
return parent + '\n' + glue + content.trimRight();
151+
} else {
152+
return parent;
153+
}
154+
}
155+
156+
/**
157+
* convert a Block to markdown format
158+
* @param block a Block to be parsed
159+
* @param indent space to be prefixed to the content per line
160+
* @returns text in markdown format
161+
*/
162+
export function parse(block: FullBlock, indent = ''): string | null {
163+
const append = (text: string): string =>
164+
appendChildren(text, block, `${indent} `);
165+
166+
switch (block.type) {
167+
case 'heading_1':
168+
return `# ${texts(block.heading_1.text)}\n`;
169+
case 'heading_2':
170+
return `## ${texts(block.heading_2.text)}\n`;
171+
case 'heading_3':
172+
return `### ${texts(block.heading_3.text)}\n`;
173+
case 'paragraph':
174+
return `${append(texts(block.paragraph.text))}\n`;
175+
case 'bulleted_list_item':
176+
return indent + append(`* ${texts(block.bulleted_list_item.text)}`);
177+
case 'numbered_list_item':
178+
return indent + append(`1. ${texts(block.numbered_list_item.text)}`);
179+
case 'to_do': {
180+
const checked = block.to_do.checked ? 'x' : ' ';
181+
182+
return indent + append(`- [${checked}] ${texts(block.to_do.text)}`);
183+
}
184+
case 'toggle':
185+
return `${append(texts(block.toggle.text))}\n`;
186+
case 'child_page':
187+
return `${append(block.child_page.title)}\n`;
188+
case 'unsupported':
189+
default:
190+
return null;
191+
}
192+
}
193+
194+
/**
195+
* convert Blocks to markdown format
196+
* @param blocks Blocks to be parsed
197+
* @param indent space to be prefixed to the content per line
198+
* @returns text in markdown format
199+
*/
200+
export function markdown(blocks: FullBlock[], indent = ''): string {
201+
return blocks
202+
.map((block) => parse(block, indent))
203+
.filter((text): text is string => text !== null)
204+
.join('\n');
205+
}

0 commit comments

Comments
 (0)