nsp-server-pages 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1143 -0
- package/README.md +147 -0
- package/cjs/index.js +1 -0
- package/cjs/package.json +3 -0
- package/cjs/src/app.js +81 -0
- package/cjs/src/bundle.js +51 -0
- package/cjs/src/catch.js +30 -0
- package/cjs/src/concat.js +35 -0
- package/cjs/src/loaders.js +123 -0
- package/cjs/src/mount.js +34 -0
- package/cjs/src/parse-attr.js +91 -0
- package/cjs/src/parse-el.js +104 -0
- package/cjs/src/parse-jsp.js +207 -0
- package/cjs/src/parse-scriptlet.js +69 -0
- package/cjs/src/parse-text.js +118 -0
- package/cjs/src/taglib.js +44 -0
- package/index.d.ts +141 -0
- package/index.js +1 -0
- package/package.json +58 -0
- package/src/app.js +77 -0
- package/src/bundle.js +47 -0
- package/src/catch.js +26 -0
- package/src/concat.js +31 -0
- package/src/loaders.js +117 -0
- package/src/mount.js +29 -0
- package/src/parse-attr.js +87 -0
- package/src/parse-el.js +100 -0
- package/src/parse-jsp.js +202 -0
- package/src/parse-scriptlet.js +65 -0
- package/src/parse-text.js +114 -0
- package/src/taglib.js +39 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nsp-server-pages",
|
|
3
|
+
"description": "NSP JavaScript Server Pages for Node.js",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"author": "@kawanet",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/kawanet/nsp-server-pages/issues"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"async-cache-queue": "^0.2.7",
|
|
11
|
+
"to-xml": "^0.1.10"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@rollup/plugin-node-resolve": "^15.2.0",
|
|
15
|
+
"@types/mocha": "^10.0.1",
|
|
16
|
+
"@types/node": "^20.5.1",
|
|
17
|
+
"mocha": "^10.2.0",
|
|
18
|
+
"typescript": "^5.1.6"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
"import": {
|
|
22
|
+
"default": "./index.js",
|
|
23
|
+
"types": "./index.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"require": "./cjs/index.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"cjs/index.js",
|
|
29
|
+
"cjs/package.json",
|
|
30
|
+
"cjs/src/*.js",
|
|
31
|
+
"index.d.ts",
|
|
32
|
+
"index.js",
|
|
33
|
+
"package.json",
|
|
34
|
+
"src/*.js"
|
|
35
|
+
],
|
|
36
|
+
"homepage": "https://github.com/kawanet/nsp-server-pages#readme",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"JSP",
|
|
39
|
+
"Struts",
|
|
40
|
+
"Tomcat",
|
|
41
|
+
"taglib"
|
|
42
|
+
],
|
|
43
|
+
"license": "Apache-2.0",
|
|
44
|
+
"main": "index.js",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/kawanet/nsp-server-pages.git"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "make all",
|
|
51
|
+
"fixpack": "fixpack",
|
|
52
|
+
"prepack": "make clean test-title all test",
|
|
53
|
+
"test": "make test"
|
|
54
|
+
},
|
|
55
|
+
"sideEffects": false,
|
|
56
|
+
"type": "module",
|
|
57
|
+
"types": "./index.d.ts"
|
|
58
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { load, mount } from "./mount.js";
|
|
2
|
+
import { FileLoader, JsLoader, JspLoader } from "./loaders.js";
|
|
3
|
+
import { parseJSP } from "./parse-jsp.js";
|
|
4
|
+
import { catchFn } from "./catch.js";
|
|
5
|
+
import { bundle } from "./bundle.js";
|
|
6
|
+
import { addTagLib, prepareTag } from "./taglib.js";
|
|
7
|
+
import { concat } from "./concat.js";
|
|
8
|
+
export const createNSP = (options) => new App(options);
|
|
9
|
+
class App {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.loaders = [];
|
|
12
|
+
this.tagMap = new Map();
|
|
13
|
+
this.fnMap = new Map();
|
|
14
|
+
this.listeners = new Map;
|
|
15
|
+
this.options = options = Object.create(options || null);
|
|
16
|
+
if (!options.conf)
|
|
17
|
+
options.conf = {};
|
|
18
|
+
if (!options.vKey)
|
|
19
|
+
options.vKey = "v";
|
|
20
|
+
if (!options.nspKey)
|
|
21
|
+
options.nspKey = "nsp";
|
|
22
|
+
}
|
|
23
|
+
on(type, fn) {
|
|
24
|
+
this.listeners.set(type, fn);
|
|
25
|
+
}
|
|
26
|
+
emit(type, arg) {
|
|
27
|
+
const fn = this.listeners.get(type);
|
|
28
|
+
if (fn)
|
|
29
|
+
return fn(arg);
|
|
30
|
+
}
|
|
31
|
+
log(message) {
|
|
32
|
+
const logger = this.options.logger || console;
|
|
33
|
+
logger.log(message);
|
|
34
|
+
}
|
|
35
|
+
concat(..._) {
|
|
36
|
+
return concat(arguments);
|
|
37
|
+
}
|
|
38
|
+
fn(name) {
|
|
39
|
+
const fn = this.fnMap.get(name);
|
|
40
|
+
if (!fn)
|
|
41
|
+
throw new Error(`Unknown function: ${name}`);
|
|
42
|
+
return fn;
|
|
43
|
+
}
|
|
44
|
+
addTagLib(tagLibDef) {
|
|
45
|
+
addTagLib(this, tagLibDef);
|
|
46
|
+
}
|
|
47
|
+
tag(name, attr, ..._) {
|
|
48
|
+
const bodyFn = bundle(arguments, 2);
|
|
49
|
+
const tagFn = prepareTag(this, name, attr, bodyFn);
|
|
50
|
+
return catchFn(this, tagFn);
|
|
51
|
+
}
|
|
52
|
+
bundle(..._) {
|
|
53
|
+
const fn = bundle(arguments);
|
|
54
|
+
return catchFn(this, fn);
|
|
55
|
+
}
|
|
56
|
+
parse(src) {
|
|
57
|
+
return parseJSP(this, src);
|
|
58
|
+
}
|
|
59
|
+
mount(match, fn) {
|
|
60
|
+
return mount(this, match, fn);
|
|
61
|
+
}
|
|
62
|
+
load(path) {
|
|
63
|
+
return load(this, path);
|
|
64
|
+
}
|
|
65
|
+
loadJS(file) {
|
|
66
|
+
const loader = this.jsLoader || (this.jsLoader = new JsLoader(this));
|
|
67
|
+
return loader.load(file);
|
|
68
|
+
}
|
|
69
|
+
loadJSP(file) {
|
|
70
|
+
const loader = this.jspLoader || (this.jspLoader = new JspLoader(this));
|
|
71
|
+
return loader.load(file);
|
|
72
|
+
}
|
|
73
|
+
loadFile(file) {
|
|
74
|
+
const loader = this.fileLoader || (this.fileLoader = new FileLoader(this));
|
|
75
|
+
return loader.load(file);
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/bundle.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const isPromise = (v) => v && (typeof v.then === "function");
|
|
2
|
+
const join = (a, b) => (a == null ? b : (b == null ? a : a + b));
|
|
3
|
+
export const bundle = (array, start, end) => {
|
|
4
|
+
start = +start || 0;
|
|
5
|
+
end = +end || array?.length || 0;
|
|
6
|
+
if (end <= start) {
|
|
7
|
+
return () => null;
|
|
8
|
+
}
|
|
9
|
+
if (end - 1 === start) {
|
|
10
|
+
const node = array[start];
|
|
11
|
+
return (typeof node === "function") ? node : () => node;
|
|
12
|
+
}
|
|
13
|
+
return context => {
|
|
14
|
+
let result;
|
|
15
|
+
let promise;
|
|
16
|
+
for (let i = start; i < end; i++) {
|
|
17
|
+
let v = array[i];
|
|
18
|
+
if (promise) {
|
|
19
|
+
promise = promise.then(result => {
|
|
20
|
+
if (typeof v === "function") {
|
|
21
|
+
v = v(context);
|
|
22
|
+
}
|
|
23
|
+
if (isPromise(v)) {
|
|
24
|
+
return v.then(v => join(result, v));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return join(result, v);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
if (typeof v === "function") {
|
|
33
|
+
v = v(context);
|
|
34
|
+
}
|
|
35
|
+
if (isPromise(v)) {
|
|
36
|
+
// upgrade to async mode
|
|
37
|
+
promise = v.then(v => join(result, v));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// sync mode per default
|
|
41
|
+
result = join(result, v);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return promise || result;
|
|
46
|
+
};
|
|
47
|
+
};
|
package/src/catch.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { toXML } from "to-xml";
|
|
2
|
+
const isPromise = (v) => v && (typeof v.catch === "function");
|
|
3
|
+
const escapeError = (e) => toXML({ "#": (e?.message || String(e)) });
|
|
4
|
+
export const catchFn = (app, fn) => {
|
|
5
|
+
return context => {
|
|
6
|
+
try {
|
|
7
|
+
const result = fn(context);
|
|
8
|
+
if (isPromise(result)) {
|
|
9
|
+
return result.catch(errorHandler);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
return errorHandler(e);
|
|
17
|
+
}
|
|
18
|
+
function errorHandler(e) {
|
|
19
|
+
const result = app.emit("error", e, context);
|
|
20
|
+
if (result == null) {
|
|
21
|
+
return `<!--\n[ERR] ${escapeError(e)}\n-->`;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
};
|
package/src/concat.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const isPromise = (v) => v && (typeof v.then === "function");
|
|
2
|
+
const join = (a, b) => (a == null ? b : (b == null ? a : a + b));
|
|
3
|
+
const single = (v) => (Array.isArray(v) ? concat(v) : v);
|
|
4
|
+
export const concat = (array) => {
|
|
5
|
+
let result;
|
|
6
|
+
let promise;
|
|
7
|
+
for (let i = 0; i < array.length; i++) {
|
|
8
|
+
let text = single(array[i]);
|
|
9
|
+
if (promise) {
|
|
10
|
+
promise = promise.then(result => {
|
|
11
|
+
if (isPromise(text)) {
|
|
12
|
+
return text.then(text => join(result, text));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
return join(result, text);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
if (isPromise(text)) {
|
|
21
|
+
// upgrade to async mode
|
|
22
|
+
promise = text.then(text => join(result, text));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// sync mode per default
|
|
26
|
+
result = join(result, text);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return promise || result;
|
|
31
|
+
};
|
package/src/loaders.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { queueFactory } from "async-cache-queue";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
const getName = (path) => path.split("/").at(-1)?.split(".").at(0) || "";
|
|
4
|
+
class BaseLoader {
|
|
5
|
+
constructor(app) {
|
|
6
|
+
/**
|
|
7
|
+
* This tests the file exists.
|
|
8
|
+
* The test cache is separated from the loader cache to avoid cache flushed by dirty requests.
|
|
9
|
+
*/
|
|
10
|
+
this.isFile = queueFactory({
|
|
11
|
+
cache: 60 * 1000,
|
|
12
|
+
maxItems: 10 * 1000,
|
|
13
|
+
negativeCache: 1000,
|
|
14
|
+
timeout: 1000,
|
|
15
|
+
})(async (file) => {
|
|
16
|
+
return await fs.stat(file).then(stat => stat?.isFile()).catch(() => false);
|
|
17
|
+
});
|
|
18
|
+
this.appRef = new WeakRef(app);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class JspLoader extends BaseLoader {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
/**
|
|
25
|
+
* Parse JSP file, cache the result function and return it.
|
|
26
|
+
*/
|
|
27
|
+
this._load = queueFactory({
|
|
28
|
+
cache: 60 * 1000,
|
|
29
|
+
maxItems: 10 * 1000,
|
|
30
|
+
negativeCache: 1000,
|
|
31
|
+
timeout: 1000,
|
|
32
|
+
})(async (path) => {
|
|
33
|
+
const app = this.appRef.deref();
|
|
34
|
+
app.log(`loading: ${path}`);
|
|
35
|
+
const text = await fs.readFile(path, "utf8");
|
|
36
|
+
return app.parse(text).toFn();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async load(file) {
|
|
40
|
+
// valid only for .jsp files
|
|
41
|
+
if (!/\.jsp$/.test(file))
|
|
42
|
+
return;
|
|
43
|
+
// skip when file does not exist
|
|
44
|
+
if (!await this.isFile(file))
|
|
45
|
+
return;
|
|
46
|
+
// load .jsp file then
|
|
47
|
+
return this._load(file);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class JsLoader extends BaseLoader {
|
|
51
|
+
constructor() {
|
|
52
|
+
super(...arguments);
|
|
53
|
+
/**
|
|
54
|
+
* Parse JS file, cache the result function and return it.
|
|
55
|
+
*/
|
|
56
|
+
this._load = queueFactory({
|
|
57
|
+
cache: 60 * 1000,
|
|
58
|
+
maxItems: 1000,
|
|
59
|
+
negativeCache: 1000,
|
|
60
|
+
timeout: 1000,
|
|
61
|
+
})(async (file) => {
|
|
62
|
+
const app = this.appRef.deref();
|
|
63
|
+
app.log(`loading: ${file}`);
|
|
64
|
+
const module = await import(file);
|
|
65
|
+
const name = getName(file);
|
|
66
|
+
const fn = module[name];
|
|
67
|
+
if (typeof fn !== "function") {
|
|
68
|
+
throw new Error(`Named export "${name}" not found in module: ${file}`);
|
|
69
|
+
}
|
|
70
|
+
const nodeFn = fn(app);
|
|
71
|
+
if (typeof nodeFn !== "function") {
|
|
72
|
+
throw new Error(`Exported "${name}" function does not returns a valid function: ${file}`);
|
|
73
|
+
}
|
|
74
|
+
return nodeFn;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async load(file) {
|
|
78
|
+
file = file?.replace(/\.jsp$/, ".js");
|
|
79
|
+
// valid only for .js files
|
|
80
|
+
if (!/\.[cm]?js$/.test(file))
|
|
81
|
+
return;
|
|
82
|
+
// skip when file does not exist
|
|
83
|
+
if (!await this.isFile(file))
|
|
84
|
+
return;
|
|
85
|
+
// import .js file then
|
|
86
|
+
return this._load(file);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export class FileLoader extends BaseLoader {
|
|
90
|
+
constructor() {
|
|
91
|
+
super(...arguments);
|
|
92
|
+
/**
|
|
93
|
+
* Parse HTML or other text files, cache the result function and return it.
|
|
94
|
+
*/
|
|
95
|
+
this._load = queueFactory({
|
|
96
|
+
cache: 60 * 1000,
|
|
97
|
+
maxItems: 1000,
|
|
98
|
+
negativeCache: 1000,
|
|
99
|
+
timeout: 1000,
|
|
100
|
+
})(async (file) => {
|
|
101
|
+
const app = this.appRef.deref();
|
|
102
|
+
app.log(`loading: ${file}`);
|
|
103
|
+
const text = await fs.readFile(file, "utf8");
|
|
104
|
+
return () => text;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async load(file) {
|
|
108
|
+
// disabled for JSP, JS and image files
|
|
109
|
+
if (/\.(jsp|[cm]?js|png|gif|jpe?g)$/i.test(file))
|
|
110
|
+
return;
|
|
111
|
+
// skip when file does not exist
|
|
112
|
+
if (!await this.isFile(file))
|
|
113
|
+
return;
|
|
114
|
+
// load HTML and other text files then
|
|
115
|
+
return this._load(file);
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/mount.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const mount = (app, match, fn) => {
|
|
2
|
+
const test = ("string" !== typeof match) ? match : {
|
|
3
|
+
test: ((path) => path.startsWith(match))
|
|
4
|
+
};
|
|
5
|
+
app.loaders.push(!test ? fn : path => {
|
|
6
|
+
if (test.test(path))
|
|
7
|
+
return fn(path);
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
export const load = async (app, path) => {
|
|
11
|
+
const { loaders } = app;
|
|
12
|
+
const search = path.replace(/^[^?]*\??/, "");
|
|
13
|
+
path = path.replace(/\?.*$/, "");
|
|
14
|
+
path = path.replace(/^\/*/, "/");
|
|
15
|
+
let fn;
|
|
16
|
+
for (const loaderFn of loaders) {
|
|
17
|
+
fn = await loaderFn(path);
|
|
18
|
+
if (fn)
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
if (!fn)
|
|
22
|
+
throw new Error(`file not found: ${path}`);
|
|
23
|
+
return context => {
|
|
24
|
+
for (const [key, value] of new URLSearchParams(search)) {
|
|
25
|
+
context[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return fn(context);
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parseText } from "./parse-text.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parser for HTML tag attributes <tagName attr="value"/>
|
|
4
|
+
*/
|
|
5
|
+
export const parseAttr = (app, src) => new AttrParser(app, src);
|
|
6
|
+
class AttrParser {
|
|
7
|
+
constructor(app, src) {
|
|
8
|
+
this.app = app;
|
|
9
|
+
this.src = src;
|
|
10
|
+
//
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Transpile HTML tag attributes to JavaScript source code
|
|
14
|
+
*/
|
|
15
|
+
toJS(option) {
|
|
16
|
+
return attrToJS(this.app, this.src, option);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compile HTML tag attributes to JavaScript function instance
|
|
20
|
+
*/
|
|
21
|
+
toFn() {
|
|
22
|
+
const { app } = this;
|
|
23
|
+
const { nspKey, vKey } = app.options;
|
|
24
|
+
const js = this.toJS();
|
|
25
|
+
try {
|
|
26
|
+
const fn = Function(nspKey, vKey, `return ${js}`);
|
|
27
|
+
return (context) => fn(app, context);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
app.log("AttrParser: " + js.substring(0, 1000));
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Transpile HTML tag attributes to JavaScript source code
|
|
37
|
+
*/
|
|
38
|
+
const attrToJS = (app, tag, option) => {
|
|
39
|
+
tag = tag?.replace(/^\s*<\S+\s*/s, "");
|
|
40
|
+
tag = tag?.replace(/\s*\/?>\s*$/s, "");
|
|
41
|
+
const indent = +app.options.indent || 0;
|
|
42
|
+
const currentIndent = +option?.indent || 0;
|
|
43
|
+
const nextIndent = currentIndent + indent;
|
|
44
|
+
const currentLF = currentIndent ? "\n" + " ".repeat(currentIndent) : "\n";
|
|
45
|
+
const nextLF = nextIndent ? "\n" + " ".repeat(nextIndent) : "\n";
|
|
46
|
+
const keys = [];
|
|
47
|
+
const index = {};
|
|
48
|
+
tag?.replace(/([^\s='"]+)(\s*=(?:\s*"([^"]*)"|\s*'([^']*)'|([^\s='"]*)))?/g, (_, key, eq, v1, v2, v3) => {
|
|
49
|
+
if (!index[key])
|
|
50
|
+
keys.push(key);
|
|
51
|
+
index[key] = (eq ? unescapeXML(v1 || v2 || v3 || "") : true);
|
|
52
|
+
return "";
|
|
53
|
+
});
|
|
54
|
+
const items = keys.map(key => {
|
|
55
|
+
let value = index[key];
|
|
56
|
+
if (!/^[A-Za-z_]\w+$/.test(key)) {
|
|
57
|
+
key = JSON.stringify(key);
|
|
58
|
+
}
|
|
59
|
+
if ("string" === typeof value) {
|
|
60
|
+
value = parseText(app, value).toJS({ indent: nextIndent });
|
|
61
|
+
}
|
|
62
|
+
return `${key}: ${value}`;
|
|
63
|
+
});
|
|
64
|
+
// no arguments
|
|
65
|
+
if (!keys.length)
|
|
66
|
+
return 'null';
|
|
67
|
+
const js = items.join(`,${nextLF}`);
|
|
68
|
+
const trailingComma = (keys.length > 1) ? "," : "";
|
|
69
|
+
return `{${nextLF}${js}${trailingComma}${currentLF}}`;
|
|
70
|
+
};
|
|
71
|
+
const UNESCAPE = {
|
|
72
|
+
"&": "&",
|
|
73
|
+
"<": "<",
|
|
74
|
+
">": ">",
|
|
75
|
+
"'": "'",
|
|
76
|
+
""": '"'
|
|
77
|
+
};
|
|
78
|
+
const unescapeXML = (str) => {
|
|
79
|
+
return str?.replace(/(&(?:lt|gt|amp|apos|quot|#(?:\d{1,6}|x[0-9a-fA-F]{1,5}));)/g, (str) => {
|
|
80
|
+
if (str[1] === "#") {
|
|
81
|
+
const code = (str[2] === "x") ? parseInt(str.substring(3), 16) : parseInt(str.substr(2), 10);
|
|
82
|
+
if (code > -1)
|
|
83
|
+
return String.fromCharCode(code);
|
|
84
|
+
}
|
|
85
|
+
return UNESCAPE[str] || str;
|
|
86
|
+
});
|
|
87
|
+
};
|
package/src/parse-el.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const trim = (str) => str.replace(/^\s+/s, "").replace(/\s+$/s, "");
|
|
2
|
+
const wordMap = {
|
|
3
|
+
and: "&&",
|
|
4
|
+
div: "/",
|
|
5
|
+
empty: "!",
|
|
6
|
+
eq: "==",
|
|
7
|
+
false: "false",
|
|
8
|
+
ge: ">=",
|
|
9
|
+
gt: ">",
|
|
10
|
+
instanceof: "instanceof",
|
|
11
|
+
le: "<=",
|
|
12
|
+
lt: "<",
|
|
13
|
+
mod: "%",
|
|
14
|
+
ne: "!=",
|
|
15
|
+
not: "!",
|
|
16
|
+
null: "null",
|
|
17
|
+
or: "||",
|
|
18
|
+
true: "true",
|
|
19
|
+
};
|
|
20
|
+
const numericRE = `[0-9]+(?![0-9])`;
|
|
21
|
+
const floatRE = `${numericRE}(?:\\.${numericRE})?(?!\\.)`;
|
|
22
|
+
const stringRE = `"(?:\\\\\\.|[^\\\\"])*"|'(?:\\\\\\.|[^\\\\'])*'`;
|
|
23
|
+
const nameRE = `[A-Za-z_][A-Za-z_0-9]*(?![A-Za-z_0-9])`;
|
|
24
|
+
const tagFnRE = `${nameRE}:${nameRE}\\(`;
|
|
25
|
+
const variableRE = `${nameRE}(?:\\.${nameRE}|\\[(?:${numericRE}|${stringRE})\\])*`;
|
|
26
|
+
const opRE = `[+\\-\\*/%=<>()!|&:?,]+(?![+\\-\\*/%=<>()!|&:?,])`;
|
|
27
|
+
const itemRE = [tagFnRE, variableRE, floatRE, stringRE, opRE].join("|");
|
|
28
|
+
const tagFnRegExp = new RegExp(`^${tagFnRE}$`, "s");
|
|
29
|
+
const variableRegExp = new RegExp(`^${variableRE}$`, "s");
|
|
30
|
+
const itemRegExp = new RegExp(`(${itemRE})`, "s");
|
|
31
|
+
/**
|
|
32
|
+
* Simplified transformer for expression language
|
|
33
|
+
*/
|
|
34
|
+
export const parseEL = (app, src) => new ElParser(app, src);
|
|
35
|
+
class ElParser {
|
|
36
|
+
constructor(app, src) {
|
|
37
|
+
this.app = app;
|
|
38
|
+
this.src = src;
|
|
39
|
+
//
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compile ${EL} to JavaScript function instance
|
|
43
|
+
*/
|
|
44
|
+
toFn() {
|
|
45
|
+
const { app } = this;
|
|
46
|
+
const { nspKey, vKey } = app.options;
|
|
47
|
+
const js = this.toJS();
|
|
48
|
+
try {
|
|
49
|
+
const fn = Function(nspKey, vKey, `return ${js}`);
|
|
50
|
+
return (context) => fn(app, context);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
app.log("ElParser: " + js?.substring(0, 1000));
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Transpile ${EL} to JavaScript source code
|
|
59
|
+
*/
|
|
60
|
+
toJS(_) {
|
|
61
|
+
const { app } = this;
|
|
62
|
+
const { prefilter, postfilter } = app.options;
|
|
63
|
+
let src = trim(this.src);
|
|
64
|
+
if (prefilter)
|
|
65
|
+
src = prefilter(src);
|
|
66
|
+
if (src == null)
|
|
67
|
+
return 'null';
|
|
68
|
+
const array = src.split(itemRegExp);
|
|
69
|
+
const { nspKey, vKey } = app.options;
|
|
70
|
+
for (let i = 0; i < array.length; i++) {
|
|
71
|
+
let exp = array[i];
|
|
72
|
+
if (i & 1) {
|
|
73
|
+
if (wordMap[exp]) {
|
|
74
|
+
// eq, and, or
|
|
75
|
+
array[i] = wordMap[exp];
|
|
76
|
+
}
|
|
77
|
+
else if (tagFnRegExp.test(exp)) {
|
|
78
|
+
// taglib function
|
|
79
|
+
exp = exp.replace(/\($/, "");
|
|
80
|
+
array[i] = `${nspKey}.fn(${JSON.stringify(exp)})(`;
|
|
81
|
+
}
|
|
82
|
+
else if (variableRegExp.test(exp)) {
|
|
83
|
+
// variable
|
|
84
|
+
exp = exp.replace(/\./g, "?.");
|
|
85
|
+
exp = exp.replace(/\[/g, "?.[");
|
|
86
|
+
exp = exp.replace(/\s+$/, "");
|
|
87
|
+
array[i] = `${vKey}.${exp}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// less spaces
|
|
92
|
+
array[i] = exp.replace(/\s+/sg, " ");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let js = array.join("");
|
|
96
|
+
if (postfilter)
|
|
97
|
+
js = postfilter(js);
|
|
98
|
+
return js;
|
|
99
|
+
}
|
|
100
|
+
}
|