Skip to content

fix(syntax): highlight Vue setup scripts and component templates - #4250

Open
chris-paganon wants to merge 11 commits into
micro-editor:masterfrom
chris-paganon:fix/vue-syntax
Open

chris-paganon wants to merge 11 commits into
micro-editor:masterfrom
chris-paganon:fix/vue-syntax

Conversation

@chris-paganon

Copy link
Copy Markdown

<script setup> and <script lang="ts"> were not supported by the syntax highlighter.

Nested <template> tags as well as any custom components would also break syntax highlighting in the "html".

I made the following changes to vue syntax highlighting:

  • Check the TypeScript script rule before the general JavaScript rule if the script tags contains lang="ts". Both accept arbitrary attributes in either order. Style tags also accept arbitrary attributes, such as scoped and lang="scss".
  • Match opening script and style tags through the final > on their line. This handles a > in a quoted generic type, for example <script setup generic="T extends Map<string, number>" lang="ts">. This not fully robust but it keeps the regex a lot simpler.
  • Replace the template tag allowlist with a general tag region for HTML tags and Vue component names, including multiline attributes. HTML comments, entities, and {{ ... }} javascript have separate rules. Tags are matched at the top level, so an inner </template> does not end highlighting (vue can nest <template> tags).
  • Remove limit-group to allow the included Typescript, JavaScript and CSS rules to color their contents.

This addresses #2381 and follows the attribute-order and arbitrary-attribute feedback on #2918.

Tested with several Vue components, options API and compositions API with Typescript and Javascript. All tests pass.

Here are some before and after screenshots with TS and compositions API.

script setup

Before:
image
After:
image

html and custom components

Before:
image

After:
image

Nested template tag

Before:
image

After:
image

Better recognize script setup and try typescript rule when relevant.

More flexible attributes handling to accomodate any component.

@Andriamanitra Andriamanitra left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes some issues but causes some new ones. Vue-specific attributes (v-if etc.) don't get special highlighting any more compared to attributes with no special meaning. I'm not sure if this is an improvement on the whole (feedback from actual Vue users would be appreciated).

Comment thread runtime/syntax/vue.yaml

- default:
start: "<script>"
start: "<script.*>$"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.* is greedy so if you have eg. <script>console.log("hello")</script> this will match the whole line instead of just the start tag. I think the $ should also not be there?

