From 21bab8751f792ef87491dd9302f3a7cfb04222e4 Mon Sep 17 00:00:00 2001 From: deathaxe Date: Thu, 12 Dec 2024 19:30:37 +0100 Subject: [PATCH 1/3] Initial support for Angular expression syntax This commit... 1. adds syntax definitions for Angular expression syntax, which is a subset of JavaScript. see: https://angular.dev/guide/templates/expression-syntax Required rules are directly implemented in favor of re-using/including JavaScript to reduce dependencies and keep syntax lean (from parser's point of view). 2. adds syntax based folding rules to be able to fold control flow blocks regardless used indentation style. 3. adds symbol list definition to index `@let` variables. --- Fold.tmPreferences | 51 ++ NgxHTML.sublime-syntax | 565 +++++++++++++++-- Symbol List - Variables.tmPreferences | 14 + tests/syntax_test_control_flow.component.html | 139 ---- tests/syntax_test_scopes.component.html | 598 ++++++++++++++++++ 5 files changed, 1174 insertions(+), 193 deletions(-) create mode 100644 Fold.tmPreferences create mode 100644 Symbol List - Variables.tmPreferences delete mode 100644 tests/syntax_test_control_flow.component.html create mode 100644 tests/syntax_test_scopes.component.html diff --git a/Fold.tmPreferences b/Fold.tmPreferences new file mode 100644 index 0000000..206bb9a --- /dev/null +++ b/Fold.tmPreferences @@ -0,0 +1,51 @@ + + + + scope + text.html.ngx + settings + + foldScopes + + + begin + punctuation.definition.comment.begin + end + punctuation.definition.comment.end + excludeTrailingNewlines + + + + begin + punctuation.section.block.begin + end + punctuation.section.block.end + + + begin + punctuation.section.embedded.begin + end + punctuation.section.embedded.end + + + begin + punctuation.section.group.begin + end + punctuation.section.group.end + + + begin + punctuation.section.mapping.begin + end + punctuation.section.mapping.end + + + begin + punctuation.section.sequence.begin + end + punctuation.section.sequence.end + + + + + diff --git a/NgxHTML.sublime-syntax b/NgxHTML.sublime-syntax index 93099fb..7d90116 100644 --- a/NgxHTML.sublime-syntax +++ b/NgxHTML.sublime-syntax @@ -1,30 +1,39 @@ %YAML 1.2 --- name: Ngx HTML -file_extensions: - - component.html scope: text.html.ngx version: 2 extends: Packages/HTML/HTML.sublime-syntax +file_extensions: + - component.html + variables: - angular_blocks: (?xi:for | if | else | else if | switch | case | empty | placeholder | defer | default | error | loading | let)\b + + bin_digit: '[01_]' + oct_digit: '[0-7_]' + dec_digit: '[0-9_]' + hex_digit: '[\h_]' + dec_integer: (?:0|[1-9]{{dec_digit}}*) + dec_exponent: '[Ee](?:[-+]|(?![-+])){{dec_digit}}*' + + # JavaScript identifier + ident_name: (?:{{ident_start}}{{ident_part}}*{{ident_break}}) + ident_break: (?!{{ident_part}}) + ident_escape: (?:\\u(?:\h{4}|\{\h+\})) + ident_start: (?:[_$\p{L}\p{Nl}]|{{ident_escape}}) + ident_part: (?:[_$\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]|{{ident_escape}}) contexts: - main: - - meta_prepend: true - - include: interpolation - - include: ng-control-flow - interpolation: - - match: '{{' - scope: meta.interpolation.ngx.html punctuation.section.interpolation.begin.ngx.html - embed: scope:source.js - embed_scope: meta.interpolation.ngx.html source.js.embedded.ngx.html - escape: '}}' - escape_captures: - 0: meta.interpolation.ngx.html punctuation.section.interpolation.end.ngx.html +###[ HTML CUSTOMIZATIONS ]##################################################### + + tag: + - meta_prepend: true + - include: ng-declarations + - include: ng-statements + - include: ng-interpolations tag-attributes: - meta_prepend: true @@ -34,8 +43,25 @@ contexts: - include: tag-ng-on-attribute - include: tag-ng-bindon-attribute +###[ ANGULAR EXPRESSIONS ]##################################################### + + ng-expressions: + # https://angular.dev/guide/templates/expression-syntax + - include: ng-arrays + - include: ng-groups + - include: ng-objects + - include: ng-function-calls + - include: ng-filters + - include: ng-operators + - include: ng-constants + - include: ng-numbers + - include: ng-strings + - include: ng-variables + +###[ ANGULAR TAG ATTRIBUTES ]################################################## + tag-ng-template-attribute: - - match: '(\*)([a-zA-Z]\w*)' + - match: (\*)([a-zA-Z]\w*) scope: meta.attribute-with-value.template.html captures: 1: punctuation.definition.template.html @@ -45,7 +71,7 @@ contexts: - tag-event-attribute-assignment tag-ng-reference-attribute: - - match: '(\#)([a-zA-Z]\w*)' + - match: (\#)([a-zA-Z]\w*) scope: meta.attribute.reference.html captures: 1: punctuation.definition.reference.html @@ -55,7 +81,7 @@ contexts: - tag-event-attribute-assignment tag-ng-bind-attribute: - - match: '(\[)([a-zA-Z@][!\w.-]*)(\])' + - match: (\[)([a-zA-Z@][!\w.-]*)(\]) scope: meta.attribute-with-value.bind.html captures: 1: punctuation.section.bind.begin.html @@ -66,7 +92,7 @@ contexts: - tag-event-attribute-assignment tag-ng-on-attribute: - - match: '(\()([a-zA-Z@][\w:.]*)(\))' + - match: (\()([a-zA-Z@][\w:.]*)(\)) scope: meta.attribute-with-value.on.html captures: 1: punctuation.section.on.begin.html @@ -77,7 +103,7 @@ contexts: - tag-event-attribute-assignment tag-ng-bindon-attribute: - - match: '(\[\()([a-zA-Z][\w.]*)(\)\])\s*' + - match: (\[\()([a-zA-Z][\w.]*)(\)\]) scope: meta.attribute-with-value.bindon.html captures: 1: punctuation.section.bindon.begin.html @@ -87,40 +113,471 @@ contexts: - tag-event-attribute-meta - tag-event-attribute-assignment +###[ ANGULAR DECLARATIONS ]#################################################### + + ng-declarations: + - match: (@)let{{ident_break}} + scope: meta.let.ngx keyword.declaration.variable.ngx + captures: + 1: punctuation.definition.keyword.ngx + push: + - ng-let-assignment + - ng-let-variable + + ng-let-variable: + - match: '{{ident_name}}' + scope: variable.other.readwrite.ngx + pop: 1 + - include: else-pop - ng-control-flow: - - match: '(@){{angular_blocks}}' - scope: keyword.control.control-flow.html + ng-let-assignment: + - meta_content_scope: meta.let.identifier.ngx + - match: = + scope: meta.let.ngx keyword.operator.assignment.ngx + set: ng-let-value + - include: else-pop + + ng-let-value: + - meta_include_prototype: false + - meta_content_scope: meta.let.value.ngx + - match: ';' + scope: punctuation.terminator.expression.ngx + pop: 1 + - include: ng-expressions + +###[ ANGULAR STATEMENTS ]###################################################### + + ng-statements: + # conditionals + # https://angular.dev/guide/templates/control-flow#conditionally-display-content-with-if-else-if-and-else + - match: (@)if{{ident_break}} + scope: keyword.control.conditional.if.ngx + captures: + 1: punctuation.definition.keyword.ngx + push: + - ng-block + - ng-conditional-group + - match: (@)else\s+if{{ident_break}} + scope: keyword.control.conditional.elseif.ngx + captures: + 1: punctuation.definition.keyword.ngx + push: + - ng-block + - ng-conditional-group + - match: (@)else{{ident_break}} + scope: keyword.control.conditional.else.ngx + captures: + 1: punctuation.definition.keyword.ngx + push: ng-block + # https://angular.dev/guide/templates/control-flow#conditionally-display-content-with-the-switch-block + - match: (@)case{{ident_break}} + scope: keyword.control.conditional.case.ngx + captures: + 1: punctuation.definition.keyword.ngx + push: + - ng-block + - ng-conditional-group + - match: (@)default{{ident_break}} + scope: keyword.control.conditional.case.ngx + captures: + 1: punctuation.definition.keyword.ngx + push: ng-block + - match: (@)switch{{ident_break}} + scope: keyword.control.conditional.switch.ngx + captures: + 1: punctuation.definition.keyword.ngx + push: + - ng-block + - ng-conditional-group + # https://angular.dev/guide/templates/control-flow#providing-a-fallback-for-for-blocks-with-the-empty-block + - match: (@)for{{ident_break}} + scope: keyword.control.loop.for.ngx + captures: + 1: punctuation.definition.keyword.ngx + push: + - ng-block + - ng-for-group + - match: (@)empty{{ident_break}} + scope: keyword.control.loop.else.ngx captures: - 1: punctuation.definition.keyword.html + 1: punctuation.definition.keyword.ngx + push: ng-block + # https://angular.dev/guide/templates/defer + - match: (@)(?:defer|error|loading|placeholder){{ident_break}} + scope: keyword.control.flow.ngx + captures: + 1: punctuation.definition.keyword.ngx push: - - match: '=' - scope: punctuation.section.let.begin.ngx.html - push: - - meta_scope: meta.let.ngx.html - - match: ';' - scope: punctuation.section.let.end.ngx.html - pop: 2 - - match: \{ - scope: punctuation.section.block.begin.html - push: - - meta_scope: meta.block.html - - include: main - - match: \} - scope: punctuation.section.block.end.html - pop: 2 - - match: \( - scope: punctuation.section.group.begin.html - push: - - meta_scope: meta.group.html - - match: \b(async)\b - scope: keyword.control.html - - match: \b(track|of|prefetch on|on|prefetch when|when|as)\b - scope: keyword.operator.word.html - - match: \b(minimum|after)\b(\?) - captures: - 1: keyword.operator.word.html - 2: keyword.control.question.html - - match: \) - scope: punctuation.section.group.end.html - pop: 1 + - ng-block + - ng-defer-group + + ng-block: + - meta_include_prototype: false + - match: \{ + scope: punctuation.section.block.begin.ngx + set: ng-block-body + - include: else-pop + + ng-block-body: + - meta_scope: meta.block.ngx + - match: \} + scope: punctuation.section.block.end.ngx + pop: 1 + - include: main + + ng-conditional-group: + - meta_include_prototype: false + - match: \( + scope: punctuation.section.group.begin.ngx + set: ng-conditional-group-body + - include: else-pop + + ng-conditional-group-body: + - meta_scope: meta.group.ngx + - match: \) + scope: punctuation.section.group.end.ngx + pop: 1 + - match: as{{ident_break}} + scope: keyword.operator.assignment.as.ngx + - include: ng-expressions + + ng-defer-group: + - meta_include_prototype: false + - match: \( + scope: punctuation.section.group.begin.ngx + set: ng-defer-group-body + - include: else-pop + + ng-defer-group-body: + - meta_scope: meta.group.ngx + - match: \) + scope: punctuation.section.group.end.ngx + pop: 1 + - match: (?:idle|viewport|interaction|hover|immediatetimer){{ident_break}} + scope: constant.language.event.ngx + - match: (?:prefetch on|on|prefetch when|when){{ident_break}} + scope: keyword.control.flow.ngx + - match: (minimum|after)(\?) + captures: + 1: keyword.operator.word.ngx + 2: keyword.control.question.ngx + - include: ng-expressions + + ng-for-group: + - meta_include_prototype: false + - match: \( + scope: punctuation.section.group.begin.ngx + set: ng-for-group-body + - include: else-pop + + ng-for-group-body: + - meta_scope: meta.group.ngx + - match: \) + scope: punctuation.section.group.end.ngx + pop: 1 + - match: track{{ident_break}} + scope: keyword.control.flow.ngx + - match: let{{ident_break}} + scope: keyword.declation.variable.ngx + - match: of{{ident_break}} + scope: keyword.operator.iteration.ngx + - include: ng-expressions + +###[ ANGULAR INTERPOLATIONS ]################################################## + + ng-interpolations: + - match: '{{' + scope: meta.embedded.ngx.html punctuation.section.embedded.begin.ngx.html + push: ng-interpolation-body + + ng-interpolation-body: + - meta_include_prototype: false + - meta_content_scope: meta.embedded.ngx.html source.ngx.embedded.html + - match: '}}' + scope: meta.embedded.ngx.html punctuation.section.embedded.end.ngx.html + pop: 1 + - include: ng-expressions + +###[ ANGULAR ARRAYS ]########################################################### + + ng-arrays: + - match: \[ + scope: punctuation.section.sequence.begin.ngx + push: ng-array-body + + ng-array-body: + - meta_scope: meta.sequence.array.ngx + - match: \] + scope: punctuation.section.sequence.end.ngx + pop: 1 + - include: ng-expressions + +###[ ANGULAR GROUPS ]########################################################## + + ng-groups: + - match: \( + scope: punctuation.section.group.begin.ngx + push: ng-group-body + + ng-group-body: + - meta_scope: meta.group.ngx + - match: \) + scope: punctuation.section.group.end.ngx + pop: 1 + - include: ng-expressions + +###[ ANGULAR OBJECTS ]######################################################### + + ng-objects: + - match: \{ + scope: meta.mapping.ngx punctuation.section.mapping.begin.ngx + push: ng-object-body + + ng-object-body: + - meta_content_scope: meta.mapping.ngx + - match: \} + scope: meta.mapping.ngx punctuation.section.mapping.end.ngx + pop: 1 + - match: ',' + scope: punctuation.separator.mapping.pair.ngx + - match: ':' + scope: meta.mapping.ngx punctuation.separator.mapping.key-value.ngx + set: ng-object-value + - match: (?=\S) + set: ng-object-key + + ng-object-key: + - meta_include_prototype: false + - match: '{{ident_name}}(?=\s*:)' + scope: meta.mapping.key.ngx meta.string.ngx string.unquoted.ngx + set: ng-object-body + - match: '' + set: ng-object-key-expression + + ng-object-key-expression: + - meta_content_scope: meta.mapping.key.ngx + - match: (?=[,:}]) + set: ng-object-body + - include: ng-block-pop + - include: ng-expressions + + ng-object-value: + - meta_content_scope: meta.mapping.value.ngx + - match: (?=[,:}]) + set: ng-object-body + - include: ng-block-pop + - include: ng-expressions + +###[ ANGULAR FILTERS ]######################################################### + + ng-filters: + - match: \|(?!\|) + scope: keyword.operator.assignment.pipe.ngx + push: + - ng-filter-meta + - ng-filter-function + + ng-filter-function: + - match: (({{ident_name}})\s*)(\() + captures: + 1: meta.function-call.identifier.ngx + 2: variable.function.filter.ngx + 3: meta.function-call.arguments.ngx punctuation.section.arguments.begin.ngx + set: ng-function-call-arguments + - match: '{{ident_name}}' + scope: meta.function-call.identifier.ngx variable.function.filter.ngx + pop: 1 + - include: else-pop + + ng-filter-meta: + - meta_include_prototype: false + - meta_scope: meta.filter.ngx + - include: immediately-pop + +###[ ANGULAR FUNCTION CALLS ]################################################## + + ng-function-calls: + - match: (({{ident_name}})\s*)(\() + captures: + 1: meta.function-call.identifier.ngx + 2: variable.function.ngx + 3: meta.function-call.arguments.ngx punctuation.section.arguments.begin.ngx + push: ng-function-call-arguments + + ng-function-call-arguments: + - meta_content_scope: meta.function-call.arguments.ngx + - match: \) + scope: meta.function-call.arguments.ngx punctuation.section.arguments.end.ngx + pop: 1 + - match: ',' + scope: punctuation.separator.arguments.ngx + - include: ng-expressions + +###[ ANGULAR OPERATORS ]####################################################### + + ng-operators: + # https://angular.dev/guide/templates/expression-syntax#what-operators-are-supported + - match: ',' + scope: punctuation.separator.sequence.ngx + - match: ';' + scope: punctuation.terminator.expression.ngx + - match: '[!=]==?|[<>]=?' + scope: keyword.operator.comparison.ngx + - match: '[-+*/%]' + scope: keyword.operator.arithmetic.ngx + - match: '&&|\|\||!' + scope: keyword.operator.logical.ngx + - match: '=' + scope: keyword.operator.assignment.ngx + - match: \?\? + scope: keyword.operator.null-coalescing.ngx + - match: '\?' + scope: keyword.operator.ternary.ngx + push: ng-ternary-expression + + ng-ternary-expression: + - match: ':' + scope: keyword.operator.ternary.ngx + pop: 1 + - include: ng-block-pop + - include: ng-expressions + +###[ ANGULAR LITERALS ]######################################################## + + ng-constants: + - match: true{{ident_break}} + scope: constant.language.boolean.true.ngx + - match: false{{ident_break}} + scope: constant.language.boolean.false.ngx + - match: null{{ident_break}} + scope: constant.language.null.ngx + - match: undefined{{ident_break}} + scope: constant.language.undefined.ngx + + ng-numbers: + # floats + - match: |- + (?x: + # 1., 1.1, 1.1e1, 1.1e-1, 1.e1, 1.e-1 | 1e1, 1e-1 + {{dec_integer}} (?: (\.) {{dec_digit}}* (?:{{dec_exponent}})? | {{dec_exponent}} ) + # .1, .1e1, .1e-1 + | (\.) {{dec_digit}}+ (?:{{dec_exponent}})? + ){{ident_break}} + scope: meta.number.float.decimal.ngx constant.numeric.value.ngx + captures: + 1: punctuation.separator.decimal.ngx + 2: punctuation.separator.decimal.ngx + # integers + - match: (0)({{dec_digit}}+){{ident_break}} + scope: meta.number.integer.octal.ngx + captures: + 1: constant.numeric.base.ngx invalid.deprecated.numeric.octal.ngx + 2: constant.numeric.value.ngx invalid.deprecated.numeric.octal.ngx + - match: (0[Xx])({{hex_digit}}*)(n)?{{ident_break}} + scope: meta.number.integer.hexadecimal.ngx + captures: + 1: constant.numeric.base.ngx + 2: constant.numeric.value.ngx + 3: constant.numeric.suffix.ngx + - match: (0[Oo])({{oct_digit}}*)(n)?{{ident_break}} + scope: meta.number.integer.octal.ngx + captures: + 1: constant.numeric.base.ngx + 2: constant.numeric.value.ngx + 3: constant.numeric.suffix.ngx + - match: (0[Bb])({{bin_digit}}*)(n)?{{ident_break}} + scope: meta.number.integer.binary.ngx + captures: + 1: constant.numeric.base.ngx + 2: constant.numeric.value.ngx + 3: constant.numeric.suffix.ngx + - match: ({{dec_integer}})(n|(?!\.)){{ident_break}} + scope: meta.number.integer.decimal.ngx + captures: + 1: constant.numeric.value.ngx + 2: constant.numeric.suffix.ngx + + ng-strings: + - match: \" + scope: punctuation.definition.string.begin.ngx + push: ng-double-quoted-string-body + - match: \' + scope: punctuation.definition.string.begin.ngx + push: ng-single-quoted-string-body + + ng-double-quoted-string-body: + - meta_include_prototype: false + - meta_scope: meta.string.ngx string.quoted.double.ngx + - match: \" + scope: punctuation.definition.string.end.ngx + pop: 1 + - match: \n + scope: invalid.illegal.newline.ngx + pop: 1 + - include: ng-string-content + + ng-single-quoted-string-body: + - meta_include_prototype: false + - meta_scope: meta.string.ngx string.quoted.single.ngx + - match: \' + scope: punctuation.definition.string.end.ngx + pop: 1 + - match: \n + scope: invalid.illegal.newline.ngx + pop: 1 + - include: ng-string-content + + ng-string-content: + - match: \\x\h{2} + scope: constant.character.escape.hex.ngx + - match: \\u\h{4} + scope: constant.character.escape.unicode.16bit.ngx + - match: \\. + scope: constant.character.escape.ngx + +###[ ANGULAR VARIABLES ]####################################################### + + ng-variables: + - match: ({{ident_name}})\s*(\??\.) + captures: + 1: variable.other.object.ngx + 2: punctuation.accessor.ngx + push: ng-members + - match: '{{ident_name}}' + scope: variable.other.readwrite.ngx + push: ng-subscription + + ng-members: + - match: ({{ident_name}})\s*(\??\.) + captures: + 1: variable.other.object.ngx + 2: punctuation.accessor.ngx + - match: (({{ident_name}})\s*)(\() + captures: + 1: meta.function-call.identifier.ngx + 2: variable.function.ngx + 3: meta.function-call.arguments.ngx punctuation.section.arguments.begin.ngx + set: ng-function-call-arguments + - match: '{{ident_name}}' + scope: variable.other.member.ngx + set: ng-subscription + - include: else-pop + + ng-subscription: + - match: \[ + scope: punctuation.section.subscription.begin.ngx + push: ng-subscription-body + - include: else-pop + + ng-subscription-body: + - meta_scope: meta.subscription.ngx + - match: \] + scope: punctuation.section.subscription.end.ngx + pop: 1 + - include: ng-expressions + +###[ ANGULAR PROTOTYPES ]###################################################### + + ng-block-pop: + - match: (?=[)}]) + pop: 1 diff --git a/Symbol List - Variables.tmPreferences b/Symbol List - Variables.tmPreferences new file mode 100644 index 0000000..c028302 --- /dev/null +++ b/Symbol List - Variables.tmPreferences @@ -0,0 +1,14 @@ + + + + scope + meta.let.identifier.ngx variable + settings + + showInSymbolList + 1 + showInIndexedSymbolList + 1 + + + diff --git a/tests/syntax_test_control_flow.component.html b/tests/syntax_test_control_flow.component.html deleted file mode 100644 index 3b452bc..0000000 --- a/tests/syntax_test_control_flow.component.html +++ /dev/null @@ -1,139 +0,0 @@ - - -@if (loggedIn) { - - - - - The user is logged in -} @else { - - - - - - - - The user is not logged in -} - - -@if (a > b) { - {{ a }} is greater than {{ b }} - - - - - - - -} - -@if (a > b) { - {{ a }} is greater than {{ b }} -} @else if (b > a) { - {{ a }} is less than {{ b }} -} @else { - {{ a }} is equal to {{ b }} -} - -@switch (accessLevel) { - @case ('admin') { - - - - - - } - @case ('moderator') { - - } - @default { - - } -} - -@for (user of users; track user.id) { - {{ user.name }} -} @empty { - Empty list of users -} - -@defer { - -} - -@defer (on viewport) { - -} @placeholder { - - -} - -@defer (on viewport) { - -} @loading { - Loading… -} @error { - Loading failed :( -} @placeholder { - -} - -@defer (on viewport; prefetch on idle) { - -} - -@for (item of items; track item.id; let idx = $index, e = $even) { - Item #{{ idx }}: {{ item.name }} -} - -@for (item of items) { - {{ item.title }} -} - -@for (item of items) { - {{ item.title }} -} @empty { -

