mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 00:07:50 +00:00
feat(editor): highlight JSP pages (#937)
JSP files had no highlighting and showed a `?` icon; the HTML mode left directives, scriptlets, JSP comments and EL as plain text and flagged `<c:out/>` / `<jsp:include/>` as unclosed elements. - A JSP grammar layer (comments, directives, scriptlets, expressions, declarations, EL) with HTML and Java overlays, so markup split by JSP elements still nests and braces match across scriptlets. - EL as its own grammar top: member/index/method calls, namespaced functions, keyword operators, ternaries, lambdas and collection literals. - Block comments use `<%-- --%>` in markup and `//` in scriptlets; JSP comments and scriptlets fold. - `.jsp .jspf .jspx .jsph .tag .tagf .tagx` map to the JSP language with a file icon; `gen:jsp-grammar` regenerates the parser and a test checks it matches the grammar. Closes #870. Co-authored-by: jaw52 <53564781+jaw52@users.noreply.github.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
"target",
|
||||
"src/bindings.ts",
|
||||
"src/editor/core/cm/angular/parser*.ts",
|
||||
"src/editor/core/cm/jsp/parser*.ts",
|
||||
"docs/plugins/themes.md",
|
||||
"pnpm-lock.yaml",
|
||||
"**/*.toml",
|
||||
|
||||
@@ -45,6 +45,7 @@ const BUILTIN: &[(&str, &str)] = &[
|
||||
("javascriptreact", include_str!("../../../languages/javascriptreact.toml")),
|
||||
("json", include_str!("../../../languages/json.toml")),
|
||||
("jsonc", include_str!("../../../languages/jsonc.toml")),
|
||||
("jsp", include_str!("../../../languages/jsp.toml")),
|
||||
("julia", include_str!("../../../languages/julia.toml")),
|
||||
("kotlin", include_str!("../../../languages/kotlin.toml")),
|
||||
("latex", include_str!("../../../languages/latex.toml")),
|
||||
@@ -522,6 +523,7 @@ mod tests {
|
||||
"Vue",
|
||||
"Angular Template",
|
||||
"ERB",
|
||||
"JSP",
|
||||
];
|
||||
|
||||
fn reg() -> LanguageRegistry {
|
||||
@@ -803,6 +805,8 @@ mod tests {
|
||||
("a.less", "less"),
|
||||
("App.vue", "vue"),
|
||||
("App.svelte", "svelte"),
|
||||
("index.jsp", "jsp"),
|
||||
("WEB-INF/tags/card.tag", "jsp"),
|
||||
("package.json", "json"),
|
||||
("tsconfig.json", "jsonc"),
|
||||
("tsconfig.app.json", "jsonc"),
|
||||
|
||||
@@ -201,11 +201,11 @@ impl Syntax {
|
||||
|
||||
/// The grammar web features parse `lang` with: its own for HTML and CSS, the
|
||||
/// CSS grammar for SCSS / Less (selectors and custom properties parse alike),
|
||||
/// HTML for Vue / Svelte single-file components.
|
||||
/// HTML for Vue / Svelte single-file components and JSP pages.
|
||||
fn web_grammar(&self, lang: &LanguageId) -> Option<&Grammar> {
|
||||
let name = match lang.as_str() {
|
||||
"scss" | "less" => "css",
|
||||
"vue" | "svelte" => "html",
|
||||
"vue" | "svelte" | "jsp" => "html",
|
||||
_ => self.grammar(lang).map(|g| g.spec.name)?,
|
||||
};
|
||||
web::is_web_grammar(name).then(|| self.grammar(&LanguageId::new(name))).flatten()
|
||||
|
||||
@@ -5,6 +5,7 @@ const config: KnipConfig = {
|
||||
project: [
|
||||
"src/**/*.{ts,tsx,css}",
|
||||
"!src/editor/core/cm/angular/parser.terms.ts",
|
||||
"!src/editor/core/cm/jsp/parser.terms.ts",
|
||||
"scripts/**/*.ts",
|
||||
"perf/**/*.ts",
|
||||
"e2e/**/*.ts",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "jsp"
|
||||
name = "JSP"
|
||||
extensions = ["jsp", "jspx", "jspf", "jsph", "tag", "tagf", "tagx"]
|
||||
block-comment = ["<%--", "--%>"]
|
||||
indent = { tab-size = 2, insert-spaces = true }
|
||||
ui-mode = "JSP"
|
||||
@@ -19,6 +19,7 @@
|
||||
"typecheck": "tsrs --noEmit && tsrs -p tsconfig.node.json --noEmit && tsrs -p perf --noEmit && tsrs -p scripts --noEmit",
|
||||
"gen:bindings": "cargo test -p openidea --test export_bindings",
|
||||
"gen:angular-grammar": "lezer-generator src/editor/core/cm/angular/angular.grammar --typeScript -o src/editor/core/cm/angular/parser.ts",
|
||||
"gen:jsp-grammar": "lezer-generator src/editor/core/cm/jsp/jsp.grammar --typeScript -o src/editor/core/cm/jsp/parser.ts",
|
||||
"gen:theme-docs": "node scripts/theme/gen-docs.ts",
|
||||
"theme:import-intellij": "node scripts/theme/import-intellij-theme.ts",
|
||||
"dev:app": "node scripts/tauri.ts dev --config src-tauri/tauri.dev.conf.json",
|
||||
@@ -39,6 +40,7 @@
|
||||
"@codemirror/autocomplete": "^6.20.3",
|
||||
"@codemirror/commands": "^6.11.1",
|
||||
"@codemirror/lang-html": "^6.4.12",
|
||||
"@codemirror/lang-java": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/language": "^6.13.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
|
||||
Generated
+3
@@ -17,6 +17,9 @@ importers:
|
||||
'@codemirror/lang-html':
|
||||
specifier: ^6.4.12
|
||||
version: 6.4.12
|
||||
'@codemirror/lang-java':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
'@codemirror/lang-javascript':
|
||||
specifier: ^6.2.5
|
||||
version: 6.2.5
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"jsx",
|
||||
"vue",
|
||||
"svelte",
|
||||
"jsp",
|
||||
"dart",
|
||||
"cs",
|
||||
"fs",
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* JSP pages, layered like IntelliJ's JSP view provider: `jsp.grammar` parses
|
||||
* the JSP layer, and the template text around its elements is one HTML
|
||||
* document. That is an overlay, so markup split by a scriptlet still nests
|
||||
* and `value="<%= id %>"` is an attribute with a hole. The scriptlets are
|
||||
* one Java method body, an overlay too (`<% for (…) { %> … <% } %>` balances
|
||||
* its braces), each expression and declaration Java of its own.
|
||||
*/
|
||||
import { LRLanguage, LanguageSupport, foldInside, foldNodeProp } from "@codemirror/language";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { javaLanguage } from "@codemirror/lang-java";
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
import {
|
||||
Parser,
|
||||
parseMixed,
|
||||
type Input,
|
||||
type NestedParse,
|
||||
type PartialParse,
|
||||
type SyntaxNode,
|
||||
type SyntaxNodeRef,
|
||||
type TreeFragment,
|
||||
} from "@lezer/common";
|
||||
import { JAVA_TAGS } from "../languages";
|
||||
import { parser } from "./parser";
|
||||
|
||||
const baseHTML = html({ selfClosingTags: true });
|
||||
|
||||
const JSP_COMMENT = { block: { open: "<%--", close: "--%>" } };
|
||||
|
||||
const baseParser = parser.configure({
|
||||
props: [
|
||||
styleTags({
|
||||
JspComment: t.blockComment,
|
||||
'"<%" "<%=" "<%!" "<%@" "%>"': t.processingInstruction,
|
||||
// Not `ELExpression/"}"`: the cursor highlighting a page does not match
|
||||
// that context once the expression's content is mounted.
|
||||
'"${" "#{" ELEnd/...': t.special(t.brace),
|
||||
DirectiveName: t.keyword,
|
||||
AttributeName: t.attributeName,
|
||||
AttributeValue: t.attributeValue,
|
||||
Is: t.definitionOperator,
|
||||
VariableName: t.variableName,
|
||||
PropertyName: t.propertyName,
|
||||
"CallExpression/MemberExpression/PropertyName": t.function(t.propertyName),
|
||||
"CallExpression/VariableName FunctionName": t.function(t.variableName),
|
||||
Namespace: t.namespace,
|
||||
Number: t.number,
|
||||
String: t.string,
|
||||
BooleanLiteral: t.bool,
|
||||
null: t.null,
|
||||
"not empty div mod lt gt le ge instanceof eq ne and or": t.operatorKeyword,
|
||||
ArithOp: t.arithmeticOperator,
|
||||
CompareOp: t.compareOperator,
|
||||
LogicOp: t.logicOperator,
|
||||
'"->"': t.operator,
|
||||
'"."': t.derefOperator,
|
||||
"( )": t.paren,
|
||||
"[ ]": t.squareBracket,
|
||||
'SetOrMapLiteral/"{" SetOrMapLiteral/"}"': t.brace,
|
||||
", ;": t.separator,
|
||||
'":"': t.punctuation,
|
||||
}),
|
||||
foldNodeProp.add({
|
||||
"Scriptlet Declaration JspExpression": foldInside,
|
||||
JspComment: (node, state) => ({
|
||||
from: node.from + 4,
|
||||
to: state.sliceDoc(node.to - 4, node.to) == "--%>" ? node.to - 4 : node.to,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const javaParser = javaLanguage.parser.configure({ props: [styleTags(JAVA_TAGS)] });
|
||||
|
||||
/** `input` with a `;` at `at`. */
|
||||
class TerminatedInput implements Input {
|
||||
constructor(
|
||||
private readonly input: Input,
|
||||
private readonly at: number,
|
||||
) {}
|
||||
|
||||
get length() {
|
||||
return this.input.length;
|
||||
}
|
||||
|
||||
get lineChunks() {
|
||||
return this.input.lineChunks;
|
||||
}
|
||||
|
||||
chunk(from: number): string {
|
||||
if (from == this.at) return ";";
|
||||
const text = this.input.chunk(from);
|
||||
return from < this.at && this.at < from + text.length ? text.slice(0, this.at - from) : text;
|
||||
}
|
||||
|
||||
read(from: number, to: number): string {
|
||||
const text = this.input.read(from, to);
|
||||
return from <= this.at && this.at < to
|
||||
? text.slice(0, this.at - from) + ";" + text.slice(this.at - from + 1)
|
||||
: text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Java has no expression top: an expression is parsed as a statement, its
|
||||
* trailing space read as the `;` (`<%= name %>`), so that a lone name is not
|
||||
* a call missing its arguments. Without one, it ends in an error node.
|
||||
*/
|
||||
class ExpressionParser extends Parser {
|
||||
private readonly java = javaParser.configure({});
|
||||
|
||||
createParse(
|
||||
input: Input,
|
||||
fragments: readonly TreeFragment[],
|
||||
ranges: readonly { from: number; to: number }[],
|
||||
): PartialParse {
|
||||
const end = ranges[ranges.length - 1]!.to;
|
||||
const terminated = /\s/.test(input.read(end - 1, end)) ? new TerminatedInput(input, end - 1) : input;
|
||||
return this.java.createParse(terminated, fragments, ranges);
|
||||
}
|
||||
}
|
||||
|
||||
// To reuse old inner trees, parseMixed takes those that the node at a mount's
|
||||
// start and its ancestors had with the same parser, so each kind of mount has
|
||||
// its own instance: an expression at the start of a page would otherwise take
|
||||
// the scriptlets' tree.
|
||||
const htmlMixed: NestedParse = { parser: baseHTML.language.parser, overlay: (node) => node.name == "Text" },
|
||||
scriptletParser = javaParser.configure({}),
|
||||
expressionMixed = { parser: new ExpressionParser() },
|
||||
declarationMixed = { parser: javaParser.configure({ top: "ClassContent" }) },
|
||||
elMixed = { parser: baseParser.configure({ top: "EL" }) };
|
||||
|
||||
/** The code of the scriptlets in `template`. */
|
||||
function scriptletCode(template: SyntaxNode): { from: number; to: number }[] {
|
||||
const code: { from: number; to: number }[] = [];
|
||||
for (const scriptlet of template.getChildren("Scriptlet")) {
|
||||
const java = scriptlet.getChild("JavaCode");
|
||||
if (java) code.push({ from: java.from, to: java.to });
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// The HTML overlay is on the document and the scriptlets' on the template it
|
||||
// holds: a node mounts one tree, and an overlay's predicate does not see the
|
||||
// nodes of an overlay inside it.
|
||||
function mixJsp(node: SyntaxNodeRef): NestedParse | null {
|
||||
switch (node.name) {
|
||||
case "Document":
|
||||
return htmlMixed;
|
||||
case "Template": {
|
||||
const overlay = scriptletCode(node.node);
|
||||
return overlay.length ? { parser: scriptletParser, overlay } : null;
|
||||
}
|
||||
case "JavaCode": {
|
||||
const element = node.node.parent?.name;
|
||||
return element == "JspExpression" ? expressionMixed : element == "Declaration" ? declarationMixed : null;
|
||||
}
|
||||
case "ELContent":
|
||||
return elMixed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const jspLanguage = LRLanguage.define({
|
||||
name: "jsp",
|
||||
parser: baseParser.configure({ wrap: parseMixed(mixJsp) }),
|
||||
languageData: { commentTokens: JSP_COMMENT },
|
||||
});
|
||||
|
||||
export function jsp(): LanguageSupport {
|
||||
return new LanguageSupport(jspLanguage, [
|
||||
baseHTML.support,
|
||||
// Markup comments out of the page, with a JSP comment, as in IntelliJ.
|
||||
baseHTML.language.data.of({ commentTokens: JSP_COMMENT }),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// JSP pages: the JSP layer of a page, as IntelliJ's JSP view provider sees it.
|
||||
// Comments, directives, scripting elements and EL expressions are parsed here;
|
||||
// the template text between them is HTML and the scripting elements' code is
|
||||
// Java, both mounted by `./index.ts`. EL expressions (`${…}`, `#{…}`) are their
|
||||
// own top. Regenerate with `pnpm gen:jsp-grammar`.
|
||||
|
||||
@top Document { Template }
|
||||
|
||||
// The page's content, a node of its own: the HTML overlay is mounted on the
|
||||
// document and the scriptlets' Java overlay on this.
|
||||
Template {
|
||||
(Text | JspComment | Directive | Declaration | JspExpression | Scriptlet | ELExpression)*
|
||||
}
|
||||
|
||||
Scriptlet { "<%" JavaCode? "%>" }
|
||||
|
||||
JspExpression { "<%=" JavaCode? "%>" }
|
||||
|
||||
Declaration { "<%!" JavaCode? "%>" }
|
||||
|
||||
ELExpression { ("${" | "#{") ELContent? ELEnd { "}" } }
|
||||
|
||||
@skip { space } {
|
||||
Directive { "<%@" DirectiveName { name } Attribute* "%>" }
|
||||
|
||||
Attribute { AttributeName { name } (Is AttributeValue)? }
|
||||
|
||||
@top EL { expression? (";" expression?)* }
|
||||
|
||||
expression[@isGroup=Expression] {
|
||||
literal |
|
||||
VariableName |
|
||||
ParenthesizedExpression { "(" (expression ("," expression)*)? ")" } |
|
||||
ListLiteral { "[" (expression ("," expression)*)? "]" } |
|
||||
SetOrMapLiteral { "{" (entry ("," entry)*)? "}" } |
|
||||
MemberExpression { expression !member ("." PropertyName | "[" expression "]") } |
|
||||
CallExpression { expression !call ArgList } |
|
||||
FunctionCall { Namespace ":" FunctionName ArgList } |
|
||||
UnaryExpression { (kw<"not"> | kw<"empty"> | LogicOp<"!"> | ArithOp<"-">) !prefix expression } |
|
||||
BinaryExpression {
|
||||
expression !mult (ArithOp<"*" | "/" | "%"> | kw<"div"> | kw<"mod">) expression |
|
||||
expression !add ArithOp<"+" | "-"> expression |
|
||||
expression !concat ArithOp<"+="> expression |
|
||||
expression !rel (CompareOp<"<" | ">" | "<=" | ">="> | kw<"lt"> | kw<"gt"> | kw<"le"> | kw<"ge"> | kw<"instanceof">) expression |
|
||||
expression !equal (CompareOp<"==" | "!="> | kw<"eq"> | kw<"ne">) expression |
|
||||
expression !and (LogicOp<"&&"> | kw<"and">) expression |
|
||||
expression !or (LogicOp<"||"> | kw<"or">) expression
|
||||
} |
|
||||
ConditionalExpression { expression !ternary LogicOp<"?"> expression LogicOp<":"> expression } |
|
||||
LambdaExpression { expression !lambda "->" expression } |
|
||||
AssignmentExpression { expression !assign Is expression }
|
||||
}
|
||||
|
||||
entry { expression (":" expression)? }
|
||||
|
||||
ArgList { "(" (expression ("," expression)*)? ")" }
|
||||
}
|
||||
|
||||
@precedence {
|
||||
member,
|
||||
call,
|
||||
prefix @right,
|
||||
mult @left,
|
||||
add @left,
|
||||
concat @left,
|
||||
rel @left,
|
||||
equal @left,
|
||||
and @left,
|
||||
or @left,
|
||||
ternary @right,
|
||||
lambda @right,
|
||||
assign @right
|
||||
}
|
||||
|
||||
literal {
|
||||
Number |
|
||||
String |
|
||||
@specialize[@name=BooleanLiteral]<identifier, "true" | "false"> |
|
||||
kw<"null">
|
||||
}
|
||||
|
||||
VariableName { identifier }
|
||||
|
||||
PropertyName { identifier }
|
||||
|
||||
FunctionName { identifier }
|
||||
|
||||
Is { "=" }
|
||||
|
||||
kw<term> { @specialize[@name={term}]<identifier, term> }
|
||||
|
||||
ArithOp<expr> { expr }
|
||||
CompareOp<expr> { expr }
|
||||
LogicOp<expr> { expr }
|
||||
|
||||
// Tried in this order, before the tokens below: a comment before `<%`.
|
||||
@external tokens jspComment from "./tokens" { JspComment }
|
||||
@external tokens text from "./tokens" { Text }
|
||||
@external tokens javaCode from "./tokens" { JavaCode }
|
||||
@external tokens elContent from "./tokens" { ELContent }
|
||||
// `fn` of `fn:join(…)`, so that an identifier before a `:` stays a ternary's.
|
||||
@external tokens namespace from "./tokens" { Namespace }
|
||||
|
||||
@tokens {
|
||||
space { $[ \t\n\r]+ }
|
||||
|
||||
identifier { $[a-zA-Z_$\u{a1}-\u{10ffff}] $[a-zA-Z0-9_$\u{a1}-\u{10ffff}]* }
|
||||
|
||||
// Directive and attribute names: `include`, `pageEncoding`, `name-given`.
|
||||
name { $[a-zA-Z_] $[a-zA-Z0-9_\-]* }
|
||||
|
||||
Number {
|
||||
(@digit+ ("." @digit*)? | "." @digit+) ($[eE] $[+\-]? @digit+)?
|
||||
}
|
||||
|
||||
String {
|
||||
'"' (!["\\] | "\\" _)* '"'? |
|
||||
"'" (!['\\] | "\\" _)* "'"?
|
||||
}
|
||||
|
||||
AttributeValue {
|
||||
'"' !["]* '"'? |
|
||||
"'" ![']* "'"?
|
||||
}
|
||||
|
||||
@precedence { Number, "." }
|
||||
|
||||
"<%" "<%=" "<%!" "<%@" "%>" "${" "#{"
|
||||
"(" ")" "[" "]" "{" "}" "." "," ";" ":" "->"
|
||||
}
|
||||
|
||||
@detectDelim
|
||||
@@ -0,0 +1,252 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { ensureSyntaxTree, foldable, matchBrackets } from "@codemirror/language";
|
||||
import type { Tree } from "@lezer/common";
|
||||
import { buildParserFile } from "@lezer/generator";
|
||||
import { highlightTree, tagHighlighter, tags as t } from "@lezer/highlight";
|
||||
import { findLanguageDescription, loadSupport } from "../languages";
|
||||
import { jsp, jspLanguage } from "./index";
|
||||
|
||||
const highlighter = tagHighlighter([
|
||||
{ tag: t.processingInstruction, class: "pi" },
|
||||
{ tag: t.special(t.brace), class: "el" },
|
||||
{ tag: t.blockComment, class: "comment" },
|
||||
{ tag: t.operatorKeyword, class: "opkw" },
|
||||
{ tag: t.keyword, class: "keyword" },
|
||||
{ tag: t.bool, class: "bool" },
|
||||
{ tag: t.namespace, class: "ns" },
|
||||
{ tag: t.typeName, class: "type" },
|
||||
{ tag: t.definition(t.variableName), class: "def" },
|
||||
{ tag: t.function(t.variableName), class: "call" },
|
||||
{ tag: t.function(t.propertyName), class: "method" },
|
||||
{ tag: t.variableName, class: "var" },
|
||||
{ tag: t.propertyName, class: "prop" },
|
||||
{ tag: t.attributeName, class: "attr" },
|
||||
{ tag: t.attributeValue, class: "value" },
|
||||
{ tag: t.string, class: "str" },
|
||||
{ tag: t.number, class: "num" },
|
||||
{ tag: t.tagName, class: "tag" },
|
||||
]);
|
||||
|
||||
/** `text:class` of the styled tokens of `doc`, from `tree` or a fresh parse. */
|
||||
function tokens(doc: string, tree: Tree = jspLanguage.parser.parse(doc)): string[] {
|
||||
const out: string[] = [];
|
||||
highlightTree(tree, highlighter, (from, to, cls) => out.push(`${doc.slice(from, to)}:${cls}`));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The error nodes of `doc`'s tree, with the text they start. */
|
||||
function errors(doc: string): string[] {
|
||||
const out: string[] = [];
|
||||
jspLanguage.parser.parse(doc).iterate({
|
||||
enter: (node) => void (node.type.isError && out.push(doc.slice(node.from, node.from + 8))),
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The tree of `state`, parsed to the end. */
|
||||
function stateTree(state: EditorState): Tree {
|
||||
const tree = ensureSyntaxTree(state, state.doc.length, 5000);
|
||||
if (!tree) throw new Error("parse did not finish");
|
||||
return tree;
|
||||
}
|
||||
|
||||
/** A ~8 KB page with directives, scriptlets around markup, expressions and EL. */
|
||||
function page(): string {
|
||||
let doc = `<%@ page contentType="text/html;charset=UTF-8" %>\n<%@ taglib prefix="c" uri="jakarta.tags.core" %>\n`;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
doc +=
|
||||
`<%-- row ${i} --%>\n<% if (rows${i} != null) { %>\n` +
|
||||
` <tr class="r${i}"><td><%= rows${i}.size() %></td><td>\${fn:length(names${i})}</td></tr>\n` +
|
||||
`<% } %>\n<c:out value="\${user${i}.name}"/>\n`;
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
describe("JSP pages", () => {
|
||||
it("highlights directives and JSP comments", () => {
|
||||
expect(tokens(`<%@ page import="java.util.*" %><%-- <b>off</b> --%>`)).toEqual([
|
||||
"<%@:pi",
|
||||
"page:keyword",
|
||||
"import:attr",
|
||||
'"java.util.*":value',
|
||||
"%>:pi",
|
||||
"<%-- <b>off</b> --%>:comment",
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses the scriptlets as one Java body, around the markup", () => {
|
||||
const doc = `<% for (String s : list) { %><li><%= s %></li><% } %>`;
|
||||
expect(errors(doc)).toEqual([]);
|
||||
expect(tokens(doc)).toEqual([
|
||||
"<%:pi",
|
||||
"for:keyword",
|
||||
"String:type",
|
||||
"s:def",
|
||||
"list:var",
|
||||
"%>:pi",
|
||||
"li:tag",
|
||||
"<%=:pi",
|
||||
"s:var",
|
||||
"%>:pi",
|
||||
"li:tag",
|
||||
"<%:pi",
|
||||
"%>:pi",
|
||||
]);
|
||||
const state = EditorState.create({ doc, extensions: jsp() });
|
||||
stateTree(state);
|
||||
const open = doc.indexOf("{");
|
||||
expect(matchBrackets(state, open, 1)?.end?.from).toBe(doc.lastIndexOf("}"));
|
||||
});
|
||||
|
||||
it("keeps markup in a scriptlet's strings out of the page", () => {
|
||||
const doc = `<% String cell = "<td>"; %><p>x</p>`;
|
||||
expect(errors(doc)).toEqual([]);
|
||||
expect(tokens(doc)).toContain('"<td>":str');
|
||||
expect(tokens(doc).filter((s) => s.endsWith(":tag"))).toEqual(["p:tag", "p:tag"]);
|
||||
});
|
||||
|
||||
it("highlights expressions and declarations as Java", () => {
|
||||
const doc = `<%= name %><%= req.getParameter("q") %><%! private int count = 0; %>`;
|
||||
expect(errors(doc)).toEqual([]);
|
||||
expect(tokens(doc)).toEqual([
|
||||
"<%=:pi",
|
||||
"name:var",
|
||||
"%>:pi",
|
||||
"<%=:pi",
|
||||
"req:var",
|
||||
"getParameter:call",
|
||||
'"q":str',
|
||||
"%>:pi",
|
||||
"<%!:pi",
|
||||
"private:keyword",
|
||||
"int:type",
|
||||
"count:def",
|
||||
"0:num",
|
||||
"%>:pi",
|
||||
]);
|
||||
});
|
||||
|
||||
it("highlights EL expressions", () => {
|
||||
const doc = `<p>\${fn:escapeXml(item.title)} #{a ? b : not empty c.d[0] and x.size() gt 2}</p>`;
|
||||
expect(errors(doc)).toEqual([]);
|
||||
expect(tokens(doc)).toEqual([
|
||||
"p:tag",
|
||||
"${:el",
|
||||
"fn:ns",
|
||||
"escapeXml:call",
|
||||
"item:var",
|
||||
"title:prop",
|
||||
"}:el",
|
||||
"#{:el",
|
||||
"a:var",
|
||||
"b:var",
|
||||
"not:opkw",
|
||||
"empty:opkw",
|
||||
"c:var",
|
||||
"d:prop",
|
||||
"0:num",
|
||||
"and:opkw",
|
||||
"x:var",
|
||||
"size:method",
|
||||
"gt:opkw",
|
||||
"2:num",
|
||||
"}:el",
|
||||
"p:tag",
|
||||
]);
|
||||
});
|
||||
|
||||
it("ends an EL expression at its own brace, past nested ones and strings", () => {
|
||||
const doc = `\${ {'k': '}'}.k == true }<b>`;
|
||||
expect(errors(doc)).toEqual([]);
|
||||
expect(tokens(doc)).toEqual(["${:el", "'k':str", "'}':str", "k:prop", "true:bool", "}:el", "b:tag"]);
|
||||
});
|
||||
|
||||
it("parses markup with elements in attribute values and custom tags", () => {
|
||||
const doc =
|
||||
`<c:forEach var="i" items="\${items}"><c:out value="\${i}"/></c:forEach>` +
|
||||
`<input value="<%= x %>" onclick="go('<%= id %>')"><jsp:include page="f.jspf"/><p></p>`;
|
||||
expect(errors(doc)).toEqual([]);
|
||||
expect(tokens(doc).slice(0, 8)).toEqual([
|
||||
"c:forEach:tag",
|
||||
"var:attr",
|
||||
'"i":value',
|
||||
"items:attr",
|
||||
'":value',
|
||||
"${:el",
|
||||
"items:var",
|
||||
"}:el",
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses EL in a script as EL, like the container", () => {
|
||||
expect(tokens(`<script>let n = \`\${user.name}\`;</script>`)).toContain("user:var");
|
||||
});
|
||||
|
||||
it("keeps escaped elements as text", () => {
|
||||
expect(tokens(`<p>\\\${a} <\\% b %></p>`)).toEqual(["p:tag", "p:tag"]);
|
||||
});
|
||||
|
||||
it("comments with JSP comments in the markup and Java ones in scriptlets", () => {
|
||||
const doc = `<p>x</p><% int a; %>`;
|
||||
const state = EditorState.create({ doc, extensions: jsp() });
|
||||
stateTree(state);
|
||||
const at = (pos: number) =>
|
||||
state.languageDataAt<{ line?: string; block?: { open: string } }>("commentTokens", pos)[0];
|
||||
expect(at(4)?.block?.open).toBe("<%--");
|
||||
expect(at(doc.indexOf("int") + 1)?.line).toBe("//");
|
||||
});
|
||||
|
||||
it("folds JSP comments and scriptlets", () => {
|
||||
const doc = `<%--\n a\n--%>\n<%\n int a;\n%>`;
|
||||
const state = EditorState.create({ doc, extensions: jsp() });
|
||||
stateTree(state);
|
||||
expect(foldable(state, 0, 4)).toEqual({ from: 4, to: doc.indexOf("--%>") });
|
||||
const line = state.doc.line(4);
|
||||
expect(foldable(state, line.from, line.to)).toEqual({ from: line.to, to: doc.lastIndexOf("%>") });
|
||||
});
|
||||
|
||||
it("highlights like a fresh parse after resuming and editing", () => {
|
||||
const doc = page();
|
||||
// Opening parses the first 3000 characters; the rest resumes from that tree.
|
||||
let state = EditorState.create({ doc, extensions: jsp() });
|
||||
expect(tokens(doc, stateTree(state))).toEqual(tokens(doc));
|
||||
const at = (text: string, n = 1) => {
|
||||
let pos = -1;
|
||||
for (let i = 0; i < n; i++) pos = state.doc.toString().indexOf(text, pos + 1);
|
||||
return pos;
|
||||
};
|
||||
const edits = [
|
||||
() => ({ from: at("rows12.size()") + 4, insert: "x" }),
|
||||
() => ({ from: at(".size() %>", 15) + 7, insert: "+1" }),
|
||||
() => ({ from: at("<% } %>", 20), insert: "<% int k = 0; %>\n" }),
|
||||
() => ({ from: at("<%-- row 7"), to: at("<%-- row 7") + 4 }),
|
||||
() => ({ from: at("names3"), to: at("names3") + 6, insert: "a.b[0]" }),
|
||||
() => ({ from: 0, insert: "${top}\n" }),
|
||||
() => ({ from: at("<tr", 10), insert: "<%= " }),
|
||||
];
|
||||
for (const edit of edits) {
|
||||
state = state.update({ changes: edit() }).state;
|
||||
const text = state.doc.toString();
|
||||
expect(tokens(text, stateTree(state))).toEqual(tokens(text));
|
||||
}
|
||||
});
|
||||
|
||||
it("is generated from jsp.grammar", () => {
|
||||
const dir = import.meta.dirname;
|
||||
const { parser, terms } = buildParserFile(readFileSync(resolve(dir, "jsp.grammar"), "utf8"), {
|
||||
fileName: "src/editor/core/cm/jsp/jsp.grammar",
|
||||
moduleStyle: "es",
|
||||
typeScript: true,
|
||||
});
|
||||
expect(parser).toBe(readFileSync(resolve(dir, "parser.ts"), "utf8"));
|
||||
expect(terms).toBe(readFileSync(resolve(dir, "parser.terms.ts"), "utf8"));
|
||||
});
|
||||
|
||||
it("is the JSP mode", async () => {
|
||||
const desc = findLanguageDescription("JSP", "/p/index.jsp");
|
||||
expect((await loadSupport(desc!))?.language).toBe(jspLanguage);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
JspComment = 1,
|
||||
Text = 2,
|
||||
JavaCode = 3,
|
||||
ELContent = 4,
|
||||
Namespace = 5,
|
||||
Document = 6,
|
||||
Template = 7,
|
||||
Directive = 10,
|
||||
Attribute = 12,
|
||||
Is = 14,
|
||||
AttributeValue = 15,
|
||||
Declaration = 17,
|
||||
JspExpression = 19,
|
||||
Scriptlet = 21,
|
||||
ELExpression = 22,
|
||||
EL = 27,
|
||||
Number = 28,
|
||||
String = 29,
|
||||
BooleanLiteral = 30,
|
||||
_null = 31,
|
||||
VariableName = 32,
|
||||
PropertyName = 45,
|
||||
ArgList = 47,
|
||||
FunctionName = 49,
|
||||
not = 51,
|
||||
empty = 52,
|
||||
div = 57,
|
||||
mod = 58,
|
||||
lt = 62,
|
||||
gt = 63,
|
||||
le = 64,
|
||||
ge = 65,
|
||||
_instanceof = 66,
|
||||
eq = 68,
|
||||
ne = 69,
|
||||
and = 71,
|
||||
or = 73
|
||||
@@ -0,0 +1,24 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {jspComment, text, javaCode, elContent, namespace} from "./tokens"
|
||||
const spec_identifier = {__proto__:null,true:60, false:60, null:62, not:102, empty:104, div:114, mod:116, lt:124, gt:126, le:128, ge:130, instanceof:132, eq:136, ne:138, and:142, or:146}
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "1WOqOsOOQ!sQ!rOOO!zQpO'#CfO#POtO'#CmO#XOtO'#CoO#aOtO'#CqO#iOxO'#CrOOOR'#EP'#EPO#qOsO'#CcQOOOOOOOQO'#E['#E[OOQO'#C|'#C|O#xQ!rO'#DPO$PQ!rO'#DTO$WQ!rO'#DVQ&bQ#tOOO&iQpO'#D^OOQ`'#Dc'#DcOOQ`'#Dd'#DdOxQ!rO'#D`OOQO'#EZ'#EZO&nQ!rO'#ETQ&xQpOOOOQO'#Cg'#CgO&}QpO,59QOOOR,59X,59XO'VOpO,59XOOOR,59Z,59ZO'[OpO,59ZOOOR,59],59]O'aOpO,59]OOOR'#Cu'#CuOOOR,59^,59^O#lOpO,59^OOOR-E7}-E7}OOQO,59k,59kO'fQ#tO,59kOOQO,59o,59oO'pQ#tO,59oO'zQ#tO'#E^OOQO,59q,59qO(XQpO,59qOOQ`'#Cj'#CjO(aQ!bO,59sOxQ!rO,59sO(fQ!rO'#D]OOQO,59v,59vOOQ`'#Df'#DfOOQ`'#Di'#DiOOQ`'#Dj'#DjOOQ`'#Dk'#DkOOQ`'#Dq'#DqOOQ`'#Dt'#DtOOQ`'#Dv'#DvOxQ!rO,5:POxQ!rO,5:POxQ!rO,5:POxQ!rO,5:POxQ!rO,5:POxQ!rO,5:POxQ!rO,5:POOQ`'#Dy'#DyOxQ!rO,5:dOxQ!rO,5:gOxQ!rO,5:iO(mQ!bO,59xO(rQ#tO,59zO+[Q#tO,5:oOOQO-E8R-E8ROOQO'#Ci'#CiO+fQpO'#ChOOQO'#EQ'#EQO+qQpO1G.lOOOR1G.l1G.lOOOR1G.s1G.sOOOR1G.u1G.uOOOR1G.w1G.wOOOR1G.x1G.xOxQ!rO'#ERO+yQpO1G/VOOQO1G/V1G/VO,RQpO1G/ZOOQO1G/Z1G/ZOxQ!rO,5:xOxQ!rO'#ESO,ZQpO1G/]OOQO1G/]1G/]OOQO'#DZ'#DZOOQO1G/_1G/_O,cQ#tO1G/_O,jQ#tO,59wOOQO,59w,59wO.}Q#tO1G/kO/bQ#tO1G/kO2[Q#tO1G/kO2cQ#tO1G/kO4vQ#tO1G/kO5WQ#tO1G/kO7[Q#tO1G/kO8UQ#tO1G0OO8]Q#tO1G0RO9VQ#tO1G0TOOQO'#D_'#D_O9pQpO1G/dO9uQpO,59SOOQO-E8O-E8OOOOR7+$W7+$WO9zQ#tO,5:mOOQO-E8P-E8POOQO7+$q7+$qOOQO7+$u7+$uO:XQ#tO1G0dOOQO,5:n,5:nOOQO-E8Q-E8QOOQO7+$w7+$wOOQO7+$y7+$yO:cQpO1G/cOOQO1G/c1G/cOOQ`'#Dz'#DzOxQ!rO7+%jOOQO7+%O7+%OOOQO1G.n1G.nOOQO7+$}7+$}O:kQ#tO<<IU",
|
||||
stateData: ";h~O!zOS~OPVOQVOXQO`RObSOdTOgUOhUO~O!xVP~PVOT`OlYOmYOnYOoYOr[Ov]Ox^O!TcO!UcO#PZO#RaO#SbO~O!reO~PxO!{gO~ORjOWiO~ORlOWkO~ORnOWmO~OSqOjoO~O!xVX~PVOqsO~PxOuuO~PxOjxO~PxOr}Ov|O|{O!Z!WO![!WO!`!ZO!a!ZO!b!ZO!c!ZO!d!ZO!f![O!g![O!i!]O!k!^O!p!aO!|zO#S!QO#T!PO#U!PO#V!PO#W!QO#X!RO#Y!SO#Z!SO#[!SO#]!SO#^!TO#_!TO#`!UO#a!VO#b!_O~O!reO~P$_Oz!cO~O!r!wX!x!wX~PxO!reO~OW!kO!{!gO~OW!lO~OW!mO~OW!nO~Oq!rOt!pO~P$_Ot!pOu!tO~P$_Oz!uOj#QXt#QX~P$_Oj!xOt!vO~O#P!yO~Oq!}O~PxO#P#YO~Or}Ov|O|{O!Z!Sa![!Sa!`!Sa!a!Sa!b!Sa!c!Sa!d!Sa!f!Sa!g!Sa!i!Sa!k!Sa!p!Sa!r!Sa!x!Sa!|!Sa#S!Sa#T!Sa#U!Sa#V!Sa#W!Sa#X!Sa#Y!Sa#Z!Sa#[!Sa#]!Sa#^!Sa#_!Sa#`!Sa#a!Sa#b!Saq!Sat!Sau!Saj!Saz!Sa~O!r!wa!x!wa~P$_O!|zOW[X!{[X~OW#^O!{!gO~Oq#aOt!pO~Ot!pOu#bO~Oj#fOt!vO~Ou#gO~P$_Oq#iOt!pO~P$_Or}Ov|O|{O!`!Xi!a!Xi!b!Xi!c!Xi!d!Xi!f!Xi!g!Xi!i!Xi!k!Xi!p!Xi!r!Xi!x!Xi!|!Xi#S!Xi#W!Xi#X!Xi#Y!Xi#Z!Xi#[!Xi#]!Xi#^!Xi#_!Xi#`!Xi#a!Xi#b!Xiq!Xit!Xiu!Xij!Xiz!Xi~O!Z!Xi![!Xi#T!Xi#U!Xi#V!Xi~P,tO!Z!WO![!WO#T!PO#U!PO#V!PO~P,tOr}Ov|O|{O!Z!WO![!WO#S!QO#T!PO#U!PO#V!PO#W!QO!`!Xi!a!Xi!b!Xi!c!Xi!d!Xi!f!Xi!g!Xi!i!Xi!k!Xi!p!Xi!r!Xi!x!Xi!|!Xi#Y!Xi#Z!Xi#[!Xi#]!Xi#^!Xi#_!Xi#`!Xi#a!Xi#b!Xiq!Xit!Xiu!Xij!Xiz!Xi~O#X!Xi~P/uO#X!RO~P/uOr}Ov|O|{O!Z!WO![!WO!`!ZO!a!ZO!b!ZO!c!ZO!d!ZO#S!QO#T!PO#U!PO#V!PO#W!QO#X!RO#Y!SO#Z!SO#[!SO#]!SO!i!Xi!k!Xi!p!Xi!r!Xi!x!Xi!|!Xi#`!Xi#a!Xi#b!Xiq!Xit!Xiu!Xij!Xiz!Xi~O!f!Xi!g!Xi#^!Xi#_!Xi~P2jO!f![O!g![O#^!TO#_!TO~P2jOr}Ov|O|{O!Z!WO![!WO!`!ZO!a!ZO!b!ZO!c!ZO!d!ZO!f![O!g![O!i!]O#S!QO#T!PO#U!PO#V!PO#W!QO#X!RO#Y!SO#Z!SO#[!SO#]!SO#^!TO#_!TO#`!UO~O!k!Xi!p!Xi!r!Xi!x!Xi!|!Xi#a!Xi#b!Xiq!Xit!Xiu!Xij!Xiz!Xi~P5hOz#jO~P$_O!k!^O!p!aO#a!VO#b!_O!r!oi!x!oi!|!oiq!oit!oiu!oij!oiz!oi~P5hO!r!qi!x!qiq!qit!qiu!qij!qiz!qi~P$_Or}O~O_#mO~Oq!uat!uau!ua~P$_Oj#Qit#Qi~P$_Oq#nOt!pO~O!k!^O#a!VO#b!_O!p!ly!r!ly!x!ly!|!lyq!lyt!lyu!lyj!lyz!ly~P5hOl|~",
|
||||
goto: "+X#RPPPPPPP#SPP#V#Z#^#b#fPP#VP#VP#V#VPP#vPPPPPP#|PP#|PPP#|P#|P#|P$f#|$i#|%T#|PP%W%W#|%pPP&V&k'OPPPPP'bPP'sP(TP#|(d(s#|P#|P(v(|)S)b)hPPPPP)o#|P+RRXOTVOWRhQT!ih!jT!hh!jf!b_tvw!e!{!|#V#X#_#cR#[!hQpUR!oq}dP[]^ce|}!W!X!Y!Z![!]!^!`!a!b!p!u!v#kR!z{z!O_tvw!d!e!{!|#O#P#Q#R#S#T#U#V#W#X#_#c#oR#l#ZR#Z!c}cP[]^ce|}!W!X!Y!Z![!]!^!`!a!b!p!u!v#kw!W_tvw!e!{!|#P#Q#R#S#T#U#V#W#X#_#c#ou!X_tvw!e!{!|#Q#R#S#T#U#V#W#X#_#c#os!Y_tvw!e!{!|#R#S#T#U#V#W#X#_#c#oq!Z_tvw!e!{!|#S#T#U#V#W#X#_#c#oo![_tvw!e!{!|#T#U#V#W#X#_#c#om!]_tvw!e!{!|#U#V#W#X#_#c#ok!^_tvw!e!{!|#V#W#X#_#c#ok!`_tvw!e!{!|#V#W#X#_#c#oR#k#VQWORrWQ!jhR#]!jQ!qtQ!svU#`!q!s#hR#h!|Q!wyR#e!wSfP_R!ffQ_PQt[Qv]Sw^!vQ!dcQ!eeQ!{|Q!|}Q#O!WQ#P!XQ#Q!YQ#R!ZQ#S![Q#T!]Q#U!^Q#V!`Q#W!aQ#X!bQ#_!pQ#c!uR#o#kQy^R#d!v",
|
||||
nodeNames: "⚠ JspComment Text JavaCode ELContent Namespace Document Template %> <%@ Directive DirectiveName Attribute AttributeName Is AttributeValue <%! Declaration <%= JspExpression <% Scriptlet ELExpression ${ #{ ELEnd } EL Number String BooleanLiteral null VariableName ) ( ParenthesizedExpression , ] [ ListLiteral { SetOrMapLiteral : MemberExpression . PropertyName CallExpression ArgList FunctionCall FunctionName UnaryExpression not empty LogicOp ArithOp BinaryExpression ArithOp div mod ArithOp ArithOp CompareOp lt gt le ge instanceof CompareOp eq ne LogicOp and LogicOp or ConditionalExpression LogicOp LogicOp LambdaExpression -> AssignmentExpression ;",
|
||||
maxTerm: 110,
|
||||
nodeProps: [
|
||||
["openedBy", 8,"<%@ <%! <%= <%",26,"{",33,"(",37,"["],
|
||||
["closedBy", -4,9,16,18,20,"%>",34,")",38,"]",40,"}"],
|
||||
["group", -16,28,29,30,31,32,35,39,41,43,46,48,50,55,74,77,79,"Expression"]
|
||||
],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 5,
|
||||
tokenData: "5S~RuXY#fYZ#f]^#fpq#fqr#wrs$Ust(etu(puv*[vw*iwx*txy.wyz.|z{/R{|/W|}/e}!O/j!O!P/w!P!Q0y!Q![1O![!]1a!]!^1f!^!_1k!_!`2j!`!a2w!a!b3U!c!}3Z!}#O4c#P#Q4h#R#S3Z#T#o3Z#o#p4m#p#q4r#q#r4}$g;'S)b;'S;=`*P<%lO)b~#kS!z~XY#fYZ#f]^#fpq#fU#|P#RQ!_!`$PS$UO#_SR$]V_PmQOr$Urs$rs#O$U#O#P$y#P;'S$U;'S;=`(_<%lO$UR$yO_PmQR%OU_POr$Urs%bs;'S$U;'S;=`'p;=`<%l&O<%lO$UR%iV_PmQOr&Ors&js#O&O#O#P&o#P;'S&O;'S;=`'j<%lO&OQ&TVmQOr&Ors&js#O&O#O#P&o#P;'S&O;'S;=`'j<%lO&OQ&oOmQQ&rRO;'S&O;'S;=`&{;=`O&OQ'QWmQOr&Ors&js#O&O#O#P&o#P;'S&O;'S;=`'j;=`<%l&O<%lO&OQ'mP;=`<%l&OR'uWmQOr&Ors&js#O&O#O#P&o#P;'S&O;'S;=`'j;=`<%l$U<%lO&OR(bP;=`<%l$U~(hP#o#p(k~(pOh~V(uX#PUtu)b!Q![)b!c!})b#R#S)b#T#o)b#o#p*V$g;'S)b;'S;=`*P<%lO)bU)gW#PUtu)b!Q![)b!c!})b#R#S)b#T#o)b$g;'S)b;'S;=`*P<%lO)bU*SP;=`<%l)bP*[OgPT*aP#VS!`!a*dP*iOWP~*lPvw*o~*tO#`~R*{V_PmQOw*twx$rx#O*t#O#P+b#P;'S*t;'S;=`.q<%lO*tR+gU_POw*twx+yx;'S*t;'S;=`.S;=`<%l,g<%lO*tR,QV_PmQOw,gwx&jx#O,g#O#P-R#P;'S,g;'S;=`-|<%lO,gQ,lVmQOw,gwx&jx#O,g#O#P-R#P;'S,g;'S;=`-|<%lO,gQ-URO;'S,g;'S;=`-_;=`O,gQ-dWmQOw,gwx&jx#O,g#O#P-R#P;'S,g;'S;=`-|;=`<%l,g<%lO,gQ.PP;=`<%l,gR.XWmQOw,gwx&jx#O,g#O#P-R#P;'S,g;'S;=`-|;=`<%l*t<%lO,gR.tP;=`<%l*t~.|Or~~/ROq~~/WO#T~~/]P#W~!_!`/`~/eO#X~~/jOt~~/oP#S~!`!a/r~/wO!p~~/|P|~!Q![0P~0URl~!Q![0P!g!h0_#X#Y0_~0bR{|0k}!O0k!Q![0q~0nP!Q![0q~0vPl~!Q![0q~1OO#U~~1TSl~!O!P0P!Q![1O!g!h0_#X#Y0_~1fOz~~1kO!r~~1pQ#YSuv1v!_!`2eP1{RdPqr2U!_!`2Z!b!c2`P2ZO`PP2`ObPP2eOXP~2jO#[~~2oP!|~!_!`2r~2wO#^~~2|P#Z~!_!`3P~3UO#]~~3ZO#b~V3bX#PU!{Ptu)b}!O3}!Q![3Z!c!}3Z#R#S3Z#T#o3Z$g;'S)b;'S;=`*P<%lO)bP4ST!{P}!O3}!Q![3}!c!}3}#R#S3}#T#o3}~4hOv~~4mOu~~4rOx~~4uP#p#q4x~4}O#a~~5SOj~",
|
||||
tokenizers: [jspComment, text, javaCode, elContent, namespace, 0, 1, 2],
|
||||
topRules: {"Document":[0,6],"EL":[1,27]},
|
||||
specialized: [{term: 93, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
|
||||
tokenPrec: 1217
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* External tokens of `jsp.grammar`: the runs the JSP layer leaves to HTML and
|
||||
* Java, the comments, and the extent of EL expressions.
|
||||
*/
|
||||
import { ExternalTokenizer, type InputStream } from "@lezer/lr";
|
||||
import { ELContent, JavaCode, JspComment, Namespace, Text } from "./parser.terms";
|
||||
|
||||
const DOUBLE_QUOTE = 34,
|
||||
HASH = 35,
|
||||
DOLLAR = 36,
|
||||
PERCENT = 37,
|
||||
SINGLE_QUOTE = 39,
|
||||
PAREN_L = 40,
|
||||
DASH = 45,
|
||||
COLON = 58,
|
||||
LT = 60,
|
||||
GT = 62,
|
||||
BACKSLASH = 92,
|
||||
UNDERSCORE = 95,
|
||||
BRACE_L = 123,
|
||||
BRACE_R = 125;
|
||||
|
||||
/** Whether `<%` starts `offset` code units ahead. */
|
||||
function scriptStart(input: InputStream, offset = 0): boolean {
|
||||
return input.peek(offset) == LT && input.peek(offset + 1) == PERCENT;
|
||||
}
|
||||
|
||||
/** Whether `${` or `#{` starts `offset` code units ahead. */
|
||||
function elStart(input: InputStream, offset = 0): boolean {
|
||||
const ch = input.peek(offset);
|
||||
return (ch == DOLLAR || ch == HASH) && input.peek(offset + 1) == BRACE_L;
|
||||
}
|
||||
|
||||
/** `<%-- … --%>`, to the end of the page when unterminated. */
|
||||
export const jspComment = new ExternalTokenizer((input) => {
|
||||
if (!scriptStart(input) || input.peek(2) != DASH || input.peek(3) != DASH) return;
|
||||
input.advance(4);
|
||||
while (input.next >= 0) {
|
||||
if (input.next == DASH && input.peek(1) == DASH && input.peek(2) == PERCENT && input.peek(3) == GT) {
|
||||
input.advance(4);
|
||||
break;
|
||||
}
|
||||
input.advance();
|
||||
}
|
||||
input.acceptToken(JspComment);
|
||||
});
|
||||
|
||||
/**
|
||||
* Template text: up to a scripting element or an EL expression. Escaped ones
|
||||
* (`<\%`, `\${`, `\#{`) are text.
|
||||
*/
|
||||
export const text = new ExternalTokenizer((input) => {
|
||||
const start = input.pos;
|
||||
while (input.next >= 0 && !scriptStart(input) && !elStart(input)) {
|
||||
input.advance(input.next == BACKSLASH && elStart(input, 1) ? 2 : 1);
|
||||
}
|
||||
if (input.pos > start) input.acceptToken(Text);
|
||||
});
|
||||
|
||||
/** The code of a scripting element, up to its `%>` (`%\>` quotes one). */
|
||||
export const javaCode = new ExternalTokenizer((input) => {
|
||||
const start = input.pos;
|
||||
while (input.next >= 0 && !(input.next == PERCENT && input.peek(1) == GT)) {
|
||||
input.advance(input.next == PERCENT && input.peek(1) == BACKSLASH && input.peek(2) == GT ? 3 : 1);
|
||||
}
|
||||
if (input.pos > start) input.acceptToken(JavaCode);
|
||||
});
|
||||
|
||||
/** An EL expression's content: up to the `}` closing it, past nested braces and strings. */
|
||||
export const elContent = new ExternalTokenizer((input) => {
|
||||
const start = input.pos;
|
||||
let depth = 0;
|
||||
for (let ch; (ch = input.next) >= 0; input.advance()) {
|
||||
if (ch == BRACE_R) {
|
||||
if (!depth) break;
|
||||
depth--;
|
||||
} else if (ch == BRACE_L) {
|
||||
depth++;
|
||||
} else if (ch == DOUBLE_QUOTE || ch == SINGLE_QUOTE) {
|
||||
input.advance();
|
||||
while (input.next >= 0 && input.next != ch) input.advance(input.next == BACKSLASH ? 2 : 1);
|
||||
if (input.next < 0) break;
|
||||
}
|
||||
}
|
||||
if (input.pos > start) input.acceptToken(ELContent);
|
||||
});
|
||||
|
||||
const isIdentifierStart = (ch: number) =>
|
||||
(ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122) || ch == UNDERSCORE || ch == DOLLAR || ch >= 0xa1;
|
||||
const isIdentifier = (ch: number) => isIdentifierStart(ch) || (ch >= 48 && ch <= 57);
|
||||
|
||||
/** An identifier followed by `:name(`, a function's namespace (`fn:join(…)`). */
|
||||
export const namespace = new ExternalTokenizer((input, stack) => {
|
||||
if (!isIdentifierStart(input.next) || !stack.canShift(Namespace)) return;
|
||||
let i = 1;
|
||||
while (isIdentifier(input.peek(i))) i++;
|
||||
const end = i;
|
||||
if (input.peek(i++) != COLON || !isIdentifierStart(input.peek(i++))) return;
|
||||
while (isIdentifier(input.peek(i))) i++;
|
||||
if (input.peek(i) == PAREN_L) input.acceptToken(Namespace, end);
|
||||
});
|
||||
@@ -102,6 +102,7 @@ const OWN_MODES = [
|
||||
extensions: ["erb", "rhtml"],
|
||||
load: () => import("./erb").then((m) => m.erb()),
|
||||
}),
|
||||
LanguageDescription.of({ name: "JSP", load: () => import("./jsp").then((m) => m.jsp()) }),
|
||||
];
|
||||
|
||||
const BATCH_KEYWORDS = new Set([
|
||||
@@ -222,14 +223,18 @@ const CSS_TAGS: Parameters<typeof styleTags>[0] = {
|
||||
// @lezer/javascript styles `.` as a deref operator, but not `?.`.
|
||||
const JS_TAGS: Parameters<typeof styleTags>[0] = { "?.": t.derefOperator };
|
||||
|
||||
// @lezer/java tags annotations as plain identifiers.
|
||||
export const JAVA_TAGS: Parameters<typeof styleTags>[0] = {
|
||||
"MarkerAnnotation/Identifier Annotation/Identifier MarkerAnnotation Annotation": t.annotation,
|
||||
};
|
||||
|
||||
/** Highlighting the stock grammars lack or colour unlike IntelliJ. */
|
||||
const EXTRA_TAGS: Record<string, Parameters<typeof styleTags>[0]> = {
|
||||
JavaScript: JS_TAGS,
|
||||
TypeScript: JS_TAGS,
|
||||
JSX: JS_TAGS,
|
||||
TSX: JS_TAGS,
|
||||
// @lezer/java tags annotations as plain identifiers.
|
||||
Java: { "MarkerAnnotation/Identifier Annotation/Identifier MarkerAnnotation Annotation": t.annotation },
|
||||
Java: JAVA_TAGS,
|
||||
CSS: CSS_TAGS,
|
||||
SCSS: CSS_TAGS,
|
||||
Sass: CSS_TAGS,
|
||||
|
||||
@@ -172,6 +172,9 @@ describe("fileIconName", () => {
|
||||
["main.dart", "file-dart"],
|
||||
["App.vue", "file-vue"],
|
||||
["App.svelte", "file-svelte"],
|
||||
["index.jsp", "file-jsp"],
|
||||
["footer.jspf", "file-jsp"],
|
||||
["card.tag", "file-jsp"],
|
||||
["styles.scss", "file-scss"],
|
||||
["styles.sass", "file-sass"],
|
||||
["styles.less", "file-less"],
|
||||
|
||||
@@ -76,6 +76,7 @@ const ICONS = {
|
||||
"file-npm": "openidea/fileTypes/npm",
|
||||
"file-vue": "fileTypes/vue",
|
||||
"file-svelte": "openidea/fileTypes/svelte",
|
||||
"file-jsp": "openidea/fileTypes/jsp",
|
||||
"file-yaml": "fileTypes/yaml",
|
||||
"file-toml": "fileTypes/toml",
|
||||
"file-html": "fileTypes/html",
|
||||
@@ -822,6 +823,7 @@ const EXT_ICONS = table({
|
||||
"file-tsx": "tsx",
|
||||
"file-vue": "vue",
|
||||
"file-svelte": "svelte",
|
||||
"file-jsp": "jsp jspx jspf jsph tag tagf tagx",
|
||||
"file-json": "json jsonc json5 jsonl ndjson geojson topojson webmanifest har avsc code-workspace code-snippets map",
|
||||
"file-md": "md mdx markdown mdown mkd mkdn mdwn ronn workbook",
|
||||
"file-rst": "rst",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 4C14 2.89543 13.1046 2 12 2H4C2.89543 2 2 2.89543 2 4V12C2 13.1046 2.89543 14 4 14H12C13.1046 14 14 13.1046 14 12V4Z" fill="#E76F00"/>
|
||||
<path d="M4.5 5.5L3 8L4.5 10.5M11.5 5.5L13 8L11.5 10.5M9 5.5L7 10.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 4C14 2.89543 13.1046 2 12 2H4C2.89543 2 2 2.89543 2 4V12C2 13.1046 2.89543 14 4 14H12C13.1046 14 14 13.1046 14 12V4Z" fill="#F0883E"/>
|
||||
<path d="M4.5 5.5L3 8L4.5 10.5M11.5 5.5L13 8L11.5 10.5M9 5.5L7 10.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 384 B |
Reference in New Issue
Block a user