]> git.madduck.net Git - etc/vim.git/blob - ftplugin/markdown.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Remove online with italic and bold syntax
[etc/vim.git] / ftplugin / markdown.vim
1 "TODO print messages when on visual mode. I only see VISUAL, not the messages.
2
3 " Function interface phylosophy:
4 "
5 " - functions take arbitrary line numbers as parameters.
6 "    Current cursor line is only a suitable default parameter.
7 "
8 " - only functions that bind directly to user actions:
9 "
10 "    - print error messages.
11 "       All intermediate functions limit themselves return `0` to indicate an error.
12 "
13 "    - move the cursor. All other functions do not move the cursor.
14 "
15 " This is how you should view headers for the header mappings:
16 "
17 "   |BUFFER
18 "   |
19 "   |Outside any header
20 "   |
21 " a-+# a
22 "   |
23 "   |Inside a
24 "   |
25 " a-+
26 " b-+## b
27 "   |
28 "   |inside b
29 "   |
30 " b-+
31 " c-+### c
32 "   |
33 "   |Inside c
34 "   |
35 " c-+
36 " d-|# d
37 "   |
38 "   |Inside d
39 "   |
40 " d-+
41 " e-|e
42 "   |====
43 "   |
44 "   |Inside e
45 "   |
46 " e-+
47
48 " For each level, contains the regexp that matches at that level only.
49 "
50 let s:levelRegexpDict = {
51     \ 1: '\v^(#[^#]@=|.+\n\=+$)',
52     \ 2: '\v^(##[^#]@=|.+\n-+$)',
53     \ 3: '\v^###[^#]@=',
54     \ 4: '\v^####[^#]@=',
55     \ 5: '\v^#####[^#]@=',
56     \ 6: '\v^######[^#]@='
57 \ }
58
59 " Maches any header level of any type.
60 "
61 " This could be deduced from `s:levelRegexpDict`, but it is more
62 " efficient to have a single regexp for this.
63 "
64 let s:headersRegexp = '\v^(#|.+\n(\=+|-+)$)'
65
66 " Returns the line number of the first header before `line`, called the
67 " current header.
68 "
69 " If there is no current header, return `0`.
70 "
71 " @param a:1 The line to look the header of. Default value: `getpos('.')`.
72 "
73 function! s:GetHeaderLineNum(...)
74     if a:0 == 0
75         let l:l = line('.')
76     else
77         let l:l = a:1
78     endif
79     while(l:l > 0)
80         if join(getline(l:l, l:l + 1), "\n") =~ s:headersRegexp
81             return l:l
82         endif
83         let l:l -= 1
84     endwhile
85     return 0
86 endfunction
87
88 " -  if inside a header goes to it.
89 "    Return its line number.
90 "
91 " -  if on top level outside any headers,
92 "    print a warning
93 "    Return `0`.
94 "
95 function! s:MoveToCurHeader()
96     let l:lineNum = s:GetHeaderLineNum()
97     if l:lineNum != 0
98         call cursor(l:lineNum, 1)
99     else
100         echo 'outside any header'
101         "normal! gg
102     endif
103     return l:lineNum
104 endfunction
105
106 " Move cursor to next header of any level.
107 "
108 " If there are no more headers, print a warning.
109 "
110 function! s:MoveToNextHeader()
111     if search(s:headersRegexp, 'W') == 0
112         "normal! G
113         echo 'no next header'
114     endif
115 endfunction
116
117 " Move cursor to previous header (before current) of any level.
118 "
119 " If it does not exist, print a warning.
120 "
121 function! s:MoveToPreviousHeader()
122     let l:curHeaderLineNumber = s:GetHeaderLineNum()
123     let l:noPreviousHeader = 0
124     if l:curHeaderLineNumber <= 1
125         let l:noPreviousHeader = 1
126     else
127         let l:previousHeaderLineNumber = s:GetHeaderLineNum(l:curHeaderLineNumber - 1)
128         if l:previousHeaderLineNumber == 0
129             let l:noPreviousHeader = 1
130         else
131             call cursor(l:previousHeaderLineNumber, 1)
132         endif
133     endif
134     if l:noPreviousHeader
135         echo 'no previous header'
136     endif
137 endfunction
138
139 " - if line is inside a header, return the header level (h1 -> 1, h2 -> 2, etc.).
140 "
141 " - if line is at top level outside any headers, return `0`.
142 "
143 function! s:GetHeaderLevel(...)
144     if a:0 == 0
145         let l:line = line('.')
146     else
147         let l:line = a:1
148     endif
149     let l:linenum = s:GetHeaderLineNum(l:line)
150     if l:linenum != 0
151         return s:GetLevelOfHeaderAtLine(l:linenum)
152     else
153         return 0
154     endif
155 endfunction
156
157 " Returns the level of the header at the given line.
158 "
159 " If there is no header at the given line, returns `0`.
160 "
161 function! s:GetLevelOfHeaderAtLine(linenum)
162     let l:lines = join(getline(a:linenum, a:linenum + 1), "\n")
163     for l:key in keys(s:levelRegexpDict)
164         if l:lines =~ get(s:levelRegexpDict, l:key)
165             return l:key
166         endif
167     endfor
168     return 0
169 endfunction
170
171 " Move cursor to parent header of the current header.
172 "
173 " If it does not exit, print a warning and do nothing.
174 "
175 function! s:MoveToParentHeader()
176     let l:linenum = s:GetParentHeaderLineNumber()
177     if l:linenum != 0
178         call cursor(l:linenum, 1)
179     else
180         echo 'no parent header'
181     endif
182 endfunction
183
184 " Return the line number of the parent header of line `line`.
185 "
186 " If it has no parent, return `0`.
187 "
188 function! s:GetParentHeaderLineNumber(...)
189     if a:0 == 0
190         let l:line = line('.')
191     else
192         let l:line = a:1
193     endif
194     let l:level = s:GetHeaderLevel(l:line)
195     if l:level > 1
196         let l:linenum = s:GetPreviousHeaderLineNumberAtLevel(l:level - 1, l:line)
197         return l:linenum
198     endif
199     return 0
200 endfunction
201
202 " Return the line number of the previous header of given level.
203 " in relation to line `a:1`. If not given, `a:1 = getline()`
204 "
205 " `a:1` line is included, and this may return the current header.
206 "
207 " If none return 0.
208 "
209 function! s:GetNextHeaderLineNumberAtLevel(level, ...)
210     if a:0 < 1
211         let l:line = line('.')
212     else
213         let l:line = a:1
214     endif
215     let l:l = l:line
216     while(l:l <= line('$'))
217         if join(getline(l:l, l:l + 1), "\n") =~ get(s:levelRegexpDict, a:level)
218             return l:l
219         endif
220         let l:l += 1
221     endwhile
222     return 0
223 endfunction
224
225 " Return the line number of the previous header of given level.
226 " in relation to line `a:1`. If not given, `a:1 = getline()`
227 "
228 " `a:1` line is included, and this may return the current header.
229 "
230 " If none return 0.
231 "
232 function! s:GetPreviousHeaderLineNumberAtLevel(level, ...)
233     if a:0 == 0
234         let l:line = line('.')
235     else
236         let l:line = a:1
237     endif
238     let l:l = l:line
239     while(l:l > 0)
240         if join(getline(l:l, l:l + 1), "\n") =~ get(s:levelRegexpDict, a:level)
241             return l:l
242         endif
243         let l:l -= 1
244     endwhile
245     return 0
246 endfunction
247
248 " Move cursor to next sibling header.
249 "
250 " If there is no next siblings, print a warning and don't move.
251 "
252 function! s:MoveToNextSiblingHeader()
253     let l:curHeaderLineNumber = s:GetHeaderLineNum()
254     let l:curHeaderLevel = s:GetLevelOfHeaderAtLine(l:curHeaderLineNumber)
255     let l:curHeaderParentLineNumber = s:GetParentHeaderLineNumber()
256     let l:nextHeaderSameLevelLineNumber = s:GetNextHeaderLineNumberAtLevel(l:curHeaderLevel, l:curHeaderLineNumber + 1)
257     let l:noNextSibling = 0
258     if l:nextHeaderSameLevelLineNumber == 0
259         let l:noNextSibling = 1
260     else
261         let l:nextHeaderSameLevelParentLineNumber = s:GetParentHeaderLineNumber(l:nextHeaderSameLevelLineNumber)
262         if l:curHeaderParentLineNumber == l:nextHeaderSameLevelParentLineNumber
263             call cursor(l:nextHeaderSameLevelLineNumber, 1)
264         else
265             let l:noNextSibling = 1
266         endif
267     endif
268     if l:noNextSibling
269         echo 'no next sibling header'
270     endif
271 endfunction
272
273 " Move cursor to previous sibling header.
274 "
275 " If there is no previous siblings, print a warning and do nothing.
276 "
277 function! s:MoveToPreviousSiblingHeader()
278     let l:curHeaderLineNumber = s:GetHeaderLineNum()
279     let l:curHeaderLevel = s:GetLevelOfHeaderAtLine(l:curHeaderLineNumber)
280     let l:curHeaderParentLineNumber = s:GetParentHeaderLineNumber()
281     let l:previousHeaderSameLevelLineNumber = s:GetPreviousHeaderLineNumberAtLevel(l:curHeaderLevel, l:curHeaderLineNumber - 1)
282     let l:noPreviousSibling = 0
283     if l:previousHeaderSameLevelLineNumber == 0
284         let l:noPreviousSibling = 1
285     else
286         let l:previousHeaderSameLevelParentLineNumber = s:GetParentHeaderLineNumber(l:previousHeaderSameLevelLineNumber)
287         if l:curHeaderParentLineNumber == l:previousHeaderSameLevelParentLineNumber
288             call cursor(l:previousHeaderSameLevelLineNumber, 1)
289         else
290             let l:noPreviousSibling = 1
291         endif
292     endif
293     if l:noPreviousSibling
294         echo 'no previous sibling header'
295     endif
296 endfunction
297
298 function! s:Toc(...)
299     if a:0 > 0
300         let l:window_type = a:1
301     else
302         let l:window_type = 'vertical'
303     endif
304
305
306     let l:bufnr = bufnr('%')
307     let l:fenced_block = 0
308     let l:front_matter = 0
309     let l:header_list = []
310     let l:header_max_len = 0
311     let l:vim_markdown_toc_autofit = get(g:, "vim_markdown_toc_autofit", 0)
312     let l:vim_markdown_frontmatter = get(g:, "vim_markdown_frontmatter", 0)
313     for i in range(1, line('$'))
314         let l:lineraw = getline(i)
315         let l:l1 = getline(i+1)
316         let l:line = substitute(l:lineraw, "#", "\\\#", "g")
317         if l:line =~ '````*' || l:line =~ '\~\~\~\~*'
318             if l:fenced_block == 0
319                 let l:fenced_block = 1
320             elseif l:fenced_block == 1
321                 let l:fenced_block = 0
322             endif
323         elseif l:vim_markdown_frontmatter == 1
324             if l:front_matter == 1
325                 if l:line == '---'
326                     let l:front_matter = 0
327                 endif
328             elseif i == 1
329                 if l:line == '---'
330                     let l:front_matter = 1
331                 endif
332             endif
333         endif
334         if l:line =~ '^#\+' || (l:l1 =~ '^=\+\s*$' || l:l1 =~ '^-\+\s*$') && l:line =~ '^\S'
335             let l:is_header = 1
336         else
337             let l:is_header = 0
338         endif
339         if l:is_header == 1 && l:fenced_block == 0 && l:front_matter == 0
340             " append line to location list
341             let l:item = {'lnum': i, 'text': l:line, 'valid': 1, 'bufnr': l:bufnr, 'col': 1}
342             let l:header_list = l:header_list + [l:item]
343             " keep track of the longest header size (heading level + title)
344             let l:total_len = stridx(l:line, ' ') + len(l:line)
345             if l:total_len > l:header_max_len
346                 let l:header_max_len = l:total_len
347             endif
348         endif
349     endfor
350     if len(l:header_list) == 0
351         echom "Toc: No headers."
352         return
353     endif
354     call setloclist(0, l:header_list)
355
356     if l:window_type ==# 'horizontal'
357         lopen
358     elseif l:window_type ==# 'vertical'
359         vertical lopen
360         " auto-fit toc window when possible to shrink it
361         if (&columns/2) > l:header_max_len && l:vim_markdown_toc_autofit == 1
362             execute 'vertical resize ' . (l:header_max_len + 1)
363         else
364             execute 'vertical resize ' . (&columns/2)
365         endif
366     elseif l:window_type ==# 'tab'
367         tab lopen
368     else
369         lopen
370     endif
371     setlocal modifiable
372     for i in range(1, line('$'))
373         " this is the location-list data for the current item
374         let d = getloclist(0)[i-1]
375         " atx headers
376         if match(d.text, "^#") > -1
377             let l:level = len(matchstr(d.text, '#*', 'g'))-1
378             let d.text = substitute(d.text, '\v^#*[ ]*', '', '')
379             let d.text = substitute(d.text, '\v[ ]*#*$', '', '')
380         " setex headers
381         else
382             let l:next_line = getbufline(d.bufnr, d.lnum+1)
383             if match(l:next_line, "=") > -1
384                 let l:level = 0
385             elseif match(l:next_line, "-") > -1
386                 let l:level = 1
387             endif
388         endif
389         call setline(i, repeat('  ', l:level). d.text)
390     endfor
391     setlocal nomodified
392     setlocal nomodifiable
393     normal! gg
394 endfunction
395
396 " Convert Setex headers in range `line1 .. line2` to Atx.
397 "
398 " Return the number of conversions.
399 "
400 function! s:SetexToAtx(line1, line2)
401     let l:originalNumLines = line('$')
402     execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n\=+$/# \1/'
403     execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n-+$/## \1/'
404     return l:originalNumLines - line('$')
405 endfunction
406
407 " If `a:1` is 0, decrease the level of all headers in range `line1 .. line2`.
408 "
409 " Otherwise, increase the level. `a:1` defaults to `0`.
410 "
411 function! s:HeaderDecrease(line1, line2, ...)
412     if a:0 > 0
413         let l:increase = a:1
414     else
415         let l:increase = 0
416     endif
417     if l:increase
418         let l:forbiddenLevel = 6
419         let l:replaceLevels = [5, 1]
420         let l:levelDelta = 1
421     else
422         let l:forbiddenLevel = 1
423         let l:replaceLevels = [2, 6]
424         let l:levelDelta = -1
425     endif
426     for l:line in range(a:line1, a:line2)
427         if join(getline(l:line, l:line + 1), "\n") =~ s:levelRegexpDict[l:forbiddenLevel]
428             echomsg 'There is an h' . l:forbiddenLevel . ' at line ' . l:line . '. Aborting.'
429             return
430         endif
431     endfor
432     let l:numSubstitutions = s:SetexToAtx(a:line1, a:line2)
433     let l:flags = (&gdefault ? '' : 'g')
434     for l:level in range(replaceLevels[0], replaceLevels[1], -l:levelDelta)
435         execute 'silent! ' . a:line1 . ',' . (a:line2 - l:numSubstitutions) . 'substitute/' . s:levelRegexpDict[l:level] . '/' . repeat('#', l:level + l:levelDelta) . '/' . l:flags
436     endfor
437 endfunction
438
439 " Format table under cursor.
440 "
441 " Depends on Tabularize.
442 "
443 function! s:TableFormat()
444     let l:pos = getpos('.')
445     normal! {
446     " Search instead of `normal! j` because of the table at beginning of file edge case.
447     call search('|')
448     normal! j
449     " Remove everything that is not a pipe othewise well formated tables would grow
450     " because of addition of 2 spaces on the separator line by Tabularize /|.
451     let l:flags = (&gdefault ? '' : 'g')
452     execute 's/[^|]//' . l:flags
453     Tabularize /|
454     execute 's/ /-/' . l:flags
455     call setpos('.', l:pos)
456 endfunction
457
458 " Wrapper to do move commands in visual mode.
459 "
460 function! s:VisMove(f)
461     norm! gv
462     call function(a:f)()
463 endfunction
464
465 " Map in both normal and visual modes.
466 "
467 function! s:MapNormVis(rhs,lhs)
468     execute 'nn <buffer><silent> ' . a:rhs . ' :call ' . a:lhs . '()<cr>'
469     execute 'vn <buffer><silent> ' . a:rhs . ' <esc>:call <sid>VisMove(''' . a:lhs . ''')<cr>'
470 endfunction
471
472 " Parameters:
473 "
474 " - step +1 for right, -1 for left
475 "
476 " TODO: multiple lines.
477 "
478 function! s:FindCornerOfSyntax(lnum, col, step)
479     let l:col = a:col
480     let l:syn = synIDattr(synID(a:lnum, l:col, 1), 'name')
481     while synIDattr(synID(a:lnum, l:col, 1), 'name') ==# l:syn
482         let l:col += a:step
483     endwhile
484     return l:col - a:step
485 endfunction
486
487 " Return the next position of the given syntax name,
488 " inclusive on the given position.
489 "
490 " TODO: multiple lines
491 "
492 function! s:FindNextSyntax(lnum, col, name)
493     let l:col = a:col
494     let l:step = 1
495     while synIDattr(synID(a:lnum, l:col, 1), 'name') !=# a:name
496         let l:col += l:step
497     endwhile
498     return [a:lnum, l:col]
499 endfunction
500
501 function! s:FindCornersOfSyntax(lnum, col)
502     return [<sid>FindLeftOfSyntax(a:lnum, a:col), <sid>FindRightOfSyntax(a:lnum, a:col)]
503 endfunction
504
505 function! s:FindRightOfSyntax(lnum, col)
506     return <sid>FindCornerOfSyntax(a:lnum, a:col, 1)
507 endfunction
508
509 function! s:FindLeftOfSyntax(lnum, col)
510     return <sid>FindCornerOfSyntax(a:lnum, a:col, -1)
511 endfunction
512
513 " Returns:
514 "
515 " - a string with the the URL for the link under the cursor
516 " - an empty string if the cursor is not on a link
517 "
518 " TODO
519 "
520 " - multiline support
521 " - give an error if the separator does is not on a link
522 "
523 function! s:Markdown_GetUrlForPosition(lnum, col)
524     let l:lnum = a:lnum
525     let l:col = a:col
526     let l:syn = synIDattr(synID(l:lnum, l:col, 1), 'name')
527
528     if l:syn ==# 'mkdInlineURL' || l:syn ==# 'mkdURL' || l:syn ==# 'mkdLinkDefTarget'
529         " Do nothing.
530     elseif l:syn ==# 'mkdLink'
531         let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
532         let l:syn = 'mkdURL'
533     elseif l:syn ==# 'mkdDelimiter'
534         let l:line = getline(l:lnum)
535         let l:char = l:line[col - 1]
536         if l:char ==# '<'
537             let l:col += 1
538         elseif l:char ==# '>' || l:char ==# ')'
539             let l:col -= 1
540         elseif l:char ==# '[' || l:char ==# ']' || l:char ==# '('
541             let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
542         else
543             return ''
544         endif
545     else
546         return ''
547     endif
548
549     let [l:left, l:right] = <sid>FindCornersOfSyntax(l:lnum, l:col)
550     return getline(l:lnum)[l:left - 1 : l:right - 1]
551 endfunction
552
553 " Front end for GetUrlForPosition.
554 "
555 function! s:OpenUrlUnderCursor()
556     let l:url = s:Markdown_GetUrlForPosition(line('.'), col('.'))
557     if l:url != ''
558         call s:VersionAwareNetrwBrowseX(l:url)
559     else
560         echomsg 'The cursor is not on a link.'
561     endif
562 endfunction
563
564 function! s:VersionAwareNetrwBrowseX(url)
565     if has('patch-7.4.567')
566         call netrw#BrowseX(a:url, 0)
567     else
568         call netrw#NetrwBrowseX(a:url, 0)
569     endif
570 endf
571
572 function! s:MapNotHasmapto(lhs, rhs)
573     if !hasmapto('<Plug>' . a:rhs)
574         execute 'nmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
575         execute 'vmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
576     endif
577 endfunction
578
579 call <sid>MapNormVis('<Plug>Markdown_MoveToNextHeader', '<sid>MoveToNextHeader')
580 call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousHeader', '<sid>MoveToPreviousHeader')
581 call <sid>MapNormVis('<Plug>Markdown_MoveToNextSiblingHeader', '<sid>MoveToNextSiblingHeader')
582 call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousSiblingHeader', '<sid>MoveToPreviousSiblingHeader')
583 call <sid>MapNormVis('<Plug>Markdown_MoveToParentHeader', '<sid>MoveToParentHeader')
584 call <sid>MapNormVis('<Plug>Markdown_MoveToCurHeader', '<sid>MoveToCurHeader')
585 nnoremap <Plug>Markdown_OpenUrlUnderCursor :call <sid>OpenUrlUnderCursor()<cr>
586
587 if !get(g:, 'vim_markdown_no_default_key_mappings', 0)
588     call <sid>MapNotHasmapto(']]', 'Markdown_MoveToNextHeader')
589     call <sid>MapNotHasmapto('[[', 'Markdown_MoveToPreviousHeader')
590     call <sid>MapNotHasmapto('][', 'Markdown_MoveToNextSiblingHeader')
591     call <sid>MapNotHasmapto('[]', 'Markdown_MoveToPreviousSiblingHeader')
592     call <sid>MapNotHasmapto(']u', 'Markdown_MoveToParentHeader')
593     call <sid>MapNotHasmapto(']c', 'Markdown_MoveToCurHeader')
594     call <sid>MapNotHasmapto('gx', 'Markdown_OpenUrlUnderCursor')
595 endif
596
597 command! -buffer -range=% HeaderDecrease call s:HeaderDecrease(<line1>, <line2>)
598 command! -buffer -range=% HeaderIncrease call s:HeaderDecrease(<line1>, <line2>, 1)
599 command! -buffer -range=% SetexToAtx call s:SetexToAtx(<line1>, <line2>)
600 command! -buffer TableFormat call s:TableFormat()
601 command! -buffer Toc call s:Toc()
602 command! -buffer Toch call s:Toc('horizontal')
603 command! -buffer Tocv call s:Toc('vertical')
604 command! -buffer Toct call s:Toc('tab')
605
606 " Heavily based on vim-notes - http://peterodding.com/code/vim/notes/
607 let s:filetype_dict = {
608     \ 'c++': 'cpp',
609     \ 'viml': 'vim'
610 \ }
611
612 function! s:MarkdownHighlightSources(force)
613     " Syntax highlight source code embedded in notes.
614     " Look for code blocks in the current file
615     let filetypes = {}
616     for line in getline(1, '$')
617         let ft = matchstr(line, '```\zs[0-9A-Za-z_+-]*')
618         if !empty(ft) && ft !~ '^\d*$' | let filetypes[ft] = 1 | endif
619     endfor
620     if !exists('b:mkd_known_filetypes')
621         let b:mkd_known_filetypes = {}
622     endif
623     if !a:force && (b:mkd_known_filetypes == filetypes || empty(filetypes))
624         return
625     endif
626
627     " Now we're ready to actually highlight the code blocks.
628     let startgroup = 'mkdCodeStart'
629     let endgroup = 'mkdCodeEnd'
630     for ft in keys(filetypes)
631         if a:force || !has_key(b:mkd_known_filetypes, ft)
632             if has_key(s:filetype_dict, ft)
633                 let filetype = s:filetype_dict[ft]
634             else
635                 let filetype = ft
636             endif
637             let group = 'mkdSnippet' . toupper(substitute(filetype, "[+-]", "_", "g"))
638             let include = s:SyntaxInclude(filetype)
639             let command = 'syntax region %s matchgroup=%s start="^\s*```%s$" matchgroup=%s end="\s*```$" keepend contains=%s%s'
640             execute printf(command, group, startgroup, ft, endgroup, include, has('conceal') ? ' concealends' : '')
641             execute printf('syntax cluster mkdNonListItem add=%s', group)
642
643             let b:mkd_known_filetypes[ft] = 1
644         endif
645     endfor
646 endfunction
647
648 function! s:SyntaxInclude(filetype)
649     " Include the syntax highlighting of another {filetype}.
650     let grouplistname = '@' . toupper(a:filetype)
651     " Unset the name of the current syntax while including the other syntax
652     " because some syntax scripts do nothing when "b:current_syntax" is set
653     if exists('b:current_syntax')
654         let syntax_save = b:current_syntax
655         unlet b:current_syntax
656     endif
657     try
658         execute 'syntax include' grouplistname 'syntax/' . a:filetype . '.vim'
659         execute 'syntax include' grouplistname 'after/syntax/' . a:filetype . '.vim'
660     catch /E484/
661         " Ignore missing scripts
662     endtry
663     " Restore the name of the current syntax
664     if exists('syntax_save')
665         let b:current_syntax = syntax_save
666     elseif exists('b:current_syntax')
667         unlet b:current_syntax
668     endif
669     return grouplistname
670 endfunction
671
672
673 function! s:MarkdownRefreshSyntax(force)
674     if &filetype == 'markdown' && line('$') > 1
675         call s:MarkdownHighlightSources(a:force)
676     endif
677 endfunction
678
679 augroup Mkd
680     autocmd!
681     au BufWinEnter * call s:MarkdownRefreshSyntax(1)
682     au BufWritePost * call s:MarkdownRefreshSyntax(0)
683     au InsertEnter,InsertLeave * call s:MarkdownRefreshSyntax(0)
684     au CursorHold,CursorHoldI * call s:MarkdownRefreshSyntax(0)
685 augroup END