fix(syntax): highlight Vue setup scripts and component templates - #4250
chris-paganon wants to merge 11 commits into
Conversation
Better recognize script setup and try typescript rule when relevant. More flexible attributes handling to accomodate any component.
Andriamanitra
left a comment
There was a problem hiding this comment.
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).
|
|
||
| - default: | ||
| start: "<script>" | ||
| start: "<script.*>$" |
There was a problem hiding this comment.
.* 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!).
There was a problem hiding this comment.
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.
| start: "<script>" | ||
| start: "<script.*>$" | ||
| end: "</script>" | ||
| limit-group: symbol.tag |
There was a problem hiding this comment.
Not sure why you got rid of this? Now the script tag is not highlighted.
There was a problem hiding this comment.
That is a bug. I've opened an issue for it: #4252
| start: "<style.*>$" | ||
| end: "</style>" | ||
| rules: | ||
| - include: "typescript" | ||
| - include: "css" |
There was a problem hiding this comment.
Same issues as above with the <script> tags.
There was a problem hiding this comment.
I made it <style[^>]*> since style tags have simpler requirements that script tags
|
|
||
| # A region allows attributes on later lines and arbitrary component names. | ||
| - symbol.tag: | ||
| start: "</?[A-Za-z][A-Za-z0-9_.:-]*" |
There was a problem hiding this comment.
What's up with the /??
There was a problem hiding this comment.
The /? let's the rule match both opening and closing tags. So <MyComponent class="something"> and </MyComponent>.
There was a problem hiding this comment.
I think it would make sense to have a separate rule for closing tags.
| - constant.string: | ||
| start: '"' | ||
| end: '"' | ||
| rules: [] | ||
| - constant.string: | ||
| start: "'" | ||
| end: "'" | ||
| rules: [] |
| start: "<template.*?>" | ||
| end: "</template.*?>" | ||
| limit-group: symbol.tag | ||
| start: "<script.* lang=['\"]ts['\"].*>$" |
There was a problem hiding this comment.
lang=(ts|'ts'|\"ts\") would be more accurate.
There was a problem hiding this comment.
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.
|
I also added special characters again for vue directives like :class, v-bind, @click, v-if, v-for and custom directives (
|
| start: "</?[A-Za-z][A-Za-z0-9_.:-]*" | ||
| end: "/?>" | ||
| # don't end on a `>` inside quotes like <div data-content="a<b>c"> | ||
| skip: "\"([^\"\\\\]|\\\\.)*\"|'([^'\\\\]|\\\\.)*'" |
There was a problem hiding this comment.
The \"[^\"]*\" part makes sense to me but what are all the \\\\ and \\\\. about?
There was a problem hiding this comment.
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.
| skip: "\"([^\"\\\\]|\\\\.)*\"|'([^'\\\\]|\\\\.)*'" | ||
| rules: | ||
| - identifier: "[A-Za-z_][A-Za-z0-9_.:@#\\[\\]-]*" | ||
| - special: "\\bv-[A-Za-z][A-Za-z0-9-]*\\b" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fair enough, I think that's a good idea. I added some comments in the code iteself to explain the new version better
Previous regex matched vue directives with dynamic content like v-on[eventName]. This added more complexity thatn necessary
|
Thanks @Andriamanitra for all the feedback, I really like where this is going:
I removed some special handling for dynamic events like |
| skip: >- | ||
| "[^"]*"|'[^']*' |
There was a problem hiding this comment.
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 ' 😅
There was a problem hiding this comment.
It's pretty neat to do multiline strings in yaml as well!
| - constant.string: '"([^"\\]|\\.)*"' | ||
| - constant.string: "'([^'\\\\]|\\\\.)*'" |
There was a problem hiding this comment.
Does Vue actually support using backslashes to escape things inside string attributes? I know html does not so that would be surprising to me.
There was a problem hiding this comment.
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.





<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:
lang="ts". Both accept arbitrary attributes in either order. Style tags also accept arbitrary attributes, such asscopedandlang="scss".>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.{{ ... }}javascript have separate rules. Tags are matched at the top level, so an inner</template>does not end highlighting (vue can nest<template>tags).limit-groupto 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:


After:
html and custom components
Before:

After:

Nested template tag
Before:

After:
