diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fe0b40a..82831c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [Version 5.0.0] + +- Remove deprecated functions in the `html` module +- Change definition of `tokens_to_classed_spans` to also take a mutable ScopeStack + ## [Version 4.5.0](https://github.com/trishume/syntect/compare/v4.4.0...v4.5.0) (2020-12-09) - Added a new function for producing classed HTML which handles newlines correctly and deprecated old one. [#307](https://github.com/trishume/syntect/pull/307) diff --git a/Cargo.toml b/Cargo.toml index 81eb3c82..a76d26e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ keywords = ["syntax", "highlighting", "highlighter", "colouring", "parsing"] categories = ["parser-implementations", "parsing", "text-processing"] readme = "Readme.md" license = "MIT" -version = "4.5.0" # remember to update html_root_url +version = "4.6.0" # remember to update html_root_url authors = ["Tristan Hume "] edition = "2018" exclude = [ diff --git a/src/html.rs b/src/html.rs index 8fff45e3..a5a3e98e 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,10 +1,13 @@ //! Rendering highlighted code as HTML+CSS -use std::fmt::Write; -use crate::parsing::{ScopeStackOp, BasicScopeStackOp, Scope, ScopeStack, SyntaxReference, ParseState, SyntaxSet, SCOPE_REPO}; -use crate::easy::{HighlightLines, HighlightFile}; +use crate::easy::{HighlightFile, HighlightLines}; +use crate::escape::Escape; use crate::highlighting::{Color, FontStyle, Style, Theme}; +use crate::parsing::{ + BasicScopeStackOp, ParseState, Scope, ScopeStack, ScopeStackOp, SyntaxReference, SyntaxSet, + SCOPE_REPO, +}; use crate::util::LinesWithEndings; -use crate::escape::Escape; +use std::fmt::Write; use std::io::{self, BufRead}; use std::path::Path; @@ -49,6 +52,7 @@ pub struct ClassedHTMLGenerator<'a> { syntax_set: &'a SyntaxSet, open_spans: isize, parse_state: ParseState, + scope_stack: ScopeStack, html: String, style: ClassStyle, } @@ -59,14 +63,20 @@ impl<'a> ClassedHTMLGenerator<'a> { Self::new_with_class_style(syntax_reference, syntax_set, ClassStyle::Spaced) } - pub fn new_with_class_style(syntax_reference: &'a SyntaxReference, syntax_set: &'a SyntaxSet, style: ClassStyle) -> ClassedHTMLGenerator<'a> { + pub fn new_with_class_style( + syntax_reference: &'a SyntaxReference, + syntax_set: &'a SyntaxSet, + style: ClassStyle, + ) -> ClassedHTMLGenerator<'a> { let parse_state = ParseState::new(syntax_reference); let open_spans = 0; let html = String::new(); + let scope_stack = ScopeStack::new(); ClassedHTMLGenerator { syntax_set, open_spans, parse_state, + scope_stack, html, style, } @@ -78,10 +88,11 @@ impl<'a> ClassedHTMLGenerator<'a> { /// also use of the `load_defaults_newlines` version of the syntaxes. pub fn parse_html_for_line_which_includes_newline(&mut self, line: &str) { let parsed_line = self.parse_state.parse_line(line, &self.syntax_set); - let (formatted_line, delta) = tokens_to_classed_spans( + let (formatted_line, delta) = line_tokens_to_classed_spans( line, parsed_line.as_slice(), self.style, + &mut self.scope_stack, ); self.open_spans += delta; self.html.push_str(formatted_line.as_str()); @@ -129,21 +140,26 @@ pub fn css_for_theme_with_class_style(theme: &Theme, style: ClassStyle) -> Strin match style { ClassStyle::Spaced => { css.push_str(".code {\n"); - }, + } ClassStyle::SpacedPrefixed { prefix } => { css.push_str(&format!(".{}code {{\n", prefix)); - }, + } }; if let Some(fgc) = theme.settings.foreground { - css.push_str(&format!(" color: #{:02x}{:02x}{:02x};\n", fgc.r, fgc.g, fgc.b)); + css.push_str(&format!( + " color: #{:02x}{:02x}{:02x};\n", + fgc.r, fgc.g, fgc.b + )); } if let Some(bgc) = theme.settings.background { - css.push_str(&format!(" background-color: #{:02x}{:02x}{:02x};\n", bgc.r, bgc.g, bgc.b)); + css.push_str(&format!( + " background-color: #{:02x}{:02x}{:02x};\n", + bgc.r, bgc.g, bgc.b + )); } css.push_str("}\n\n"); for i in &theme.scopes { - for scope_selector in &i.scope.selectors { let scopes = scope_selector.extract_scopes(); for k in &scopes { @@ -157,12 +173,15 @@ pub fn css_for_theme_with_class_style(theme: &Theme, style: ClassStyle) -> Strin css.truncate(len - 2); // remove trailing ", " css.push_str(" {\n"); - if let Some(fg) = i.style.foreground { + if let Some(fg) = i.style.foreground { css.push_str(&format!(" color: #{:02x}{:02x}{:02x};\n", fg.r, fg.g, fg.b)); } if let Some(bg) = i.style.background { - css.push_str(&format!(" background-color: #{:02x}{:02x}{:02x};\n", bg.r, bg.g, bg.b)); + css.push_str(&format!( + " background-color: #{:02x}{:02x}{:02x};\n", + bg.r, bg.g, bg.b + )); } if let Some(fs) = i.style.font_style { @@ -201,9 +220,7 @@ pub enum ClassStyle { /// file an issue; the HTML generator can also be forked /// separately from the rest of syntect, as it only uses the /// public API.) - SpacedPrefixed { - prefix: &'static str, - }, + SpacedPrefixed { prefix: &'static str }, } fn scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle) { @@ -215,11 +232,10 @@ fn scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle) { s.push_str(" ") } match style { - ClassStyle::Spaced => { - }, + ClassStyle::Spaced => {} ClassStyle::SpacedPrefixed { prefix } => { s.push_str(&prefix); - }, + } } s.push_str(atom_s); } @@ -232,11 +248,10 @@ fn scope_to_selector(s: &mut String, scope: Scope, style: ClassStyle) { let atom_s = repo.atom_str(atom); s.push_str("."); match style { - ClassStyle::Spaced => { - }, + ClassStyle::Spaced => {} ClassStyle::SpacedPrefixed { prefix } => { s.push_str(&prefix); - }, + } } s.push_str(atom_s); } @@ -248,13 +263,22 @@ fn scope_to_selector(s: &mut String, scope: Scope, style: ClassStyle) { /// /// Note that the `syntax` passed in must be from a `SyntaxSet` compiled for newline characters. /// This is easy to get with `SyntaxSet::load_defaults_newlines()`. (Note: this was different before v3.0) -pub fn highlighted_html_for_string(s: &str, ss: &SyntaxSet, syntax: &SyntaxReference, theme: &Theme) -> String { +pub fn highlighted_html_for_string( + s: &str, + ss: &SyntaxSet, + syntax: &SyntaxReference, + theme: &Theme, +) -> String { let mut highlighter = HighlightLines::new(syntax, theme); let (mut output, bg) = start_highlighted_html_snippet(theme); for line in LinesWithEndings::from(s) { let regions = highlighter.highlight(line, ss); - append_highlighted_html_for_styled_line(®ions[..], IncludeBackground::IfDifferent(bg), &mut output); + append_highlighted_html_for_styled_line( + ®ions[..], + IncludeBackground::IfDifferent(bg), + &mut output, + ); } output.push_str("\n"); output @@ -266,10 +290,11 @@ pub fn highlighted_html_for_string(s: &str, ss: &SyntaxSet, syntax: &SyntaxRefer /// /// Note that the `syntax` passed in must be from a `SyntaxSet` compiled for newline characters. /// This is easy to get with `SyntaxSet::load_defaults_newlines()`. (Note: this was different before v3.0) -pub fn highlighted_html_for_file>(path: P, - ss: &SyntaxSet, - theme: &Theme) - -> io::Result { +pub fn highlighted_html_for_file>( + path: P, + ss: &SyntaxSet, + theme: &Theme, +) -> io::Result { let mut highlighter = HighlightFile::new(path, ss, theme)?; let (mut output, bg) = start_highlighted_html_snippet(theme); @@ -277,7 +302,11 @@ pub fn highlighted_html_for_file>(path: P, while highlighter.reader.read_line(&mut line)? > 0 { { let regions = highlighter.highlight_lines.highlight(&line, ss); - append_highlighted_html_for_styled_line(®ions[..], IncludeBackground::IfDifferent(bg), &mut output); + append_highlighted_html_for_styled_line( + ®ions[..], + IncludeBackground::IfDifferent(bg), + &mut output, + ); } line.clear(); } @@ -300,13 +329,14 @@ pub fn highlighted_html_for_file>(path: P, /// Returns the HTML string and the number of `` tags opened /// (negative for closed). So that you can emit the correct number of closing /// tags at the end. -pub fn tokens_to_classed_spans(line: &str, - ops: &[(usize, ScopeStackOp)], - style: ClassStyle) - -> (String, isize) { +pub fn line_tokens_to_classed_spans( + line: &str, + ops: &[(usize, ScopeStackOp)], + style: ClassStyle, + stack: &mut ScopeStack, +) -> (String, isize) { let mut s = String::with_capacity(line.len() + ops.len() * 8); // a guess let mut cur_index = 0; - let mut stack = ScopeStack::new(); let mut span_delta = 0; // check and skip emty inner tags @@ -319,26 +349,23 @@ pub fn tokens_to_classed_spans(line: &str, write!(s, "{}", Escape(&line[cur_index..i])).unwrap(); cur_index = i } - stack.apply_with_hook(op, |basic_op, _| { - match basic_op { - BasicScopeStackOp::Push(scope) => { - span_start = s.len(); - span_empty = true; - s.push_str(""); - span_delta += 1; - } - BasicScopeStackOp::Pop => { - if span_empty == false { - s.push_str(""); - } - else { - s.truncate(span_start); - } - span_delta -= 1; - span_empty = false; + stack.apply_with_hook(op, |basic_op, _| match basic_op { + BasicScopeStackOp::Push(scope) => { + span_start = s.len(); + span_empty = true; + s.push_str(""); + span_delta += 1; + } + BasicScopeStackOp::Pop => { + if span_empty == false { + s.push_str(""); + } else { + s.truncate(span_start); } + span_delta -= 1; + span_empty = false; } }); } @@ -346,12 +373,24 @@ pub fn tokens_to_classed_spans(line: &str, (s, span_delta) } -#[deprecated(since="3.1.0", note="please use `tokens_to_classed_spans` instead")] +/// Preserved for compatibility, always use `line_tokens_to_classed_spans` +/// and keep a `ScopeStack` between lines for correct highlighting that won't +/// sometimes crash. +#[deprecated(since="4.6.0", note="Use `line_tokens_to_classed_spans` instead, this can panic and highlight incorrectly")] +pub fn tokens_to_classed_spans( + line: &str, + ops: &[(usize, ScopeStackOp)], + style: ClassStyle, +) -> (String, isize) { + line_tokens_to_classed_spans(line, ops, style, &mut ScopeStack::new()) +} + +#[deprecated(since="3.1.0", note="Use `line_tokens_to_classed_spans` instead to avoid incorrect highlighting and panics")] pub fn tokens_to_classed_html(line: &str, ops: &[(usize, ScopeStackOp)], style: ClassStyle) -> String { - tokens_to_classed_spans(line, ops, style).0 + line_tokens_to_classed_spans(line, ops, style, &mut ScopeStack::new()).0 } /// Determines how background color attributes are generated @@ -367,9 +406,9 @@ pub enum IncludeBackground { fn write_css_color(s: &mut String, c: Color) { if c.a != 0xFF { - write!(s,"#{:02x}{:02x}{:02x}{:02x}",c.r,c.g,c.b,c.a).unwrap(); + write!(s, "#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a).unwrap(); } else { - write!(s,"#{:02x}{:02x}{:02x}",c.r,c.g,c.b).unwrap(); + write!(s, "#{:02x}{:02x}{:02x}", c.r, c.g, c.b).unwrap(); } } @@ -405,12 +444,15 @@ pub fn styled_line_to_highlighted_html(v: &[(Style, &str)], bg: IncludeBackgroun /// Like `styled_line_to_highlighted_html` but appends to a `String` for increased efficiency. /// In fact `styled_line_to_highlighted_html` is just a wrapper around this function. -pub fn append_highlighted_html_for_styled_line(v: &[(Style, &str)], bg: IncludeBackground, mut s: &mut String) { +pub fn append_highlighted_html_for_styled_line( + v: &[(Style, &str)], + bg: IncludeBackground, + mut s: &mut String, +) { let mut prev_style: Option<&Style> = None; for &(ref style, text) in v.iter() { let unify_style = if let Some(ps) = prev_style { - style == ps || - (style.background == ps.background && text.trim().is_empty()) + style == ps || (style.background == ps.background && text.trim().is_empty()) } else { false }; @@ -465,18 +507,24 @@ pub fn append_highlighted_html_for_styled_line(v: &[(Style, &str)], bg: IncludeB /// helper for that :-) pub fn start_highlighted_html_snippet(t: &Theme) -> (String, Color) { let c = t.settings.background.unwrap_or(Color::WHITE); - (format!("
\n",
-            c.r,
-            c.g,
-            c.b), c)
+    (
+        format!(
+            "
\n",
+            c.r, c.g, c.b
+        ),
+        c,
+    )
 }
 