No Items

-} - -@for (item of items; track item.name) { -
  • {{ item.name }}
  • -} @empty { -
  • There are no items.
  • -} - -@if (users$ | async; as users) { - {{ users.length }} -} - -@for (item of items; track item.id) { - {{ item.name }} -} - -@switch (condition) { - @case (caseA) { - Case A. - } - @case (caseB) { - Case B. - } - @default { - Default case. - } -} - -@defer (on ; when ; prefetch on ; prefetch when ) { - - -} @placeholder (minimum? ) { - -

    Placeholder

    -} @loading (minimum? ; after? ) { - - loading image -} @error { - -

    An loading error occured

    -} diff --git a/tests/syntax_test_scopes.component.html b/tests/syntax_test_scopes.component.html new file mode 100644 index 0000000..d10da1b --- /dev/null +++ b/tests/syntax_test_scopes.component.html @@ -0,0 +1,598 @@ + + + + +@let name = user.name; + + + + + + + + + +@let greeting = 'Hello, ' + name; + + + + + + + + + + + +@let data = data$ | async; + + + + + + + + + + + + + +@if (a > b) { + + + + + + + + + + + {{ a }} is greater than {{ b }} + + + + + + + + + + + +} @else if (b > a()) { + + + + + + + + + + + + {{ a() }} is less than {{ b.c() }} + + + + + + + + + + + + + + + + +} @else { + + + + + {{ a }} is equal to {{ b }} + + + + + + + + + + +} + + +@if (user.profile.settings.startDate; as startDate) { + + + + + + + + + + + + + + + {{ startDate }} + + + + + +} + + + + + +@switch (accessLevel) { + + + + + + + @case ('admin') { + + + + + + + + + + + + + + + + } + + + + @case { + + + + + + + + + + + + } + + + @default { + + + + + + + } + + +} + + + + + +@for (user of users; track user.id) { + + + + + + + + + + + + + {{ user.name }} +} @empty { + + + + + Empty list of users +} + + +@for (item of items; track item.id; let idx = $index, e = $even) { + + + + + + + + + + + + + + + + + + + + + + Item #{{ idx }}: {{ item.name }} + + + + + + + + + + + + + +} + + +@if (users$ | async; as users) { + + + + + + + + + + + +

    {{ users.length }}

    + + + + + + + + + + + + + + + + +} + + + + + + @defer { } + + + + + + + + + + + @defer (on viewport) { + + + + + + + + + + } @loading { + + + + + Loading… + } @error { + + + + + Loading failed :( + } @placeholder { + + + + + + } + + + +@defer (on viewport; when $var prefetch on idle; prefetch when true) { + + + + + + + + + + + + + + + +} + + + + + + + {{ 'Hello', "World" }} + + + + + + + + + {{ "\xAF \u2AFF \n \"" }} + + + + + + + + + + + + + {{ true, false, null, undefined }} + + + + + + + + + + + {{ Number, Boolean, NaN, Infinity, parseInt }} + + + + {{ ['Onion', 'Cheese', 'Garlic'] }} + + + + + + + + + + + + + + + + + {{ { name: 'Alice', 'object': { array + "name": [0, 2, 3] } } }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ - + * / % === !== == != = < <= >= > && || ! ?? }} + + + + + + + + + + + + + + + + + + + + + {{ foo ? bar : baz }} + + + + + + + + + + + {{ foo = null ?? 'default' }} + + + + + + + + + + + + + {{ person['name'][0] = "Mirabel" }} + + + + + + + + + + + + + + + + + + + {{ obj?.member }} + + + + + + + + {{ obj.method() }} + + + + + + + + + + + {{ func(arg, "value") }} + + + + + + + + + + + + + + {{ var | filter | annimation ("forward") }} + + + + + + + + + + + + + + + + + From a0cbcff4ddf095dbff5aa4fabdd932be84c68a7f Mon Sep 17 00:00:00 2001 From: deathaxe Date: Mon, 16 Dec 2024 12:11:28 +0100 Subject: [PATCH 2/3] Fix qualified member access This commit adds contexts for JavaScript specific of qualified variables. It uses branching to properly match all parts even across multiple lines. --- NgxHTML.sublime-syntax | 112 +++++++++++++---- tests/syntax_test_scopes.component.html | 158 ++++++++++++++++++++++-- 2 files changed, 239 insertions(+), 31 deletions(-) diff --git a/NgxHTML.sublime-syntax b/NgxHTML.sublime-syntax index 7d90116..c71099a 100644 --- a/NgxHTML.sublime-syntax +++ b/NgxHTML.sublime-syntax @@ -25,6 +25,8 @@ variables: ident_start: (?:[_$\p{L}\p{Nl}]|{{ident_escape}}) ident_part: (?:[_$\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]|{{ident_escape}}) + dot_accessor: (?:\??\.(?!\.)) + contexts: ###[ HTML CUSTOMIZATIONS ]##################################################### @@ -431,7 +433,7 @@ contexts: scope: keyword.operator.assignment.ngx - match: \?\? scope: keyword.operator.null-coalescing.ngx - - match: '\?' + - match: \?(?!\.) scope: keyword.operator.ternary.ngx push: ng-ternary-expression @@ -538,46 +540,108 @@ contexts: ###[ ANGULAR VARIABLES ]####################################################### ng-variables: - - match: ({{ident_name}})\s*(\??\.) - captures: - 1: variable.other.object.ngx - 2: punctuation.accessor.ngx - push: ng-members + - match: (?={{ident_start}}) + branch_point: ng-variable + branch: + - ng-unqualified-variable + - ng-qualified-variable + - match: '{{dot_accessor}}' + scope: punctuation.accessor.ngx + push: + - ng-accessor + - ng-member + + ng-unqualified-variable: + - meta_include_prototype: false - match: '{{ident_name}}' scope: variable.other.readwrite.ngx - push: ng-subscription + set: + - ng-variable-check + - ng-subscription + - include: immediately-pop + + ng-variable-check: + - match: (?={{dot_accessor}}) + fail: ng-variable + - include: else-pop + + ng-qualified-variable: + - meta_include_prototype: false + - match: '{{ident_name}}' + scope: variable.other.object.ngx + set: + - ng-accessor + - ng-subscription + - include: immediately-pop - ng-members: - - match: ({{ident_name}})\s*(\??\.) + ng-accessor: + - meta_include_prototype: false + - meta_scope: meta.path.ngx + - match: \s*({{dot_accessor}}) captures: - 1: variable.other.object.ngx - 2: punctuation.accessor.ngx + 1: punctuation.accessor.ngx + push: ng-member + - match: (?=\s*\S) + pop: 1 + + ng-member: - match: (({{ident_name}})\s*)(\() captures: 1: meta.function-call.identifier.ngx - 2: variable.function.ngx + 2: variable.function.method.ngx 3: meta.function-call.arguments.ngx punctuation.section.arguments.begin.ngx - set: ng-function-call-arguments - - match: '{{ident_name}}' - scope: variable.other.member.ngx + set: + - ng-subscription + - ng-function-call-arguments + - match: (?=[$#]?{{ident_start}}) + branch_point: ng-property + branch: + - ng-property + - ng-object + pop: 1 + - include: ng-subscription + + ng-object: + - match: ([$#]?){{ident_name}} + scope: variable.other.object.ngx + captures: + 1: punctuation.definition.variable.ngx set: ng-subscription - - include: else-pop + - include: immediately-pop + + ng-property: + - match: ([$#]?){{ident_name}} + scope: variable.other.member.ngx + captures: + 1: punctuation.definition.variable.ngx + set: + - ng-property-check + - ng-subscription + - include: immediately-pop + + ng-property-check: + - match: (?=\s*{{dot_accessor}}) + fail: ng-property + - match: (?=\s*\S) + pop: 1 ng-subscription: - - match: \[ - scope: punctuation.section.subscription.begin.ngx - push: ng-subscription-body - - include: else-pop + - match: \s*(\[) + captures: + 1: meta.subscription.ngx punctuation.section.subscription.begin.ngx + set: ng-subscription-body + - match: (?=\s*\S) + pop: 1 ng-subscription-body: - - meta_scope: meta.subscription.ngx + - meta_content_scope: meta.subscription.ngx - match: \] - scope: punctuation.section.subscription.end.ngx - pop: 1 + scope: meta.subscription.ngx punctuation.section.subscription.end.ngx + set: ng-subscription - include: ng-expressions ###[ ANGULAR PROTOTYPES ]###################################################### ng-block-pop: - - match: (?=[)}]) + - match: (?=[;)}]) pop: 1 diff --git a/tests/syntax_test_scopes.component.html b/tests/syntax_test_scopes.component.html index d10da1b..d5fbd8e 100644 --- a/tests/syntax_test_scopes.component.html +++ b/tests/syntax_test_scopes.component.html @@ -15,6 +15,19 @@ +@let func = user.func(); + + + + + + + + + + + + @let greeting = 'Hello, ' + name; @@ -39,6 +52,97 @@ +@let item = var[10]['bar']; + + + + + + + + + + + + + + + + + +@let path = .foo?.bar? ; + + + + + + + + + + + + + + + +@let path = .orders.value()?.[0]?.$extra?.#currency.unit; + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@let path = orders.value()?.[0]?.$extra?.#currency.unit; + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -528,7 +632,7 @@ {{ person['name'][0] = "Mirabel" }} - + @@ -546,24 +650,64 @@ {{ obj?.member }} - - + + + + + {{ obj.member [5] }} + + + + + + + + + + + {{ obj.method() }} - - + + + + - + + {{ orders.value()?.[0]?.$extra?.#currency }} + + + + + + + + + + + + + + + + + + + + + + + {{ func(arg, "value") }} From bb25c667dd279ae4c1fa99c9d42b1a6a8680990b Mon Sep 17 00:00:00 2001 From: deathaxe Date: Mon, 16 Dec 2024 12:34:06 +0100 Subject: [PATCH 3/3] Assign ngx file extension This commit assigns *.ngx file extensions as Angular Playground detects it and assigns special syntax, too. --- NgxHTML.sublime-syntax | 1 + 1 file changed, 1 insertion(+) diff --git a/NgxHTML.sublime-syntax b/NgxHTML.sublime-syntax index c71099a..9e401b3 100644 --- a/NgxHTML.sublime-syntax +++ b/NgxHTML.sublime-syntax @@ -7,6 +7,7 @@ version: 2 extends: Packages/HTML/HTML.sublime-syntax file_extensions: + - ngx - component.html variables: