nsp-server-pages 0.0.2 → 0.1.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/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/bundle.js CHANGED
@@ -6,13 +6,16 @@ const join = (a, b) => (a == null ? b : (b == null ? a : a + b));
6
6
  const bundle = (array, start, end) => {
7
7
  start = +start || 0;
8
8
  end = +end || array?.length || 0;
9
+ // empty
9
10
  if (end <= start) {
10
11
  return () => null;
11
12
  }
13
+ // single item
12
14
  if (end - 1 === start) {
13
15
  const node = array[start];
14
16
  return (typeof node === "function") ? node : () => node;
15
17
  }
18
+ // multiple items
16
19
  return context => {
17
20
  let result;
18
21
  let promise;
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
@@ -7,23 +7,24 @@
7
7
  export const createNSP: (options?: NSP.Options) => NSP.App;
8
8
 
9
9
  declare namespace NSP {
10
- type NodeFn<T> = (context?: T) => string | Promise<string>;
10
+ type NodeFn<T> = (context: T) => string | Promise<string>;
11
11
 
12
12
  type Node<T> = string | NodeFn<T>;
13
13
 
14
- type AttrFn<A, T = any> = (context?: T) => A;
14
+ type AttrFn<A, T = any> = (context: T) => A;
15
15
 
16
- type TagFn<A, T = any> = (tag: TagDef<A, T>) => NodeFn<T>;
16
+ type VoidFn<T> = (context: T) => void | Promise<void>;
17
17
 
18
- type LoaderFn = (path: string) => Promise<NodeFn<any>>;
18
+ type TagFn<A, T = any> = (tag: TagDef<A, T>) => (NodeFn<T> | VoidFn<T>);
19
19
 
20
- type TextFlex = string | Promise<string> | (string | Promise<string>)[];
20
+ type LoaderFn = (path: string) => Promise<NodeFn<any> | undefined>;
21
21
 
22
- type Build<T> = (nsp: App) => (context?: T) => string | Promise<string>;
22
+ type Strings = string | Promise<string> | Strings[];
23
+
24
+ type Build<T> = (nsp: App) => (context: T) => string | Promise<string>;
23
25
 
24
26
  interface TagDef<A, T = any> {
25
27
  app: App;
26
- conf: any;
27
28
  name: string;
28
29
  attr: AttrFn<A, T>;
29
30
  body: NodeFn<T>;
@@ -33,35 +34,47 @@ declare namespace NSP {
33
34
  logger?: { log: (message: string) => void };
34
35
 
35
36
  /**
36
- * tag configuration
37
+ * variable name for context
38
+ * @default "v"
37
39
  */
38
- conf?: { [tagName: string]: any };
40
+ vName?: string;
39
41
 
40
42
  /**
41
- * variable name for context
43
+ * variable name for App instance
44
+ * @default "nsp"
42
45
  */
43
- vKey?: string;
46
+ nspName?: string;
44
47
 
45
48
  /**
46
- * variable name for App instance
49
+ * property name for data store in context
50
+ * @default "#nsp"
47
51
  */
48
- nspKey?: string;
52
+ storeKey?: string;
49
53
 
50
54
  /**
51
55
  * indent size for JavaScript source generated
56
+ * @default 0
52
57
  */
53
58
  indent?: number;
54
59
 
55
60
  /**
56
61
  * add comments at toJS() result
62
+ * @default false
57
63
  */
58
64
  comment?: boolean;
59
65
 
60
66
  /**
61
- * remove edge spaces in HTML in some cases
67
+ * set false not to remove edge spaces in HTML in some cases.
68
+ * @default true
62
69
  */
63
70
  trimSpaces?: boolean;
64
71
 
72
+ /**
73
+ * set true to keep EL result value of `null` and `undefined` as is.
74
+ * @default false
75
+ */
76
+ nullish?: boolean;
77
+
65
78
  /**
66
79
  * expression filter before transpile starts
67
80
  */
@@ -79,52 +92,102 @@ declare namespace NSP {
79
92
  options: Options;
80
93
  tagMap: Map<string, TagFn<any>>;
81
94
 
95
+ /**
96
+ * register a tag library
97
+ */
82
98
  addTagLib(tagLibDef: TagLibDef): void;
83
99
 
84
- bundle<T>(...node: Node<T>[]): NodeFn<T>;
100
+ /**
101
+ * build a NodeFn which returns a string for the content nodes
102
+ */
103
+ bundle<T>(...nodes: Node<T>[]): NodeFn<T>;
85
104
 
86
- concat(...text: TextFlex[]): string | Promise<string>;
105
+ /**
106
+ * concat strings even if they are Promise<string>
107
+ */
108
+ concat(...text: Strings[]): string | Promise<string>;
87
109
 
88
- emit<T>(type: "error", e: Error, context?: T): string;
110
+ /**
111
+ * retrieve a result from hook function
112
+ */
113
+ process<T>(type: "error", e: Error, context: T): string;
89
114
 
90
- emit<T>(type: "directive", src: string, context?: T): string;
115
+ process<T>(type: "directive", src: string, context: T): string;
91
116
 
92
- emit<T>(type: "declaration", src: string, context?: T): string;
117
+ process<T>(type: "declaration", src: string, context: T): string;
93
118
 
94
- emit<T>(type: "scriptlet", src: string, context?: T): string;
119
+ process<T>(type: "scriptlet", src: string, context: T): string;
95
120
 
121
+ /**
122
+ * pickup the taglib function
123
+ */
96
124
  fn(name: string): (...args: any[]) => any;
97
125
 
126
+ /**
127
+ * load a NodeFn for the path mounted by mount()
128
+ */
98
129
  load<T>(path: string): Promise<NodeFn<T>>;
99
130
 
131
+ /**
132
+ * load a NodeFn for the local filesystem path
133
+ */
100
134
  loadFile<T>(file: string): Promise<NodeFn<T>>;
101
135
 
102
136
  loadJS<T>(file: string): Promise<NodeFn<T>>;
103
137
 
104
138
  loadJSP<T>(file: string): Promise<NodeFn<T>>;
105
139
 
140
+ /**
141
+ * log a message via options.logger which defaults console.log
142
+ */
106
143
  log(message: string): void;
107
144
 
108
- mount(match: RegExp | string, fn: LoaderFn): void;
145
+ /**
146
+ * mount a loader function for the path matched
147
+ */
148
+ mount(path: RegExp | string, fn: LoaderFn): void;
109
149
 
110
- on(type: "error", fn: <T>(e: Error, context?: T) => string | void): void;
150
+ /**
151
+ * register a hook function
152
+ */
153
+ hook(type: "error", fn: <T>(e: Error, context?: T) => string | void): void;
111
154
 
112
- on(type: "directive", fn: <T>(src: string, context?: T) => string | void): void;
155
+ hook(type: "directive", fn: <T>(src: string, context?: T) => string | void): void;
113
156
 
114
- on(type: "declaration", fn: <T>(src: string, context?: T) => string | void): void;
157
+ hook(type: "declaration", fn: <T>(src: string, context?: T) => string | void): void;
115
158
 
116
- on(type: "scriptlet", fn: <T>(src: string, context?: T) => string | void): void;
159
+ hook(type: "scriptlet", fn: <T>(src: string, context?: T) => string | void): void;
117
160
 
161
+ /**
162
+ * parse a JSP document
163
+ */
118
164
  parse(src: string): Parser;
119
165
 
166
+ /**
167
+ * get a private data store in context
168
+ */
169
+ store<S>(context: any, key: string, initFn?: () => S): S;
170
+
171
+ /**
172
+ * generates a NodeFn for the tag
173
+ */
120
174
  tag<A, T = any>(name: string, attr?: A | AttrFn<A, T>, ...body: Node<T>[]): NodeFn<T>;
121
175
  }
122
176
 
123
177
  interface TagLibDef {
178
+ /**
179
+ * namespace
180
+ */
124
181
  ns: string;
125
182
 
183
+ /**
184
+ * functions
185
+ */
126
186
  fn?: { [name: string]: (...args: any[]) => any };
127
187
 
188
+ /**
189
+ * tags
190
+ */
128
191
  tag?: { [name: string]: TagFn<any> };
129
192
  }
130
193
 
@@ -136,8 +199,14 @@ declare namespace NSP {
136
199
  * Parser for JSP document
137
200
  */
138
201
  interface Parser {
202
+ /**
203
+ * transpile the JSP document to JavaScript source code
204
+ */
139
205
  toJS(option?: ToJSOption): string;
140
206
 
207
+ /**
208
+ * compile the JSP document as a NodeFn
209
+ */
141
210
  toFn<T>(): NodeFn<T>;
142
211
  }
143
212
  }
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.1",
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.4",
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/bundle.js CHANGED
@@ -3,13 +3,16 @@ const join = (a, b) => (a == null ? b : (b == null ? a : a + b));
3
3
  export const bundle = (array, start, end) => {
4
4
  start = +start || 0;
5
5
  end = +end || array?.length || 0;
6
+ // empty
6
7
  if (end <= start) {
7
8
  return () => null;
8
9
  }
10
+ // single item
9
11
  if (end - 1 === start) {
10
12
  const node = array[start];
11
13
  return (typeof node === "function") ? node : () => node;
12
14
  }
15
+ // multiple items
13
16
  return context => {
14
17
  let result;
15
18
  let promise;
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) => {