-#[cfg(all(feature = "assets", any(feature = "dump-load", feature = "dump-load-rs")))]
+#[cfg(all(
+    feature = "assets",
+    any(feature = "dump-load", feature = "dump-load-rs")
+))]
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::parsing::{SyntaxSet, ParseState, ScopeStack, SyntaxSetBuilder};
-    use crate::highlighting::{ThemeSet, Style, Highlighter, HighlightIterator, HighlightState};
+    use crate::highlighting::{HighlightIterator, HighlightState, Highlighter, Style, ThemeSet};
+    use crate::parsing::{ParseState, ScopeStack, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
     use crate::util::LinesWithEndings;
     #[test]
     fn tokens() {
@@ -485,11 +533,12 @@ mod tests {
         let mut state = ParseState::new(syntax);
         let line = "[w](t.co) *hi* **five**";
         let ops = state.parse_line(line, &ss);
+        let mut stack = ScopeStack::new();
 
         // use util::debug_print_ops;
         // debug_print_ops(line, &ops);
 
-        let (html, _) = tokens_to_classed_spans(line, &ops[..], ClassStyle::Spaced);
+        let (html, _) = line_tokens_to_classed_spans(line, &ops[..], ClassStyle::Spaced, &mut stack);
         println!("{}", html);
         assert_eq!(html, include_str!("../testdata/test2.html").trim_end());
 
@@ -513,17 +562,21 @@ mod tests {
         let html = highlighted_html_for_string(s, &ss, syntax, &ts.themes["base16-ocean.dark"]);
         // println!("{}", html);
         assert_eq!(html, include_str!("../testdata/test3.html"));
-        let html2 = highlighted_html_for_file("testdata/highlight_test.erb",
-                                                 &ss,
-                                                 &ts.themes["base16-ocean.dark"])
-            .unwrap();
+        let html2 = highlighted_html_for_file(
+            "testdata/highlight_test.erb",
+            &ss,
+            &ts.themes["base16-ocean.dark"],
+        )
+        .unwrap();
         assert_eq!(html2, html);
 
         // YAML is a tricky syntax and InspiredGitHub is a fancy theme, this is basically an integration test
-        let html3 = highlighted_html_for_file("testdata/Packages/Rust/Cargo.sublime-syntax",
-                                                 &ss,
-                                                 &ts.themes["InspiredGitHub"])
-            .unwrap();
+        let html3 = highlighted_html_for_file(
+            "testdata/Packages/Rust/Cargo.sublime-syntax",
+            &ss,
+            &ts.themes["InspiredGitHub"],
+        )
+        .unwrap();
         println!("{}", html3);
         assert_eq!(html3, include_str!("../testdata/test4.html"));
     }
@@ -536,20 +589,46 @@ mod tests {
         builder.add_from_folder("testdata", true).unwrap();
         let ss = builder.build();
         let ts = ThemeSet::load_defaults();
-        let html = highlighted_html_for_file("testdata/testing-syntax.testsyntax",
-                                                &ss,
-                                                &ts.themes["base16-ocean.dark"])
-            .unwrap();
+        let html = highlighted_html_for_file(
+            "testdata/testing-syntax.testsyntax",
+            &ss,
+            &ts.themes["base16-ocean.dark"],
+        )
+        .unwrap();
         println!("{}", html);
         assert_eq!(html, include_str!("../testdata/test5.html"));
     }
 
+    #[test]
+    fn test_classed_html_generator_doesnt_panic() {
+        let current_code = "{\n    \"headers\": [\"Number\", \"Title\"],\n    \"records\": [\n        [\"1\", \"Gutenberg\"],\n        [\"2\", \"Printing\"]\n    ],\n}\n";
+        let syntax_def = SyntaxDefinition::load_from_str(
+            include_str!("../testdata/JSON.sublime-syntax"),
+            true,
+            None,
+        )
+        .unwrap();
+        let mut syntax_set_builder = SyntaxSetBuilder::new();
+        syntax_set_builder.add(syntax_def);
+        let syntax_set = syntax_set_builder.build();
+        let syntax = syntax_set.find_syntax_by_name("JSON").unwrap();
+
+        let mut html_generator =
+            ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
+        for line in LinesWithEndings::from(current_code) {
+            html_generator.parse_html_for_line_which_includes_newline(&line);
+        }
+        html_generator.finalize();
+    }
+
     #[test]
     fn test_classed_html_generator() {
         let current_code = "x + y\n";
         let syntax_set = SyntaxSet::load_defaults_newlines();
         let syntax = syntax_set.find_syntax_by_name("R").unwrap();
-        let mut html_generator = ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
+
+        let mut html_generator =
+            ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
         for line in LinesWithEndings::from(current_code) {
             html_generator.parse_html_for_line_which_includes_newline(&line);
         }
@@ -562,7 +641,11 @@ mod tests {
         let current_code = "x + y\n";
         let syntax_set = SyntaxSet::load_defaults_newlines();
         let syntax = syntax_set.find_syntax_by_name("R").unwrap();
-        let mut html_generator = ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::SpacedPrefixed { prefix: "foo-" });
+        let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
+            &syntax,
+            &syntax_set,
+            ClassStyle::SpacedPrefixed { prefix: "foo-" },
+        );
         for line in LinesWithEndings::from(current_code) {
             html_generator.parse_html_for_line_which_includes_newline(&line);
         }
@@ -579,7 +662,8 @@ fn main() {
 ";
         let syntax_set = SyntaxSet::load_defaults_newlines();
         let syntax = syntax_set.find_syntax_by_extension("rs").unwrap();
-        let mut html_generator = ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
+        let mut html_generator =
+            ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
         for line in LinesWithEndings::from(code) {
             html_generator.parse_html_for_line_which_includes_newline(&line);
         }
diff --git a/testdata/JSON.sublime-syntax b/testdata/JSON.sublime-syntax
new file mode 100644
index 00000000..ae250451
--- /dev/null
+++ b/testdata/JSON.sublime-syntax
@@ -0,0 +1,143 @@
+%YAML 1.2
+---
+name: JSON
+file_extensions:
+  - json
+  - sublime-settings
+  - sublime-menu
+  - sublime-keymap
+  - sublime-mousemap
+  - sublime-theme
+  - sublime-build
+  - sublime-project
+  - sublime-completions
+  - sublime-commands
+  - sublime-macro
+  - sublime-color-scheme
+  - ipynb
+  - Pipfile.lock
+scope: source.json
+contexts:
+  prototype:
+    - include: comments
+  main:
+    - include: value
+  value:
+    - include: constant
+    - include: number
+    - include: string
+    - include: array
+    - include: object
+  array:
+    - match: '\['
+      scope: punctuation.section.sequence.begin.json
+      push:
+        - meta_scope: meta.sequence.json
+        - match: '\]'
+          scope: punctuation.section.sequence.end.json
+          pop: true
+        - include: value
+        - match: ","
+          scope: punctuation.separator.sequence.json
+        - match: '[^\s\]]'
+          scope: invalid.illegal.expected-sequence-separator.json
+  comments:
+    - match: /\*\*(?!/)
+      scope: punctuation.definition.comment.json
+      push:
+        - meta_scope: comment.block.documentation.json
+        - meta_include_prototype: false
+        - match: \*/
+          pop: true
+        - match: ^\s*(\*)(?!/)
+          captures:
+            1: punctuation.definition.comment.json
+    - match: /\*
+      scope: punctuation.definition.comment.json
+      push:
+        - meta_scope: comment.block.json
+        - meta_include_prototype: false
+        - match: \*/
+          pop: true
+    - match: (//).*$\n?
+      scope: comment.line.double-slash.js
+      captures:
+        1: punctuation.definition.comment.json
+  constant:
+    - match: \b(?:true|false|null)\b
+      scope: constant.language.json
+  number:
+    # handles integer and decimal numbers
+    - match: -?(?:0|[1-9]\d*)(?:(?:(\.)\d+)(?:[eE][-+]?\d+)?|(?:[eE][-+]?\d+))
+      scope: constant.numeric.float.decimal.json
+      captures:
+        1: punctuation.separator.decimal.json
+    - match: -?(?:0|[1-9]\d*)
+      scope: constant.numeric.integer.decimal.json
+  object:
+    # a JSON object
+    - match: '\{'
+      scope: punctuation.section.mapping.begin.json
+      push:
+        - meta_scope: meta.mapping.json
+        - match: '\}'
+          scope: punctuation.section.mapping.end.json
+          pop: true
+        - match: '"'
+          scope: punctuation.definition.string.begin.json
+          push:
+            - clear_scopes: 1
+            - meta_scope: meta.mapping.key.json string.quoted.double.json
+            - meta_include_prototype: false
+            - include: inside-string
+        - match: ":"
+          scope: punctuation.separator.mapping.key-value.json
+          push:
+            - match: ',|\s?(?=\})'
+              scope: invalid.illegal.expected-mapping-value.json
+              pop: true
+            - match: (?=\S)
+              set:
+                - clear_scopes: 1
+                - meta_scope: meta.mapping.value.json
+                - include: value
+                - match: ''
+                  set:
+                    - match: ','
+                      scope: punctuation.separator.mapping.pair.json
+                      pop: true
+                    - match: \s*(?=\})
+                      pop: true
+                    - match: \s(?!/[/*])(?=[^\s,])|[^\s,]
+                      scope: invalid.illegal.expected-mapping-separator.json
+                      pop: true
+        - match: '[^\s\}]'
+          scope: invalid.illegal.expected-mapping-key.json
+  string:
+    - match: '"'
+      scope: punctuation.definition.string.begin.json
+      push: inside-string
+  inside-string:
+    - meta_scope: string.quoted.double.json
+    - meta_include_prototype: false
+    - match: '"'
+      scope: punctuation.definition.string.end.json
+      pop: true
+    - include: string-escape
+    - match: $\n?
+      scope: invalid.illegal.unclosed-string.json
+      pop: true
+  string-escape:
+    - match: |-
+        (?x:                # turn on extended mode
+          \\                # a literal backslash
+          (?:               # ...followed by...
+            ["\\/bfnrt]     # one of these characters
+            |               # ...or...
+            u               # a u
+            [0-9a-fA-F]{4}  # and four hex digits
+          )
+        )
+      scope: constant.character.escape.json
+    - match: \\.
+      scope: invalid.illegal.unrecognized-string-escape.json