diff --git a/.fantomasignore b/.fantomasignore new file mode 100644 index 0000000..bc864c6 --- /dev/null +++ b/.fantomasignore @@ -0,0 +1 @@ +test/grammar.fsx diff --git a/.gitignore b/.gitignore index a41a0a7..a3259ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules *.log -/out \ No newline at end of file +out +target diff --git a/README.md b/README.md index d788ac2..d1c0e3f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ -# Syntax highlighting for template strings in F# +# Syntax highlighting for template strings in F\# -VS Code extension that highlights HTML/SVG/CSS/SQL/JS F# triple quoted strings (interpolated or not) preceded by a function named `html/svg/css/sql/js`. +VS Code extension that highlights embedded code in F# triple quoted strings (interpolated or not) preceded by a function named: + +- `html` +- `svg` +- `css` +- `js` +- `sql` ```fsharp html """

Hello World!

""" @@ -9,17 +15,54 @@ html """

Hello World!

""" You need to declare the functions by yourself. If you need to do some transformation, make the function accept `FormattableString`: ```fsharp -let sql (s: FormattableString) = ... +let sql (s: FormattableString) = // ... ``` -If you just want to trigger highlighting, use an identity function: +> The extension will also accept prefixed function names (case-insensitive) like `toHTML`, `query_sql`, etc. + +If you just want to trigger highlighting, use a language comment (also case-insensitive) instead: ```fsharp -let css (s: string) = s +(* css *) """:root::before { content: "Hello World!"; }""" ``` -> The extension will also accept functions ending with `_html`/`_sql`/etc. - For HTML/CSS/JS templates, the extension will forward completion and hover requests to the built-in language providers in VS Code. -![Example](screencast.gif) \ No newline at end of file +![Example](screencast.gif) + +## Contributing + +- Click **Run and Debug > Launch Extension** +- Open [test cases](./test/grammar.fsx) + +### Grammar Debugging + +- Install [TextMate Languages](https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage) support +- Update YAML and run **Commmand Palette > Convert to tmLanguage JSON** +- Format YAML and JSON with [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +- Add recommended debug tokens to **User Settings**: + + ```jsonc + "editor.tokenColorCustomizations": { + "textMateRules": [ + { + "scope": [ + "DEBUG"], "settings": { + "fontStyle": "underline", + "foreground": "#9A9A9A" } + }, + { + "scope": [ + "DEBUG.green"], "settings": { + // Inherits fontStyle from DEBUG + "foreground": "#17C984" } + }, + // DEBUG.orange -> #FF8B2D + // DEBUG.red -> #C62B2B + ] + } + ``` + +## Acknowledgements + +Inspired by [@mjbvz](https://github.com/mjbvz)'s JavaScript/TypeScript support for [Comment tagged templates](https://marketplace.visualstudio.com/items?itemName=bierner.comment-tagged-templates). diff --git a/package.json b/package.json index b05fc61..3f26d4c 100644 --- a/package.json +++ b/package.json @@ -4,24 +4,24 @@ "description": "Syntax highlighting for template strings in F#", "icon": "fsharp.png", "version": "1.4.1", - "publisher": "alfonsogarciacaro", + "publisher": "alfonsogarciacaro", "license": "MIT", "repository": { "url": "https://github.com/alfonsogarciacaro/vscode-template-fsharp-highlight.git" }, "bugs": { - "url": "https://github.com/alfonsogarciacaro/vscode-template-fsharp-highlight/issues" + "url": "https://github.com/alfonsogarciacaro/vscode-template-fsharp-highlight/issues" + }, + "engines": { + "vscode": "^1.43.0" }, - "engines": { - "vscode": "^1.43.0" - }, "categories": [ "Other" ], - "activationEvents": [ - "onLanguage:fsharp", + "activationEvents": [ + "onLanguage:fsharp", "onCommand:template.fsharp.comment" - ], + ], "main": "./out/extension", "contributes": { "commands": [ @@ -35,57 +35,96 @@ } ], "keybindings": [ - { - "command": "template.fsharp.addComment", - "key": "ctrl+shift+k ctrl+shift+c", - "when": "editorFocus && editorLangId == 'fsharp'" - }, - { - "command": "template.fsharp.removeComment", - "key": "ctrl+shift+k ctrl+shift+u", - "when": "editorFocus && editorLangId == 'fsharp'" - } + { + "command": "template.fsharp.addComment", + "key": "ctrl+shift+k ctrl+shift+c", + "when": "editorFocus && editorLangId == 'fsharp'" + }, + { + "command": "template.fsharp.removeComment", + "key": "ctrl+shift+k ctrl+shift+u", + "when": "editorFocus && editorLangId == 'fsharp'" + } ], "grammars": [ { "injectTo": [ "source.fsharp" ], - "scopeName": "template.fsharp.injection", - "path": "./syntaxes/grammar.json", + "scopeName": "inline.template-fsharp-highlight", + "path": "./syntaxes/grammar.json" + }, + { + "injectTo": [ + "source.fsharp" + ], + "scopeName": "inline.template-fsharp-highlight.reinjection", + "path": "./syntaxes/reinject-grammar.json", "embeddedLanguages": { - "meta.embedded.string.html": "html", - "meta.embedded.string.sql": "sql", - "meta.embedded.string.css": "css", - "meta.embedded.string.js": "js", - "meta.embedded.interpolated.html": "html", - "meta.embedded.interpolated.sql": "sql", - "meta.embedded.interpolated.css": "css", - "meta.embedded.interpolated.js": "js" + "meta.template.expression.fsharp": "fsharp" } }, { "injectTo": [ "source.fsharp" ], - "scopeName": "template.fsharp.reinjection", - "path": "./syntaxes/reinject-grammar.json", + "scopeName": "inline.template-fsharp-highlight.injection.html", + "path": "./syntaxes/inject-html.json", + "embeddedLanguages": { + "string.quoted.triple.template.html.fsharp": "html" + } + }, + { + "injectTo": [ + "source.fsharp" + ], + "scopeName": "inline.template-fsharp-highlight.injection.svg", + "path": "./syntaxes/inject-svg.json", "embeddedLanguages": { - "meta.embedded.interpolated.substitution": "fsharp" + "string.quoted.triple.template.svg.fsharp": "xml" + } + }, + { + "injectTo": [ + "source.fsharp" + ], + "scopeName": "inline.template-fsharp-highlight.injection.css", + "path": "./syntaxes/inject-css.json", + "embeddedLanguages": { + "string.quoted.triple.template.css.fsharp": "css" + } + }, + { + "injectTo": [ + "source.fsharp" + ], + "scopeName": "inline.template-fsharp-highlight.injection.js", + "path": "./syntaxes/inject-js.json", + "embeddedLanguages": { + "string.quoted.triple.template.js.fsharp": "javascript" + } + }, + { + "injectTo": [ + "source.fsharp" + ], + "scopeName": "inline.template-fsharp-highlight.injection.sql", + "path": "./syntaxes/inject-sql.json", + "embeddedLanguages": { + "string.quoted.triple.template.sql.fsharp": "sql" } } ] }, - "scripts": { + "scripts": { "compile": "tsc -b", - "watch": "tsc -b -w", + "watch": "tsc -b -w", "pack": "vsce package" - }, - "dependencies": { - }, - "devDependencies": { + }, + "dependencies": {}, + "devDependencies": { "@types/node": "^16.7.13", - "@types/vscode": "^1.43.0", + "@types/vscode": "^1.43.0", "typescript": "^4.2.2" - } -} \ No newline at end of file + } +} diff --git a/syntaxes/grammar.json b/syntaxes/grammar.json index e759013..223383e 100644 --- a/syntaxes/grammar.json +++ b/syntaxes/grammar.json @@ -1,143 +1,80 @@ { - "scopeName": "template.fsharp.injection", - "injectionSelector": "L:source.fsharp - (comment, string.quoted.double.fsharp, string.quoted.triple.fsharp)", - "patterns": [ - { - "begin": "[ ._](html|svg) *(?:(?= \\$?\"{3})|$)", - "end": "(?<=\"{3})", - "beginCaptures": { - "1": { - "name": "variable.fsharp" - } - }, - "patterns": [ - { - "contentName": "meta.embedded.interpolated.html", - "begin": "\\$\"{3}", - "end": "\"{3}", - "patterns": [ - { - "include": "text.html.derivative" - } - ] - }, - { - "contentName": "meta.embedded.string.html", - "begin": "\"{3}", - "end": "\"{3}", - "patterns": [ - { - "include": "text.html.derivative" - } - ] - }, - - { - "include": "source.fsharp" - } - ] - }, - { - "begin": "[ ._](sql) *(?:(?= \\$?\"{3})|$)", - "end": "(?<=\"{3})", - "beginCaptures": { - "1": { - "name": "variable.fsharp" - } - }, - "patterns": [ - { - "contentName": "meta.embedded.interpolated.sql", - "begin": "\\$\"{3}", - "end": "\"{3}", - "patterns": [ - { - "include": "source.sql" - } - ] - }, - { - "contentName": "meta.embedded.string.sql", - "begin": "\"{3}", - "end": "\"{3}", - "patterns": [ - { - "include": "source.sql" - } - ] - }, - { - "include": "source.fsharp" - } - ] - }, - { - "begin": "[ ._](css) *(?:(?= \\$?\"{3})|$)", - "end": "(?<=\"{3})", - "beginCaptures": { - "1": { - "name": "variable.fsharp" - } - }, - "patterns": [ - { - "contentName": "meta.embedded.interpolated.css", - "begin": "\\$\"{3}", - "end": "\"{3}", - "patterns": [ - { - "include": "source.css" - } - ] - }, - { - "contentName": "meta.embedded.string.css", - "begin": "\"{3}", - "end": "\"{3}", - "patterns": [ - { - "include": "source.css" - } - ] - }, - { - "include": "source.fsharp" - } - ] - }, - { - "begin": "[ ._](js) *(?:(?= \\$?\"{3})|$)", - "end": "(?<=\"{3})", - "beginCaptures": { - "1": { - "name": "variable.fsharp" - } - }, - "patterns": [ - { - "contentName": "meta.embedded.interpolated.js", - "begin": "\\$\"{3}", - "end": "\"{3}", - "patterns": [ - { - "include": "source.js" - } - ] - }, - { - "contentName": "meta.embedded.string.js", - "begin": "\"{3}", - "end": "\"{3}", - "patterns": [ - { - "include": "source.js" - } - ] - }, - { - "include": "source.fsharp" - } - ] - } - ] -} \ No newline at end of file + "scopeName": "inline.template-fsharp-highlight", + "injectionSelector": "L:- (comment, string)", + "patterns": [ + { + "include": "#lang_then_string" + }, + { + "include": "#lang_then_newline" + } + ], + "repository": { + "lang_then_string": { + "contentName": "string.quoted.triple.template.${2:/downcase}.fsharp", + "begin": "(?xi) # Ignore whitespace in regex and case in matches\n(?<= # Start after valid lang ($2 above) with any prefix\n ( # Optionally capture:\n \\(\\* # - Opening comment\n \\ ? # - Single optional space\n )?\n (html|svg|css|js|sql)\n (?(1) # If captured opening comment then match:\n \\ ? # - Single optional space\n \\*\\) # - Closing comment\n | # Else skip match\n )\n)\n\\ * # Match potentially 0-length spaces\n(?=\\$?\"\"\") # End before triple quoted string\n", + "end": "$", + "patterns": [ + { + "include": "#interpolated_string" + }, + { + "include": "#triple_quoted_string" + } + ] + }, + "lang_then_newline": { + "contentName": "string.quoted.triple.template.${1:/downcase}.fsharp", + "begin": "(?xi)\n(?<=\n (\n \\(\\*\n \\ ?\n )?\n (html|svg|css|js|sql)\n (?(1)\n \\ ?\n \\*\\)\n |\n )\n)\n\\ *\n$\n", + "applyEndPatternLast": true, + "end": "^", + "patterns": [ + { + "include": "#newline_then_string" + } + ] + }, + "newline_then_string": { + "begin": "(?x)\n^\n\\ *\n(?=\\$?\"\"\")\n", + "end": "$", + "patterns": [ + { + "include": "#interpolated_string" + }, + { + "include": "#triple_quoted_string" + } + ] + }, + "triple_quoted_string": { + "contentName": "meta.embedded.line", + "begin": "\"\"\"", + "end": "\"\"\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.fsharp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.fsharp" + } + } + }, + "interpolated_string": { + "contentName": "meta.embedded.block", + "begin": "\\$\"\"\"", + "end": "\"\"\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.fsharp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.fsharp" + } + } + } + } +} diff --git a/syntaxes/grammar.yaml b/syntaxes/grammar.yaml new file mode 100644 index 0000000..f9d771c --- /dev/null +++ b/syntaxes/grammar.yaml @@ -0,0 +1,92 @@ +scopeName: inline.template-fsharp-highlight +injectionSelector: "L:- (comment, string)" +patterns: + - include: "#lang_then_string" + - include: "#lang_then_newline" +repository: + lang_then_string: + # name: DEBUG + # Documented at https://macromates.com/blog/2011/format-strings/#the-case-for-replacements + contentName: string.quoted.triple.template.${2:/downcase}.fsharp + begin: | + (?xi) # Ignore whitespace in regex and case in matches + (?<= # Start after valid lang ($2 above) with any prefix + ( # Optionally capture: + \(\* # - Opening comment + \ ? # - Single optional space + )? + (html|svg|css|js|sql) + (?(1) # If captured opening comment then match: + \ ? # - Single optional space + \*\) # - Closing comment + | # Else skip match + ) + ) + \ * # Match potentially 0-length spaces + (?=\$?""") # End before triple quoted string + # Match content until line end + end: $ + patterns: + - include: "#interpolated_string" + - include: "#triple_quoted_string" + lang_then_newline: + # Related to microsoft/vscode-textmate#114 + # Same as above but split between 2 patterns + # name: DEBUG.red + contentName: string.quoted.triple.template.${1:/downcase}.fsharp + begin: | + (?xi) + (?<= + ( + \(\* + \ ? + )? + (html|svg|css|js|sql) + (?(1) + \ ? + \*\) + | + ) + ) + \ * + $ + # From end position of subpatterns + applyEndPatternLast: true + # Stop matching at start position + end: ^ + patterns: + - include: "#newline_then_string" + newline_then_string: + # name: DEBUG.orange + # From line start + begin: | + (?x) + ^ + \ * + (?=\$?""") + end: $ + patterns: + - include: "#interpolated_string" + - include: "#triple_quoted_string" + triple_quoted_string: + # name: DEBUG.green + contentName: meta.embedded.line + begin: |- + """ + end: |- + """ + beginCaptures: + 0: { name: punctuation.definition.string.begin.fsharp } + endCaptures: + 0: { name: punctuation.definition.string.end.fsharp } + interpolated_string: + # Same as above but variant scope for interpolation + # name: DEBUG.green + contentName: meta.embedded.block + begin: \$""" + end: |- + """ + beginCaptures: + 0: { name: punctuation.definition.string.begin.fsharp } + endCaptures: + 0: { name: punctuation.definition.string.end.fsharp } diff --git a/syntaxes/inject-css.json b/syntaxes/inject-css.json new file mode 100644 index 0000000..99776c2 --- /dev/null +++ b/syntaxes/inject-css.json @@ -0,0 +1,9 @@ +{ + "scopeName": "inline.template-fsharp-highlight.injection.css", + "injectionSelector": "L:meta.embedded", + "patterns": [ + { + "include": "source.css" + } + ] +} diff --git a/syntaxes/inject-css.yaml b/syntaxes/inject-css.yaml new file mode 100644 index 0000000..33f9c8f --- /dev/null +++ b/syntaxes/inject-css.yaml @@ -0,0 +1,4 @@ +scopeName: inline.template-fsharp-highlight.injection.css +injectionSelector: "L:meta.embedded" +patterns: + - include: source.css diff --git a/syntaxes/inject-html.json b/syntaxes/inject-html.json new file mode 100644 index 0000000..ead41ae --- /dev/null +++ b/syntaxes/inject-html.json @@ -0,0 +1,9 @@ +{ + "scopeName": "inline.template-fsharp-highlight.injection.html", + "injectionSelector": "L:meta.embedded", + "patterns": [ + { + "include": "text.html.basic" + } + ] +} diff --git a/syntaxes/inject-html.yaml b/syntaxes/inject-html.yaml new file mode 100644 index 0000000..c34a6f0 --- /dev/null +++ b/syntaxes/inject-html.yaml @@ -0,0 +1,4 @@ +scopeName: inline.template-fsharp-highlight.injection.html +injectionSelector: "L:meta.embedded" +patterns: + - include: text.html.basic diff --git a/syntaxes/inject-js.json b/syntaxes/inject-js.json new file mode 100644 index 0000000..e0ebc23 --- /dev/null +++ b/syntaxes/inject-js.json @@ -0,0 +1,9 @@ +{ + "scopeName": "inline.template-fsharp-highlight.injection.js", + "injectionSelector": "L:meta.embedded", + "patterns": [ + { + "include": "source.js" + } + ] +} diff --git a/syntaxes/inject-js.yaml b/syntaxes/inject-js.yaml new file mode 100644 index 0000000..90d657c --- /dev/null +++ b/syntaxes/inject-js.yaml @@ -0,0 +1,4 @@ +scopeName: inline.template-fsharp-highlight.injection.js +injectionSelector: "L:meta.embedded" +patterns: + - include: source.js diff --git a/syntaxes/inject-sql.json b/syntaxes/inject-sql.json new file mode 100644 index 0000000..c7d245c --- /dev/null +++ b/syntaxes/inject-sql.json @@ -0,0 +1,9 @@ +{ + "scopeName": "inline.template-fsharp-highlight.injection.sql", + "injectionSelector": "L:meta.embedded", + "patterns": [ + { + "include": "source.sql" + } + ] +} diff --git a/syntaxes/inject-sql.yaml b/syntaxes/inject-sql.yaml new file mode 100644 index 0000000..ecc28c7 --- /dev/null +++ b/syntaxes/inject-sql.yaml @@ -0,0 +1,4 @@ +scopeName: inline.template-fsharp-highlight.injection.sql +injectionSelector: "L:meta.embedded" +patterns: + - include: source.sql diff --git a/syntaxes/inject-svg.json b/syntaxes/inject-svg.json new file mode 100644 index 0000000..b6fd0dd --- /dev/null +++ b/syntaxes/inject-svg.json @@ -0,0 +1,12 @@ +{ + "scopeName": "inline.template-fsharp-highlight.injection.html", + "injectionSelector": "L:meta.embedded", + "patterns": [ + { + "include": "text.svg" + }, + { + "include": "text.xml" + } + ] +} diff --git a/syntaxes/inject-svg.yaml b/syntaxes/inject-svg.yaml new file mode 100644 index 0000000..1ec6e40 --- /dev/null +++ b/syntaxes/inject-svg.yaml @@ -0,0 +1,7 @@ +scopeName: inline.template-fsharp-highlight.injection.html +injectionSelector: "L:meta.embedded" +patterns: + # If installed SVG extension then + - include: text.svg + # Else + - include: text.xml diff --git a/syntaxes/reinject-grammar.json b/syntaxes/reinject-grammar.json index 3a83f3d..fbd888e 100644 --- a/syntaxes/reinject-grammar.json +++ b/syntaxes/reinject-grammar.json @@ -1,16 +1,22 @@ { - "scopeName": "template.fsharp.reinjection", - "injectionSelector": "L:source.fsharp (meta.embedded.interpolated.html, meta.embedded.interpolated.sql, meta.embedded.interpolated.css, meta.embedded.interpolated.js) - meta.embedded.interpolated.substitution", - "patterns": [ - { - "contentName": "meta.embedded.interpolated.substitution", - "begin": "(?Hello World! + """ + svg """ + Hello World! + """ + css """ + :root::before { content: "Hello World!"; } + """ + js """ + console.log("Hello World!"); + """ + sql """ + SELECT "Hello World!"; + """ + + let ``variant`` = + toHTML """

