Merge pull request #41069 from zeertzjq/vim-b0e0b22

vim-patch: runtime file updates
This commit is contained in:
zeertzjq
2026-07-31 10:48:46 +08:00
committed by GitHub
4 changed files with 481 additions and 228 deletions

View File

@@ -25,6 +25,7 @@
" 2026 Apr 14 by Vim Project: Detect more path traversal attacks on Windows
" 2026 Apr 15 by Vim Project: Detect more path traversal attacks on Windows
" 2026 Jun 20 by Vim Project: Fix wrong escaping for the powershell calls
" 2026 Jul 25 by Vim Project: Improved Compatibility for powershell 5 and pwsh 7
" License: Vim License (see vim's :help license)
" Copyright: Copyright (C) 2005-2019 Charles E. Campbell {{{1
" Permission is hereby granted to use and distribute this code,
@@ -61,6 +62,9 @@ endif
if !exists("g:zip_extractcmd")
let g:zip_extractcmd= g:zip_unzipcmd
endif
if !exists("g:zip_pwsh")
let g:zip_pwsh=''
endif
" ---------------------------------------------------------------------
" required early
@@ -77,16 +81,50 @@ if !has('nvim-0.10') && v:version < 901
call s:Mess('WarningMsg', "***warning*** this version of zip needs vim 9.1 or later")
finish
endif
" sanity checks
if !executable(g:zip_unzipcmd) && &shell !~ 'pwsh'
call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system")
finish
" ---------------------------------------------------------------------
" Compatibility checks
" PowerShell: {{{2
" Existence of powershell always means a Windows OS. By default the
" `zip`/`unzip` is not installed on Windows, but the runtime of
" powershell support relative functionalities about zip compression
" and extraction.
" In the following implementations we'll use some powershell scripts
" to manipulate zip files. All these scripts should available to both
" Windows PowerShell 5 and pwsh 7+.
let s:ps = ''
" order: `g:zip_pwsh` > `&shell`, with executable check
if &shell =~# 'powershell' || g:zip_pwsh =~# 'powershell'
if executable('powershell')
let s:ps = 'powershell'
endif
endif
if !dist#vim#IsSafeExecutable('zip', g:zip_unzipcmd) && &shell !~ 'pwsh'
call s:Mess('Error', "Warning: NOT executing " .. g:zip_unzipcmd .. " from current directory!")
finish
if (empty(g:zip_pwsh) && &shell =~# 'pwsh') || g:zip_pwsh =~# 'pwsh'
if executable('pwsh')
let s:ps = 'pwsh'
endif
endif
fun! s:isPS()
return !empty(s:ps)
endfun
" ---------------------------------------------------------------------
" sanity checks
" s:SafeExecutable: {{{2
fun! s:SafeExecutable(exe)
" fails when exe is a full path with spaces
let exe = substitute(a:exe, '\s\+.*$', '', '')
if !executable(exe) && !s:isPS()
return v:false
endif
if !dist#vim#IsSafeExecutable('zip', exe) && !s:isPS()
call s:Mess('Error', "Warning: NOT executing " .. a:exe .. " from current directory!")
return v:false
endif
return v:true
endfun
" ----------------
" PowerShell: {{{1
" ----------------
@@ -94,8 +132,8 @@ endif
function! s:TryExecGnuFallBackToPs(executable, gnu_func_call, ...)
" Check that a gnu executable is available, run the gnu_func_call if so. If
" the gnu executable is not available or if gnu_func_call fails, try
" ps_func_call if &shell =~ 'pwsh'. If all attempts fail, print errors.
" a:executable - one of (g:zip_zipcmd, g:zip_unzipcmd, g:zip_extractcmd)
" ps_func_call if `s:isPS()`. If all attempts fail, print errors.
" a:executable - (string) name of the executable program
" a:gnu_func_call - (string) a gnu function call to execute
" a:1 - (optional string) a PowerShell function call to execute.
let failures = []
@@ -109,12 +147,12 @@ function! s:TryExecGnuFallBackToPs(executable, gnu_func_call, ...)
else
call add(failures, a:executable.' not available on your system')
endif
if &shell =~ 'pwsh' && a:0 == 1
if s:isPS() && a:0 == 1
try
exe a:1
return
catch
call add(failures, 'Fallback to PowerShell attempted but failed')
call add(failures, 'Fallback to PowerShell attempted but failed: '.a:1)
endtry
endif
for msg in failures
@@ -127,20 +165,22 @@ function! s:ZipBrowsePS(zipfile)
" Browse the contents of a zip file using PowerShell's
" Equivalent `unzip -Z1 -- zipfile`
let cmds = [
\ 'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
\ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . s:PSEscape(a:zipfile) . ');',
\ '$zip.Entries | ForEach-Object { $_.FullName };',
\ '$zip.Dispose()'
\ ]
return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
return s:ps . ' -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
endfunction
function! s:ZipReadPS(zipfile, fname, tempfile)
" Read a filename within a zipped file to a temporary file.
" Equivalent to `unzip -p -- zipfile fname > tempfile`
if &shell =~ 'pwsh'
if s:isPS()
call s:Mess('WarningMsg', "***warning*** PowerShell can display, but cannot update, files in archive subfolders")
endif
let cmds = [
\ 'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
\ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . s:PSEscape(a:zipfile) . ');',
\ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . s:PSEscape(a:fname) . ' };',
\ '$stream = $fileEntry.Open();',
@@ -150,13 +190,13 @@ function! s:ZipReadPS(zipfile, fname, tempfile)
\ '$stream.Close();',
\ '$zip.Dispose()'
\ ]
return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
return s:ps . ' -NoProfile -Command ' . s:Escape(join(cmds, ' '))
endfunction
function! s:ZipUpdatePS(zipfile, fname)
" Update a filename within a zipped file
" Equivalent to `zip -u zipfile fname`
if &shell =~ 'pwsh' && a:fname =~ '/'
if s:isPS() && a:fname =~ '/'
call s:Mess('Error', "***error*** PowerShell cannot update files in archive subfolders")
return ':'
endif
@@ -171,6 +211,7 @@ function! s:ZipExtractFilePS(zipfile, fname)
return ':'
endif
let cmds = [
\ 'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
\ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . s:PSEscape(a:zipfile) . ');',
\ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . s:PSEscape(a:fname) . ' };',
\ '$stream = $fileEntry.Open();',
@@ -180,7 +221,7 @@ function! s:ZipExtractFilePS(zipfile, fname)
\ '$stream.Close();',
\ '$zip.Dispose()'
\ ]
return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
return s:ps . ' -NoProfile -Command ' . s:Escape(join(cmds, ' '))
endfunction
function! s:ZipDeleteFilePS(zipfile, fname)
@@ -193,7 +234,7 @@ function! s:ZipDeleteFilePS(zipfile, fname)
\ 'if ($entry) { $entry.Delete(); $zip.Dispose() }',
\ 'else { $zip.Dispose() }'
\ ]
return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
return s:ps . ' -NoProfile -Command ' . s:Escape(join(cmds, ' '))
endfunction
" ----------------
@@ -214,8 +255,8 @@ fun! zip#Browse(zipfile)
defer s:RestoreOpts(dict)
" sanity checks
if !executable(g:zip_unzipcmd) && &shell !~ 'pwsh'
call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system")
if !s:SafeExecutable(g:zip_unzipcmd)
call s:Mess('Error', "***error*** (zip#Browse) sorry, your system doesn't appear to have the ".g:zip_unzipcmd." program")
return
endif
if !filereadable(a:zipfile)
@@ -322,7 +363,7 @@ fun! zip#Read(fname,mode)
endif
let fname = fname->substitute('[', '[[]', 'g')->escape('?*\\')
" sanity check
if !executable(substitute(g:zip_unzipcmd,'\s\+.*$','','')) && &shell !~ 'pwsh'
if !s:SafeExecutable(g:zip_unzipcmd)
call s:Mess('Error', "***error*** (zip#Read) sorry, your system doesn't appear to have the ".g:zip_unzipcmd." program")
return
endif
@@ -358,7 +399,7 @@ fun! zip#Write(fname)
defer s:RestoreOpts(dict)
" sanity checks
if !executable(substitute(g:zip_zipcmd,'\s\+.*$','','')) && &shell !~ 'pwsh'
if !s:SafeExecutable(g:zip_zipcmd)
call s:Mess('Error', "***error*** (zip#Write) sorry, your system doesn't appear to have the ".g:zip_zipcmd." program")
return
endif
@@ -442,7 +483,7 @@ fun! zip#Write(fname)
let ps_cmd = s:ZipUpdatePS(zip, fname)
let ps_cmd = 'call system(''' . substitute(ps_cmd, "'", "''", 'g') . ''')'
call s:TryExecGnuFallBackToPs(g:zip_zipcmd, gnu_cmd, ps_cmd)
if &shell =~ 'pwsh'
if s:isPS()
" Vim flashes 'creation in progress ...' from what I believe is the
" ProgressAction stream of PowerShell. Unfortunately, this cannot be
" suppressed (as of 250824) due to an open PowerShell issue.
@@ -486,6 +527,13 @@ fun! zip#Extract()
let dict = s:SetSaneOpts()
defer s:RestoreOpts(dict)
" sanity checks
if !s:SafeExecutable(g:zip_extractcmd)
call s:Mess('Error', "***error*** (zip#Extract) sorry, your system doesn't appear to have the ".g:zip_extractcmd." program")
return
endif
let fname= getline(".")
" sanity check
@@ -540,7 +588,7 @@ fun! zip#Extract()
if v:shell_error != 0
call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." ".fname.": failed!")
elseif !filereadable(fname) && &shell !~ 'pwsh'
elseif !filereadable(fname) && !s:isPS()
call s:Mess('Error', "***error*** attempted to extract ".fname." but it doesn't appear to be present!")
else
echomsg "***note*** successfully extracted ".fname

View File

@@ -71,16 +71,22 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell *zip-copyright*
"0": >
let g:zip_exec=0
<
FALLBACK TO POWERSHELL CORE~
FALLBACK TO POWERSHELL~
*g:zip_pwsh*
This plugin will first attempt to use the (more capable) GNU zip/unzip
commands. If these commands are not available or fail, and the user is
using PowerShell Core (i.e., the 'shell' option matches "pwsh"), the
plugin will fall back to a PowerShell Core cmdlet. The PowerShell Core
cmdlets are limited: they cannot write or extract files within
subdirectories of a zip archive. The advantage, however, is that no
separate unzip binary needs to be installed.
using PowerShell, the plugin will fall back to a PowerShell cmdlet.
The PowerShell cmdlets are limited: they cannot write or extract files
within subdirectories of a zip archive. The advantage, however, is that
no separate unzip binary needs to be installed. The 'shell' option will
be checked whether to choose "pwsh" or "powershell", or set `g:zip_pwsh`.
If both are available, this plugin will prefer pwsh. Default value of
"g:zip_pwsh" is an empty string, which would not overwrite the 'shell'
option configuration. This functionality is not turned on by default on
MS-Windows, since the default 'shell' is "cmd.exe". For example: >
let g:zip_pwsh='powershell'
<
PREVENTING LOADING~
If for some reason you do not wish to use vim to examine zipped files,
@@ -116,6 +122,7 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell *zip-copyright*
==============================================================================
4. History *zip-history* {{{1
unreleased:
Jul 25, 2026 * support both Windows PowerShell 5 and pwsh 7+
Sep 19, 2025 * support PowerShell Core
Jul 12, 2025 * drop ../ on write to prevent path traversal attacks
Mar 11, 2025 * handle filenames with leading '-' correctly

View File

@@ -1,21 +1,18 @@
" Vim syntax file
" Language: Marko
" Maintainer: Brian Carbone <cordial.coffee1365@fastmail.com>
" Maintainer: Brian Carbone <brian@briancarbone.com>
" URL: https://markojs.com
" Filenames: *.marko
" Last Change: 2026 Jul 28
" Last Change: 2026 Jul 29
" Marko interleaves HTML-like tags, TypeScript expressions and CSS style
" blocks, in two modes: an HTML-like mode and an indentation-based concise
" mode. Mode is tracked structurally: the body of an html-mode element is
" an html-mode region (bare text stays plain there), while everywhere else
" is concise context, where a line-start name is a tag:
" Marko interleaves HTML-like tags, TypeScript expressions and CSS in two
" modes. Mode is tracked structurally: element bodies and "--" text blocks
" are html mode (bare text stays plain), everywhere else is concise, where
" a line-start name is a tag:
" <p>if you want, stop by</p> body text, plain
" my-tag size="large" concise tag, highlighted
"
" Same-position priority goes to the item defined last, so definition order
" is deliberate throughout; the load-bearing orderings are called out in
" the comments below.
" Same-position priority goes to the item defined last; definition order
" is deliberate throughout.
if !exists("main_syntax")
if exists("b:current_syntax")
@@ -27,11 +24,11 @@ endif
let s:cpo_save = &cpo
set cpo&vim
" Top level is mostly markup; regions holding prose opt into @Spell.
syn spell toplevel
" TypeScript for expressions and statement tags, CSS for style blocks.
" Both includes mutate shared state ("syn case", "syn iskeyword", sync
" rules, 'iskeyword'), all of it restored or restated afterwards.
" Both includes mutate shared state ("syn case", "syn iskeyword", sync,
" 'iskeyword'), restored or restated afterwards.
let s:iskeyword_save = &l:iskeyword
syn include @markoTS syntax/typescript.vim
unlet! b:current_syntax
@@ -41,107 +38,190 @@ let &l:iskeyword = s:iskeyword_save
unlet s:iskeyword_save
syn case match
" typescript.vim (yats) attaches type arguments via nextgroup after every
" identifier, so an unspaced "<" in any expression opens a region hunting a
" ">" that may never come:
" A "\%#=1" prefix pins a pattern to the backtracking regexp engine: the
" default NFA engine is much slower on this file's characteristic shapes
" (a lookbehind or bounded repeat in front of an alternation). Both
" engines answer alike; only patterns measured faster carry the prefix.
" yats attaches type arguments via nextgroup after every identifier, so an
" unspaced "<" in any expression would hunt a ">" that may never come:
" <div count=i<3> would swallow the rest of the file
" Its block comment carries "extend", which would let one stray "/*" escape
" every containing region below. Both are redefined bounded; the cost is
" generic-call coloring inside script blocks.
" That group stays cleared. The comment region replaces yats' block
" comment, whose "extend" would escape every containing region below.
silent! syn clear typescriptTypeArguments
silent! syn clear typescriptComment
syn region typescriptComment contained start="/\*" end="\*/" contains=@Spell,typescriptCommentTodo
" Everything an embedded TypeScript expression may contain. The bracket
" pairs keep an outer region open across nested delimiters and shield the
" spaces inside groupings from attribute-value termination:
" a={ b: { c: 1 } } d=2
" Without the matchgroup each region would re-match at its own start
" character through its own contains and consume the closing delimiter.
" markoTSDelim is deliberately unlinked: the delimiters render as plain
" text, like brackets in a TypeScript buffer.
syn cluster markoExpr contains=@markoTS,markoTSBraces,markoTSParens,markoTSBrackets
syn region markoTSBraces contained transparent matchgroup=markoTSDelim
\ start="{" end="}"
\ contains=@markoExpr extend
syn region markoTSParens contained transparent matchgroup=markoTSDelim
\ start="(" end=")"
\ contains=@markoExpr extend
syn region markoTSBrackets contained transparent matchgroup=markoTSDelim
\ start="\[" end="]"
\ contains=@markoExpr extend
" yats claims call arguments, bodies, indexing, arrays, templates and
" parenthesized expressions via nextgroup, so the markoTS* pairs below
" never see those delimiters. Each row is yats' own definition plus
" "contained extend", the one difference from upstream: a multiline group
" keeps its containing attribute value open, and a bracketed one shields
" the spaces inside, as the parser's group tracking does:
" count=Math.max(0, tiers.findIndex((t) => t.featured))
" pair=raw as [string, string]
for s:def in [
\ 'typescriptBlock matchgroup=typescriptBraces start=/{/ end=/}/ contains=@typescriptStatement,@typescriptComments fold',
\ 'typescriptFuncCallArg matchgroup=typescriptParens start=/(/ end=/)/ contains=@typescriptValue,@typescriptComments,typescriptCastKeyword nextgroup=@typescriptSymbols,typescriptDotNotation skipwhite skipempty skipnl',
\ 'typescriptArray matchgroup=typescriptBraces start=/\[/ end=/]/ contains=@typescriptValue,@typescriptComments,typescriptCastKeyword nextgroup=@typescriptSymbols,typescriptDotNotation skipwhite skipempty fold',
\ 'typescriptTemplate start=/`/ skip=/\\\\\|\\`\|\n/ end=/`\|$/ contains=typescriptTemplateSubstitution,typescriptSpecial,@Spell nextgroup=@typescriptSymbols skipwhite skipempty',
\ 'typescriptObjectLiteral matchgroup=typescriptBraces start=/{/ end=/}/ contains=@typescriptComments,typescriptObjectLabel,typescriptStringProperty,typescriptComputedPropertyName,typescriptObjectAsyncKeyword,typescriptTernary,typescriptCastKeyword fold',
\ 'typescriptObjectType matchgroup=typescriptBraces start=/{/ end=/}/ contains=@typescriptTypeMember,typescriptEndColons,@typescriptComments,typescriptAccessibilityModifier,typescriptReadonlyModifier nextgroup=@typescriptTypeOperator skipwhite skipnl fold',
\ 'typescriptParenExp matchgroup=typescriptParens start=/(/ end=/)/ contains=@typescriptComments,@typescriptValue,typescriptCastKeyword nextgroup=@typescriptSymbols skipwhite skipempty',
\ 'typescriptIndexExpr matchgroup=typescriptProperty start=/\[/ end=/]/ contains=@typescriptValue,typescriptCastKeyword nextgroup=@typescriptSymbols,typescriptDotNotation,typescriptFuncCallArg skipwhite skipempty',
\ 'typescriptCall matchgroup=typescriptParens start=/(/ end=/)/ contains=typescriptDecorator,@typescriptParameterList,@typescriptComments nextgroup=typescriptTypeAnnotation,typescriptBlock skipwhite skipnl',
\ 'typescriptTupleType matchgroup=typescriptBraces start=/\[/ end=/\]/ contains=@typescriptType,@typescriptComments,typescriptRestOrSpread,typescriptTupleLable skipwhite',
\ 'typescriptParenthesizedType matchgroup=typescriptParens start=/(/ end=/)/ contains=@typescriptType nextgroup=@typescriptTypeOperator skipwhite skipempty fold',
\ 'typescriptFuncType matchgroup=typescriptParens start=/(\(\k\+:\|)\)\@=/ end=/)\s*=>/me=e-2 contains=@typescriptParameterList nextgroup=typescriptFuncTypeArrow skipwhite skipnl oneline',
\ 'typescriptStringLiteralType start=/\z(["'']\)/ skip=/\\\\\|\\\z1\|\\\n/ end=/\z1\|$/ nextgroup=typescriptUnion skipwhite skipempty',
\ 'typescriptTemplateLiteralType start=/`/ skip=/\\\\\|\\`\|\n/ end=/`\|$/ contains=typescriptTemplateSubstitutionType nextgroup=typescriptTypeOperator skipwhite skipempty',
\ ]
silent! exe 'syn clear ' . matchstr(s:def, '^\S\+')
exe 'syn region ' . s:def . ' contained extend'
endfor
unlet s:def
" yats' own arrow-function definitions, restated to stay last-defined at
" a "(": params eaten as a parenthesized expression turn the arrow's "{"
" body into an object literal, whose ternary region has no closing bound:
" f((x) => { value = x ?? other; })
syntax match typescriptArrowFuncDef contained /\K\k*\s*=>/
\ contains=typescriptArrowFuncArg,typescriptArrowFunc
\ nextgroup=@typescriptExpression,typescriptBlock
\ skipwhite skipempty
syntax match typescriptArrowFuncDef contained /(\%(\_[^()]\+\|(\_[^()]*)\)*)\_s*=>/
\ contains=typescriptArrowFuncArg,typescriptArrowFunc,@typescriptCallSignature
\ nextgroup=@typescriptExpression,typescriptBlock
\ skipwhite skipempty
syntax region typescriptArrowFuncDef contained start=/(\%(\_[^()]\+\|(\_[^()]*)\)*):/ matchgroup=typescriptArrowFunc end=/=>/
\ contains=typescriptArrowFuncArg,typescriptTypeAnnotation,@typescriptCallSignature
\ nextgroup=@typescriptExpression,typescriptBlock
\ skipwhite skipempty keepend
" Comments are recognized where the parser allows text to start; anchoring
" to the line start keeps "//" inside URLs as text:
" <!-- note --> // note /* note */ https://example.com
" The bracket pairs keep an outer region open across nested delimiters
" and shield the spaces inside groupings from value termination:
" a={ b: { c: 1 } } d=2
" markoTSDelim is deliberately unlinked: delimiters render plain.
syn cluster markoExpr contains=@markoTS,markoTSBraces,markoTSParens,markoTSBrackets
for [s:pair, s:open, s:close] in [["Braces", '{', '}'], ["Parens", '(', ')'], ["Brackets", '\[', ']']]
exe 'syn region markoTS' . s:pair . ' contained transparent matchgroup=markoTSDelim'
\ . ' start="' . s:open . '" end="' . s:close . '"'
\ . ' contains=@markoExpr extend'
endfor
unlet s:pair s:open s:close
" html-mode "//" and "/* */" comments open wherever whitespace precedes
" the slash (a URL's "//" follows ":", so it stays plain); concise context
" takes only the line-anchored forms, defined second so they win at a line
" start and keep their fold and indent handling:
" // note <p>done // for now</p> <a href="https://x.co">plain</a>
" No comment item may take "display": a match skipped during offscreen
" state computation exposes its text, and a "<tag" inside a comment would
" open a body whose highlighting depends on scroll position.
syn match markoHtmlComment "\%(^\|\s\)\@1<=//.*$" contains=@Spell
syn region markoHtmlComment start="\%(^\|\s\)\@1<=/\*" end="\*/" contains=@Spell fold
syn region markoComment start="<!--" end="-->" contains=@Spell fold
syn match markoComment "^\s*//.*$" contains=@Spell display
syn match markoComment "^\s*//.*$" contains=@Spell
syn region markoComment start="^\s*/\*" end="\*/" contains=@Spell fold
syn region markoTagComment contained start="/\*" end="\*/"
syn region markoTagComment contained start="/\*" end="\*/" extend
syn match markoTagComment contained "//.*"
" Declarations before CDATA: both open at "<!" and the later wins:
" <!doctype html> <?xml version="1.0"?> <![CDATA[ raw ]]>
syn region markoDeclaration start="<!\(--\)\@!" end=">"
syn region markoDeclaration start="<?" end=">"
syn region markoCData start="<!\[CDATA\[" end="]]>"
" Declarations; "<?...>" may close with ">" or "?>":
" <!doctype html> <?xml version="1.0"?>
syn region markoDeclaration start="<!\%(--\|\[CDATA\)\@!" end=">"
syn region markoDeclaration start="<?" end="?\=>"
" Placeholders interpolate into text, and backslashes escape pairwise:
" Placeholders interpolate into text; backslashes escape pairwise:
" ${user.name} $!{rawHtml} \${literal} \\${live}
" The containedin lets them apply inside style rules and property values.
" markoEscape must never take "display": it exists to suppress
" markoPlaceholder, so skipping it during state computation would change
" what matches.
syn match markoEscape "\%(\\\\\)\+\ze\$!\={"
syn match markoEscape "\%(\\\\\)*\\\$!\={"
" markoEscape must never take "display" (it exists to suppress
" markoPlaceholder); it consumes an escaped "$", or stops before a "${"
" an even backslash run leaves live.
syn match markoEscape "\%(\\\\\)*\%(\\\$!\={\|\\\\\ze\$!\={\)"
syn region markoPlaceholder matchgroup=markoPlaceholderDelim
\ start="\$!\={" end="}"
\ contains=@markoExpr containedin=cssDefinition,cssAttrRegion extend
\ contains=@markoExpr extend
" The no-"extend" variant for keepend parsed-text bodies and the CSS
" contexts: an unterminated "\${" must stop at the container's close.
syn region markoPlaceholderText contained matchgroup=markoPlaceholderDelim
\ start="\$!\={" end="}"
\ contains=@markoExpr
\ containedin=cssDefinition,cssAttrRegion,cssStringQ,cssStringQQ
syn match markoEntity "&\%(#\d\+\|#[xX]\x\+\|\w\+\);" display
" Scriptlets; the whitespace after "$" is required, so a line-start
" placeholder is not a scriptlet:
" $ count += 1
" $ { let total = 0; }
" ${count} placeholder
" Scriptlets; the whitespace after "$" keeps line-start placeholders out.
" No keepend: an open bracket continues, as the parser's group tracking
" does. The "$ {" block form is second, so it wins where both start:
" $ count += 1 $ { let total = 0; }
" $ const rows = [
" "a",
" ]
syn region markoScriptlet matchgroup=markoScriptletDelim
\ start="^\s*\$\s\+{\@!" end="$"
\ contains=@markoExpr keepend
\ start="^\s*\$\s\+" end="$"
\ contains=@markoExpr
syn region markoScriptlet matchgroup=markoScriptletDelim
\ start="^\s*\$\s\+{" end="};\="
\ contains=@markoExpr
" "--" marks one line of text in concise mode:
" "--" marks the rest of the line as text; at end of line it opens an
" indented block instead:
" p -- some text
syn match markoBlockDelim "\%(^\|\s\)\@1<=--\+\ze\%(\s\|$\)" display
" p --
" A block of text, <b>html mode</b>.
" The inline form is a one-line html region, so later dash runs on the
" line sit inside it and are never delimiters (the parser's rule), and
" keepend bounds what the line opens. Both forms stand down on a line
" whose first non-blank is "<" (html content, where dashes are text).
" They overlap at a trailing "--": the nextgroup form is second and wins,
" and must not take "display". A bare "--" line belongs to the
" later-defined markoTextDelimBlock.
syn region markoInlineText oneline keepend matchgroup=markoBlockDelim
\ start="\%#=1\%(^\s*<.\{-}\)\@200<!\%(^\|\s\)\@1<=--\+\ze\s" end="$"
\ contains=@markoHtmlContent
syn match markoBlockDelim
\ "\%#=1\%(^\s*<.\{-}\)\@200<!\%(^\|\s\)\@1<=--\+\s*$"
\ nextgroup=markoTextIndent skipnl
" The shared block body: its indent is the first following line's leading
" whitespace run, blank lines included (the parser's rule); an empty run
" leaves only the column-0 delimiter close, the dedent end being a
" negative lookahead on an empty match.
let s:indent_block = ' start="^\z(\s*\)\ze\%(\S\|$\)"'
\ . ' matchgroup=markoBlockDelim end="^\z1--\+\s*$"'
\ . ' matchgroup=NONE end="^\%(\z1\)\@!\ze\s*\S"'
exe 'syn region markoTextIndent contained' . s:indent_block
\ . ' contains=@markoHtmlContent'
" Attributes. An unquoted value ends at whitespace (quoted strings and
" bracket pairs shield theirs), does not start at the "=" of an arrow, and
" continues across " => " so arrow values stay whole:
" size="large" total:=sum ...spread onClick=(e) => handle(e)
" The attr name must not match the "${" of a dynamic tag name.
syn match markoAttrName contained "\%(\$!\={\)\@![[:alnum:]_$@][-[:alnum:]_$.]*" display
" A name may hold ":" (not the ":=" of a bound value) and any keyword
" character, and must not match a dynamic tag name's "${":
" size="large" total:=sum xml:lang="es" <spinner café="ok"/>
" An unquoted value ends at whitespace or the mode's terminators, except
" that the parser continues across whitespace flanked by an operator:
" if=tag === "select" content=a.desc ?? b.desc onClick=(e) => go(e)
syn match markoAttrName contained "\%#=1\%(\$!\={\)\@![[:alnum:]_$@]\%(\k\|[$.]\|:=\@!\)*" display
syn match markoSpread contained "\.\.\." display
syn region markoAttrValue contained matchgroup=markoOperator
\ start=":\==>\@!\s*" end="\ze\%(\%(=>\)\@2<!\s\%(\s*=>\)\@!\|=\@1<!>\|/>\)" end="$"
\ contains=@markoExpr
syn region markoString contained start=+\z(["']\)+ skip=+\\\z1+ end=+\z1+ contains=@Spell
" Shorthands after the tag name:
" <div.card#main>
syn match markoShorthandId contained "#[-[:alnum:]_$]\+" display
syn match markoShorthandClass contained "\.[-[:alnum:]_$]\+" display
" Regions so an interpolation can sit inside one (no keepend: the
" placeholder carries the region past its "}"):
" <div.card#main> <div#item-${id}>
syn region markoShorthandId contained oneline
\ start="#[-[:alnum:]_$]\@=" end="[-[:alnum:]_$]\@!"
\ contains=markoPlaceholder
syn region markoShorthandClass contained oneline
\ start="\.[-[:alnum:]_$]\@=" end="[-[:alnum:]_$]\@!"
\ contains=markoPlaceholder
" Tag variables, arguments, |parameters|, <type args> and attribute method
" shorthand bodies:
" Tag variables, arguments, |parameters|, <type args> and method bodies:
" <let/count=1/> <for|item, i| of=list> <while(cond)>
" <generic-list<string>> <button onClick(e) { handle(e) }>
" Params and type args must hug the preceding name (the "\k" follows the
" "syn iskeyword" set below, which is what admits hyphenated names) so a
" "|" or "<" inside an expression cannot open them, and both are oneline
" so an unmatched delimiter cannot run past its line.
syn match markoTagVar contained "/\%([[:alnum:]_$]\+\|\ze\s*[{[]\)" display
" Params and type args hug the preceding name, so a "|" or "<" inside an
" expression cannot open them; params and the plain type-args form are
" oneline so an unmatched delimiter stops at its line.
syn match markoTagVar contained "/\%([[:alnum:]_$]\+\|\ze\s*[{[]\)"
\ nextgroup=markoTagVarDestructure skipwhite display
syn region markoTagVarDestructure contained matchgroup=markoTagDelim
\ start="\[" end="]"
\ contains=@markoExpr extend
syn region markoArgs contained matchgroup=markoTagDelim
\ start="(" end=")"
\ contains=@markoExpr extend
@@ -150,48 +230,122 @@ syn region markoParams contained oneline matchgroup=markoTagDelim
\ contains=@markoExpr
syn region markoTypeArgs contained oneline matchgroup=markoTagDelim
\ start="\%(\k\|\$\)\@4<=<" end=">"
\ contains=@markoExpr,markoTypeArgs
\ contains=@typescriptType,markoTypeArgs
" The braced form spans lines: the object type bounds it, and the parser
" reads "<{" after a name in tag scope as type args too.
syn region markoTypeArgs contained matchgroup=markoTagDelim
\ start="\%(\k\|\$\)\@4<=<\ze\s*{" end=">"
\ contains=@typescriptType,markoTypeArgs extend
syn region markoMethodBody contained matchgroup=markoTagDelim
\ start="{" end="}"
\ contains=@markoExpr extend
" The three value variants differ only in terminators: html ends at ","
" "/>" or the tag's ">"; concise at "," ";" or whitespace-led "--" (">"
" is a comparison there); grouped at "," or "]". Defined after markoArgs
" so a "...spread" value wins at "(". keepend bounds foreign TypeScript
" at the value's end; the markoTS* and yats groups carry extend and punch
" through. A value survives its line end when either side of the break
" is an operator in html mode (the lookahead crosses blank lines), only
" a trailing one in concise:
" <a href=translated
" ? localizeUrl($global.url.href, { locale }).href
" : baseHref>
" s:val_nocont, the trailing set, mirrors the parser: "=>" and ">>"
" continue but a bare ">" ends (operator only outside type position,
" untrackable, and honoring it swallowed "</div>"-style line ends;
" mid-line "x > 1" continues via the break end's own guard); single
" "+"/"-" continue, postfix "i++" ends; unary keywords count unless a
" word character precedes ("a-new Date()" stays whole), binary and type
" keywords only after whitespace ("class=step-in" ends); a trailing "."
" continues when a word character follows, across blank lines. A "/"
" counts only when spaces flank it (ahead of the break it is ambiguous
" with "/>"), and the break demands a non-space before the space, so a
" continuation line's indent cannot end the value it continues:
" ratio=total / count max=10
" total=base +
" tax
let s:val_nocont = '[&*^:=!<%|?~]\@1<!\%(=>\|>>\)\@2<!\%(+\@1<!+\|-\@1<!-\)\@1<!\%(\s/\)\@2<!'
\ . '\%([0-9A-Za-z_$.]\@1<!\%(async\|await\|class\|function\|new\|typeof\|void\)\|\s\%(as\|extends\|instanceof\|in\|satisfies\|asserts\|infer\|is\|keyof\|readonly\|unique\)\)\@12<!'
\ . '\%(\.\@1<=\_s*[0-9A-Za-z_$]\)\@!'
" Cheap zero-width lead tests keep the wide lookbehinds off most columns.
" s:val_last matches after the line's last non-blank, so trailing spaces
" and blank lines never host a value end.
let s:val_last = '\%#=1\%(\s*$\)\@=' . s:val_nocont . '\S\@1<='
let s:val_break = '\%#=1\s\@=>\@1<!' . s:val_nocont . '\S\@1<=\s'
let s:val_op = '[&*^!<%|~+?:={(]\|\.\%(\_s*\w\)\@=\|\<\%(as\|extends\|instanceof\|in\|satisfies\)\%(\_s\+[^:,=/>; \t]\)\@='
let s:val_head = ' contained matchgroup=markoOperator start=":\==>\@!\s*"'
\ . ' matchgroup=NONE start="\.\@1<=\%(\.\.\.\)\@3<="'
let s:val_tail = ' contains=@markoExpr keepend extend'
exe 'syn region markoAttrValue' . s:val_head
\ . ' end="\ze," end="\ze/>" end="=\@1<!\ze>"'
\ . ' end="' . s:val_last . '\ze\s*\n\%(\_s*\%(' . s:val_op . '\|-\|/[/*>]\@!\)\)\@!"'
\ . ' end="' . s:val_break . '\%(\_s*\%(' . s:val_op . '\|-\|/[/*>]\@!\)\)\@!"' . s:val_tail
exe 'syn region markoAttrValueConcise' . s:val_head
\ . ' end="\ze[,;]" end="' . s:val_last . '"'
\ . ' end="' . s:val_break . '\%(\s*\%(' . s:val_op . '\|--\@!\|/[/*]\@!\|>\)\)\@!"' . s:val_tail
exe 'syn region markoAttrValueInGroup' . s:val_head
\ . ' end="\ze[,\]]" end="' . s:val_last . '"'
\ . ' end="' . s:val_break . '\%(\s*\%(' . s:val_op . '\|-\|/[/*]\@!\|>\)\)\@!"' . s:val_tail
unlet s:val_nocont s:val_last s:val_break s:val_op s:val_head s:val_tail
syn cluster markoTagStuff contains=markoTagVar,markoShorthandId,markoShorthandClass,
\markoAttrName,markoAttrValue,markoString,markoArgs,markoParams,markoTypeArgs,
\markoAttrName,markoString,markoArgs,markoParams,markoTypeArgs,
\markoMethodBody,markoSpread,markoTagComment,markoPlaceholder
syn cluster markoTagNames contains=markoTagName,markoAttrTagName,markoTagNameConditional,
\markoTagNameRepeat,markoTagNameException,markoTagNameStatement,markoTagNameBuiltin
" The name groups themselves are defined near the end of the file; a
" cluster may name them before they exist.
syn cluster markoTagNames contains=markoTagName,markoTagNameTail,markoAttrTagName,
\markoTagNameConditional,markoTagNameRepeat,markoTagNameException,
\markoTagNameStatement,markoTagNameBuiltin
" Concise attribute groups may span lines:
" define [
" size = "large"
" ]
" Attribute groups may span lines. The lookbehind keeps a spread's array
" literal out; that "[" opens a value, not a group:
" define [size = "large"] my-tag ...[1, 2]
syn region markoAttrGroup contained matchgroup=markoTagDelim
\ start="\[" end="]"
\ contains=@markoTagStuff extend
\ start="\%(\.\.\.\)\@3<!\[" end="]"
\ contains=@markoTagStuff,markoAttrValueInGroup extend
" One name list per highlight class, shared by the concise regions here
" and the tag-name matches below.
" One list per highlight class. The builtin list is factored by first
" letter; no name is a prefix of another, so grouping changes nothing.
let s:tag_name_classes = [
\ ["Conditional", 'if\|else-if\|else'],
\ ["Repeat", 'for\|while'],
\ ["Exception", 'try'],
\ ["Statement", 'await\|return'],
\ ["Builtin", 'let\|const\|effect\|lifecycle\|id\|log\|debug\|context\|define\|macro\|html-comment'],
\ ["Builtin", 'l\%(et\|ifecycle\|og\)\|c\%(onst\|ontext\)\|d\%(ebug\|efine\)\|s\%(tyle\|cript\)\|effect\|id\|macro\|textarea\|html-\%(comment\|style\|script\)'],
\ ]
" Concise mode: a line-start name is a tag. The generic rule is defined
" first, so the builtin classes, "@" attribute tags and the statement tags
" below all win over it for their names. keepend bounds each region to
" its line; brackets, method bodies and attr groups carry extend, which
" deliberately punches through for multiline constructs.
let s:concise_tag_end = ' matchgroup=markoTagDelim end=";" end="$" end="\s\zs\ze--\%(\s\|$\)"'
\ . ' contains=@markoTagStuff,markoAttrGroup keepend'
" Concise mode: a line-start name is a tag; so are "${tag}", ".class" and
" "#id". The generic rule is first, so class names, "@" tags and the
" statement tags win over it. A tag ends at its line unless a comma
" trails or leads the continuation; comments and blank lines may sit
" between, and a blank line never hosts the end. Brackets, method bodies
" and attr groups carry extend, punching through keepend for multiline
" constructs:
" link
" ,rel="canonical"
" // a comment between continued lines
" ,href=baseHref
" input type="text",
" value=name
" Whitespace-led dashes end the tag and start text. That end begins at
" the first dash with the space as lookbehind: the space is also the
" value's own break-end, and an end starting inside the value would be
" suppressed by its extend:
" p ---- some text
let s:concise_tag_end = ' matchgroup=markoTagDelim end=";"'
\ . ' end="\%#=1\%(,\s*\)\@9<!\%(^\s\{0,60}\)\@61<!\ze\n\%(\%(\s*\%(//.*\|/\*\_.\{-,200}\*/\s*\)\=\n\)\{0,20}\s*,\)\@!"'
\ . ' end="\s\@1<=\ze-\{2,}\%(\s\|$\)"'
\ . ' contains=@markoTagStuff,markoAttrGroup,markoAttrValueConcise,markoTagNameTail keepend'
exe 'syn region markoConciseTag matchgroup=markoTagName'
\ . ' start="^\s*\zs[[:alnum:]][-[:alnum:]_$:]*\%([-[:alnum:]_$:]\)\@!"'
\ . ' start="^\s*\zs\%(-\@!\k\|\$[{ \t]\@!\)\%(\k\|:\|\${\@!\)*"'
\ . s:concise_tag_end
exe 'syn region markoConciseTag'
\ . ' start="^\s*\ze\%(\$!\={\|[.#][-[:alnum:]_$]\)"'
\ . s:concise_tag_end
for [s:class_suffix, s:name_pattern] in s:tag_name_classes
exe 'syn region markoConciseTag matchgroup=markoTagName' . s:class_suffix
\ . ' start="^\s*\zs\%(' . s:name_pattern . '\)\%([-[:alnum:]_$:]\)\@!"'
\ . ' start="^\s*\zs\%(' . s:name_pattern . '\)\%(\k\|\$\|:=\@!\)\@!"'
\ . s:concise_tag_end
endfor
exe 'syn region markoConciseTag matchgroup=markoAttrTagName'
@@ -199,151 +353,190 @@ exe 'syn region markoConciseTag matchgroup=markoAttrTagName'
\ . s:concise_tag_end
unlet s:concise_tag_end
" Statement tags contain one TypeScript statement. static/server/client
" are Marko keywords; import/export/class take their color from the
" TypeScript include. A statement may span lines through an open brace or
" class body, so an unterminated "/*" runs on, as it would in a TypeScript
" buffer. Defined after the concise regions so these names win there.
" One TypeScript statement each. static/server/client are Marko keywords;
" import/export/class take the include's colors. Defined after the
" concise regions so these names win there.
syn region markoStatement matchgroup=markoStatementKeyword
\ start="^\s*\zs\%(static\|server\|client\)\>"
\ end="$" contains=@markoExpr
syn region markoStatement
\ start="^\s*\ze\%(import\|export\|class\)\>"
\ matchgroup=NONE start="^\s*\ze\%(import\|export\|class\)\>"
\ end="$" contains=@markoExpr
" A tag name may be empty (<.card>), dynamic (<${tag}>) or an attribute
" tag (<@body>); the ">" of an arrow does not end the tag.
" tag (<@body>); an arrow's ">" does not end the tag.
syn region markoTag matchgroup=markoTagDelim
\ start="<\ze[^/ \t!?<>=]" end="/>" end="=\@1<!>"
\ contains=@markoTagStuff,@markoTagNames
\ start="<\ze[^/ \t!?<>=]" end="/>" end="\%#=1=\@1<!>"
\ contains=@markoTagStuff,@markoTagNames,markoAttrValue
" Recovers a close tag no body end consumed, eg a stray one in concise
" context.
syn region markoCloseTag matchgroup=markoTagDelim
\ start="</" end=">"
\ contains=@markoTagNames,markoPlaceholder
" The name of a close tag whose "</" was consumed by an element body's end
" match below, plus its trailing ">".
syn match markoCloseName "\%(</\)\@2<=@\=\%(\k\|[$:]\)\+\s*" nextgroup=markoCloseGt display
" The name of a close tag whose "</" a body end consumed, plus its ">".
" It stops before "${", so a dynamic close name's placeholder colors:
" </${tag}>
syn match markoCloseName
\ "/\@1<=\%(</\)\@2<=\%(\k\|:\|\$\%(!\={\)\@!\)\+\s*"
\ nextgroup=markoCloseGt,markoClosePlaceholder display
syn match markoCloseGt contained ">" display
" A dynamic close name's placeholder, chained so the closing ">" keeps
" its color: </${tag}> via the body's nextgroup, </h${size}> via the
" close name's.
syn region markoClosePlaceholder contained matchgroup=markoPlaceholderDelim
\ start="\$!\={" end="}"
\ contains=@markoExpr
\ nextgroup=markoCloseGt,markoCloseTail
syn match markoCloseTail contained "\%(\k\|:\)\+\s*"
\ nextgroup=markoCloseGt,markoClosePlaceholder display
" The anonymous form (</>): a body end consumed the "</" and there is no
" close name to carry the nextgroup, so the ">" takes its color directly.
syn match markoCloseAnon "\%(</\)\@2<=>" display
" Everything html-mode content may contain. Statement tags and concise
" tags are absent: bare text inside an element body is prose.
" Statement and concise tags are absent: bare text in a body is prose.
" The close-name refinements keep </if> Conditional at any depth; generic
" markoTagName stays out or it would outrank markoCloseName.
syn cluster markoHtmlContent contains=markoTag,markoHtmlBody,markoDynamicBody,
\markoStyle,markoScript,markoTextareaTag,markoHtmlCommentTag,
\markoComment,markoCData,markoDeclaration,markoEscape,markoPlaceholder,
\markoEntity,markoScriptlet,markoCloseName
\markoComment,markoHtmlComment,markoCData,markoDeclaration,markoEscape,markoPlaceholder,
\markoEntity,markoScriptlet,markoCloseName,markoCloseAnon,markoAttrTagName,
\markoTagNameConditional,markoTagNameRepeat,markoTagNameException,
\markoTagNameStatement,markoTagNameBuiltin
" Whether "<name" begins an element body turns on how its open tag ends:
" an atomic (no-backtrack) run over quoted strings, (args), <type args>,
" "=>" and spaced comparisons finds the structural ">", and a "/>" there
" means self-closing, so no body:
" Whether "<name" begins a body turns on how its open tag ends: an atomic
" run over the groupings that may hold a ">" finds the structural one,
" and a "/>" there means self-closing, no body:
" <my-tag onClick=() => go(a > b) />
" <div onClick() { hidden = a > b }/>
" Nothing else hides a ">" (the parser ends an open tag at any ">" not
" preceded by "="), and the repeat counts bound the scan so a stray "<"
" costs a fixed lookahead; past them no body opens.
let s:tag_end_ahead = '\%(\%(\%(<\%([^<>]\|<\%([^<>]\|<[^<>]\{-,40}>\)\{-,60}>\)\{-,100}>'
\ . '\|"[^"]\{-,200}"\|' . "'[^']\\{-,200}'"
\ . '\|([^()]\{-,200})\|=>\|[ \t]>[ \t]\@=\|\_[^>]\)\{0,400}\)\@>/\@1<!=\@1<!>\)\@='
\ . '\|`[^`]\{-,200}`'
\ . '\|(\%(\_[^()]\|(\%(\_[^()]\|([^()]\{-,60})\)\{-,100})\)\{-,300})'
\ . '\|{\%(\_[^{}]\|{\_[^{}]\{-,100}}\)\{-,300}}'
\ . '\|=>\|\_[^>]\)\{0,800}\)\@>/\@1<!=\@1<!>\)\@='
" Element bodies: html mode from a non-void, non-self-closing open tag to
" its matching close. \z1 pairs the names, and the end consumes only
" "</" so an inner same-name close cannot end the outer bodies
" (markoCloseName colors the rest):
" its matching close. \z1 pairs the names; the end consumes only "</" so
" an inner same-name close cannot end the outer body:
" <div><div>nested</div>still the outer body</div>
" No body opens for html voids, parsed-text tags, bodiless core tags
" (<let/x=1> takes no close tag) or "@" attribute tags (those auto-close
" at their parent, which no name pairing can track). Defined after
" markoTag so open tags still overlay inside bodies, and after the
" concise regions so html mode wins at a line-start "<". A mismatched or
" missing close leaves its body open for the rest of the buffer, as an
" unclosed tag does in html.vim.
" The excluded names are the parser's void and text lists (<let/x=1>
" takes no close tag; <effect> is in neither and opens a body). Defined
" after markoTag and the concise regions so a body wins at "<"; a missing
" close leaves the body open, as in html.vim.
exe 'syn region markoHtmlBody'
\ . ' start=+<\ze\%(\%(area\|base\|br\|col\|embed\|hr\|img\|input\|link\|meta\|param'
\ . '\|source\|track\|wbr\|style\|script\|textarea\|html-comment\|html-style'
\ . '\|html-script\|let\|const\|id\|log\|debug\|effect\|lifecycle\|return\)'
\ . '\%(\k\|[$:]\)\@!\)\@!'
\ . '\|html-script\|let\|const\|id\|log\|debug\|lifecycle\|return\)'
\ . '\%(\k\|\$\|:=\@!\)\@!\)\@!'
\ . '\z(\%(\k\|[$:]\)\+\)\%(\_s\|[/>|(<.#=]\)\@=' . s:tag_end_ahead . '+'
\ . ' matchgroup=markoTagDelim'
\ . ' end="</\ze\z1\%(\k\|[$:]\)\@!\s*>" end="</\ze\s*>"'
\ . ' end="</\ze\z1\s*>" end="</\ze\s*>"'
\ . ' contains=@markoHtmlContent'
" A dynamic name -- any mix of literal characters and "${...}"
" placeholders -- cannot be paired, so a dynamic body closes at the first
" close tag of dynamic shape, regardless of nesting:
" <h${size}>heading</h${size}> <${tag}>body</${tag}>
exe 'syn region markoDynamicBody'
\ . ' start=+<\ze\%(\$!\={\)\@=' . s:tag_end_ahead . '+'
\ . ' start=+<\ze\%(\%(\k\|:\)*\$!\={\)\@=' . s:tag_end_ahead . '+'
\ . ' matchgroup=markoTagDelim'
\ . ' end="</\ze\%(\$!\={\_[^}]*}\)\=\s*>"'
\ . ' contains=@markoHtmlContent'
\ . ' end="</\ze\%(\%(\k\|:\)*\%(\$!\={\_[^{}]*\%({\_[^{}]*}\_[^{}]*\)\{0,3}}\%(\k\|:\)*\)\+\)\=\s*>"'
\ . ' contains=@markoHtmlContent nextgroup=markoClosePlaceholder'
unlet s:tag_end_ahead
" Delimited text blocks in concise mode contain html-mode content. An
" indented block closes at a matching delimiter line or at any line
" indented less than its opener; a column-0 block closes only at its
" matching delimiter:
" Standalone delimited blocks hold html content, like markoTextIndent.
" A block closes at its exact delimiter -- same indent, same dash count,
" longer or shorter runs are text -- or on any dedent; a column-0 block
" only at its delimiter (the empty \z1 kills the dedent end). Defined
" after the markoBlockDelim forms so a bare "--" line opens a block:
" --
" A block of text, <b>html mode</b>.
" --
syn region markoTextBlock matchgroup=markoBlockDelim
\ start="^\z(\s\+\)--\+\s*$"
\ end="^\z1--\+\s*$" end="^\%(\z1\)\@!\ze\s*\S"
\ contains=@markoHtmlContent
syn region markoTextBlock matchgroup=markoBlockDelim
\ start="^--\+\s*$"
\ end="^--\+\s*$"
syn region markoTextDelimBlock matchgroup=markoBlockDelim
\ start="^\z(\s*\)\z(--\+\)\s*$"
\ end="^\z1\z2\s*$" end="^\%(\z1\)\@!\ze\s*\S"
\ contains=@markoHtmlContent
" Parsed-text tags: bodies contain only text and placeholders (style and
" script embed CSS and TypeScript, a textarea body is plain text, an
" html-comment body is comment text). Defined after the generic tag and
" body regions to win at the same "<". The "/\@1<!" keeps self-closing
" open tags, which have no body, out:
" <script src="x" />
" keepend stops an unterminated embedded construct at the close tag.
syn region markoStyle matchgroup=markoTagNameBuiltin
\ start=+<\%(html-\)\=style\>\_[^>]\{-,300}/\@1<!=\@1<!>+
\ end="</\%(html-\)\=style\s*>"
\ contains=@markoCSS,markoPlaceholder keepend fold
" Parsed-text tags: bodies hold only text and placeholders (style/script
" embed CSS and TypeScript). Each region consumes only its "<"; the
" markoParsedOpen overlay colors the open tag normally:
" <style/styles> <script src="x" async>
" Defined after the generic tag and body regions to win at "<". The
" "/\@1<!" keeps self-closing tags out; keepend stops an unterminated
" embedded construct at the close tag.
syn region markoParsedOpen contained matchgroup=markoTagDelim
\ start="<\ze\%(html-\%(style\|script\|comment\)\|style\|script\|textarea\)\>\%(:=\@!\|\$\)\@!"
\ matchgroup=markoTagDelim end="/\@1<!=\@1<!>"
\ contains=@markoTagStuff,@markoTagNames,markoAttrValue
" The bounded lookahead for the open tag's own ">", shared by all four.
let s:open_end_ahead = '\%(\%("[^"]\{-,200}"\|''[^'']\{-,200}''\|`[^`]\{-,200}`\|=>\|\_[^>]\)\{-,300}/\@1<!=\@1<!>\)\@='
for [s:name, s:pat, s:body] in [
\ ['markoStyle', '\%(html-\)\=style', '@markoCSS,markoPlaceholderText,markoParsedOpen keepend fold'],
\ ['markoScript', '\%(html-\)\=script', '@markoTS,markoPlaceholderText,markoParsedOpen keepend fold'],
\ ['markoTextareaTag', 'textarea', 'markoPlaceholderText,markoEntity,@Spell,markoParsedOpen keepend'],
\ ['markoHtmlCommentTag', 'html-comment', 'markoPlaceholderText,@Spell,markoParsedOpen keepend'],
\ ]
exe 'syn region ' . s:name
\ . ' start=+<\ze' . s:pat . '\>\%(:=\@!\|\$\)\@!' . s:open_end_ahead . '+'
\ . ' matchgroup=markoTagDelim end="</\ze' . s:pat . '\s*>"'
\ . ' contains=' . s:body
endfor
unlet s:name s:pat s:body
syn region markoStyle matchgroup=markoTagNameBuiltin
\ start="^\s*style\%([./][^ \t{]*\)\=\s*{" end="}"
\ contains=@markoCSS,markoPlaceholder fold
syn region markoScript matchgroup=markoTagNameBuiltin
\ start=+<\%(html-\)\=script\>\_[^>]\{-,300}/\@1<!=\@1<!>+
\ end="</\%(html-\)\=script\s*>"
\ contains=@markoTS,markoPlaceholder keepend fold
syn region markoTextareaTag matchgroup=markoTagNameBuiltin
\ start=+<textarea\>\_[^>]\{-,300}/\@1<!=\@1<!>+
\ end="</textarea\s*>"
\ contains=markoPlaceholder,markoEntity,@Spell keepend
syn region markoHtmlCommentTag matchgroup=markoTagNameBuiltin
\ start=+<html-comment\>\_[^>]\{-,300}/\@1<!=\@1<!>+
\ end="</html-comment\s*>"
\ contains=markoPlaceholder,@Spell keepend
\ contains=@markoCSS,markoPlaceholderText fold
" A trailing "--" on a concise style tag opens a delimited block of CSS;
" the lookbehind claims only the dashes, winning over the generic
" trailing-"--" form. script works the same way with TypeScript:
" style/styles --
" .root { color: red }
" --
for [s:kind, s:body] in [["Style", '@markoCSS'], ["Script", '@markoTS']]
exe 'syn match marko' . s:kind . 'Delim'
\ . ' "\%#=1\%(^\s*' . tolower(s:kind) . '\%(\k\|[$:]\)\@![^;{]*\)\@200<=--\+\s*$"'
\ . ' nextgroup=marko' . s:kind . 'Text skipnl'
exe 'syn region marko' . s:kind . 'Text contained' . s:indent_block
\ . ' contains=' . s:body . ',markoPlaceholderText keepend fold'
endfor
unlet s:kind s:body s:indent_block
unlet s:open_end_ahead
" Tag names inside "<...>"/"</...>" (concise names are colored by the
" region matchgroups above). The generic name is defined after the
" attribute name, so it wins right after "<"; the class-specific names are
" defined after the generic one, so they win over it.
syn match markoTagName contained "\%(</\=\)\@2<=[[:alnum:]][-[:alnum:]_$:]*" display
syn match markoAttrTagName contained "\%(</\=\)\@2<=@[-[:alnum:]_$:]\+" display
" Names inside "<...>"/"</...>": a name stops before "${" and
" markoTagNameTail resumes after "}"; the tail's lookbehind demands the
" placeholder, so a method body's "}" cannot start one:
" <h${size}> <div onClick() { go() }disabled> "disabled" is an attr
" Generic after attr name, class names after generic: later wins.
syn match markoTagName contained "\%(</\=\)\@2<=\%(-\@!\k\|\$[{ \t]\@!\)\%(\k\|:\|\${\@!\)*" display
syn match markoTagNameTail contained
\ "}\@1<=\%(\$!\={[^{}]*}\)\@120<=\%(\k\|:\|\${\@!\)\+" display
syn match markoAttrTagName contained "\%#=1\%(</\=\)\@2<=@[-[:alnum:]_$:]\+" display
" The un-contained variants refine markoCloseName after a body's end has
" consumed the "</", so builtin and "@" close names keep their classes.
syn match markoAttrTagName "\%(</\)\@2<=@\%(\k\|[$:]\)\+\s*" nextgroup=markoCloseGt display
syn match markoAttrTagName "\%#=1\%(</\)\@2<=@\%(\k\|[$:]\)\+\s*" nextgroup=markoCloseGt display
for [s:class_suffix, s:name_pattern] in s:tag_name_classes
exe 'syn match markoTagName' . s:class_suffix
\ . ' contained "\%(</\=\)\@2<=\%(' . s:name_pattern . '\)\%([-[:alnum:]_$]\)\@!" display'
\ . ' contained "\%#=1\%(</\=\)\@2<=\%(' . s:name_pattern . '\)\%(\k\|\$\|:=\@!\)\@!" display'
exe 'syn match markoTagName' . s:class_suffix
\ . ' "\%(</\)\@2<=\%(' . s:name_pattern . '\)\%(\k\|[$:]\)\@!\s*"'
\ . ' "\%#=1\%(</\)\@2<=\%(' . s:name_pattern . '\)\%(\k\|[$:]\)\@!\s*"'
\ . ' nextgroup=markoCloseGt display'
endfor
unlet s:tag_name_classes s:class_suffix s:name_pattern
" The includes install their own sync rules; clear them and restate the
" keyword characters ("syn sync clear" clears "syn iskeyword" too). "-"
" keeps css.vim's hyphenated value keywords whole:
" font-family: sans-serif
" fromstart is the only safe strategy, since element bodies, concise
" regions and style/script bodies are all unbounded or line-anchored; a
" cold parse of a 5000 line file stays well under a second on current
" hardware.
" The includes install sync rules; clear them and restate the keyword set
" ("syn sync clear" clears "syn iskeyword" too; "-" keeps css.vim's
" font-family whole). fromstart is the only safe strategy -- bodies and
" blocks are unbounded -- and a 5000-line cold parse stays well under a
" second.
syn sync clear
syn sync fromstart
syn iskeyword @,48-57,_,192-255,-
" Scoping regions (tags, bodies, brackets, values) take no link: they
" bound contents that carry their own colors.
hi def link markoComment Comment
hi def link markoTagComment Comment
hi def link markoHtmlComment Comment
hi def link markoHtmlCommentTag Comment
hi def link markoDeclaration PreProc
hi def link markoCData String
@@ -355,8 +548,11 @@ hi def link markoScriptletDelim Special
hi def link markoBlockDelim Special
hi def link markoTagDelim Special
hi def link markoTagName Statement
hi def link markoTagNameTail Statement
hi def link markoCloseName Statement
hi def link markoCloseTail Statement
hi def link markoCloseGt Special
hi def link markoCloseAnon Special
hi def link markoAttrTagName Identifier
hi def link markoTagNameConditional Conditional
hi def link markoTagNameRepeat Repeat
@@ -365,6 +561,8 @@ hi def link markoTagNameStatement Statement
hi def link markoTagNameBuiltin Macro
hi def link markoShorthandId Constant
hi def link markoShorthandClass Identifier
hi def link markoStyleDelim Special
hi def link markoScriptDelim Special
hi def link markoTagVar Identifier
hi def link markoAttrName Type
hi def link markoString String

View File

@@ -87,7 +87,7 @@ func Test_zip_basic()
"## Check opening zip when "unzip" program is missing
let save_zip_unzipcmd = g:zip_unzipcmd
let g:zip_unzipcmd = "/"
call assert_match('unzip not available on your system', execute("e X.zip"))
call assert_match('(zip#Browse) sorry, your system doesn''t appear to have the / program', execute("e X.zip"))
"## Check when "unzip" don't work
if executable("false")