-
-
Notifications
You must be signed in to change notification settings - Fork 240
Expand file tree
/
Copy pathutils.ts
More file actions
200 lines (175 loc) · 5.59 KB
/
utils.ts
File metadata and controls
200 lines (175 loc) · 5.59 KB
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import { Marked, type Renderer, type TokenizerObject, type MarkedExtension } from 'marked';
import json5 from 'json5';
// helps map a highlighter for languages not recognised or aliased by Shiki
// see https://shiki.style/languages for a full list of official languages
export const SHIKI_LANGUAGE_MAP = {
env: 'dotenv',
html: 'svelte',
sv: 'svelte',
dts: 'typescript',
json: 'jsonc',
// we don't need the coffeescript highlighter because it's only used once
// in a blog post from 2019
cson: '',
// there's no syntax highlighter for tree syntax
tree: '',
'': '',
// already recognised by Shiki but they're here to satisfy TypeScript
js: 'js',
ts: 'ts'
};
export function is_in_code_block(body: string, index: number) {
const code_blocks = [...body.matchAll(/(`{3,}).*\n(.|\n)+?\1/gm)].map((match) => {
return [match.index ?? 0, match[0].length + (match.index ?? 0)] as const;
});
return code_blocks.some(([start, end]) => {
if (index >= start && index <= end) return true;
return false;
});
}
/**
* Strip styling/links etc from markdown
*/
export function clean(markdown: string) {
return (
markdown
.replace(/(?:^|b)\*\*(.+?)\*\*(?:\b|$)/g, '$1') // bold
.replace(/(?:^|b)_(.+?)_(?:\b|$)/g, '$1') // Italics
.replace(/(?:^|b)\*(.+?)\*(?:\b|$)/g, '$1') // Italics
// italic markdown notation such as "bind:_property_ for components"
// should be stripped without affecting compiler error titles such as "animation_missing_key"
.replace(/:_(.*)_ /g, ':$1 ')
.replace(/(?:^|b)`(.+?)`(?:\b|$)/g, '$1') // Inline code
.replace(/(?:^|b)~~(.+?)~~(?:\b|$)/g, '$1') // Strikethrough
.replace(/\[(.+?)\]\(.+?\)/g, '$1') // Link
.replace(/\n/g, ' ') // New line
.replace(/ {2,}/g, ' ')
.trim()
);
}
export function decode_html_entities(text: string): string {
return text
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
return String.fromCharCode(parseInt(hex, 16));
})
.replace(/&#(\d+);/g, (_, dec) => {
return String.fromCharCode(parseInt(dec, 10));
});
}
export const slugify = (str: string) => {
return (
decode_html_entities(clean(str).replace(/(’|’)/g, "'"))
// removes <code>...</code> or <em>...</em> etc, but leaves the contents intact
.replace(/<([a-z\-]+)>(.*?)<\/\1>/g, '$2')
// <audio> should be converted to audio
// <details bind:open> should be converted to details-bind-open
// <script module> should be converted to script-module
// <script lang="ts"> should be converted to script-lang-ts
.replace(/[<>]/g, '')
.replace(/\.\.\./g, '')
.replace(/[^a-zA-Z0-9-$(.):'_]/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^-/, '')
.replace(/-$/, '')
);
};
export function create_slug_deduper() {
const seen = new Map<string, number>();
return (slug: string) => {
const count = seen.get(slug) ?? 0;
seen.set(slug, count + 1);
return count > 0 ? `${slug}-${count}` : slug;
};
}
export function smart_quotes(
str: string,
{ first = true, html = false }: { first?: boolean; html?: boolean } = {}
) {
// replace dumb quotes with smart quotes. This isn't a perfect algorithm — it
// wouldn't correctly handle `That '70s show` or `My country 'tis of thee`
// but a) it's very unlikely they'll occur in our docs, and
// b) they can be dealt with manually
return str.replace(
html ? /(.|^)('|")(.|$)/g : /(.|^)('|")(.|$)/g,
(m, before, quote, after) => {
const left = (first && before === '') || [' ', '\n', '('].includes(before);
let replacement = '';
if (html) {
const double = quote === '"';
replacement = `&${left ? 'l' : 'r'}${double ? 'd' : 's'}quo;`;
} else {
const double = quote === '"';
replacement = double ? (left ? '“' : '”') : left ? '‘' : '’';
}
return (before ?? '') + replacement + (after ?? '');
}
);
}
const tokenizer: TokenizerObject = {
url(src) {
// if `src` is a package version string, eg: [email protected]
// do not tokenize it as email
if (/@\d+\.\d+\.\d+/.test(src)) {
return undefined;
}
// else, use the default tokenizer behavior
return false;
}
};
export async function transform(
markdown: string,
{
walkTokens,
...renderer
}: Partial<Renderer> & { walkTokens?: MarkedExtension['walkTokens'] } = {}
) {
const marked = new Marked({
async: true,
renderer,
tokenizer,
walkTokens
});
return (await marked.parse(markdown)) ?? '';
}
export function extract_frontmatter(markdown: string) {
const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(markdown);
if (!match) return { metadata: {}, body: markdown };
const frontmatter = match[1];
const body = markdown.slice(match[0].length).trim();
const metadata: Record<string, string> = {};
// Prettier might split things awkwardly, so we can't just go line-by-line
let key = '';
let value = '';
for (const line of frontmatter.split('\n')) {
const match = /^(\w+):\s*(.*)$/.exec(line);
if (match) {
if (key) metadata[key] = parse(value);
key = match[1];
value = match[2];
} else {
value += '\n' + line;
}
}
if (key) metadata[key] = parse(value);
return { metadata, body };
}
const parse = (str: string) => {
try {
return json5.parse(str);
} catch (err) {
return str;
}
};
/**
* Type declarations include fully qualified URLs so that they become links when
* you hover over names in an editor with TypeScript enabled. We need to remove
* the origin so that they become root-relative, so that they work in preview
* deployments and when developing locally
*/
export function strip_origin(str: string) {
return str.replaceAll('https://svelte.dev', '');
}