Hello World!

""" + to_html """

Hello World!

""" + toSVG """Hello World!""" + to_svg """Hello World!""" + toCSS """:root::before { content: "Hello World!"; }""" + to_css """:root::before { content: "Hello World!"; }""" + toJS """console.log("Hello World!");""" + to_js """console.log("Hello World!");""" + toSQL """SELECT "Hello World!";""" + to_sql """SELECT "Hello World!";""" + + let ``followed by line break WITHOUT affecting subsequent lines`` = + html + """

Hello World!

""" + ignore """

Hello World!

""" + svg + """Hello World!""" + ignore """Hello World!""" + css + """:root::before { content: "Hello World!"; }""" + ignore """:root::before { content: "Hello World!"; }""" + js + """console.log("Hello World!");""" + ignore """console.log("Hello World!");""" + sql + """SELECT "Hello World!";""" + ignore """SELECT "Hello World!";""" + + let ``when embedded languages are nested`` = + html """ + + + Hello World! + + + + + Hello World! + + + + """ + + endTestBindings + + let ``comment`` = + ignore (* html *) """ +

Hello World!

+ """ + ignore (* svg *) """ + Hello World! + """ + ignore (* css *) """ + :root::before { content: "Hello World!"; } + """ + ignore (* js *) """ + console.log("Hello World!"); + """ + ignore (* sql *) """ + SELECT "Hello World!"; + """ + + let ``variant`` = + ignore (* HTML *) """

Hello World!

""" + ignore (*html*) """

Hello World!

""" + ignore (* SVG *) """Hello World!""" + ignore (*svg*) """Hello World!""" + ignore (* CSS *) """:root::before { content: "Hello World!"; }""" + ignore (*css*) """:root::before { content: "Hello World!"; }""" + ignore (* JS *) """console.log("Hello World!");""" + ignore (*js*) """console.log("Hello World!");""" + ignore (* SQL *) """SELECT "Hello World!";""" + ignore (*sql*) """SELECT "Hello World!";""" + + let ``followed by line break WITHOUT affecting subsequent lines`` = + ignore (* html *) + """

Hello World!

""" + ignore """

Hello World!

""" + ignore (* svg *) + """Hello World!""" + ignore """Hello World!""" + ignore (* css *) + """:root::before { content: "Hello World!"; }""" + ignore """:root::before { content: "Hello World!"; }""" + ignore (* js *) + """console.log("Hello World!");""" + ignore """console.log("Hello World!");""" + ignore (* sql *) + """SELECT "Hello World!";""" + ignore """SELECT "Hello World!";""" + + endTestBindings + + endTestBindings + +let ``should NOT affect triple quoted string after language`` = + + let ``function followed by additional tokens`` = + html <| """

Hello World!

""" + svg <| """Hello World!""" + css <| """:root::before { content: "Hello World!"; }""" + js <| """console.log("Hello World!");""" + sql <| """SELECT "Hello World!";""" + + let ``then line break`` = + html <| + """

