Commit: patch 9.2.0870: filetype: marko files are not recognized

1 view
Skip to first unread message

Christian Brabandt

unread,
3:15 PM (6 hours ago) 3:15 PM
to vim...@googlegroups.com
patch 9.2.0870: filetype: marko files are not recognized

Commit: https://github.com/vim/vim/commit/95da62a910809787cc8fdf26b131721359812d8f
Author: Brian Carbone <br...@briancarbone.com>
Date: Tue Jul 28 19:03:49 2026 +0000

patch 9.2.0870: filetype: marko files are not recognized

Problem: filetype: marko files are not recognized
Solution: Detect *.marko files as marko filetype, include a syntax
plugin and add syntax tests (Brian Carbone)

References:
https://markojs.com
https://github.com/marko-js/tree-sitter
https://github.com/marko-js/language-server
https://v5.markojs.com/docs/editor-plugins/

closes: #20863

Signed-off-by: Brian Carbone <br...@briancarbone.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/.github/MAINTAINERS b/.github/MAINTAINERS
index ea7682488..44ccadf6b 100644
--- a/.github/MAINTAINERS
+++ b/.github/MAINTAINERS
@@ -629,6 +629,7 @@ runtime/syntax/m3quake.vim @dkearns
runtime/syntax/mailcap.vim @dkearns
runtime/syntax/mallard.vim @jhradilek
runtime/syntax/markdown.vim @tpope
+runtime/syntax/marko.vim @briancarbone
runtime/syntax/mason.vim @petdance
runtime/syntax/mbsync.vim @fymyte
runtime/syntax/mediawiki.vim @avidseeker
diff --git a/runtime/autoload/dist/ft.vim b/runtime/autoload/dist/ft.vim
index 7ed41785f..0d1e7119c 100644
--- a/runtime/autoload/dist/ft.vim
+++ b/runtime/autoload/dist/ft.vim
@@ -3,7 +3,7 @@ vim9script
# Vim functions for file type detection
#
# Maintainer: The Vim Project <https://github.com/vim/vim>
-# Last Change: 2026 Jul 27
+# Last Change: 2026 Jul 28
# Former Maintainer: Bram Moolenaar <Br...@vim.org>

