Подсветка синтаксиса в Go
Мне мягко намекнули, что читать код без подсветки синтаксиса довольно трудно. А я не готов был мириться с тем, что вы начнёте уходить с сайта, поэтому решил эту самую подсветку реализовать.
Как и в случае с markdown, парсить и красить всё руками я не собирался, поэтому использовал готовую библиотеку chroma. Но так как у меня уже есть markdown, да ещё и свой велосипед поверх него, пришлось немного поплясать с бубном.
Порядок действий такой:
- Находим все блоки кода в нашем контенте и вырезаем их в отдельный массив.
- Вместо блока кода в тексте оставляем идентификатор вида
CODE_BLOCK_N
. - Отдельно форматируем код.
- Отдельно форматируем markdown.
- Объединяем.
Это разделение сделано для того, чтобы ни один из парсеров не сработал где не нужно.
Парсим код
У меня получилась вот такая функция:
1type CodeBlock struct {
2 Code string
3 Lang string
4 Id string
5}
6
7func ParseCodeBlocks(md []byte) ([]CodeBlock, []byte) {
8 lines := strings.Split(string(md), "\n")
9
10 newLines := make([]string, 0, len(lines))
11
12 var blockLines []string
13
14 blocks := make([]CodeBlock, 0, len(lines))
15
16 lang := ""
17
18 for i, line := range lines {
19 if blockLines != nil {
20 if line == "```" {
21 id := fmt.Sprintf("CODE_BLOCK_%d", i)
22
23 blocks = append(blocks, CodeBlock{
24 Lang: lang,
25 Id: id,
26 Code: strings.Join(blockLines, "\n"),
27 })
28
29 newLines = append(newLines, id)
30
31 blockLines = nil
32 continue
33 }
34 blockLines = append(blockLines, line)
35 continue
36 }
37
38 if len(line) > 2 && line[0:3] == "```" {
39 lang = strings.Replace(line, "```", "", 1)
40 blockLines = make([]string, 0, len(lines))
41 continue
42 }
43
44 newLines = append(newLines, line)
45 }
46
47 return blocks, []byte(strings.Join(newLines, "\n"))
48}
Если говорить человеческим языком, то происходит следующее:
- Разбиваем текст на отдельные строки и циклом пробегаемся по ним.
- Если встречаем строку, которая начинаеся с трёх символов `, то это начало блока кода. Для корректной подсветки на этой же строке добавляется название языка.
- Все строки после начала блока кода мы добавляем в отдельный массив.
- Когда встречаем вторую строку с тремя символами `, мы понимаем, что код кончился.
- Вместо блока мы добавляем заглушку с номером.
- Все остальные строки вставляем как обычно.
На входе:
1Это просто какой-то текст.
2
3\`\`\`go
4package main
5
6import "fmt"
7
8func main() {
9 fmt.Println("Hello, World!")
10}
11\`\`\`
На выходе:
1Это просто какой-то текст.
2
3CODE_BLOCK_1
Ну и функция возвращает два значения: новый текст и отдельно сниппеты кода.
Форматируем код на Go
Как я уже сказал, мы используем готовую библиотеку. И у неё даже есть возможность сделать всё вызовом одной функции. Жаль только, что функция генерирует целую страницу да ещё и с inline стилями — нам это не подходит, поэтому придётся настраивать руками:
1func FormatCode(block CodeBlock) ([]byte, error) {
2 lexer := lexers.Get(block.Lang)
3
4 if lexer == nil {
5 lexer = lexers.Fallback
6 }
7
8 lexer = chroma.Coalesce(lexer)
9
10 style := styles.Get("monokai")
11
12 iterator, err := lexer.Tokenise(nil, block.Code)
13
14 if err != nil {
15 return []byte{}, err
16 }
17
18 formatter := html.New(
19 html.Standalone(false),
20 html.WithClasses(true),
21 html.WithLineNumbers(true),
22 )
23
24 var buf bytes.Buffer
25
26 err = formatter.Format(&buf, style, iterator)
27
28 return buf.Bytes(), err
29}
30
31func RenderCode(h []byte, blocks []CodeBlock) []byte {
32 content := string(h)
33 for _, v := range blocks {
34 code, err := FormatCode(v)
35
36 if err != nil {
37 fmt.Println(err.Error())
38 continue
39 }
40
41 content = strings.Replace(content, v.Id, string(code), 1)
42 }
43
44 return []byte(content)
45}
Компануем страницу
Всё, что нам осталось сделать, это отформатировать markdown и вставить наши блоки.
1func MdToHTML(input []byte) []byte {
2 codeBlocks, md := ParseCodeBlocks(input)
3
4 // create markdown parser with extensions
5 extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
6 p := parser.NewWithExtensions(extensions)
7 doc := p.Parse(md)
8
9 // create HTML renderer with extensions
10 htmlFlags := mdHtml.CommonFlags | mdHtml.HrefTargetBlank | mdHtml.LazyLoadImages | mdHtml.NofollowLinks | mdHtml.NoreferrerLinks | mdHtml.NoopenerLinks
11 opts := mdHtml.RendererOptions{Flags: htmlFlags}
12 renderer := mdHtml.NewRenderer(opts)
13
14 h := markdown.Render(doc, renderer)
15
16 return RenderCode(h, codeBlocks)
17}
Примеры подсветки вы видите прямо на этой странице. Главное, не забыть подключить отдельный файл со стилями.
Заключение
В целом, я доволен, что код теперь более читаемый. Надеюсь, вам это тоже удобно. Однако без компромисса не обошлось — итоговой размер страниц с кодом увеличился в два раза и теперь в среднем составляет 35kb
. А вы говорите, что покраска буков ничего не стоит.
Также в 10 раз увеличилось время сборки статики — с 5мс
до 50мс
. Пока что я не вижу в этом проблемы, но скоро мне придётся думать об оптимизации.