Hello World!

""" + svg <| + """Hello World!""" + css <| + """:root::before { content: "Hello World!"; }""" + js <| + """console.log("Hello World!");""" + sql <| + """SELECT "Hello World!";""" + + endTestBindings + + let ``comment followed by additional tokens`` = + ignore (* html *) <| """

Hello World!

""" + ignore (* svg *) <| """Hello World!""" + ignore (* css *) <| """:root::before { content: "Hello World!"; }""" + ignore (* js *) <| """console.log("Hello World!");""" + ignore (* sql *) <| """SELECT "Hello World!";""" + + let ``then line break`` = + ignore (* html *) <| + """

Hello World!

""" + ignore (* svg *) <| + """Hello World!""" + ignore (* css *) <| + """:root::before { content: "Hello World!"; }""" + ignore (* js *) <| + """console.log("Hello World!");""" + ignore (* sql *) <| + """SELECT "Hello World!";""" + + endTestBindings + + let ``function in line comment`` = + // html """

Hello World!

""" + // svg """Hello World!""" + // css """:root::before { content: "Hello World!"; }""" + // js """console.log("Hello World!");""" + // sql """SELECT "Hello World!";""" + + let ``followed by line break`` = + // html + // """

Hello World!

""" + // svg + // """Hello World!""" + // css + // """:root::before { content: "Hello World!"; }""" + // js + // """console.log("Hello World!");""" + // sql + // """SELECT "Hello World!";""" + endTestBindings + + endTestBindings + + let ``comment in line comment`` = + // ignore (* html *) """

