Евгений Кучерявый

Подсветка синтаксиса в Go

Мне мягко намекнули, что читать код без подсветки синтаксиса довольно трудно. А я не готов был мириться с тем, что вы начнёте уходить с сайта, поэтому решил эту самую подсветку реализовать.

Как и в случае с markdown, парсить и красить всё руками я не собирался, поэтому использовал готовую библиотеку chroma. Но так как у меня уже есть 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мс. Пока что я не вижу в этом проблемы, но скоро мне придётся думать об оптимизации.