-
Notifications
You must be signed in to change notification settings - Fork 2
/
markdown-render.js
154 lines (145 loc) · 3.78 KB
/
markdown-render.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { marked } from 'marked';
import { chalk } from "zx";
import { reflowText } from './reflow-text.js';
// stateful variable to control left-padding by header level
let currentHeader = 1;
const listItemSeparator = 'LISTITEMSEPARATOR'; // Helper string for rendering ListItems
/**
* @description get padding spaces depending on the last header level used
* @returns string
*/
function getLeftTextPadding() {
return ' '.repeat(
currentHeader === 1 || currentHeader === 2 ? 1 : currentHeader - 1,
);
}
/**
* @description Reads current terminal width if available to limit column width for text-reflowing
* @returns {number}
*/
const defaultMaximumLineWidth = 100;
function getIdealTextWidth(maximumLineWidth = defaultMaximumLineWidth) {
if (typeof process.stdout.columns === 'number') {
if (process.stdout.columns < maximumLineWidth) {
return process.stdout.columns - getLeftTextPadding().length - 5;
}
}
return maximumLineWidth - getLeftTextPadding().length;
}
// Marked custom renderer class
const renderer = {
em(text) {
return chalk.italic(text);
},
strong(text) {
return chalk.bold(text);
},
link(href, title, text) {
// Don't render links to relative paths (like local files)
if (href.startsWith('./') || !href.includes('://')) {
return text;
}
const renderedLink = chalk.bold.blueBright(href);
if (text && text !== href) {
return `${text} ${renderedLink}`;
}
return renderedLink;
},
blockquote(quote) {
return quote;
},
list(body, ordered, start) {
return (
body
.split(listItemSeparator)
.map((listItem, listItemIndex) => {
const bulletPoint = ordered ? `${listItemIndex + start}. ` : '- ';
return reflowText(listItem, getIdealTextWidth())
.split('\n')
.map((listItemLine, listItemLineIndex) => {
if (!listItemLine) {
return '';
}
return `${getLeftTextPadding()}${
listItemLineIndex === 0 ? bulletPoint : ' '
}${listItemLine}`;
})
.join('\n');
})
.join('\n') + '\n'
);
},
listitem(text) {
return text + listItemSeparator;
},
paragraph(text) {
return (
reflowText(text, getIdealTextWidth())
.split('\n')
.map((s) => getLeftTextPadding() + chalk.reset() + s)
.join('\n') + '\n\n'
);
},
codespan(text) {
return chalk.italic.blueBright(`${text}`);
},
code(code) {
return (
code
.split('\n')
.map((s) => getLeftTextPadding() + chalk.reset() + s)
.join('\n') + '\n\n'
);
},
heading(text, level) {
currentHeader = level;
let coloring;
switch (level) {
case 1:
coloring = chalk.bold.underline;
break;
case 3:
case 4:
coloring = chalk;
break;
default:
coloring = chalk.bold;
break;
}
return `${' '.repeat(level === 1 ? 0 : currentHeader - 2)}${coloring(
text,
)}\n`;
},
image(href, title, text) {
// Ignore images by returning an empty string
return '';
},
};
marked.use({ renderer });
marked.setOptions({
mangle: false,
});
const htmlUnescapes = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
''': "'",
'`': '`',
' ': '',
};
/**
* @description Replace HTML entities with their non-encoded variant
* @param {string} text
* @returns {string}
*/
function unescape(text) {
Object.entries(htmlUnescapes).forEach(([escapedChar, unescapedChar]) => {
const escapedCharRegExp = new RegExp(escapedChar, 'g');
text = text.replace(escapedCharRegExp, unescapedChar);
});
return text;
}
export default function renderMarkdown(markdown) {
return unescape(marked.parse(markdown));
}