Hello World!

""" + // ignore (* svg *) """Hello World!""" + // ignore (* css *) """:root::before { content: "Hello World!"; }""" + // ignore (* js *) """console.log("Hello World!");""" + // ignore (* sql *) """SELECT "Hello World!";""" + + let ``followed by line break`` = + // ignore (* html *) + // """

Hello World!

""" + // ignore (* svg *) + // """Hello World!""" + // ignore (* css *) + // """:root::before { content: "Hello World!"; }""" + // ignore (* js *) + // """console.log("Hello World!");""" + // ignore (* sql *) + // """SELECT "Hello World!";""" + endTestBindings + + endTestBindings + + let ``function in block comment`` = + (* + html """

Hello World!

""" + svg """Hello World!""" + css """:root::before { content: "Hello World!"; }""" + js """console.log("Hello World!");""" + sql """SELECT "Hello World!";""" + *) + + let ``followed by line break`` = + (* + html + """

Hello World!

""" + svg + """Hello World!""" + css + """:root::before { content: "Hello World!"; }""" + js + """console.log("Hello World!");""" + sql + """SELECT "Hello World!";""" + *) + endTestBindings + + endTestBindings + + let ``comment in line comment`` = + (* + ignore (* html *) """

Hello World!

""" + ignore (* svg *) """Hello World!""" + ignore (* css *) """:root::before { content: "Hello World!"; }""" + ignore (* js *) """console.log("Hello World!");""" + ignore (* sql *) """SELECT "Hello World!";""" + *) + + let ``followed by line break`` = + (* + ignore (* html *) + """

