Всё это прекрасно. Однако выяснилось, что некоторые статьи данного блога (например эта) требуют прямого цитирования подсветки vim, поскольку в них, собственно, обсуждается как vim подсвечивает код и приводятся примеры. Я рассказывал, как я вставляю подсвеченный исходный код из vim в статьи блога здесь. Главная идея - волшебная команда vim MakeBlogArticle, которая преобразует выделенный текст (или весь буфер) в формат HTML с сохраненной подсветкой синтаксиса. MakeBlogArticle использует в качестве бэкенда скрипт TOhtml, поэтому реализовать ее не сложно. Теперь же нам нужна подобная команда MakeTexCodeHighlight, которая будет транслировать выделенный текст (или весь буфер) в некоторое представление tex, совместимое с документами, сгенерированными pandoc. Требования к совместимости понятны. Поскольку pandoc для подсветки исходного кода генерирует среду Shaded, реализованную через fancyvrb, то результирующий документ должен находиться внутри тэгов
\begin{Shaded} \begin{Highlighting}[] ... \end{Highlighting} \end{Shaded}, а внутри своего тела использовать цветовые тэги \textcolor.
Давайте реализуем команду MakeTexCodeHighlight.
Прежде всего нам понадобятся функции, определяющие цвета текста и фона синтаксической группы элемента под курсором. Вот их реализация:
fun! <SID>get_color_under_cursor(bg) let synId = synID(line("."), col("."), 1) let name = synIDattr(synId, "name") if name == '' return 'none' endif let layer = a:bg ? "bg" : "fg" return <SID>Xterm2rgb256(synIDattr(synIDtrans(synId), layer)) endfun fun! GetFgColorUnderCursor() return <SID>get_color_under_cursor(0) endfun fun! GetBgColorUnderCursor() return <SID>get_color_under_cursor(1) endfunФункция get_color_under_cursor() - рабочая лошадка, выполняющая задания двух основных функций: GetFgColorUnderCursor() и GetBgColorUnderCursor(). Ее задача очень простая - выдать шестнадцатиричное значение цвета под курсором. Выражение
synIDattr(synIDtrans(synId), layer)возвращает номер этого цвета для 256-цветного терминала (да, я ограничился текстовой версией vim, в gvim это работать не будет!), а функция Xterm2rgb256() должна вернуть окончательное шестнадцатиричное число в виде строки. Функции Xterm2rgb256() в vim нет, мы должны ее реализовать. К счастью, подобных функций в разных плагинах vim полно. Многие из них используют обычную таблицу соответствия, однако я нашел аналитическую реализацию данной функции в плагине Colorizer. Вот адаптированная версия Xterm2rgb256(), возвращающая шестнадцатиричное число в виде строки:
" next Xterm2rgb... conversion functions are adopted from plugin Colorizer.vim fun! <SID>Xterm2rgb16(color) " 16 basic colors let r=0 let g=0 let b=0 let basic16 = [ \ [ 0x00, 0x00, 0x00 ], \ [ 0xCD, 0x00, 0x00 ], \ [ 0x00, 0xCD, 0x00 ], \ [ 0xCD, 0xCD, 0x00 ], \ [ 0x00, 0x00, 0xEE ], \ [ 0xCD, 0x00, 0xCD ], \ [ 0x00, 0xCD, 0xCD ], \ [ 0xE5, 0xE5, 0xE5 ], \ [ 0x7F, 0x7F, 0x7F ], \ [ 0xFF, 0x00, 0x00 ], \ [ 0x00, 0xFF, 0x00 ], \ [ 0xFF, 0xFF, 0x00 ], \ [ 0x5C, 0x5C, 0xFF ], \ [ 0xFF, 0x00, 0xFF ], \ [ 0x00, 0xFF, 0xFF ], \ [ 0xFF, 0xFF, 0xFF ] \ ] let r = basic16[a:color][0] let g = basic16[a:color][1] let b = basic16[a:color][2] return printf("%02x%02x%02x", r, g, b) endfun fun! <SID>Xterm2rgb256(color) let r=0 let g=0 let b=0 " 16 basic colors if a:color < 16 return <SID>Xterm2rgb16(a:color) " color cube color elseif a:color >= 16 && a:color < 232 " the 6 value iterations in the xterm color cube let valuerange6 = [ 0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF ] let color=a:color-16 let r = valuerange6[(color/36)%6] let g = valuerange6[(color/6)%6] let b = valuerange6[color%6] " gray tone elseif a:color >= 232 && a:color <= 255 let r = 8 + (a:color-232) * 0x0a let g = r let b = r endif return printf("%02x%02x%02x", r, g, b) endfunТеперь мы можем определить команды vim для того, чтобы комфортно получать значения цветов синтаксического элемента под курсором из терминала (для нашей основной цели эти команды не нужны, разве что только для отладки):
command GetFgColorUnderCursor echo GetFgColorUnderCursor() command GetBgColorUnderCursor echo GetBgColorUnderCursor()Следующим шагом будет реализация функции split_synids(), которая будет разбивать текст внутри области текста, ограниченной значениями номеров строк, переданных ей в качестве аргументов, на отдельные элементы, включающие в себя имя синтаксического элемента, его содержание (т.е. соответствующую подстроку), номер строки, в которой он находится, а также значения цветов текста и фона в шестнадцатиричном формате.
fun! <SID>split_synids(fst_line, last_line) let result = [] let save_cursor = getpos('.') let save_winline = winline() call setpos('.', [0, a:fst_line, 1, 0]) let cursor = getpos('.') while cursor[1] <= a:last_line let old_synId = '^' let old_start = cursor[2] let cols = col('$') if cols == 1 let cursor[1] += 1 let cursor[2] = 1 call setpos('.', cursor) continue endif while cursor[2] <= cols let synId = synIDattr(synID(line('.'), col('.'), 1), 'name') let fg = toupper(GetFgColorUnderCursor()) let bg = toupper(GetBgColorUnderCursor()) let cursor[2] += 1 call setpos('.', cursor) if synId != old_synId if old_synId != '^' call add(result, \ {'name': old_synId, \ 'content': strpart(getline('.'), old_start - 1, \ cursor[2] - old_start - 1), \ 'line': line('.'), 'fg': old_fg, 'bg': old_bg}) endif let old_synId = synId let old_start = cursor[2] - 1 endif let old_fg = fg let old_bg = bg endwhile call add(result, \ {'name': synId, \ 'content': strpart(getline('.'), old_start - 1, \ cursor[2] - old_start - 1), \ 'line': line('.'), 'fg': fg, 'bg': bg}) let cursor[1] += 1 let cursor[2] = 1 call setpos('.', cursor) endwhile call setpos('.', save_cursor) let move = winline() - save_winline if move != 0 let dir = move < 0 ? '^Y' : '^E' exe "normal ".abs(move).dir endif return result endfunВ переменной result будем сохранять список синтаксических элементов. Манипуляции с save_cursor и save_winline нужны для восстановления положения курсора и окна после завершения работы функции. Внутренний цикл while обходит элементы одной строки слева направо, добавляя информацию о встреченных синтаксических областях в result. Для этой цели вызываются функции GetFgColorUnderCursor() и GetBgColorUnderCursor(), которые идентифицируют синтаксический элемент под курсором. Возвращенное ими значение (строка, представляющая шестнадцатиричное число) переводится в верхний регистр: это будет нужно в итоговом документе, так как latex почему-то не любит нижний регистр в шестнадцатиричных числах (во всяком случае это верно для тэгов \textcolor). Внешний цикл while перебирает строки сверху вниз.
Теперь напишем функцию write_latex_code_highlights(). Она будет выполнять основную работу по созданию документа: открывать окно для нового буфера, вводить открывающие и закрывающие тэги tex и, собственно, тело документа на основании списка, возвращенного функцией split_synids().
fun! <SID>write_latex_code_highlights(fst_line, last_line, ...) let colors = g:colors_name colorscheme lucius let parts = <SID>split_synids(a:fst_line, a:last_line) exe "colorscheme ".colors let save_paste = &paste new +set\ nowrap\ paste normal a\begin{Shaded}^M\begin{Highlighting}[]^M let old_line = a:fst_line let line = old_line for hl in parts let line = hl['line'] while line > old_line normal o let old_line += 1 endwhile let part = escape(hl['content'], '\{}_$%') let part = substitute(part, '\\\\', '\\textbackslash{}', 'g') let fg = hl['fg'] " small hack to paint syntax group links that could have been lost " after switching colorscheme in black instead white if a:0 && a:1 && fg == 'FFFFFF' let fg = '000000' endif if fg != 'NONE' && part !~ '^\s*$' let part = '\textcolor[HTML]{'.fg.'}{'.part.'}' endif exe "normal a".part endfor while line < a:last_line normal o let line += 1 endwhile normal a^M0^D\end{Highlighting}^M\end{Shaded}^[gg set ft=tex if !save_paste set nopaste endif endfunКод, в общем-то, не должен вызывать затруднений. Прокомментирую лишь некоторые моменты. Я уже писал, зачем при создании документов для публикации я заменяю рабочую цветовую схему на схему lucius: просто она светлая и более подходит для создания статей, чем моя стандартная темная схема. Элементы ^M, ^D и ^[ в коде состоят не из двух символов; это одиночные символы, соответствующие вводу <Enter>, Ctrl-D и <Esc>. Чтобы их ввести в редакторе vim, наберите Ctrl-V и, удерживая клавишу Ctrl, соответствующий символ (M, D или <Esc>). Команда new устанавливает в новом буфере значения nowrap (чтобы строки не заворачивались - нет смысла) и paste (чтобы vim не выполнял вредную автоматическую индентацию текста). Поскольку значение paste устанавливается глобально, то оно восстанавливается при выходе из функции. Внутри цикла for собранные функцией split_synids() синтаксические элементы выводятся с помощью normal a, новые строки добавляются с помощью normal o. При выводе в буфер все символы {, }, _, $ и % слэшируются, а символ \ заменяется на \textbackslash{} - если этого не сделать, то возможны ложные интерпретации строк буфера как команд latex! Если цвет fg данного элемента не NONE, то он обрамляется тэгом \textcolor[HTML]{}{}.
Опциональный третий аргумент write_latex_code_highlights() преобразует белый текст в черный. Белый текст может появиться в результате исчезновения синтаксических групп при переходе в новую цветовую схему. Дело в том, что я использую расширенную цветовую схему для отображения тэгов ctags (см. эту статью), и при замене ее на lucius все группы, добавленные ctags, исчезают, при этом функция GetFgColorUnderCursor() возвращает почему-то значение ffffff, соответствующее белому цвету, хотя реальный цвет "потерянных" элементов черный. В общем этот третий аргумент можно рассматривать как хак и в реальности он вряд ли нужен.
Вот определение команды MakeTexCodeHighlight:
command -range=% MakeTexCodeHighlight \ silent call <SID>write_latex_code_highlights(<line1>, <line2>, 1)Теперь можно выделять нужные строки в буфере (без выделения команда обработает весь буфер) и, после ввода команды MakeTexCodeHighlight, копировать текст в новом окне и вставлять его в документ tex, созданный pandoc.
Пример.
Код на C++:
int main( void ) { return 0; }Код tex, сгенерированный MakeTexCodeHighlight:
\begin{Shaded} \begin{Highlighting}[] \textcolor[HTML]{005F87}{int} \textcolor[HTML]{008700}{main}\textcolor[HTML]{870087}{(} \textcolor[HTML]{005F87}{void} \textcolor[HTML]{870087}{)} \textcolor[HTML]{870087}{\{} \textcolor[HTML]{005FAF}{return} \textcolor[HTML]{AF5F00}{0}\textcolor[HTML]{870087}{;} \textcolor[HTML]{870087}{\}} \end{Highlighting} \end{Shaded}(кстати, чтобы вставить сюда этот результат я сначала выполнил MakeTexCodeHighlight, а затем, уже в новом буфере, MakeBlogArticle).
Update. Заметил, что MakeTexCodeHighlight неправильно обрабатывает эти дословные символы ^M, ^D и т.п. Команда MakeBlogArticle делает это правильно, видимо в TOhtml они предварительно транслируются.
Update 2. В функции split_synids() тоже есть контрольные символы ^Y и ^E. Во всех случаях их использования можно избежать. Вообще, их появление в скрипте связано с эмуляцией действий пользователя в терминале с помощью комады normal. Если мы найдем соответствующие функции в API VimL, то команда normal будет не нужна. В самом деле, для сохранения и восстановления окна в API VimL предусмотрены функции winsaveview() и winrestview() - использование их вместо эмуляции действий пользователя в split_synids() упростит эту функцию и поможет избежать побочных эффектов, связанных с возможным ремаппингом ^Y и ^E пользователем. В функции write_latex_code_highlights() команда normal с контрольными символами используется для эмуляции ввода пользователем строк. В VimL есть функция append(), которая может легко заменить эту эмуляцию и, соответственно, убрать контрольные символы из кода write_latex_code_highlights() и упростить ее понимание.