[^>]* would work better (it would still fail if you have something like <script src="src/>.js"> but that's hopefully rare!).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .*>$ is meant to work with anything like this: <script setup generic="T extends Record<string, unknown>">. Note the extra > from the generic. Matching the last > on the line means we don't need to worry about anything that goes inside the script tag, even >.

<script>console.log("hello")</script> should be very uncommon. The script tag in vue is always supposed to be on a single line. It's technically valid VueJS to write <script>console.log("hello")</script> but I have never seen this in the wild. Actually the default vue linter rules gives me A line break is required before '</script>'. eslint[vue/block-tag-newline](https://eslint.vuejs.org/rules/block-tag-newline.html) if I try.

I initially had a much more complex regex that would cover both cases but it seemed like this very simple one would actually cover almost all cases and remain simple.

Here is the original one Astra came up with which I imagine is a lot more robust but completely unreadable: start: "<script\\s+(?:(?:[^>\"']|\"[^\"]*\"|'[^']*')*\\s)?lang\\s*=\\s*(?:\"ts\"|'ts'|ts\\b)(?:[^>\"']|\"[^\"]*\"|'[^']*')*>"

When asked to simplify I got this start: "<script(?:[ \t].*)?[ \t]lang[ \t]*=[ \t]*['\"]ts['\"].*>[ \t]*$".

The one I landed on is my idea to avoid these wild regex and still work in all VueJS cases I've seen.

Comment thread runtime/syntax/vue.yaml
start: "<script>"
start: "<script.*>$"
end: "</script>"
limit-group: symbol.tag

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why you got rid of this? Now the script tag is not highlighted.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With limit-group, the script tag is highlighted but the typescript/javascript rules don't apply anymore.

image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a bug. I've opened an issue for it: #4252

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the current state of #3127 which is supposed to fix this bug and the complexity of getting that PR merged. Is it ok if I leave these limit-group out for now? I'll happilly add it back as soon as #3127 is merged!

Comment thread runtime/syntax/vue.yaml Outdated
Comment on lines +20 to +23
start: "<style.*>$"
end: "</style>"
rules:
- include: "typescript"
- include: "css"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issues as above with the <script> tags.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it <style[^>]*> since style tags have simpler requirements that script tags

Comment thread runtime/syntax/vue.yaml Outdated

# A region allows attributes on later lines and arbitrary component names.
- symbol.tag:
start: "</?[A-Za-z][A-Za-z0-9_.:-]*"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up with the /??

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /? let's the rule match both opening and closing tags. So <MyComponent class="something"> and </MyComponent>.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to have a separate rule for closing tags.

Comment thread runtime/syntax/vue.yaml Outdated
Comment on lines +44 to +51
- constant.string:
start: '"'
end: '"'
rules: []
- constant.string:
start: "'"
end: "'"
rules: []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regions inside regions don't really work properly so I would prefer to avoid them in built-in syntaxes. You get weird stuff like:

screenshot of broken syntax highlighting

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced it with a skip and more regex. I really like how this turned out, but let me know if you'd rather I remove the constant.string handling completly instead.

With constant.string and skip:
image

Without:
image

Comment thread runtime/syntax/vue.yaml Outdated
start: "<template.*?>"
end: "</template.*?>"
limit-group: symbol.tag
start: "<script.* lang=['\"]ts['\"].*>$"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lang=(ts|'ts'|\"ts\") would be more accurate.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I didn't even know lang=ts was valid. Fixed it, thank you!

Previously only lang="ts" and lang='ts' were handled
style tags don't have special attributes like generics so `<style[^>]*>` works here.
Previous changes didn't match v-if, v-for, :class, etc.
The identifier's regex had some overlap with special vue directive charcaters (`:@#`).
- Skip closing `>` inside quotes to keep this `<div data-content="a<b>c">` working.
- Use more robust regex for constant.string.
@chris-paganon

Copy link
Copy Markdown
Author

I also added special characters again for vue directives like :class, v-bind, @click, v-if, v-for and custom directives (v-anything)

image

Comment thread runtime/syntax/vue.yaml Outdated
start: "</?[A-Za-z][A-Za-z0-9_.:-]*"
end: "/?>"
# don't end on a `>` inside quotes like <div data-content="a<b>c">
skip: "\"([^\"\\\\]|\\\\.)*\"|'([^'\\\\]|\\\\.)*'"

@Andriamanitra Andriamanitra Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The \"[^\"]*\" part makes sense to me but what are all the \\\\ and \\\\. about?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was to escape the yaml parser's handling of \ and if the html/vuejs also had \. Sorry I suck at regex but I got a simpler version working now.

Comment thread runtime/syntax/vue.yaml Outdated
skip: "\"([^\"\\\\]|\\\\.)*\"|'([^'\\\\]|\\\\.)*'"
rules:
- identifier: "[A-Za-z_][A-Za-z0-9_.:@#\\[\\]-]*"
- special: "\\bv-[A-Za-z][A-Za-z0-9-]*\\b"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference would be to color only the built-in ones instead of anything that starts with v-, because that way you'll know when you got the syntax right (eg. v-else-if instead of v-elseif). I'm open to being convinced otherwise though if you think having nice-looking custom directives is more important.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, I think that's a good idea. I added some comments in the code iteself to explain the new version better

@chris-paganon

Copy link
Copy Markdown
Author

Thanks @Andriamanitra for all the feedback, I really like where this is going:

image

I removed some special handling for dynamic events like @[eventName] which is not all that common. Other than that it's looking very nice now!

Comment thread runtime/syntax/vue.yaml
Comment on lines +45 to +46
skip: >-
"[^"]*"|'[^']*'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't say I'm a fan of this yaml trickery, it took me a moment to understand wtf is going on but I guess the >- is used to avoid escaping either " or ' 😅

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's pretty neat to do multiline strings in yaml as well!

Comment thread runtime/syntax/vue.yaml Outdated
Comment on lines +56 to +57
- constant.string: '"([^"\\]|\\.)*"'
- constant.string: "'([^'\\\\]|\\\\.)*'"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Vue actually support using backslashes to escape things inside string attributes? I know html does not so that would be surprising to me.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can write javascript inside @click handlers where you can put backslashes. But yeah, the current handling doesn't parse that JS anyways so I simplified it for now. I could add support for inline JS/TS in a future PR if you are interested.

For stuff like:

@click="(event) => {
  console.log(event);
}"

We don't handle JS in event handlers anyways, so backslash doesn't need to be supported.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants