168 lines
8.5 KiB
Rust
168 lines
8.5 KiB
Rust
//! The embedded template and language grammars: fence labels and aliases resolve, real samples
|
|
//! classify the expected kinds, and hostile bodies come back fast without panicking.
|
|
|
|
use std::time::Instant;
|
|
|
|
use notescraft_syntax::load::is_known;
|
|
use notescraft_syntax::{Highlighter, TokenKind, suggest_language};
|
|
|
|
/// One language: its labels (the first is the canonical one), a sample and the kinds it must show.
|
|
struct Case {
|
|
labels: &'static [&'static str],
|
|
sample: &'static str,
|
|
kinds: &'static [TokenKind],
|
|
}
|
|
|
|
use TokenKind::{Comment, Function, Keyword, Number, Operator, Property, String as Str, Type};
|
|
|
|
const CASES: &[Case] = &[
|
|
Case {
|
|
labels: &["twig", "html.twig"],
|
|
sample: "{# note #}\n<p>{{ author|upper }}</p>\n{% if author and 3 > 2 %}x{% endif %}\n{% set s = \"a\" %}\n",
|
|
kinds: &[Comment, Operator, Keyword, Number, Str, Function],
|
|
},
|
|
Case {
|
|
labels: &["jinja", "j2", "jinja2", "njk", "nunjucks", "django"],
|
|
sample: "{# note #}\n{% for u in users %}{{ u.name|title }}{% endfor %}\n{% if x == 1 %}y{% else %}{% set s = 'a' %}{% endif %}\n",
|
|
kinds: &[Comment, Operator, Keyword, Number, Str, Function],
|
|
},
|
|
Case {
|
|
labels: &["liquid"],
|
|
sample: "{% comment %}hidden{% endcomment %}\n{% assign n = 2 %}{{ page.title | upcase }}\n{% if x == \"a\" %}y{% endif %}\n",
|
|
kinds: &[Comment, Operator, Keyword, Number, Str, Function],
|
|
},
|
|
Case {
|
|
labels: &["handlebars", "hbs", "mustache", "mst"],
|
|
sample: "{{! note }}\n{{#if user}}{{name}} {{{raw}}}{{else}}no{{/if}}\n{{> partial \"a\" 3}}\n",
|
|
kinds: &[Comment, Operator, Keyword, Number, Str],
|
|
},
|
|
Case {
|
|
labels: &["blade", "blade.php"],
|
|
sample: "{{-- note --}}\n@extends('layout')\n@foreach ($a as $b)\n{{ $b->name }}{!! 'a' !!}\n@endforeach\n",
|
|
kinds: &[Comment, Operator, Keyword, Str],
|
|
},
|
|
Case {
|
|
labels: &["ejs"],
|
|
sample: "<%# note %>\n<% if (user) { %><%= user.name %><% } %>\n<%- include(\"a\") %> 3\n",
|
|
kinds: &[Comment, Operator, Keyword, Str],
|
|
},
|
|
Case { labels: &["erb"], sample: "<%# note %>\n<% if @a %><%= \"x\" %><% end %>\n", kinds: &[Comment, Keyword, Str] },
|
|
Case { labels: &["toml"], sample: "# c\nname = \"a\"\nn = 3\n", kinds: &[Comment, Str, Number, Property] },
|
|
Case {
|
|
labels: &["ini", "cfg", "conf", "desktop"],
|
|
sample: "; c\n[core]\nname = \"a\"\nn = 3\nok = true\n",
|
|
kinds: &[Comment, Property, Str, Number, Keyword],
|
|
},
|
|
Case {
|
|
labels: &["dockerfile", "containerfile"],
|
|
sample: "# c\nFROM rust:1 AS build\nRUN echo \"hi\" && cp --from=a x\nEXPOSE 8080\n",
|
|
kinds: &[Comment, Keyword, Str, Number],
|
|
},
|
|
Case { labels: &["makefile", "make", "mk"], sample: "# c\nall: a.o\n\techo \"x\"\n", kinds: &[Comment, Str] },
|
|
Case {
|
|
labels: &["nginx"],
|
|
sample: "# c\nserver {\n listen 80;\n location / { proxy_pass \"http://a\"; }\n}\n",
|
|
kinds: &[Comment, Keyword, Number, Str, Function],
|
|
},
|
|
Case {
|
|
labels: &["graphql", "gql"],
|
|
sample: "# c\nquery Q($id: Int = 3) { user(id: $id) @skip(if: true) { name } }\n",
|
|
kinds: &[Comment, Keyword, Number, Type, Property],
|
|
},
|
|
Case {
|
|
labels: &["proto", "protobuf"],
|
|
sample: "// c\nsyntax = \"proto3\";\nmessage A { repeated int32 id = 1; string s = 2; }\n",
|
|
kinds: &[Comment, Keyword, Str, Number, Type],
|
|
},
|
|
Case {
|
|
labels: &["hcl", "tf", "terraform", "tfvars"],
|
|
sample: "# c\nresource \"aws_s3\" \"b\" {\n count = 3\n on = true\n}\n",
|
|
kinds: &[Comment, Keyword, Str, Number, Property],
|
|
},
|
|
Case { labels: &["cmake"], sample: "# c\nif(WIN32)\n set(A \"x\" 3)\nendif()\nmessage(${A})\n", kinds: &[Comment, Keyword, Str, Number, Function] },
|
|
Case {
|
|
labels: &["powershell", "ps1", "pwsh"],
|
|
sample: "# c\nfunction Get-Thing { param($a) if ($a -eq 3) { Write-Host 'x' } }\n",
|
|
kinds: &[Comment, Keyword, Str, Number, Function, Operator],
|
|
},
|
|
Case { labels: &["lua"], sample: "-- c\nlocal x = 3\nif x then print(\"a\") end\n", kinds: &[Comment, Keyword, Str, Number] },
|
|
Case { labels: &["kotlin", "kt"], sample: "// c\nfun main() { val x = 3; println(\"a\") }\n", kinds: &[Comment, Keyword, Str, Number, Function] },
|
|
Case { labels: &["swift"], sample: "// c\nfunc f() -> Int { let x = 3; return x }\nlet s = \"a\"\n", kinds: &[Comment, Keyword, Str, Number, Function] },
|
|
Case { labels: &["dart"], sample: "// c\nvoid main() { var x = 3; print('a'); }\n", kinds: &[Comment, Keyword, Str, Number, Function] },
|
|
Case { labels: &["scala"], sample: "// c\nobject A { val x = 3; def f() = \"a\" }\n", kinds: &[Comment, Keyword, Str, Number] },
|
|
Case { labels: &["r"], sample: "# c\nx <- 3\nif (x > 1) print(\"a\")\n", kinds: &[Comment, Keyword, Str, Number] },
|
|
Case { labels: &["julia", "jl"], sample: "# c\nfunction f(x)\n return x + 3\nend\ns = \"a\"\n", kinds: &[Comment, Keyword, Str, Number, Function] },
|
|
Case { labels: &["zig"], sample: "// c\nconst x = 3;\npub fn main() void { @import(\"std\"); }\n", kinds: &[Comment, Keyword, Str, Number, Function] },
|
|
Case {
|
|
labels: &["elixir", "ex", "exs"],
|
|
sample: "# c\ndefmodule A do\n def f(x), do: x + 3\n @s \"a\"\nend\n",
|
|
kinds: &[Comment, Keyword, Str, Number, Type],
|
|
},
|
|
Case { labels: &["haskell", "hs"], sample: "-- c\nmain :: IO ()\nmain = print (3 + 1)\n", kinds: &[Comment, Number] },
|
|
Case { labels: &["ocaml", "ml"], sample: "(* c *)\nlet x = 3 in print_string \"a\"\n", kinds: &[Comment, Keyword, Str, Number] },
|
|
Case { labels: &["nix"], sample: "# c\nlet x = 3; in { a = \"s\"; b = if true then x else null; }\n", kinds: &[Comment, Keyword, Str, Number, Property] },
|
|
Case { labels: &["vue"], sample: "<!-- c -->\n<div class=\"a\" :id=\"x\">{{ n + 3 }}</div>\n", kinds: &[Comment, Property, Str, Number, Operator] },
|
|
Case { labels: &["svelte"], sample: "<!-- c -->\n<p class=\"a\">{#if ok}{n + 3}{/if}</p>\n", kinds: &[Comment, Property, Str, Number, Operator, Keyword] },
|
|
Case { labels: &["latex", "tex"], sample: "% c\n\\section{A}\n$x$\n", kinds: &[Comment] },
|
|
Case { labels: &["rs", "rust"], sample: "// c\nfn main() {}\n", kinds: &[Comment, Keyword] },
|
|
];
|
|
|
|
/// The kinds that appear anywhere when `body` is highlighted as `label`.
|
|
fn kinds(label: &str, body: &str) -> Vec<TokenKind> {
|
|
let Some(mut highlighter) = Highlighter::new(label, body) else { panic!("{label} should be known") };
|
|
let lines = body.split_inclusive('\n').count();
|
|
highlighter.lines(0..lines).lines.iter().flatten().map(|span| span.kind).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn every_label_and_alias_resolves_and_gets_no_suggestion() {
|
|
for case in CASES {
|
|
for label in case.labels {
|
|
assert!(is_known(label), "{label} is unknown");
|
|
assert_eq!(suggest_language(label), None, "{label}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn samples_classify_the_expected_kinds() {
|
|
let mut missing = Vec::new();
|
|
for case in CASES {
|
|
for label in case.labels {
|
|
let found = kinds(label, case.sample);
|
|
for want in case.kinds {
|
|
if !found.contains(want) {
|
|
missing.push(format!("{label}: {want:?}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert!(missing.is_empty(), "missing kinds: {missing:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn twig_template_text_between_delimiters_stays_plain() {
|
|
let body = "<p>hello</p>{{ a }}";
|
|
let Some(mut highlighter) = Highlighter::new("twig", body) else { panic!("twig is known") };
|
|
let spans = &highlighter.lines(0..1).lines;
|
|
let first = spans.first().and_then(|line| line.first());
|
|
assert!(first.is_none_or(|span| span.start >= "<p>hello</p>".len()), "{spans:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_bodies_return_without_panicking_within_ten_seconds() {
|
|
let start = Instant::now();
|
|
let megabyte_line = "{{ \"".repeat(256 * 1024);
|
|
let megabyte_open = "{% if ".repeat(170_000);
|
|
let unterminated = format!("{{# open\n{}\n'str \"dq `bt \"\"\" ''' <% <# #[[ (* {{{{\n", "x {{ y\n".repeat(200));
|
|
let deep = "{{{{{{{{[[[[((((<%<%".repeat(5_000);
|
|
for case in CASES {
|
|
for body in [megabyte_line.as_str(), megabyte_open.as_str(), unterminated.as_str(), deep.as_str()] {
|
|
let Some(mut highlighter) = Highlighter::new(case.labels[0], body) else { panic!("{} should accept a hostile body", case.labels[0]) };
|
|
let lines = body.split_inclusive('\n').count();
|
|
let _ = highlighter.lines(0..lines);
|
|
}
|
|
}
|
|
assert!(start.elapsed().as_secs() < 10, "hostile bodies took {:?}", start.elapsed());
|
|
}
|