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 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
- const html = render(context);
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.conf)
21
- options.conf = {};
22
- if (!options.vKey)
23
- options.vKey = "v";
24
- if (!options.nspKey)
25
- options.nspKey = "nsp";
26
- }
27
- on(type, fn) {
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
- emit(type, arg) {
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(match, fn) {
64
- return (0, mount_js_1.mount)(this, match, fn);
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
- const result = app.emit("error", e, context);
23
- if (result == null) {
24
- return `<!--\n[ERR] ${escapeError(e)}\n-->`;
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
- return result;
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
  };
@@ -24,10 +24,10 @@ class AttrParser {
24
24
  */
25
25
  toFn() {
26
26
  const { app } = this;
27
- const { nspKey, vKey } = app.options;
27
+ const { nspName, vName } = app.options;
28
28
  const js = this.toJS();
29
29
  try {
30
- const fn = Function(nspKey, vKey, `return ${js}`);
30
+ const fn = Function(nspName, vName, `return ${js}`);
31
31
  return (context) => fn(app, context);
32
32
  }
33
33
  catch (e) {
@@ -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}(?:\\.${nameRE}|\\[(?:${numericRE}|${stringRE})\\])*`;
29
- const opRE = `[+\\-\\*/%=<>()!|&:?,]+(?![+\\-\\*/%=<>()!|&:?,])`;
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 { nspKey, vKey } = app.options;
49
+ const { nspName, vName } = app.options;
51
50
  const js = this.toJS();
52
51
  try {
53
- const fn = Function(nspKey, vKey, `return ${js}`);
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 { nspKey, vKey } = app.options;
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] = `${nspKey}.fn(${JSON.stringify(exp)})(`;
83
+ array[i] = `${nspName}.fn(${JSON.stringify(exp)})(`;
85
84
  }
86
85
  else if (variableRegExp.test(exp)) {
87
86
  // variable
88
- exp = exp.replace(/\./g, "?.");
89
- exp = exp.replace(/\[/g, "?.[");
87
+ exp = exp.replace(/(\?)?\./g, "?.");
88
+ exp = exp.replace(/(\?\.)?\[/g, "?.[");
90
89
  exp = exp.replace(/\s+$/, "");
91
- array[i] = `${vKey}.${exp}`;
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;
@@ -37,7 +37,7 @@ class Element {
37
37
  */
38
38
  toJS(option) {
39
39
  const { app, tagLine } = this;
40
- const { comment, nspKey, vKey } = app.options;
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 = `${vKey} => ${js}`; // array function
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 `${nspKey}.bundle(${bodyL}${body}${bodyR})`; // root element
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 = `${vKey} => (${attr})`; // array function
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}${nspKey}.tag(${nameV}${attrV}${bodyV})`;
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 { nspKey } = app.options;
153
+ const { nspName } = app.options;
148
154
  const js = this.toJS();
149
155
  try {
150
- const fn = Function(nspKey, `return ${js}`);
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 { nspKey } = app.options;
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(nspKey, `return ${js}`);
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 { nspKey, vKey } = app.options;
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(/\s\s+/sg, " ");
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 `${vKey} => (${src})`;
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 = `${vKey} => ${nspKey}.emit("${type}", ${src}, ${vKey})`;
68
+ src = `${vName} => ${nspName}.process("${type}", ${src}, ${vName})`;
67
69
  return src;
68
70
  }
69
71
  }
@@ -38,10 +38,10 @@ class TextParser {
38
38
  */
39
39
  toFn() {
40
40
  const { app } = this;
41
- const { nspKey, vKey } = app.options;
41
+ const { nspName, vName } = app.options;
42
42
  const js = this.toJS();
43
43
  try {
44
- const fn = Function(nspKey, vKey, `return ${js}`);
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, options } = app;
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, conf, attr: attrFn, body };
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
- * tag configuration
35
+ * variable name for context
36
+ * @default "v"
37
37
  */
38
- conf?: { [tagName: string]: any };
38
+ vName?: string;
39
39
 
40
40
  /**
41
- * variable name for context
41
+ * variable name for App instance
42
+ * @default "nsp"
42
43
  */
43
- vKey?: string;
44
+ nspName?: string;
44
45
 
45
46
  /**
46
- * variable name for App instance
47
+ * property name for data store in context
48
+ * @default "#nsp"
47
49
  */
48
- nspKey?: string;
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
- bundle<T>(...node: Node<T>[]): NodeFn<T>;
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
- emit<T>(type: "error", e: Error, context?: T): string;
108
+ /**
109
+ * retrieve a result from hook function
110
+ */
111
+ process<T>(type: "error", e: Error, context?: T): string;
89
112
 
90
- emit<T>(type: "directive", src: string, context?: T): string;
113
+ process<T>(type: "directive", src: string, context?: T): string;
91
114
 
92
- emit<T>(type: "declaration", src: string, context?: T): string;
115
+ process<T>(type: "declaration", src: string, context?: T): string;
93
116
 
94
- emit<T>(type: "scriptlet", src: string, context?: T): string;
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
- mount(match: RegExp | string, fn: LoaderFn): void;
143
+ /**
144
+ * mount a loader function for the path matched
145
+ */
146
+ mount(path: RegExp | string, fn: LoaderFn): void;
109
147
 
110
- on(type: "error", fn: <T>(e: Error, context?: T) => string | void): void;
148
+ /**
149
+ * register a hook function
150
+ */
151
+ hook(type: "error", fn: <T>(e: Error, context?: T) => string | void): void;
111
152
 
112
- on(type: "directive", fn: <T>(src: string, context?: T) => string | void): void;
153
+ hook(type: "directive", fn: <T>(src: string, context?: T) => string | void): void;
113
154
 
114
- on(type: "declaration", fn: <T>(src: string, context?: T) => string | void): void;
155
+ hook(type: "declaration", fn: <T>(src: string, context?: T) => string | void): void;
115
156
 
116
- on(type: "scriptlet", fn: <T>(src: string, context?: T) => string | void): void;
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.2",
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.7",
11
- "to-xml": "^0.1.10"
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.0",
14
+ "@rollup/plugin-node-resolve": "^15.2.1",
15
15
  "@types/mocha": "^10.0.1",
16
- "@types/node": "^20.5.1",
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.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) {
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
- emit(type, arg) {
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(match, fn) {
60
- return mount(this, match, fn);
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
- const result = app.emit("error", e, context);
20
- if (result == null) {
21
- return `<!--\n[ERR] ${escapeError(e)}\n-->`;
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
- return result;
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 { nspKey, vKey } = app.options;
23
+ const { nspName, vName } = app.options;
24
24
  const js = this.toJS();
25
25
  try {
26
- const fn = Function(nspKey, vKey, `return ${js}`);
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}(?:\\.${nameRE}|\\[(?:${numericRE}|${stringRE})\\])*`;
26
- const opRE = `[+\\-\\*/%=<>()!|&:?,]+(?![+\\-\\*/%=<>()!|&:?,])`;
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 { nspKey, vKey } = app.options;
45
+ const { nspName, vName } = app.options;
47
46
  const js = this.toJS();
48
47
  try {
49
- const fn = Function(nspKey, vKey, `return ${js}`);
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 { nspKey, vKey } = app.options;
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] = `${nspKey}.fn(${JSON.stringify(exp)})(`;
79
+ array[i] = `${nspName}.fn(${JSON.stringify(exp)})(`;
81
80
  }
82
81
  else if (variableRegExp.test(exp)) {
83
82
  // variable
84
- exp = exp.replace(/\./g, "?.");
85
- exp = exp.replace(/\[/g, "?.[");
83
+ exp = exp.replace(/(\?)?\./g, "?.");
84
+ exp = exp.replace(/(\?\.)?\[/g, "?.[");
86
85
  exp = exp.replace(/\s+$/, "");
87
- array[i] = `${vKey}.${exp}`;
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, nspKey, vKey } = app.options;
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 = `${vKey} => ${js}`; // array function
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 `${nspKey}.bundle(${bodyL}${body}${bodyR})`; // root element
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 = `${vKey} => (${attr})`; // array function
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}${nspKey}.tag(${nameV}${attrV}${bodyV})`;
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 { nspKey } = app.options;
149
+ const { nspName } = app.options;
144
150
  const js = this.toJS();
145
151
  try {
146
- const fn = Function(nspKey, `return ${js}`);
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
  }
@@ -25,13 +25,13 @@ class ScriptletParser {
25
25
  */
26
26
  toFn() {
27
27
  const { app } = this;
28
- const { nspKey } = app.options;
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(nspKey, `return ${js}`);
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 { nspKey, vKey } = app.options;
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(/\s\s+/sg, " ");
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 `${vKey} => (${src})`;
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 = `${vKey} => ${nspKey}.emit("${type}", ${src}, ${vKey})`;
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 { nspKey, vKey } = app.options;
37
+ const { nspName, vName } = app.options;
38
38
  const js = this.toJS();
39
39
  try {
40
- const fn = Function(nspKey, vKey, `return ${js}`);
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, options } = app;
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, conf, attr: attrFn, body };
20
+ const tagDef = { name, app, attr: attrFn, body };
22
21
  return tagFn(tagDef);
23
22
  };
24
23
  const defaultTagFn = (tagDef) => {