From 1eba3a75da32e039585344fe7daa6e29748f005b Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 29 Jul 2026 09:14:48 +0800 Subject: [PATCH] vim-patch:9.2.0870: filetype: marko files are not recognized (#41038) 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: vim/vim#20863 https://github.com/vim/vim/commit/95da62a910809787cc8fdf26b131721359812d8f Co-authored-by: Brian Carbone --- runtime/lua/vim/filetype.lua | 1 + runtime/syntax/marko.vim | 382 +++++++++++++++++++++++++++++ test/old/testdir/test_filetype.vim | 1 + 3 files changed, 384 insertions(+) create mode 100644 runtime/syntax/marko.vim diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 64b597943c..dfb9c931f5 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -820,6 +820,7 @@ local extension = { mkd = detect.markdown, markdown = detect.markdown, mdown = detect.markdown, + marko = 'marko', masm = 'masm', mhtml = 'mason', mason = 'mason', diff --git a/runtime/syntax/marko.vim b/runtime/syntax/marko.vim new file mode 100644 index 0000000000..be589534c3 --- /dev/null +++ b/runtime/syntax/marko.vim @@ -0,0 +1,382 @@ +" 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 "\%(