nsp-server-pages 0.0.2 → 0.1.0
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/README.md +19 -1
- package/cjs/src/app.js +24 -11
- package/cjs/src/catch.js +11 -4
- package/cjs/src/parse-attr.js +2 -2
- package/cjs/src/parse-el.js +16 -11
- package/cjs/src/parse-jsp.js +14 -15
- package/cjs/src/parse-scriptlet.js +8 -6
- package/cjs/src/parse-text.js +2 -2
- package/cjs/src/taglib.js +2 -3
- package/index.d.ts +85 -18
- package/package.json +5 -5
- package/src/app.js +24 -11
- package/src/catch.js +11 -4
- package/src/parse-attr.js +2 -2
- package/src/parse-el.js +16 -11
- package/src/parse-jsp.js +14 -15
- package/src/parse-scriptlet.js +8 -6
- package/src/parse-text.js +2 -2
- package/src/taglib.js +2 -3
package/README.md
CHANGED
|
@@ -112,6 +112,22 @@ render(context);
|
|
|
112
112
|
|
|
113
113
|
Launch web server with Express.js:
|
|
114
114
|
|
|
115
|
+
```js
|
|
116
|
+
const nsp = createNSP();
|
|
117
|
+
const app = express();
|
|
118
|
+
|
|
119
|
+
app.use("/", async (req, res, next) => {
|
|
120
|
+
const {path} = req;
|
|
121
|
+
const context = {indexDto: {name: req.query.name || "nsp"}};
|
|
122
|
+
const render = nsp.loadJSP(`${BASE}/src/main/webapp/WEB-INF/${path}`);
|
|
123
|
+
if (!render) return next();
|
|
124
|
+
const html = await render(context);
|
|
125
|
+
res.type("html").send(html);
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or use its own router `nsp.mount()` which helps to load JSP, HTML and JS files.
|
|
130
|
+
|
|
115
131
|
```js
|
|
116
132
|
const nsp = createNSP();
|
|
117
133
|
|
|
@@ -125,10 +141,12 @@ nsp.mount("/include/", (path) => nsp.loadFile(`${BASE}/htdocs/${path}`));
|
|
|
125
141
|
nsp.mount("/cached/", (path) => nsp.loadJS(`${BASE}/cached/${path}`));
|
|
126
142
|
|
|
127
143
|
const app = express();
|
|
144
|
+
|
|
128
145
|
app.use("/", async (req, res, next) => {
|
|
129
146
|
const context = {indexDto: {name: req.query.name || "nsp"}};
|
|
130
147
|
const render = await nsp.load(req.path);
|
|
131
|
-
|
|
148
|
+
if (!render) return next();
|
|
149
|
+
const html = await render(context);
|
|
132
150
|
res.type("html").send(html);
|
|
133
151
|
});
|
|
134
152
|
```
|
package/cjs/src/app.js
CHANGED
|
@@ -17,17 +17,17 @@ class App {
|
|
|
17
17
|
this.fnMap = new Map();
|
|
18
18
|
this.listeners = new Map;
|
|
19
19
|
this.options = options = Object.create(options || null);
|
|
20
|
-
if (!options.
|
|
21
|
-
options.
|
|
22
|
-
if (!options.
|
|
23
|
-
options.
|
|
24
|
-
if (!options.
|
|
25
|
-
options.
|
|
26
|
-
}
|
|
27
|
-
|
|
20
|
+
if (!options.vName)
|
|
21
|
+
options.vName = "v";
|
|
22
|
+
if (!options.nspName)
|
|
23
|
+
options.nspName = "nsp";
|
|
24
|
+
if (!options.storeKey)
|
|
25
|
+
options.storeKey = "#nsp";
|
|
26
|
+
}
|
|
27
|
+
hook(type, fn) {
|
|
28
28
|
this.listeners.set(type, fn);
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
process(type, arg) {
|
|
31
31
|
const fn = this.listeners.get(type);
|
|
32
32
|
if (fn)
|
|
33
33
|
return fn(arg);
|
|
@@ -60,8 +60,8 @@ class App {
|
|
|
60
60
|
parse(src) {
|
|
61
61
|
return (0, parse_jsp_js_1.parseJSP)(this, src);
|
|
62
62
|
}
|
|
63
|
-
mount(
|
|
64
|
-
return (0, mount_js_1.mount)(this,
|
|
63
|
+
mount(path, fn) {
|
|
64
|
+
return (0, mount_js_1.mount)(this, path, fn);
|
|
65
65
|
}
|
|
66
66
|
load(path) {
|
|
67
67
|
return (0, mount_js_1.load)(this, path);
|
|
@@ -78,4 +78,17 @@ class App {
|
|
|
78
78
|
const loader = this.fileLoader || (this.fileLoader = new loaders_js_1.FileLoader(this));
|
|
79
79
|
return loader.load(file);
|
|
80
80
|
}
|
|
81
|
+
store(context, key, initFn) {
|
|
82
|
+
if ("object" !== typeof context && context == null) {
|
|
83
|
+
throw new Error("Context must be an object");
|
|
84
|
+
}
|
|
85
|
+
const { storeKey } = this.options;
|
|
86
|
+
const map = context[storeKey] || (context[storeKey] = new Map());
|
|
87
|
+
let value = map.get(key);
|
|
88
|
+
if (value == null) {
|
|
89
|
+
value = initFn();
|
|
90
|
+
map.set(key, value);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
81
94
|
}
|
package/cjs/src/catch.js
CHANGED
|
@@ -19,11 +19,18 @@ const catchFn = (app, fn) => {
|
|
|
19
19
|
return errorHandler(e);
|
|
20
20
|
}
|
|
21
21
|
function errorHandler(e) {
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
22
|
+
// just throw the error if it's already handled
|
|
23
|
+
if (context != null) {
|
|
24
|
+
const data = app.store(context, "error", () => ({}));
|
|
25
|
+
if (data.error === e)
|
|
26
|
+
throw e;
|
|
27
|
+
data.error = e;
|
|
25
28
|
}
|
|
26
|
-
|
|
29
|
+
// call the error handler
|
|
30
|
+
const result = app.process("error", e, context);
|
|
31
|
+
if (result != null)
|
|
32
|
+
return result;
|
|
33
|
+
return `<!--\n[ERR] ${escapeError(e)}\n-->`;
|
|
27
34
|
}
|
|
28
35
|
};
|
|
29
36
|
};
|
package/cjs/src/parse-attr.js
CHANGED
|
@@ -24,10 +24,10 @@ class AttrParser {
|
|
|
24
24
|
*/
|
|
25
25
|
toFn() {
|
|
26
26
|
const { app } = this;
|
|
27
|
-
const {
|
|
27
|
+
const { nspName, vName } = app.options;
|
|
28
28
|
const js = this.toJS();
|
|
29
29
|
try {
|
|
30
|
-
const fn = Function(
|
|
30
|
+
const fn = Function(nspName, vName, `return ${js}`);
|
|
31
31
|
return (context) => fn(app, context);
|
|
32
32
|
}
|
|
33
33
|
catch (e) {
|
package/cjs/src/parse-el.js
CHANGED
|
@@ -25,9 +25,8 @@ const floatRE = `${numericRE}(?:\\.${numericRE})?(?!\\.)`;
|
|
|
25
25
|
const stringRE = `"(?:\\\\\\.|[^\\\\"])*"|'(?:\\\\\\.|[^\\\\'])*'`;
|
|
26
26
|
const nameRE = `[A-Za-z_][A-Za-z_0-9]*(?![A-Za-z_0-9])`;
|
|
27
27
|
const tagFnRE = `${nameRE}:${nameRE}\\(`;
|
|
28
|
-
const variableRE = `${nameRE}(
|
|
29
|
-
const
|
|
30
|
-
const itemRE = [tagFnRE, variableRE, floatRE, stringRE, opRE].join("|");
|
|
28
|
+
const variableRE = `${nameRE}(?:\\??\\.${nameRE}|(?:\\?\\.)?\\[(?:${numericRE}|${stringRE})\\]|\\[)*`;
|
|
29
|
+
const itemRE = [tagFnRE, variableRE, floatRE, stringRE].join("|");
|
|
31
30
|
const tagFnRegExp = new RegExp(`^${tagFnRE}$`, "s");
|
|
32
31
|
const variableRegExp = new RegExp(`^${variableRE}$`, "s");
|
|
33
32
|
const itemRegExp = new RegExp(`(${itemRE})`, "s");
|
|
@@ -47,10 +46,10 @@ class ElParser {
|
|
|
47
46
|
*/
|
|
48
47
|
toFn() {
|
|
49
48
|
const { app } = this;
|
|
50
|
-
const {
|
|
49
|
+
const { nspName, vName } = app.options;
|
|
51
50
|
const js = this.toJS();
|
|
52
51
|
try {
|
|
53
|
-
const fn = Function(
|
|
52
|
+
const fn = Function(nspName, vName, `return ${js}`);
|
|
54
53
|
return (context) => fn(app, context);
|
|
55
54
|
}
|
|
56
55
|
catch (e) {
|
|
@@ -63,14 +62,14 @@ class ElParser {
|
|
|
63
62
|
*/
|
|
64
63
|
toJS(_) {
|
|
65
64
|
const { app } = this;
|
|
66
|
-
const { prefilter, postfilter } = app.options;
|
|
65
|
+
const { nullish, prefilter, postfilter } = app.options;
|
|
67
66
|
let src = trim(this.src);
|
|
68
67
|
if (prefilter)
|
|
69
68
|
src = prefilter(src);
|
|
70
69
|
if (src == null)
|
|
71
70
|
return 'null';
|
|
72
71
|
const array = src.split(itemRegExp);
|
|
73
|
-
const {
|
|
72
|
+
const { nspName, vName } = app.options;
|
|
74
73
|
for (let i = 0; i < array.length; i++) {
|
|
75
74
|
let exp = array[i];
|
|
76
75
|
if (i & 1) {
|
|
@@ -81,14 +80,14 @@ class ElParser {
|
|
|
81
80
|
else if (tagFnRegExp.test(exp)) {
|
|
82
81
|
// taglib function
|
|
83
82
|
exp = exp.replace(/\($/, "");
|
|
84
|
-
array[i] = `${
|
|
83
|
+
array[i] = `${nspName}.fn(${JSON.stringify(exp)})(`;
|
|
85
84
|
}
|
|
86
85
|
else if (variableRegExp.test(exp)) {
|
|
87
86
|
// variable
|
|
88
|
-
exp = exp.replace(
|
|
89
|
-
exp = exp.replace(
|
|
87
|
+
exp = exp.replace(/(\?)?\./g, "?.");
|
|
88
|
+
exp = exp.replace(/(\?\.)?\[/g, "?.[");
|
|
90
89
|
exp = exp.replace(/\s+$/, "");
|
|
91
|
-
array[i] = `${
|
|
90
|
+
array[i] = `${vName}.${exp}`;
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
93
|
else {
|
|
@@ -97,6 +96,12 @@ class ElParser {
|
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
let js = array.join("");
|
|
99
|
+
if (!nullish) {
|
|
100
|
+
if (array.filter(v => /\S/.test(v)).length > 1) {
|
|
101
|
+
js = `(${js})`;
|
|
102
|
+
}
|
|
103
|
+
js = `${js} ?? ""`;
|
|
104
|
+
}
|
|
100
105
|
if (postfilter)
|
|
101
106
|
js = postfilter(js);
|
|
102
107
|
return js;
|
package/cjs/src/parse-jsp.js
CHANGED
|
@@ -37,7 +37,7 @@ class Element {
|
|
|
37
37
|
*/
|
|
38
38
|
toJS(option) {
|
|
39
39
|
const { app, tagLine } = this;
|
|
40
|
-
const { comment,
|
|
40
|
+
const { comment, nspName, trimSpaces, vName } = app.options;
|
|
41
41
|
const indent = +app.options.indent || 0;
|
|
42
42
|
const currentIndent = +option?.indent || 0;
|
|
43
43
|
const nextIndent = currentIndent + indent;
|
|
@@ -50,12 +50,18 @@ class Element {
|
|
|
50
50
|
}
|
|
51
51
|
else if (!/\S/.test(item)) {
|
|
52
52
|
// item with only whitespace
|
|
53
|
-
return '""';
|
|
53
|
+
return (trimSpaces !== false) ? '""' : JSON.stringify(item);
|
|
54
54
|
}
|
|
55
55
|
else {
|
|
56
|
+
if (trimSpaces !== false) {
|
|
57
|
+
item = item.replace(/^\s*[\r\n]/s, "\n");
|
|
58
|
+
item = item.replace(/\s*[\r\n]\s*$/s, "\n");
|
|
59
|
+
item = item.replace(/^[ \t]+/s, " ");
|
|
60
|
+
item = item.replace(/[ \t]+$/s, " ");
|
|
61
|
+
}
|
|
56
62
|
let js = (0, parse_text_js_1.parseText)(app, item).toJS({ indent: nextIndent });
|
|
57
63
|
if (/\(.+?\)|\$\{.+?}/s.test(js)) {
|
|
58
|
-
js = `${
|
|
64
|
+
js = `${vName} => ${js}`; // array function
|
|
59
65
|
}
|
|
60
66
|
return js;
|
|
61
67
|
}
|
|
@@ -81,19 +87,19 @@ class Element {
|
|
|
81
87
|
const bodyL = /^`\n/s.test(body) ? (isRoot ? "" : " ") : nextLF;
|
|
82
88
|
const bodyR = /(\n`|[)\s])$/s.test(body) ? "" : currentLF;
|
|
83
89
|
if (isRoot) {
|
|
84
|
-
return `${
|
|
90
|
+
return `${nspName}.bundle(${bodyL}${body}${bodyR})`; // root element
|
|
85
91
|
}
|
|
86
92
|
// attributes as the second argument
|
|
87
93
|
let attr = (0, parse_attr_js_1.parseAttr)(app, tagLine).toJS({ indent: args.length ? nextIndent : currentIndent });
|
|
88
94
|
if (/\(.+?\)|\$\{.+?}/s.test(attr)) {
|
|
89
|
-
attr = `${
|
|
95
|
+
attr = `${vName} => (${attr})`; // array function
|
|
90
96
|
}
|
|
91
97
|
const commentV = comment ? `// ${tagLine?.replace(/\s*[\r\n]\s*/g, " ") ?? ""}${currentLF}` : "";
|
|
92
98
|
const nameV = JSON.stringify(tagName);
|
|
93
99
|
const hasAttr = /:/.test(attr);
|
|
94
100
|
const attrV = (hasBody || hasAttr) ? `, ${attr}` : "";
|
|
95
101
|
const bodyV = hasBody ? `,${bodyL}${body}${bodyR}` : "";
|
|
96
|
-
return `${commentV}${
|
|
102
|
+
return `${commentV}${nspName}.tag(${nameV}${attrV}${bodyV})`;
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
/**
|
|
@@ -144,10 +150,10 @@ class JspParser {
|
|
|
144
150
|
*/
|
|
145
151
|
toFn() {
|
|
146
152
|
const { app } = this;
|
|
147
|
-
const {
|
|
153
|
+
const { nspName } = app.options;
|
|
148
154
|
const js = this.toJS();
|
|
149
155
|
try {
|
|
150
|
-
const fn = Function(
|
|
156
|
+
const fn = Function(nspName, `return ${js}`);
|
|
151
157
|
return fn(app);
|
|
152
158
|
}
|
|
153
159
|
catch (e) {
|
|
@@ -163,7 +169,6 @@ const tagRegExp = new RegExp(`(</?${nameRE}:(?:${insideRE})*?>)|(<%(?:${insideRE
|
|
|
163
169
|
const jspToJS = (app, src, option) => {
|
|
164
170
|
const root = new Element(app);
|
|
165
171
|
const tree = new Tree(root);
|
|
166
|
-
const { trimSpaces } = app.options;
|
|
167
172
|
const array = src.split(tagRegExp);
|
|
168
173
|
for (let i = 0; i < array.length; i++) {
|
|
169
174
|
const i3 = i % 3;
|
|
@@ -190,12 +195,6 @@ const jspToJS = (app, src, option) => {
|
|
|
190
195
|
}
|
|
191
196
|
else if (i3 === 0) {
|
|
192
197
|
// text node
|
|
193
|
-
if (trimSpaces !== false) {
|
|
194
|
-
str = str.replace(/^\s*[\r\n]/s, "\n");
|
|
195
|
-
str = str.replace(/\s*[\r\n]\s*$/s, "\n");
|
|
196
|
-
str = str.replace(/^[ \t]+/s, " ");
|
|
197
|
-
str = str.replace(/[ \t]+$/s, " ");
|
|
198
|
-
}
|
|
199
198
|
tree.append(str);
|
|
200
199
|
}
|
|
201
200
|
}
|
|
@@ -29,13 +29,13 @@ class ScriptletParser {
|
|
|
29
29
|
*/
|
|
30
30
|
toFn() {
|
|
31
31
|
const { app } = this;
|
|
32
|
-
const {
|
|
32
|
+
const { nspName } = app.options;
|
|
33
33
|
const js = this.toJS();
|
|
34
34
|
const isComment = /^\/\/[^\n]*$/s.test(js);
|
|
35
35
|
if (isComment)
|
|
36
36
|
return () => null;
|
|
37
37
|
try {
|
|
38
|
-
const fn = Function(
|
|
38
|
+
const fn = Function(nspName, `return ${js}`);
|
|
39
39
|
return fn(app);
|
|
40
40
|
}
|
|
41
41
|
catch (e) {
|
|
@@ -48,22 +48,24 @@ class ScriptletParser {
|
|
|
48
48
|
*/
|
|
49
49
|
toJS(option) {
|
|
50
50
|
const { app } = this;
|
|
51
|
-
const {
|
|
51
|
+
const { nspName, vName } = app.options;
|
|
52
|
+
const currentIndent = +option?.indent || 0;
|
|
53
|
+
const currentLF = currentIndent ? "\n" + " ".repeat(currentIndent) : "\n";
|
|
52
54
|
let { src } = this;
|
|
53
55
|
const type = typeMap[src.substring(0, 3)] || "scriptlet";
|
|
54
56
|
if (type === "comment") {
|
|
55
|
-
src = src.replace(
|
|
57
|
+
src = src.replace(/[ \t]*[\r\n]+/sg, `${currentLF}// `);
|
|
56
58
|
return `// ${src}`;
|
|
57
59
|
}
|
|
58
60
|
if (type === "expression") {
|
|
59
61
|
src = src.replace(/^<%=\s*/s, "");
|
|
60
62
|
src = src.replace(/\s*%>$/s, "");
|
|
61
63
|
src = (0, parse_el_js_1.parseEL)(app, src).toJS(option);
|
|
62
|
-
return `${
|
|
64
|
+
return `${vName} => (${src})`;
|
|
63
65
|
}
|
|
64
66
|
app.log(`${type} found: ${src?.substring(0, 1000)}`);
|
|
65
67
|
src = /`|\$\{/.test(src) ? JSON.stringify(src) : "`" + src + "`";
|
|
66
|
-
src = `${
|
|
68
|
+
src = `${vName} => ${nspName}.process("${type}", ${src}, ${vName})`;
|
|
67
69
|
return src;
|
|
68
70
|
}
|
|
69
71
|
}
|
package/cjs/src/parse-text.js
CHANGED
|
@@ -38,10 +38,10 @@ class TextParser {
|
|
|
38
38
|
*/
|
|
39
39
|
toFn() {
|
|
40
40
|
const { app } = this;
|
|
41
|
-
const {
|
|
41
|
+
const { nspName, vName } = app.options;
|
|
42
42
|
const js = this.toJS();
|
|
43
43
|
try {
|
|
44
|
-
const fn = Function(
|
|
44
|
+
const fn = Function(nspName, vName, `return ${js}`);
|
|
45
45
|
return (context) => fn(app, context);
|
|
46
46
|
}
|
|
47
47
|
catch (e) {
|
package/cjs/src/taglib.js
CHANGED
|
@@ -18,11 +18,10 @@ const addTagLib = (app, tagLibDef) => {
|
|
|
18
18
|
};
|
|
19
19
|
exports.addTagLib = addTagLib;
|
|
20
20
|
const prepareTag = (app, name, attr, body) => {
|
|
21
|
-
const { tagMap
|
|
21
|
+
const { tagMap } = app;
|
|
22
22
|
const tagFn = tagMap.get(name) || defaultTagFn;
|
|
23
|
-
const conf = options.conf[name];
|
|
24
23
|
const attrFn = !attr ? () => ({}) : (typeof attr !== "function") ? () => attr : attr;
|
|
25
|
-
const tagDef = { name, app,
|
|
24
|
+
const tagDef = { name, app, attr: attrFn, body };
|
|
26
25
|
return tagFn(tagDef);
|
|
27
26
|
};
|
|
28
27
|
exports.prepareTag = prepareTag;
|
package/index.d.ts
CHANGED
|
@@ -23,7 +23,6 @@ declare namespace NSP {
|
|
|
23
23
|
|
|
24
24
|
interface TagDef<A, T = any> {
|
|
25
25
|
app: App;
|
|
26
|
-
conf: any;
|
|
27
26
|
name: string;
|
|
28
27
|
attr: AttrFn<A, T>;
|
|
29
28
|
body: NodeFn<T>;
|
|
@@ -33,35 +32,47 @@ declare namespace NSP {
|
|
|
33
32
|
logger?: { log: (message: string) => void };
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
|
-
*
|
|
35
|
+
* variable name for context
|
|
36
|
+
* @default "v"
|
|
37
37
|
*/
|
|
38
|
-
|
|
38
|
+
vName?: string;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* variable name for
|
|
41
|
+
* variable name for App instance
|
|
42
|
+
* @default "nsp"
|
|
42
43
|
*/
|
|
43
|
-
|
|
44
|
+
nspName?: string;
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
*
|
|
47
|
+
* property name for data store in context
|
|
48
|
+
* @default "#nsp"
|
|
47
49
|
*/
|
|
48
|
-
|
|
50
|
+
storeKey?: string;
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* indent size for JavaScript source generated
|
|
54
|
+
* @default 0
|
|
52
55
|
*/
|
|
53
56
|
indent?: number;
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
59
|
* add comments at toJS() result
|
|
60
|
+
* @default false
|
|
57
61
|
*/
|
|
58
62
|
comment?: boolean;
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
|
-
* remove edge spaces in HTML in some cases
|
|
65
|
+
* set false not to remove edge spaces in HTML in some cases.
|
|
66
|
+
* @default true
|
|
62
67
|
*/
|
|
63
68
|
trimSpaces?: boolean;
|
|
64
69
|
|
|
70
|
+
/**
|
|
71
|
+
* set true to keep EL result value of `null` and `undefined` as is.
|
|
72
|
+
* @default false
|
|
73
|
+
*/
|
|
74
|
+
nullish?: boolean;
|
|
75
|
+
|
|
65
76
|
/**
|
|
66
77
|
* expression filter before transpile starts
|
|
67
78
|
*/
|
|
@@ -79,52 +90,102 @@ declare namespace NSP {
|
|
|
79
90
|
options: Options;
|
|
80
91
|
tagMap: Map<string, TagFn<any>>;
|
|
81
92
|
|
|
93
|
+
/**
|
|
94
|
+
* register a tag library
|
|
95
|
+
*/
|
|
82
96
|
addTagLib(tagLibDef: TagLibDef): void;
|
|
83
97
|
|
|
84
|
-
|
|
98
|
+
/**
|
|
99
|
+
* build a NodeFn which returns a string for the content nodes
|
|
100
|
+
*/
|
|
101
|
+
bundle<T>(...nodes: Node<T>[]): NodeFn<T>;
|
|
85
102
|
|
|
103
|
+
/**
|
|
104
|
+
* concat strings even if they are Promise<string>
|
|
105
|
+
*/
|
|
86
106
|
concat(...text: TextFlex[]): string | Promise<string>;
|
|
87
107
|
|
|
88
|
-
|
|
108
|
+
/**
|
|
109
|
+
* retrieve a result from hook function
|
|
110
|
+
*/
|
|
111
|
+
process<T>(type: "error", e: Error, context?: T): string;
|
|
89
112
|
|
|
90
|
-
|
|
113
|
+
process<T>(type: "directive", src: string, context?: T): string;
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
process<T>(type: "declaration", src: string, context?: T): string;
|
|
93
116
|
|
|
94
|
-
|
|
117
|
+
process<T>(type: "scriptlet", src: string, context?: T): string;
|
|
95
118
|
|
|
119
|
+
/**
|
|
120
|
+
* pickup the taglib function
|
|
121
|
+
*/
|
|
96
122
|
fn(name: string): (...args: any[]) => any;
|
|
97
123
|
|
|
124
|
+
/**
|
|
125
|
+
* load a NodeFn for the path mounted by mount()
|
|
126
|
+
*/
|
|
98
127
|
load<T>(path: string): Promise<NodeFn<T>>;
|
|
99
128
|
|
|
129
|
+
/**
|
|
130
|
+
* load a NodeFn for the local filesystem path
|
|
131
|
+
*/
|
|
100
132
|
loadFile<T>(file: string): Promise<NodeFn<T>>;
|
|
101
133
|
|
|
102
134
|
loadJS<T>(file: string): Promise<NodeFn<T>>;
|
|
103
135
|
|
|
104
136
|
loadJSP<T>(file: string): Promise<NodeFn<T>>;
|
|
105
137
|
|
|
138
|
+
/**
|
|
139
|
+
* log a message via options.logger which defaults console.log
|
|
140
|
+
*/
|
|
106
141
|
log(message: string): void;
|
|
107
142
|
|
|
108
|
-
|
|
143
|
+
/**
|
|
144
|
+
* mount a loader function for the path matched
|
|
145
|
+
*/
|
|
146
|
+
mount(path: RegExp | string, fn: LoaderFn): void;
|
|
109
147
|
|
|
110
|
-
|
|
148
|
+
/**
|
|
149
|
+
* register a hook function
|
|
150
|
+
*/
|
|
151
|
+
hook(type: "error", fn: <T>(e: Error, context?: T) => string | void): void;
|
|
111
152
|
|
|
112
|
-
|
|
153
|
+
hook(type: "directive", fn: <T>(src: string, context?: T) => string | void): void;
|
|
113
154
|
|
|
114
|
-
|
|
155
|
+
hook(type: "declaration", fn: <T>(src: string, context?: T) => string | void): void;
|
|
115
156
|
|
|
116
|
-
|
|
157
|
+
hook(type: "scriptlet", fn: <T>(src: string, context?: T) => string | void): void;
|
|
117
158
|
|
|
159
|
+
/**
|
|
160
|
+
* parse a JSP document
|
|
161
|
+
*/
|
|
118
162
|
parse(src: string): Parser;
|
|
119
163
|
|
|
164
|
+
/**
|
|
165
|
+
* get a private data store in context
|
|
166
|
+
*/
|
|
167
|
+
store<S>(context: any, key: string, initFn?: () => S): S;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* generates a NodeFn for the tag
|
|
171
|
+
*/
|
|
120
172
|
tag<A, T = any>(name: string, attr?: A | AttrFn<A, T>, ...body: Node<T>[]): NodeFn<T>;
|
|
121
173
|
}
|
|
122
174
|
|
|
123
175
|
interface TagLibDef {
|
|
176
|
+
/**
|
|
177
|
+
* namespace
|
|
178
|
+
*/
|
|
124
179
|
ns: string;
|
|
125
180
|
|
|
181
|
+
/**
|
|
182
|
+
* functions
|
|
183
|
+
*/
|
|
126
184
|
fn?: { [name: string]: (...args: any[]) => any };
|
|
127
185
|
|
|
186
|
+
/**
|
|
187
|
+
* tags
|
|
188
|
+
*/
|
|
128
189
|
tag?: { [name: string]: TagFn<any> };
|
|
129
190
|
}
|
|
130
191
|
|
|
@@ -136,8 +197,14 @@ declare namespace NSP {
|
|
|
136
197
|
* Parser for JSP document
|
|
137
198
|
*/
|
|
138
199
|
interface Parser {
|
|
200
|
+
/**
|
|
201
|
+
* transpile the JSP document to JavaScript source code
|
|
202
|
+
*/
|
|
139
203
|
toJS(option?: ToJSOption): string;
|
|
140
204
|
|
|
205
|
+
/**
|
|
206
|
+
* compile the JSP document as a NodeFn
|
|
207
|
+
*/
|
|
141
208
|
toFn<T>(): NodeFn<T>;
|
|
142
209
|
}
|
|
143
210
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nsp-server-pages",
|
|
3
3
|
"description": "NSP JavaScript Server Pages for Node.js",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"author": "@kawanet",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/kawanet/nsp-server-pages/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"async-cache-queue": "^0.2.
|
|
11
|
-
"to-xml": "^0.1.
|
|
10
|
+
"async-cache-queue": "^0.2.8",
|
|
11
|
+
"to-xml": "^0.1.11"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@rollup/plugin-node-resolve": "^15.2.
|
|
14
|
+
"@rollup/plugin-node-resolve": "^15.2.1",
|
|
15
15
|
"@types/mocha": "^10.0.1",
|
|
16
|
-
"@types/node": "^20.5.
|
|
16
|
+
"@types/node": "^20.5.3",
|
|
17
17
|
"mocha": "^10.2.0",
|
|
18
18
|
"typescript": "^5.1.6"
|
|
19
19
|
},
|
package/src/app.js
CHANGED
|
@@ -13,17 +13,17 @@ class App {
|
|
|
13
13
|
this.fnMap = new Map();
|
|
14
14
|
this.listeners = new Map;
|
|
15
15
|
this.options = options = Object.create(options || null);
|
|
16
|
-
if (!options.
|
|
17
|
-
options.
|
|
18
|
-
if (!options.
|
|
19
|
-
options.
|
|
20
|
-
if (!options.
|
|
21
|
-
options.
|
|
22
|
-
}
|
|
23
|
-
|
|
16
|
+
if (!options.vName)
|
|
17
|
+
options.vName = "v";
|
|
18
|
+
if (!options.nspName)
|
|
19
|
+
options.nspName = "nsp";
|
|
20
|
+
if (!options.storeKey)
|
|
21
|
+
options.storeKey = "#nsp";
|
|
22
|
+
}
|
|
23
|
+
hook(type, fn) {
|
|
24
24
|
this.listeners.set(type, fn);
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
process(type, arg) {
|
|
27
27
|
const fn = this.listeners.get(type);
|
|
28
28
|
if (fn)
|
|
29
29
|
return fn(arg);
|
|
@@ -56,8 +56,8 @@ class App {
|
|
|
56
56
|
parse(src) {
|
|
57
57
|
return parseJSP(this, src);
|
|
58
58
|
}
|
|
59
|
-
mount(
|
|
60
|
-
return mount(this,
|
|
59
|
+
mount(path, fn) {
|
|
60
|
+
return mount(this, path, fn);
|
|
61
61
|
}
|
|
62
62
|
load(path) {
|
|
63
63
|
return load(this, path);
|
|
@@ -74,4 +74,17 @@ class App {
|
|
|
74
74
|
const loader = this.fileLoader || (this.fileLoader = new FileLoader(this));
|
|
75
75
|
return loader.load(file);
|
|
76
76
|
}
|
|
77
|
+
store(context, key, initFn) {
|
|
78
|
+
if ("object" !== typeof context && context == null) {
|
|
79
|
+
throw new Error("Context must be an object");
|
|
80
|
+
}
|
|
81
|
+
const { storeKey } = this.options;
|
|
82
|
+
const map = context[storeKey] || (context[storeKey] = new Map());
|
|
83
|
+
let value = map.get(key);
|
|
84
|
+
if (value == null) {
|
|
85
|
+
value = initFn();
|
|
86
|
+
map.set(key, value);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
77
90
|
}
|
package/src/catch.js
CHANGED
|
@@ -16,11 +16,18 @@ export const catchFn = (app, fn) => {
|
|
|
16
16
|
return errorHandler(e);
|
|
17
17
|
}
|
|
18
18
|
function errorHandler(e) {
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
19
|
+
// just throw the error if it's already handled
|
|
20
|
+
if (context != null) {
|
|
21
|
+
const data = app.store(context, "error", () => ({}));
|
|
22
|
+
if (data.error === e)
|
|
23
|
+
throw e;
|
|
24
|
+
data.error = e;
|
|
22
25
|
}
|
|
23
|
-
|
|
26
|
+
// call the error handler
|
|
27
|
+
const result = app.process("error", e, context);
|
|
28
|
+
if (result != null)
|
|
29
|
+
return result;
|
|
30
|
+
return `<!--\n[ERR] ${escapeError(e)}\n-->`;
|
|
24
31
|
}
|
|
25
32
|
};
|
|
26
33
|
};
|
package/src/parse-attr.js
CHANGED
|
@@ -20,10 +20,10 @@ class AttrParser {
|
|
|
20
20
|
*/
|
|
21
21
|
toFn() {
|
|
22
22
|
const { app } = this;
|
|
23
|
-
const {
|
|
23
|
+
const { nspName, vName } = app.options;
|
|
24
24
|
const js = this.toJS();
|
|
25
25
|
try {
|
|
26
|
-
const fn = Function(
|
|
26
|
+
const fn = Function(nspName, vName, `return ${js}`);
|
|
27
27
|
return (context) => fn(app, context);
|
|
28
28
|
}
|
|
29
29
|
catch (e) {
|
package/src/parse-el.js
CHANGED
|
@@ -22,9 +22,8 @@ const floatRE = `${numericRE}(?:\\.${numericRE})?(?!\\.)`;
|
|
|
22
22
|
const stringRE = `"(?:\\\\\\.|[^\\\\"])*"|'(?:\\\\\\.|[^\\\\'])*'`;
|
|
23
23
|
const nameRE = `[A-Za-z_][A-Za-z_0-9]*(?![A-Za-z_0-9])`;
|
|
24
24
|
const tagFnRE = `${nameRE}:${nameRE}\\(`;
|
|
25
|
-
const variableRE = `${nameRE}(
|
|
26
|
-
const
|
|
27
|
-
const itemRE = [tagFnRE, variableRE, floatRE, stringRE, opRE].join("|");
|
|
25
|
+
const variableRE = `${nameRE}(?:\\??\\.${nameRE}|(?:\\?\\.)?\\[(?:${numericRE}|${stringRE})\\]|\\[)*`;
|
|
26
|
+
const itemRE = [tagFnRE, variableRE, floatRE, stringRE].join("|");
|
|
28
27
|
const tagFnRegExp = new RegExp(`^${tagFnRE}$`, "s");
|
|
29
28
|
const variableRegExp = new RegExp(`^${variableRE}$`, "s");
|
|
30
29
|
const itemRegExp = new RegExp(`(${itemRE})`, "s");
|
|
@@ -43,10 +42,10 @@ class ElParser {
|
|
|
43
42
|
*/
|
|
44
43
|
toFn() {
|
|
45
44
|
const { app } = this;
|
|
46
|
-
const {
|
|
45
|
+
const { nspName, vName } = app.options;
|
|
47
46
|
const js = this.toJS();
|
|
48
47
|
try {
|
|
49
|
-
const fn = Function(
|
|
48
|
+
const fn = Function(nspName, vName, `return ${js}`);
|
|
50
49
|
return (context) => fn(app, context);
|
|
51
50
|
}
|
|
52
51
|
catch (e) {
|
|
@@ -59,14 +58,14 @@ class ElParser {
|
|
|
59
58
|
*/
|
|
60
59
|
toJS(_) {
|
|
61
60
|
const { app } = this;
|
|
62
|
-
const { prefilter, postfilter } = app.options;
|
|
61
|
+
const { nullish, prefilter, postfilter } = app.options;
|
|
63
62
|
let src = trim(this.src);
|
|
64
63
|
if (prefilter)
|
|
65
64
|
src = prefilter(src);
|
|
66
65
|
if (src == null)
|
|
67
66
|
return 'null';
|
|
68
67
|
const array = src.split(itemRegExp);
|
|
69
|
-
const {
|
|
68
|
+
const { nspName, vName } = app.options;
|
|
70
69
|
for (let i = 0; i < array.length; i++) {
|
|
71
70
|
let exp = array[i];
|
|
72
71
|
if (i & 1) {
|
|
@@ -77,14 +76,14 @@ class ElParser {
|
|
|
77
76
|
else if (tagFnRegExp.test(exp)) {
|
|
78
77
|
// taglib function
|
|
79
78
|
exp = exp.replace(/\($/, "");
|
|
80
|
-
array[i] = `${
|
|
79
|
+
array[i] = `${nspName}.fn(${JSON.stringify(exp)})(`;
|
|
81
80
|
}
|
|
82
81
|
else if (variableRegExp.test(exp)) {
|
|
83
82
|
// variable
|
|
84
|
-
exp = exp.replace(
|
|
85
|
-
exp = exp.replace(
|
|
83
|
+
exp = exp.replace(/(\?)?\./g, "?.");
|
|
84
|
+
exp = exp.replace(/(\?\.)?\[/g, "?.[");
|
|
86
85
|
exp = exp.replace(/\s+$/, "");
|
|
87
|
-
array[i] = `${
|
|
86
|
+
array[i] = `${vName}.${exp}`;
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
else {
|
|
@@ -93,6 +92,12 @@ class ElParser {
|
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
let js = array.join("");
|
|
95
|
+
if (!nullish) {
|
|
96
|
+
if (array.filter(v => /\S/.test(v)).length > 1) {
|
|
97
|
+
js = `(${js})`;
|
|
98
|
+
}
|
|
99
|
+
js = `${js} ?? ""`;
|
|
100
|
+
}
|
|
96
101
|
if (postfilter)
|
|
97
102
|
js = postfilter(js);
|
|
98
103
|
return js;
|
package/src/parse-jsp.js
CHANGED
|
@@ -33,7 +33,7 @@ class Element {
|
|
|
33
33
|
*/
|
|
34
34
|
toJS(option) {
|
|
35
35
|
const { app, tagLine } = this;
|
|
36
|
-
const { comment,
|
|
36
|
+
const { comment, nspName, trimSpaces, vName } = app.options;
|
|
37
37
|
const indent = +app.options.indent || 0;
|
|
38
38
|
const currentIndent = +option?.indent || 0;
|
|
39
39
|
const nextIndent = currentIndent + indent;
|
|
@@ -46,12 +46,18 @@ class Element {
|
|
|
46
46
|
}
|
|
47
47
|
else if (!/\S/.test(item)) {
|
|
48
48
|
// item with only whitespace
|
|
49
|
-
return '""';
|
|
49
|
+
return (trimSpaces !== false) ? '""' : JSON.stringify(item);
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
|
+
if (trimSpaces !== false) {
|
|
53
|
+
item = item.replace(/^\s*[\r\n]/s, "\n");
|
|
54
|
+
item = item.replace(/\s*[\r\n]\s*$/s, "\n");
|
|
55
|
+
item = item.replace(/^[ \t]+/s, " ");
|
|
56
|
+
item = item.replace(/[ \t]+$/s, " ");
|
|
57
|
+
}
|
|
52
58
|
let js = parseText(app, item).toJS({ indent: nextIndent });
|
|
53
59
|
if (/\(.+?\)|\$\{.+?}/s.test(js)) {
|
|
54
|
-
js = `${
|
|
60
|
+
js = `${vName} => ${js}`; // array function
|
|
55
61
|
}
|
|
56
62
|
return js;
|
|
57
63
|
}
|
|
@@ -77,19 +83,19 @@ class Element {
|
|
|
77
83
|
const bodyL = /^`\n/s.test(body) ? (isRoot ? "" : " ") : nextLF;
|
|
78
84
|
const bodyR = /(\n`|[)\s])$/s.test(body) ? "" : currentLF;
|
|
79
85
|
if (isRoot) {
|
|
80
|
-
return `${
|
|
86
|
+
return `${nspName}.bundle(${bodyL}${body}${bodyR})`; // root element
|
|
81
87
|
}
|
|
82
88
|
// attributes as the second argument
|
|
83
89
|
let attr = parseAttr(app, tagLine).toJS({ indent: args.length ? nextIndent : currentIndent });
|
|
84
90
|
if (/\(.+?\)|\$\{.+?}/s.test(attr)) {
|
|
85
|
-
attr = `${
|
|
91
|
+
attr = `${vName} => (${attr})`; // array function
|
|
86
92
|
}
|
|
87
93
|
const commentV = comment ? `// ${tagLine?.replace(/\s*[\r\n]\s*/g, " ") ?? ""}${currentLF}` : "";
|
|
88
94
|
const nameV = JSON.stringify(tagName);
|
|
89
95
|
const hasAttr = /:/.test(attr);
|
|
90
96
|
const attrV = (hasBody || hasAttr) ? `, ${attr}` : "";
|
|
91
97
|
const bodyV = hasBody ? `,${bodyL}${body}${bodyR}` : "";
|
|
92
|
-
return `${commentV}${
|
|
98
|
+
return `${commentV}${nspName}.tag(${nameV}${attrV}${bodyV})`;
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
/**
|
|
@@ -140,10 +146,10 @@ class JspParser {
|
|
|
140
146
|
*/
|
|
141
147
|
toFn() {
|
|
142
148
|
const { app } = this;
|
|
143
|
-
const {
|
|
149
|
+
const { nspName } = app.options;
|
|
144
150
|
const js = this.toJS();
|
|
145
151
|
try {
|
|
146
|
-
const fn = Function(
|
|
152
|
+
const fn = Function(nspName, `return ${js}`);
|
|
147
153
|
return fn(app);
|
|
148
154
|
}
|
|
149
155
|
catch (e) {
|
|
@@ -159,7 +165,6 @@ const tagRegExp = new RegExp(`(</?${nameRE}:(?:${insideRE})*?>)|(<%(?:${insideRE
|
|
|
159
165
|
export const jspToJS = (app, src, option) => {
|
|
160
166
|
const root = new Element(app);
|
|
161
167
|
const tree = new Tree(root);
|
|
162
|
-
const { trimSpaces } = app.options;
|
|
163
168
|
const array = src.split(tagRegExp);
|
|
164
169
|
for (let i = 0; i < array.length; i++) {
|
|
165
170
|
const i3 = i % 3;
|
|
@@ -186,12 +191,6 @@ export const jspToJS = (app, src, option) => {
|
|
|
186
191
|
}
|
|
187
192
|
else if (i3 === 0) {
|
|
188
193
|
// text node
|
|
189
|
-
if (trimSpaces !== false) {
|
|
190
|
-
str = str.replace(/^\s*[\r\n]/s, "\n");
|
|
191
|
-
str = str.replace(/\s*[\r\n]\s*$/s, "\n");
|
|
192
|
-
str = str.replace(/^[ \t]+/s, " ");
|
|
193
|
-
str = str.replace(/[ \t]+$/s, " ");
|
|
194
|
-
}
|
|
195
194
|
tree.append(str);
|
|
196
195
|
}
|
|
197
196
|
}
|
package/src/parse-scriptlet.js
CHANGED
|
@@ -25,13 +25,13 @@ class ScriptletParser {
|
|
|
25
25
|
*/
|
|
26
26
|
toFn() {
|
|
27
27
|
const { app } = this;
|
|
28
|
-
const {
|
|
28
|
+
const { nspName } = app.options;
|
|
29
29
|
const js = this.toJS();
|
|
30
30
|
const isComment = /^\/\/[^\n]*$/s.test(js);
|
|
31
31
|
if (isComment)
|
|
32
32
|
return () => null;
|
|
33
33
|
try {
|
|
34
|
-
const fn = Function(
|
|
34
|
+
const fn = Function(nspName, `return ${js}`);
|
|
35
35
|
return fn(app);
|
|
36
36
|
}
|
|
37
37
|
catch (e) {
|
|
@@ -44,22 +44,24 @@ class ScriptletParser {
|
|
|
44
44
|
*/
|
|
45
45
|
toJS(option) {
|
|
46
46
|
const { app } = this;
|
|
47
|
-
const {
|
|
47
|
+
const { nspName, vName } = app.options;
|
|
48
|
+
const currentIndent = +option?.indent || 0;
|
|
49
|
+
const currentLF = currentIndent ? "\n" + " ".repeat(currentIndent) : "\n";
|
|
48
50
|
let { src } = this;
|
|
49
51
|
const type = typeMap[src.substring(0, 3)] || "scriptlet";
|
|
50
52
|
if (type === "comment") {
|
|
51
|
-
src = src.replace(
|
|
53
|
+
src = src.replace(/[ \t]*[\r\n]+/sg, `${currentLF}// `);
|
|
52
54
|
return `// ${src}`;
|
|
53
55
|
}
|
|
54
56
|
if (type === "expression") {
|
|
55
57
|
src = src.replace(/^<%=\s*/s, "");
|
|
56
58
|
src = src.replace(/\s*%>$/s, "");
|
|
57
59
|
src = parseEL(app, src).toJS(option);
|
|
58
|
-
return `${
|
|
60
|
+
return `${vName} => (${src})`;
|
|
59
61
|
}
|
|
60
62
|
app.log(`${type} found: ${src?.substring(0, 1000)}`);
|
|
61
63
|
src = /`|\$\{/.test(src) ? JSON.stringify(src) : "`" + src + "`";
|
|
62
|
-
src = `${
|
|
64
|
+
src = `${vName} => ${nspName}.process("${type}", ${src}, ${vName})`;
|
|
63
65
|
return src;
|
|
64
66
|
}
|
|
65
67
|
}
|
package/src/parse-text.js
CHANGED
|
@@ -34,10 +34,10 @@ class TextParser {
|
|
|
34
34
|
*/
|
|
35
35
|
toFn() {
|
|
36
36
|
const { app } = this;
|
|
37
|
-
const {
|
|
37
|
+
const { nspName, vName } = app.options;
|
|
38
38
|
const js = this.toJS();
|
|
39
39
|
try {
|
|
40
|
-
const fn = Function(
|
|
40
|
+
const fn = Function(nspName, vName, `return ${js}`);
|
|
41
41
|
return (context) => fn(app, context);
|
|
42
42
|
}
|
|
43
43
|
catch (e) {
|
package/src/taglib.js
CHANGED
|
@@ -14,11 +14,10 @@ export const addTagLib = (app, tagLibDef) => {
|
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
export const prepareTag = (app, name, attr, body) => {
|
|
17
|
-
const { tagMap
|
|
17
|
+
const { tagMap } = app;
|
|
18
18
|
const tagFn = tagMap.get(name) || defaultTagFn;
|
|
19
|
-
const conf = options.conf[name];
|
|
20
19
|
const attrFn = !attr ? () => ({}) : (typeof attr !== "function") ? () => attr : attr;
|
|
21
|
-
const tagDef = { name, app,
|
|
20
|
+
const tagDef = { name, app, attr: attrFn, body };
|
|
22
21
|
return tagFn(tagDef);
|
|
23
22
|
};
|
|
24
23
|
const defaultTagFn = (tagDef) => {
|