Hello World!

""" + ignore (* svg *) + """Hello World!""" + ignore (* css *) + """:root::before { content: "Hello World!"; }""" + ignore (* js *) + """console.log("Hello World!");""" + ignore (* sql *) + """SELECT "Hello World!";""" + *) + endTestBindings + + endTestBindings + + endTestBindings + +let ``should highlight embedded code in interpolated string EXCEPT inside curly braces after language`` = + let greeting = "Hello World from F#!" + + let ``function`` = + html $""" +

{greeting}

+ """ + svg $""" + {greeting} + """ + css $""" + :root::before {{ content: "{greeting}"; }} + """ + js $""" + console.log("{greeting}"); + """ + sql $""" + SELECT "{greeting}; + """ + + let ``variant`` = + toHTML $"""

{greeting}

""" + to_html $"""

{greeting}

""" + toSVG $"""{greeting}""" + to_svg $"""{greeting}""" + toCSS $""":root::before {{ content: "{greeting}"; }}""" + to_css $""":root::before {{ content: "{greeting}"; }}""" + toJS $"""console.log("{greeting}");""" + to_js $"""console.log("{greeting}");""" + toSQL $"""SELECT "{greeting};""" + to_sql $"""SELECT "{greeting};""" + + let ``followed by line break WITHOUT affecting subsequent lines`` = + html + $"""

{greeting}

""" + ignore $"""

{greeting}

""" + svg + $"""{greeting}""" + ignore $"""{greeting}""" + css + $""":root::before {{ content: "{greeting}"; }}""" + ignore $""":root::before {{ content: "{greeting}"; }}""" + js + $"""console.log("{greeting}");""" + ignore $"""console.log("{greeting}");""" + sql + $"""SELECT "{greeting}";""" + ignore $"""SELECT "{greeting}";""" + + let ``when embedded languages are nested`` = + html $""" + + + {greeting} + + + + + {greeting} + + + + """ + + endTestBindings + + let ``comment`` = + ignore (* html *) $""" +

{greeting}

+ """ + ignore (* svg *) $""" + {greeting} + """ + ignore (* css *) $""" + :root::before {{ content: "{greeting}"; }} + """ + ignore (* js *) $""" + console.log("{greeting}"); + """ + ignore (* sql *) $""" + SELECT "{greeting}; + """ + + let ``variant`` = + ignore (* HTML *) $"""

{greeting}

""" + ignore (*html*) $"""

{greeting}

""" + ignore (* SVG *) $"""{greeting}""" + ignore (*svg*) $"""{greeting}""" + ignore (* CSS *) $""":root::before {{ content: "{greeting}"; }}""" + ignore (*css*) $""":root::before {{ content: "{greeting}"; }}""" + ignore (* JS *) $"""console.log("{greeting}");""" + ignore (*js*) $"""console.log("{greeting}");""" + ignore (* SQL *) $"""SELECT "{greeting};""" + ignore (*sql*) $"""SELECT "{greeting};""" + + let ``followed by line break WITHOUT affecting subsequent lines`` = + ignore (* html *) + $"""

{greeting}

""" + ignore $"""

{greeting}

""" + ignore (* svg *) + $"""{greeting}""" + ignore $"""{greeting}""" + ignore (* css *) + $""":root::before {{ content: "{greeting}"; }}""" + ignore $""":root::before {{ content: "{greeting}"; }}""" + ignore (* js *) + $"""console.log("{greeting}");""" + ignore $"""console.log("{greeting}");""" + ignore (* sql *) + $"""SELECT "{greeting}";""" + ignore $"""SELECT "{greeting}";""" + + endTestBindings + + endTestBindings + +let ``should NOT affect interpolated string after language`` = + let greeting = "Hello World from F#!" + + let ``function followed by additional tokens`` = + html <| $"""

{greeting}

""" + svg <| $"""{greeting}""" + css <| $""":root::before {{ content: "{greeting}"; }}""" + js <| $"""console.log("{greeting}");""" + sql <| $"""SELECT "{greeting}";""" + + let ``then line break`` = + html <| + $"""

{greeting}

""" + svg <| + $"""{greeting}""" + css <| + $""":root::before {{ content: "{greeting}"; }}""" + js <| + $"""console.log("{greeting}");""" + sql <| + $"""SELECT "{greeting}";""" + + endTestBindings + + let ``comment followed by additional tokens`` = + ignore (* html *) <| $"""

{greeting}

""" + ignore (* svg *) <| $"""{greeting}""" + ignore (* css *) <| $""":root::before {{ content: "{greeting}"; }}""" + ignore (* js *) <| $"""console.log("{greeting}");""" + ignore (* sql *) <| $"""SELECT "{greeting}";""" + + let ``then line break`` = + ignore (* html *) <| + $"""

{greeting}

""" + ignore (* svg *) <| + $"""{greeting}""" + ignore (* css *) <| + $""":root::before {{ content: "{greeting}"; }}""" + ignore (* js *) <| + $"""console.log("{greeting}");""" + ignore (* sql *) <| + $"""SELECT "{greeting}";""" + + endTestBindings + + let ``function in line comment`` = + // html $"""