# These functions are moved here from runtime/filetype.vim to make startup
@@ -2416,6 +2416,8 @@ const ft_from_ext = {
"page": "mallard",
# Manpage
"man": "man",
+ # Marko
+ "marko": "marko",
# Maple V
"mv": "maple",
"mpl": "maple",
diff --git a/runtime/syntax/marko.vim b/runtime/syntax/marko.vim
new file mode 100644
index 000000000..be589534c
--- /dev/null
+++ b/runtime/syntax/marko.vim
@@ -0,0 +1,382 @@
+" Vim syntax file
+" Language: Marko
+" Maintainer: Brian Carbone <cordial.c...@fastmail.com>
+" 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:
+" <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.
+
+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:
+" <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.
+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 /* note */ https://example.com
+syn region markoComment start="<!--" end="-->" 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="<!\[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:
+" ${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<!\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
+
+" Tag variables, arguments, |parameters|, <type args> and attribute method
+" shorthand 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
+syn region markoArgs contained matchgroup=markoTagDelim
+ \ start="(" end=")"
+ \ contains=@markoExpr extend
+syn region markoParams contained oneline matchgroup=markoTagDelim
+ \ start="\%(\k\|[$)\]}>]\)\@4<=|" skip="||" end="|"
+ \ contains=@markoExpr
+syn region markoTypeArgs contained oneline matchgroup=markoTagDelim
+ \ start="\%(\k\|\$\)\@4<=<" end=">"
+ \ contains=@markoExpr,markoTypeArgs
+syn region markoMethodBody contained matchgroup=markoTagDelim
+ \ start="{" end="}"
+ \ contains=@markoExpr extend
+
+syn cluster markoTagStuff contains=markoTagVar,markoShorthandId,markoShorthandClass,
+ \markoAttrName,markoAttrValue,markoString,markoArgs,markoParams,markoTypeArgs,
+ \markoMethodBody,markoSpread,markoTagComment,markoPlaceholder
+syn cluster markoTagNames contains=markoTagName,markoAttrTagName,markoTagNameConditional,
+ \markoTagNameRepeat,markoTagNameException,markoTagNameStatement,markoTagNameBuiltin
+
+" Concise attribute groups may span lines:
+" define [
+" size = "large"
+" ]
+syn region markoAttrGroup contained matchgroup=markoTagDelim
+ \ start="\[" end="]"
+ \ contains=@markoTagStuff extend
+
+" One name list per highlight class, shared by the concise regions here
+" and the tag-name matches below.
+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'],
+ \ ]
+
+" 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'
+exe 'syn region markoConciseTag matchgroup=markoTagName'
+ \ . ' start="^\s*\zs[[:alnum:]][-[:alnum:]_$:]*\%([-[: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:]_$:]\)\@!"'
+ \ . s:concise_tag_end
+endfor
+exe 'syn region markoConciseTag matchgroup=markoAttrTagName'
+ \ . ' start="^\s*\zs@[-[:alnum:]_$:]\+"'
+ \ . 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.
+syn region markoStatement matchgroup=markoStatementKeyword
+ \ start="^\s*\zs\%(static\|server\|client\)\>"
+ \ end="$" contains=@markoExpr
+syn region markoStatement
+ \ 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.
+syn region markoTag matchgroup=markoTagDelim
+ \ start="<\ze[^/ !?<>=]" end="/>" end="=\@1<!>"
+ \ contains=@markoTagStuff,@markoTagNames
+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
+syn match markoCloseGt contained ">" display
+
+" Everything html-mode content may contain. Statement tags and concise
+" tags are absent: bare text inside an element body is prose.
+syn cluster markoHtmlContent contains=markoTag,markoHtmlBody,markoDynamicBody,
+ \markoStyle,markoScript,markoTextareaTag,markoHtmlCommentTag,
+ \markoComment,markoCData,markoDeclaration,markoEscape,markoPlaceholder,
+ \markoEntity,markoScriptlet,markoCloseName
+
+" 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:
+" <my-tag onClick=() => go(a > b) />
+let s:tag_end_ahead = '\%(\%(\%(<\%([^<>]\|<\%([^<>]\|<[^<>]\{-,40}>\)\{-,60}>\)\{-,100}>'
+ \ . '\|"[^"]\{-,200}"\|' . "'[^']\{-,200}'"
+ \ . '\|([^()]\{-,200})\|=>\|[ ]>[ ]\@=\|\_[^>]\)\{0,400}\)\@>/\@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):
+" <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.
+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="</\ze\z1\%(\k\|[$:]\)\@!\s*>" end="</\ze\s*>"'
+ \ . ' contains=@markoHtmlContent'
+exe 'syn region markoDynamicBody'
+ \ . ' start=+<\ze\%(\$!\={\)\@=' . s:tag_end_ahead . '+'
+ \ . ' matchgroup=markoTagDelim'
+ \ . ' end="</\ze\%(\$!\={\_[^}]*}\)\=\s*>"'
+ \ . ' 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, <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*$"
+ \ 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
+syn region markoStyle matchgroup=markoTagNameBuiltin
+ \ start="^\s*style\%([./][^ {]*\)\=\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
+
+" 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
+" 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
+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'
+ exe 'syn match markoTagName' . s:class_suffix
+ \ . ' "\%(</\)\@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.
+syn sync clear
+syn sync fromstart
+syn iskeyword @,48-57,_,192-255,-
+
+hi def link markoComment Comment
+hi def link markoTagComment Comment
+hi def link markoHtmlCommentTag Comment
+hi def link markoDeclaration PreProc
+hi def link markoCData String
+hi def link markoEscape Special
+hi def link markoEntity Special
+hi def link markoPlaceholderDelim Special
+hi def link markoStatementKeyword Keyword
+hi def link markoScriptletDelim Special
+hi def link markoBlockDelim Special
+hi def link markoTagDelim Special
+hi def link markoTagName Statement
+hi def link markoCloseName Statement
+hi def link markoCloseGt Special
+hi def link markoAttrTagName Identifier
+hi def link markoTagNameConditional Conditional
+hi def link markoTagNameRepeat Repeat
+hi def link markoTagNameException Exception
+hi def link markoTagNameStatement Statement
+hi def link markoTagNameBuiltin Macro
+hi def link markoShorthandId Constant
+hi def link markoShorthandClass Identifier
+hi def link markoTagVar Identifier
+hi def link markoAttrName Type
+hi def link markoString String
+hi def link markoSpread Operator
+hi def link markoOperator Operator
+
+let b:current_syntax = "marko"
+if main_syntax ==# "marko"
+ unlet main_syntax
+endif
+
+let &cpo = s:cpo_save
+unlet s:cpo_save
+
+" vim: ts=8
diff --git a/runtime/syntax/testdir/dumps/marko_00.dump b/runtime/syntax/testdir/dumps/marko_00.dump
new file mode 100644
index 000000000..becbe3883
--- /dev/null
+++ b/runtime/syntax/testdir/dumps/marko_00.dump
@@ -0,0 +1,20 @@
+><+0#e000e06#ffffff0|!|d|o|c|t|y|p|e| |h|t|m|l|>| +0#0000000&@59
+|<+0#0000e05&|!|-@1| |h|t|m|l| |m|o|d|e|:| |t|a|g|s|,| |a|t@1|r|i|b|u|t|e|s|,| |e|m|b|e|d@1|e|d| |l|a|n|g|u|a|g|e|s| |-@1|>| +0#0000000&@18
+|i+0#af5f00255&|m|p|o|r|t| +0#0000000&|{+0#00e0e07&| +0#0000000&|f|o|r|m|a|t|P|r|i|c|e| |}+0#00e0e07&| +0#0000000&|f+0#af5f00255&|r|o|m| +0#0000000&|"+0#e000002&|.|/|u|t|i|l|"| +0#0000000&@38
+|s+0#af5f00255&|t|a|t|i|c| +0#0000000&|c+0#af5f00255&|o|n|s|t| +0#0000000&|V|E|R|S|I|O|N| |=| |1+0#e000002&| +0#0000000&@50
+@75
+|<+0#e000e06&|s|t|y|l|e|>| +0#0000000&@67
+@2|.+0#00e0e07&|p|r|i|c|e| +0#0000000&|{+0#00e0e07&| +0#0000000&|c+0#00e0003&|o|l|o|r|:+0#0000000&| |g+0#e000002&|r|e@1|n| +0#0000000&|}+0#00e0e07&| +0#0000000&@49
+|<+0#e000e06&|/|s|t|y|l|e|>| +0#0000000&@66
+@75
+|<+0#e000e06&|l|e|t|/+0#00e0e07&|c|o|u|n|t|=+0#af5f00255&|1+0#e000002&|/+0#e000e06&|>| +0#0000000&@60
+|<+0#e000e06&|c|o|n|s|t|/+0#00e0e07&|{+0#e000e06&| +0#0000000&|i|t|e|m|s| |}+0#e000e06&|=+0#af5f00255&|i+0#0000000&|n|p|u|t|/+0#e000e06&|>| +0#0000000&@50
+@75
+|<+0#e000e06&|d+0#af5f00255&|i|v|.+0#00e0e07&|c|a|r|d|#+0#e000002&|m|a|i|n| +0#0000000&|c+0#00e0003&|l|a|s@1|=+0#af5f00255&|"+0#e000002&|c|a|r|t|"| +0#0000000&|d+0#00e0003&|a|t|a|-|t|o|t|a|l|:+0#af5f00255&|=|t+0#0000000&|o|t|a|l| |.+0#af5f00255&@2|r+0#00e0003&|e|s|t|>+0#e000e06&| +0#0000000&@20
+@2|<+0#e000e06&|i+0#af5f00255&|f|=|c+0#0000000&|o|u|n|t|>+0#e000e06&| +0#0000000&@62
+@4|<+0#e000e06&|m+0#af5f00255&|y|-|b|u|t@1|o|n| +0#0000000&|s+0#00e0003&|i|z|e|=+0#af5f00255&|"+0#e000002&|l|a|r|g|e|"| +0#0000000&|o+0#00e0003&|n|C|l|i|c|k|=+0#af5f00255&|(+0#0000000&|)| |=+0#00e0003&|>| +0#0000000&|b|u|y|(|)|>+0#e000e06&|B+0#0000000&|u|y| |$+0#e000e06&|{|c+0#0000000&|o|u|n|t| |+| |1+0#e000002&|}+0#e000e06&|<|/|m+0#af5f00255&|y|-|b|u|t@1|o|n
+|>+0#e000e06&| +0#0000000&@73
+@2|<+0#e000e06&|/|i+0#af5f00255&|f|>+0#e000e06&| +0#0000000&@67
+@2|<+0#e000e06&|f+0#af5f00255&|o|r||+0#e000e06&|i+0#0000000&|t|e|m|,| |i|n|d|e|x||+0#e000e06&| +0#0000000&|o+0#00e0003&|f|=+0#af5f00255&|i+0#0000000&|t|e|m|s|>+0#e000e06&| +0#0000000&@45
+@4|<+0#e000e06&|s+0#af5f00255&|p|a|n|>+0#e000e06&|$|{|f+0#0000000&|o|r|m|a|t|P|r|i|c|e|(|i+0#e000e06&|t|e|m|.+0#0000000&|p+0#e000e06&|r|i|c|e|)+0#0000000&|}+0#e000e06&|<|/|s+0#af5f00255&|p|a|n|>+0#e000e06&| +0#0000000&@31
+@57|1|,|1| @10|T|o|p|
diff --git a/runtime/syntax/testdir/dumps/marko_01.dump b/runtime/syntax/testdir/dumps/marko_01.dump
new file mode 100644
index 000000000..fc2493b8f
--- /dev/null
+++ b/runtime/syntax/testdir/dumps/marko_01.dump
@@ -0,0 +1,20 @@
+| +0&#ffffff0@1|<+0#e000e06&|i+0#af5f00255&|f|=|c+0#0000000&|o|u|n|t|>+0#e000e06&| +0#0000000&@62
+@4|<+0#e000e06&|m+0#af5f00255&|y|-|b|u|t@1|o|n| +0#0000000&|s+0#00e0003&|i|z|e|=+0#af5f00255&|"+0#e000002&|l|a|r|g|e|"| +0#0000000&|o+0#00e0003&|n|C|l|i|c|k|=+0#af5f00255&|(+0#0000000&|)| |=+0#00e0003&|>| +0#0000000&|b|u|y|(|)|>+0#e000e06&|B+0#0000000&|u|y| |$+0#e000e06&|{|c+0#0000000&|o|u|n|t| |+| |1+0#e000002&|}+0#e000e06&|<|/|m+0#af5f00255&|y|-|b|u|t@1|o|n
+|>+0#e000e06&| +0#0000000&@73
+@2|<+0#e000e06&|/|i+0#af5f00255&|f|>+0#e000e06&| +0#0000000&@67
+@2|<+0#e000e06&|f+0#af5f00255&|o|r||+0#e000e06&|i+0#0000000&|t|e|m|,| |i|n|d|e|x||+0#e000e06&| +0#0000000&|o+0#00e0003&|f|=+0#af5f00255&|i+0#0000000&|t|e|m|s|>+0#e000e06&| +0#0000000&@45
+@4><+0#e000e06&|s+0#af5f00255&|p|a|n|>+0#e000e06&|$|{|f+0#0000000&|o|r|m|a|t|P|r|i|c|e|(|i+0#e000e06&|t|e|m|.+0#0000000&|p+0#e000e06&|r|i|c|e|)+0#0000000&|}+0#e000e06&|<|/|s+0#af5f00255&|p|a|n|>+0#e000e06&| +0#0000000&@31
+@2|<+0#e000e06&|/|f+0#af5f00255&|o|r|>+0#e000e06&| +0#0000000&@66
+@2|<+0#e000e06&|g+0#af5f00255&|e|n|e|r|i|c|-|l|i|s|t|<+0#e000e06&|s+0#0000000&|t|r|i|n|g|>+0#e000e06&| +0#0000000&|a+0#00e0003&|=+0#af5f00255&|1+0#e000002&|/+0#e000e06&|>| +0#0000000&@45
+@2|<+0#e000e06&|$|{|d+0#0000000&|y|n|a|m|i|c|T|a|g|}+0#e000e06&| +0#0000000&|t+0#00e0003&|i|t|l|e|=+0#af5f00255&|"+0#e000002&|d|y|n|a|m|i|c|"|>+0#e000e06&|b+0#0000000&|o|d|y|<+0#e000e06&|/|>+0#0000000&| @34
+@2|<+0#e000e06&|@+0#00e0e07&|f|o@1|t|e|r|>+0#e000e06&|s+0#0000000&|l|o|t| |c|o|n|t|e|n|t|<|/|@+0#af5f00255&|f|o@1|t|e|r|>+0#e000e06&| +0#0000000&@41
+|<+0#e000e06&|/|d+0#af5f00255&|i|v|>+0#e000e06&| +0#0000000&@68
+@75
+|<+0#e000e06&|d+0#af5f00255&|i|v| +0#0000000&@70
+@2|c+0#00e0003&|l|a|s@1|=+0#af5f00255&|"+0#e000002&|m|u|l|t|i|l|i|n|e|"| +0#0000000&@55
+@2|d+0#00e0003&|a|t|a|-|x|=+0#af5f00255&|1+0#e000002&| +0#0000000&@64
+|>+0#e000e06&|t+0#0000000&|e|x|t| |w|i|t|h| |&+0#e000e06&|a|m|p|;| +0#0000000&|e|n|t|i|t|y| |a|n|d| |\+0#e000e06&|$|{|a+0#0000000&|n| |e|s|c|a|p|e|d| |p|l|a|c|e|h|o|l|d|e|r|}|<+0#e000e06&|/|d+0#af5f00255&|i|v|>+0#e000e06&| +0#0000000&@14
+@75
+|<+0#e000e06&|t|e|x|t|a|r|e|a| |r|o|w|s|=|3|>|l+0#0000000&|i|t|e|r|a|l| |<|b|>|t|e|x|t|<|/|b|>| |w|i|t|h| |$+0#e000e06&|{|c+0#0000000&|o|u|n|t|}+0#e000e06&|<|/|t|e|x|t|a|r|e|a|>| +0#0000000&@13
+|<+0#e000e06&|h|t|m|l|-|c|o|m@1|e|n|t|>|r+0#0000e05&|e|n|d|e|r|e|d| |<|i|>|c|o|m@1|e|n|t|<|/|i|>| |b|o|d|y|<+0#e000e06&|/|h|t|m|l|-|c|o|m@1|e|n|t|>| +0#0000000&@17
+@57|1|8|,|5| @9|7|6|%|
diff --git a/runtime/syntax/testdir/dumps/marko_02.dump b/runtime/syntax/testdir/dumps/marko_02.dump
new file mode 100644
index 000000000..a9c90cd69
--- /dev/null
+++ b/runtime/syntax/testdir/dumps/marko_02.dump
@@ -0,0 +1,20 @@
+|<+0#e000e06#ffffff0|h|t|m|l|-|c|o|m@1|e|n|t|>|r+0#0000e05&|e|n|d|e|r|e|d| |<|i|>|c|o|m@1|e|n|t|<|/|i|>| |b|o|d|y|<+0#e000e06&|/|h|t|m|l|-|c|o|m@1|e|n|t|>| +0#0000000&@17
+@75
+|<+0#e000e06&|s|c|r|i|p|t|>| +0#0000000&@66
+@2|c+0#af5f00255&|o|n|s|t| +0#0000000&|e|l| |=| |d+0#00e0003&|o|c|u|m|e|n|t|.+0#0000000&|q+0#af5f00255&|u|e|r|y|S|e|l|e|c|t|o|r|(+0#0000000&|"+0#e000002&|.|p|r|i|c|e|"|)+0#0000000&|;| @28
+><+0#e000e06&|/|s|c|r|i|p|t|>| +0#0000000&@65
+|~+0#4040ff13&| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+| +0#0000000&@56|3|5|,|1| @9|B|o|t|
diff --git a/runtime/syntax/testdir/dumps/marko_concise_00.dump b/runtime/syntax/testdir/dumps/marko_concise_00.dump
new file mode 100644
index 000000000..3dcec6ce3
--- /dev/null
+++ b/runtime/syntax/testdir/dumps/marko_concise_00.dump
@@ -0,0 +1,20 @@
+>/+0#0000e05#ffffff0@1| |c|o|n|c|i|s|e| |m|o|d|e|:| |c|u|s|t|o|m| |t|a|g|s|,| |b|u|i|l|t|i|n|s|,| |s|t|a|t|e|m|e|n|t|s|,| |s|c|r|i|p|t|l|e|t|s|,| |t|e|x|t| |b|l|o|c|k|s
+|i+0#af5f00255&|m|p|o|r|t| +0#0000000&|{+0#00e0e07&| +0#0000000&|f|e|t|c|h|I|t|e|m|s| |}+0#00e0e07&| +0#0000000&|f+0#af5f00255&|r|o|m| +0#0000000&|"+0#e000002&|.|/|a|p|i|"| +0#0000000&@40
+@75
+|$+0#e000e06&| |c+0#af5f00255&|o|n|s|t| +0#0000000&|d|o|u|b|l|e|d| |=| |i|n|p|u|t|.|c+0#af5f00255&|o|u|n|t| +0#0000000&|*| |2+0#e000002&|;+0#0000000&| @40
+@75
+|l+0#e000e06&|e|t|/+0#00e0e07&|b|a|s|k|e|t|=+0#af5f00255&|[+0#0000000&|]| @61
+|l+0#e000e06&|e|t|<|n+0#0000000&|u|m|b|e|r|>+0#e000e06&|/+0#00e0e07&|t|y|p|e|d|=+0#af5f00255&|0+0#e000002&| +0#0000000&@55
+@75
+|c+0#af5f00255&|o|n|t|a|i|n|e|r| +0#0000000&|f+0#00e0003&|l|u|i|d| +0#0000000&@59
+@2|s+0#af5f00255&|e|c|t|i|o|n|-|h|e|a|d|i|n|g| +0#0000000&|a+0#00e0003&|s|=+0#af5f00255&|"+0#e000002&|h|2|"| +0#0000000&|-+0#e000e06&@1| +0#0000000&|E|v|e|r|y|t|h|i|n|g| |p|a|r|t|n|e|r|s|h|i|p|s| |n|e@1|d| @17
+@2|b+0#af5f00255&|e|n|t|o| +0#0000000&|c+0#00e0003&|o|l|s|=+0#af5f00255&|4+0#e000002&| +0#0000000&@60
+@4|b+0#af5f00255&|e|n|t|o|-|c|e|l@1| +0#0000000&|[+0#e000e06&| +0#0000000&@58
+@6|f+0#00e0003&|e|a|t|u|r|e|d| +0#0000000&@60
+@6|c+0#00e0003&|o|l|S|p|a|n|=+0#af5f00255&|2+0#e000002&| +0#0000000&@59
+@6|t+0#00e0003&|i|t|l|e|=+0#af5f00255&|"+0#e000002&|O|n|e| |p|l|a|t|f|o|r|m|"| +0#0000000&@48
+@4|]+0#e000e06&| +0#0000000&@69
+@6|p+0#af5f00255&| +0#0000000&|-+0#e000e06&@1| +0#0000000&|O|n|b|o|a|r|d|i|n|g| |a|n|d| |e|n|a|b|l|e|m|e|n|t|,| |u|n|i|f|i|e|d|.| @28
+@75
+|i+0#af5f00255&|f|=|i+0#0000000&|n|p|u|t|.|r+0#af5f00255&|e|a|d|y| +0#0000000&@60
+@57|1|,|1| @10|T|o|p|
diff --git a/runtime/syntax/testdir/dumps/marko_concise_01.dump b/runtime/syntax/testdir/dumps/marko_concise_01.dump
new file mode 100644
index 000000000..fb80cfb9f
--- /dev/null
+++ b/runtime/syntax/testdir/dumps/marko_concise_01.dump
@@ -0,0 +1,20 @@
+| +0&#ffffff0@5|c+0#00e0003&|o|l|S|p|a|n|=+0#af5f00255&|2+0#e000002&| +0#0000000&@59
+@6|t+0#00e0003&|i|t|l|e|=+0#af5f00255&|"+0#e000002&|O|n|e| |p|l|a|t|f|o|r|m|"| +0#0000000&@48
+@4|]+0#e000e06&| +0#0000000&@69
+@6|p+0#af5f00255&| +0#0000000&|-+0#e000e06&@1| +0#0000000&|O|n|b|o|a|r|d|i|n|g| |a|n|d| |e|n|a|b|l|e|m|e|n|t|,| |u|n|i|f|i|e|d|.| @28
+@75
+>i+0#af5f00255&|f|=|i+0#0000000&|n|p|u|t|.|r+0#af5f00255&|e|a|d|y| +0#0000000&@60
+@2|f+0#af5f00255&|o|r||+0#e000e06&|i+0#0000000&|t|e|m|,| |i||+0#e000e06&| +0#0000000&|o+0#00e0003&|f|=+0#af5f00255&|f+0#0000000&|e|t|c|h|I|t|e|m|s|(|)| @44
+@4|l+0#e000e06&|o|g| +0#0000000&|"+0#e000002&|i|t|e|m| |l|o|a|d|e|d|"| +0#0000000&@53
+@4|-+0#e000e06&@1| +0#0000000&|P|r|i|c|e|:| |$+0#e000e06&|{|i+0#0000000&|t|e|m|.|p|r|i|c|e|}+0#e000e06&| +0#0000000&@47
+@2|w+0#af5f00255&|h|i|l|e|(+0#e000e06&|i+0#0000000&|<|1+0#e000002&|0|)+0#e000e06&| +0#0000000&@61
+@4|-+0#e000e06&@1| +0#0000000&|c|o|u|n|t|i|n|g| @59
+|t+0#af5f00255&|r|y| +0#0000000&@71
+@2|a+0#af5f00255&|w|a|i|t|(+0#e000e06&|p+0#0000000&|r|o|m|i|s|e|)+0#e000e06&| +0#0000000&@58
+|e+0#af5f00255&|l|s|e| +0#0000000&@70
+@2|r+0#af5f00255&|e|t|u|r|n|=|n+0#e000002&|u|l@1| +0#0000000&@61
+|@+0#00e0e07&|t|h|e|n||+0#e000e06&|r+0#0000000&|e|s|u|l|t||+0#e000e06&| +0#0000000&@61
+@2|-+0#e000e06&@1| +0#0000000&|r|e|s|o|l|v|e|d| @61
+@75
+|s+0#e000e06&|t|y|l|e|.|s|c|s@1| |{| +0#0000000&@62
+@57|1|9|,|1| @9|5|4|%|
diff --git a/runtime/syntax/testdir/dumps/marko_concise_02.dump b/runtime/syntax/testdir/dumps/marko_concise_02.dump
new file mode 100644
index 000000000..2c1356382
--- /dev/null
+++ b/runtime/syntax/testdir/dumps/marko_concise_02.dump
@@ -0,0 +1,20 @@
+|s+0#e000e06#ffffff0|t|y|l|e|.|s|c|s@1| |{| +0#0000000&@62
+@2|.+0#00e0e07&|b|a|s|k|e|t| +0#0000000&|{+0#00e0e07&| +0#0000000&|c+0#00e0003&|o|l|o|r|:+0#0000000&| |r+0#e000002&|e|d| +0#0000000&|}+0#00e0e07&| +0#0000000&@50
+|}+0#e000e06&| +0#0000000&@73
+@75
+|-+0#e000e06&@1| +0#0000000&@72
+>A| |d|e|l|i|m|i|t|e|d| |b|l|o|c|k| |i|s| |h|t|m|l| |m|o|d|e|,| |s|o| |b|a|r|e| |w|o|r|d|s| |a|r|e| |p|r|o|s|e|.| @18
+|<+0#e000e06&|b+0#af5f00255&|>+0#e000e06&|b+0#0000000&|o|l|d| |$+0#e000e06&|{|d+0#0000000&|o|u|b|l|e|d|}+0#e000e06&|<|/|b+0#af5f00255&|>+0#e000e06&| +0#0000000&@52
+|-+0#e000e06&@1| +0#0000000&@72
+@75
+|<+0#e000e06&|f+0#af5f00255&|o@1|t|e|r|>+0#e000e06&| +0#0000000&@66
+|i|f| |y|o|u| |r|e|a|d| |t|h|i|s| |i|n|s|i|d|e| |a|n| |e|l|e|m|e|n|t| |b|o|d|y|,| |i|t| |s|t|a|y|s| |p|l|a|i|n| |p|r|o|s|e| @13
+|<+0#e000e06&|/|f+0#af5f00255&|o@1|t|e|r|>+0#e000e06&| +0#0000000&@65
+|~+0#4040ff13&| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+| +0#0000000&@56|3|7|,|1| @9|B|o|t|
diff --git a/runtime/syntax/testdir/input/marko.marko b/runtime/syntax/testdir/input/marko.marko
new file mode 100644
index 000000000..1281ce0bd
--- /dev/null
+++ b/runtime/syntax/testdir/input/marko.marko
@@ -0,0 +1,35 @@
+<!doctype html>
+<!-- html mode: tags, attributes, embedded languages -->
+import { formatPrice } from "./util"
+static const VERSION = 1
+
+<style>
+ .price { color: green }
+</style>
+
+<let/count=1/>
+<const/{ items }=input/>
+
+<div.card#main class="cart" data-total:=total ...rest>
+ <if=count>
+ <my-button size="large" onClick=() => buy()>Buy ${count + 1}</my-button>
+ </if>
+ <for|item, index| of=items>
+ <span>${formatPrice(item.price)}</span>
+ </for>
+ <generic-list<string> a=1/>
+ <${dynamicTag} title="dynamic">body</>
+ <@footer>slot content</@footer>
+</div>
+
+<div
+ class="multiline"
+ data-x=1
+>text with &amp; entity and \${an escaped placeholder}</div>
+
+<textarea rows=3>literal <b>text</b> with ${count}</textarea>
+<html-comment>rendered <i>comment</i> body</html-comment>
+
+<script>
+ const el = document.querySelector(".price");
+</script>
diff --git a/runtime/syntax/testdir/input/marko_concise.marko b/runtime/syntax/testdir/input/marko_concise.marko
new file mode 100644
index 000000000..d8994cb46
--- /dev/null
+++ b/runtime/syntax/testdir/input/marko_concise.marko
@@ -0,0 +1,43 @@
+// concise mode: custom tags, builtins, statements, scriptlets, text blocks
+import { fetchItems } from "./api"
+
+$ const doubled = input.count * 2;
+
+let/basket=[]
+let<number>/typed=0
+
+container fluid
+ section-heading as="h2" -- Everything partnerships need
+ bento cols=4
+ bento-cell [
+ featured
+ colSpan=2
+ title="One platform"
+ ]
+ p -- Onboarding and enablement, unified.
+
+if=input.ready
+ for|item, i| of=fetchItems()
+ log "item loaded"
+ -- Price: ${item.price}
+ while(i<10)
+ -- counting
+try
+ await(promise)
+else
+ return=null
+@then|result|
+ -- resolved
+
+style.scss {
+ .basket { color: red }
+}
+
+--
+A delimited block is html mode, so bare words are prose.
+<b>bold ${doubled}</b>
+--
+
+<footer>
+if you read this inside an element body, it stays plain prose
+</footer>
diff --git a/src/testdir/test_filetype.vim b/src/testdir/test_filetype.vim
index e98f308af..9eff14029 100644
--- a/src/testdir/test_filetype.vim
+++ b/src/testdir/test_filetype.vim
@@ -505,6 +505,7 @@ def s:GetFilenameChecks(): dict<list<string>>
manconf: ['/etc/man.conf', 'man.config', 'any/etc/man.conf'],
maple: ['file.mv', 'file.mpl', 'file.mws'],
markdown: ['file.markdown', 'file.mdown', 'file.mkd', 'file.mkdn', 'file.mdwn', 'file.md'],
+ marko: ['file.marko'],
masm: ['file.masm'],
mason: ['file.mason', 'file.mhtml'],
master: ['file.mas', 'file.master'],
diff --git a/src/version.c b/src/version.c
index 915e1c8e2..167dd4938 100644
--- a/src/version.c
+++ b/src/version.c
@@ -758,6 +758,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 870,
/**/
869,
/**/
Reply all
Reply to author
Forward
0 new messages