|
| 1 | +// @ts-expect-error has no types |
| 2 | +import PrismJS from 'prismjs'; |
1 | 3 | import { read } from '$app/server';
|
2 | 4 | import { index } from '$lib/server/content';
|
3 |
| -import { transform } from './markdown.server'; |
| 5 | +import { markedTransform } from '@sveltejs/site-kit/markdown'; |
4 | 6 | import type { Exercise, ExerciseStub, PartStub, Scope } from '$lib/tutorial';
|
5 | 7 | import { error } from '@sveltejs/kit';
|
6 | 8 | import { text_files } from './shared';
|
7 | 9 | import type { Document } from '@sveltejs/site-kit';
|
| 10 | +import { escape_html } from '$lib/utils/escape'; |
| 11 | +import type { Renderer } from 'marked'; |
8 | 12 |
|
9 | 13 | const lookup: Record<
|
10 | 14 | string,
|
@@ -87,6 +91,108 @@ async function get(assets: Record<string, string>, key: string) {
|
87 | 91 | : Buffer.from(await response.arrayBuffer()).toString('base64');
|
88 | 92 | }
|
89 | 93 |
|
| 94 | +const languages = { |
| 95 | + bash: 'bash', |
| 96 | + env: 'bash', |
| 97 | + html: 'markup', |
| 98 | + svelte: 'svelte', |
| 99 | + js: 'javascript', |
| 100 | + css: 'css', |
| 101 | + diff: 'diff', |
| 102 | + ts: 'typescript', |
| 103 | + '': '' |
| 104 | +}; |
| 105 | + |
| 106 | +const delimiter_substitutes = { |
| 107 | + '+++': ' ', |
| 108 | + '---': ' ', |
| 109 | + ':::': ' ' |
| 110 | +}; |
| 111 | + |
| 112 | +function highlight_spans(content: string, classname: string) { |
| 113 | + return `<span class="${classname}">${content}</span>`; |
| 114 | + // return content.replace(/<span class="([^"]+)"/g, (_, classnames) => { |
| 115 | + // return `<span class="${classname} ${classnames}"`; |
| 116 | + // }); |
| 117 | +} |
| 118 | + |
| 119 | +const default_renderer: Partial<Renderer> = { |
| 120 | + code: ({ text, lang = '' }) => { |
| 121 | + /** @type {Record<string, string>} */ |
| 122 | + const options: Record<string, string> = {}; |
| 123 | + |
| 124 | + let source = text |
| 125 | + .replace(/\/\/\/ (.+?)(?:: (.+))?\n/gm, (_, key, value) => { |
| 126 | + options[key] = value; |
| 127 | + return ''; |
| 128 | + }) |
| 129 | + .replace(/^([\-\+])?((?: )+)/gm, (match, prefix = '', spaces) => { |
| 130 | + if (prefix && lang !== 'diff') return match; |
| 131 | + |
| 132 | + // for no good reason at all, marked replaces tabs with spaces |
| 133 | + let tabs = ''; |
| 134 | + for (let i = 0; i < spaces.length; i += 4) { |
| 135 | + tabs += '\t'; |
| 136 | + } |
| 137 | + return prefix + tabs; |
| 138 | + }) |
| 139 | + .replace(/(\+\+\+|---|:::)/g, (_, delimiter: keyof typeof delimiter_substitutes) => { |
| 140 | + return delimiter_substitutes[delimiter]; |
| 141 | + }) |
| 142 | + .replace(/\*\\\//g, '*/'); |
| 143 | + |
| 144 | + let html = '<div class="code-block"><div class="controls">'; |
| 145 | + |
| 146 | + if (options.file) { |
| 147 | + html += `<span class="filename">${options.file}</span>`; |
| 148 | + } |
| 149 | + |
| 150 | + html += '</div>'; |
| 151 | + |
| 152 | + if (lang === 'diff') { |
| 153 | + const lines = source.split('\n').map((content) => { |
| 154 | + let type = null; |
| 155 | + if (/^[\+\-]/.test(content)) { |
| 156 | + type = content[0] === '+' ? 'inserted' : 'deleted'; |
| 157 | + content = content.slice(1); |
| 158 | + } |
| 159 | + |
| 160 | + return { |
| 161 | + type, |
| 162 | + content: escape_html(content) |
| 163 | + }; |
| 164 | + }); |
| 165 | + |
| 166 | + html += `<pre class="language-diff"><code>${lines |
| 167 | + .map((line) => { |
| 168 | + if (line.type) return `<span class="${line.type}">${line.content}\n</span>`; |
| 169 | + return line.content + '\n'; |
| 170 | + }) |
| 171 | + .join('')}</code></pre>`; |
| 172 | + } else { |
| 173 | + const plang = languages[lang as keyof typeof languages]; |
| 174 | + const highlighted = plang |
| 175 | + ? PrismJS.highlight(source, PrismJS.languages[plang], lang) |
| 176 | + : escape_html(source); |
| 177 | + |
| 178 | + html += `<pre class='language-${plang}'><code>${highlighted}</code></pre>`; |
| 179 | + } |
| 180 | + |
| 181 | + html += '</div>'; |
| 182 | + |
| 183 | + return html |
| 184 | + .replace(/ {13}([^ ][^]+?) {13}/g, (_, content) => { |
| 185 | + return highlight_spans(content, 'highlight add'); |
| 186 | + }) |
| 187 | + .replace(/ {11}([^ ][^]+?) {11}/g, (_, content) => { |
| 188 | + return highlight_spans(content, 'highlight remove'); |
| 189 | + }) |
| 190 | + .replace(/ {9}([^ ][^]+?) {9}/g, (_, content) => { |
| 191 | + return highlight_spans(content, 'highlight'); |
| 192 | + }); |
| 193 | + } |
| 194 | +}; |
| 195 | + |
90 | 196 | export async function load_exercise(slug: string): Promise<Exercise> {
|
91 | 197 | if (!(slug in lookup)) {
|
92 | 198 | error(404, 'No such tutorial found');
|
@@ -147,7 +253,8 @@ export async function load_exercise(slug: string): Promise<Exercise> {
|
147 | 253 | prev: prev && { slug: prev.slug },
|
148 | 254 | next,
|
149 | 255 | markdown: exercise.body,
|
150 |
| - html: await transform(exercise.body, { |
| 256 | + html: await markedTransform(exercise.body, { |
| 257 | + ...default_renderer, |
151 | 258 | codespan: ({ text }) =>
|
152 | 259 | filenames.size > 1 && filenames.has(text)
|
153 | 260 | ? `<code data-file="${scope.prefix + text}">${text}</code>`
|
|
0 commit comments