{greeting}

""" + // svg $"""{greeting}""" + // css $""":root::before {{ content: "{greeting}"; }}""" + // js $"""console.log("{greeting}");""" + // sql $"""SELECT "{greeting}";""" + + let ``followed by line break`` = + // html + // $"""

{greeting}

""" + // svg + // $"""{greeting}""" + // css + // $""":root::before {{ content: "{greeting}"; }}""" + // js + // $"""console.log("{greeting}");""" + // sql + // $"""SELECT "{greeting}";""" + endTestBindings + + endTestBindings + + let ``comment in line comment`` = + // ignore (* html *) $"""

{greeting}

""" + // ignore (* svg *) $"""{greeting}""" + // ignore (* css *) $""":root::before {{ content: "{greeting}"; }}""" + // ignore (* js *) $"""console.log("{greeting}");""" + // ignore (* sql *) $"""SELECT "{greeting}";""" + + let ``followed by line break`` = + // ignore (* html *) + // $"""

{greeting}

""" + // ignore (* svg *) + // $"""{greeting}""" + // ignore (* css *) + // $""":root::before {{ content: "{greeting}"; }}""" + // ignore (* js *) + // $"""console.log("{greeting}");""" + // ignore (* sql *) + // $"""SELECT "{greeting}";""" + endTestBindings + + endTestBindings + + let ``function in block comment`` = + (* + html $"""

