" Vim syntax file " Language: Marko " Maintainer: Brian Carbone " URL: https://markojs.com " Filenames: *.marko " Last Change: 2026 Jul 28 " 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: "

if you want, stop by

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. if !exists("main_syntax") if exists("b:current_syntax") finish endif let main_syntax = "marko" endif let s:cpo_save = &cpo set cpo&vim 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. let s:iskeyword_save = &l:iskeyword syn include @markoTS syntax/typescript.vim unlet! b:current_syntax syn include @markoCSS syntax/css.vim unlet! b:current_syntax 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: "
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. 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 " Comments are recognized where the parser allows text to start; anchoring " to the line start keeps "//" inside URLs as text: " // note /* note */ https://example.com syn region markoComment start="" contains=@Spell fold syn match markoComment "^\s*//.*$" contains=@Spell display syn region markoComment start="^\s*/\*" end="\*/" contains=@Spell fold syn region markoTagComment contained start="/\*" end="\*/" syn match markoTagComment contained "//.*" syn region markoCData start="" " Declarations; "" may close with ">" or "?>": " syn region markoDeclaration start="" syn region markoDeclaration start="" " Placeholders interpolate into text, and 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 "\%(\\\\\)*\\\$!\={" syn region markoPlaceholder matchgroup=markoPlaceholderDelim \ start="\$!\={" end="}" \ contains=@markoExpr containedin=cssDefinition,cssAttrRegion extend 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 syn region markoScriptlet matchgroup=markoScriptletDelim \ start="^\s*\$\s\+{\@!" end="$" \ contains=@markoExpr keepend syn region markoScriptlet matchgroup=markoScriptletDelim \ start="^\s*\$\s\+{" end="};\=" \ contains=@markoExpr " "--" marks one line of text in concise mode: " p -- some text syn match markoBlockDelim "\%(^\|\s\)\@1<=--\+\ze\%(\s\|$\)" display " 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 syn match markoSpread contained "\.\.\." display syn region markoAttrValue contained matchgroup=markoOperator \ start=":\==>\@!\s*" end="\ze\%(\%(=>\)\@2\)\@!\|=\@1\|/>\)" end="$" \ contains=@markoExpr syn region markoString contained start=+\z(["']\)+ skip=+\\\z1+ end=+\z1+ contains=@Spell " Shorthands after the tag name: " syn match markoShorthandId contained "#[-[:alnum:]_$]\+" display syn match markoShorthandClass contained "\.[-[:alnum:]_$]\+" display " Tag variables, arguments, |parameters|, and attribute method " shorthand bodies: " " >
" No body opens for html voids, parsed-text tags, bodiless core tags " ( 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. 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\|[$:]\)\@!\)\@!' \ . '\z(\%(\k\|[$:]\)\+\)\%(\_s\|[/>|(<.#=]\)\@=' . s:tag_end_ahead . '+' \ . ' matchgroup=markoTagDelim' \ . ' end="" end=""' \ . ' contains=@markoHtmlContent' exe 'syn region markoDynamicBody' \ . ' start=+<\ze\%(\$!\={\)\@=' . s:tag_end_ahead . '+' \ . ' matchgroup=markoTagDelim' \ . ' end=""' \ . ' contains=@markoHtmlContent' 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: " -- " A block of text, html mode. " -- 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*$" \ 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 " keepend stops an unterminated embedded construct at the close tag. syn region markoStyle matchgroup=markoTagNameBuiltin \ start=+<\%(html-\)\=style\>\_[^>]\{-,300}/\@1+ \ end="" \ contains=@markoCSS,markoPlaceholder keepend fold 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+ \ end="" \ contains=@markoTS,markoPlaceholder keepend fold syn region markoTextareaTag matchgroup=markoTagNameBuiltin \ start=+\_[^>]\{-,300}/\@1+ \ end="" \ contains=markoPlaceholder,markoEntity,@Spell keepend syn region markoHtmlCommentTag matchgroup=markoTagNameBuiltin \ start=+\_[^>]\{-,300}/\@1+ \ end="" \ contains=markoPlaceholder,@Spell keepend " 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 "\%(