{greeting}

""" + svg $"""{greeting}""" + css $""":root::before {{ content: "{greeting}"; }}""" + js $"""console.log("{greeting}");""" + sql $"""SELECT "{greeting}";""" + *) + + let ``followed by line break`` = + (* + html + $"""

{greeting}

""" + svg + $"""{greeting}""" + css + $""":root::before {{ content: "{greeting}"; }}""" + js + $"""console.log("{greeting}");""" + sql + $"""SELECT "{greeting}";""" + *) + endTestBindings + + endTestBindings + + let ``comment in line comment`` = + (* + ignore (* html *) $"""

{greeting}

""" + ignore (* svg *) $"""{greeting}""" + ignore (* css *) $""":root::before {{ content: "{greeting}"; }}""" + ignore (* js *) $"""console.log("{greeting}");""" + ignore (* sql *) $"""SELECT "{greeting}";""" + *) + + let ``followed by line break`` = + (* + ignore (* html *) + $"""

{greeting}

""" + ignore (* svg *) + $"""{greeting}""" + ignore (* css *) + $""":root::before {{ content: "{greeting}"; }}""" + ignore (* js *) + $"""console.log("{greeting}");""" + ignore (* sql *) + $"""SELECT "{greeting}";""" + *) + endTestBindings + + endTestBindings + + endTestBindings