nsp-server-pages 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ import { parseText } from "./parse-text.js";
2
+ import { parseAttr } from "./parse-attr.js";
3
+ import { parseScriptlet } from "./parse-scriptlet.js";
4
+ const emptyText = {
5
+ '""': true,
6
+ "''": true,
7
+ "``": true,
8
+ "null": true,
9
+ "undefined": true,
10
+ "": true,
11
+ };
12
+ const isElement = (node) => ("function" === typeof node?.toJS);
13
+ /**
14
+ * Parser for JSP document
15
+ */
16
+ export const parseJSP = (app, src) => new JspParser(app, src);
17
+ /**
18
+ * Root element or an taglib element
19
+ */
20
+ class Element {
21
+ constructor(app, tagName, tagLine) {
22
+ this.app = app;
23
+ this.tagName = tagName;
24
+ this.tagLine = tagLine;
25
+ this.children = [];
26
+ //
27
+ }
28
+ append(node) {
29
+ this.children.push(node);
30
+ }
31
+ /**
32
+ * Transpile JSP document to JavaScript source code
33
+ */
34
+ toJS(option) {
35
+ const { app, tagLine } = this;
36
+ const { comment, nspKey, vKey } = app.options;
37
+ const indent = +app.options.indent || 0;
38
+ const currentIndent = +option?.indent || 0;
39
+ const nextIndent = currentIndent + indent;
40
+ const currentLF = currentIndent ? "\n" + " ".repeat(currentIndent) : "\n";
41
+ const nextLF = nextIndent ? "\n" + " ".repeat(nextIndent) : "\n";
42
+ const { children } = this;
43
+ const args = children.map(item => {
44
+ if (isElement(item)) {
45
+ return item.toJS({ indent: nextIndent });
46
+ }
47
+ else if (!/\S/.test(item)) {
48
+ // item with only whitespace
49
+ return '""';
50
+ }
51
+ else {
52
+ let js = parseText(app, item).toJS({ indent: nextIndent });
53
+ if (/\(.+?\)|\$\{.+?}/s.test(js)) {
54
+ js = `${vKey} => ${js}`; // array function
55
+ }
56
+ return js;
57
+ }
58
+ }).filter(v => !emptyText[v]);
59
+ const hasBody = !!children.length;
60
+ // keep at least single empty string if all arguments are empty strings
61
+ if (hasBody && !args.length) {
62
+ args.push('""');
63
+ }
64
+ const { tagName } = this;
65
+ const isRoot = !tagName;
66
+ const last = args.length - 1;
67
+ args.forEach((v, idx) => {
68
+ const isComment = /^\/\/[^\n]*$/s.test(v);
69
+ if (idx !== last && !isComment) {
70
+ args[idx] += ",";
71
+ }
72
+ else if (idx === last && isComment) {
73
+ args[idx] += currentLF;
74
+ }
75
+ });
76
+ let body = args.join(nextLF);
77
+ const bodyL = /^`\n/s.test(body) ? (isRoot ? "" : " ") : nextLF;
78
+ const bodyR = /(\n`|[)\s])$/s.test(body) ? "" : currentLF;
79
+ if (isRoot) {
80
+ return `${nspKey}.bundle(${bodyL}${body}${bodyR})`; // root element
81
+ }
82
+ // attributes as the second argument
83
+ let attr = parseAttr(app, tagLine).toJS({ indent: args.length ? nextIndent : currentIndent });
84
+ if (/\(.+?\)|\$\{.+?}/s.test(attr)) {
85
+ attr = `${vKey} => (${attr})`; // array function
86
+ }
87
+ const commentV = comment ? `// ${tagLine?.replace(/\s*[\r\n]\s*/g, " ") ?? ""}${currentLF}` : "";
88
+ const nameV = JSON.stringify(tagName);
89
+ const hasAttr = /:/.test(attr);
90
+ const attrV = (hasBody || hasAttr) ? `, ${attr}` : "";
91
+ const bodyV = hasBody ? `,${bodyL}${body}${bodyR}` : "";
92
+ return `${commentV}${nspKey}.tag(${nameV}${attrV}${bodyV})`;
93
+ }
94
+ }
95
+ /**
96
+ * Tree of elements
97
+ */
98
+ class Tree {
99
+ constructor(root) {
100
+ this.root = root;
101
+ this.tree = [];
102
+ this.tree.push(root);
103
+ }
104
+ append(node) {
105
+ this.tree.at(0).append(node);
106
+ }
107
+ open(node) {
108
+ this.append(node);
109
+ this.tree.unshift(node);
110
+ }
111
+ close(tagName) {
112
+ const openTag = this.getTagName();
113
+ if (openTag !== tagName) {
114
+ throw new Error(`mismatch closing tag: ${openTag} !== ${tagName}`);
115
+ }
116
+ this.tree.shift();
117
+ }
118
+ getTagName() {
119
+ return this.tree.at(0).tagName;
120
+ }
121
+ isRoot() {
122
+ return this.tree.length === 1;
123
+ }
124
+ }
125
+ class JspParser {
126
+ constructor(app, src) {
127
+ this.app = app;
128
+ this.src = src;
129
+ //
130
+ }
131
+ /**
132
+ * Transpile JSP document to JavaScript source code
133
+ */
134
+ toJS(option) {
135
+ const { app, src } = this;
136
+ return jspToJS(app, src, option);
137
+ }
138
+ /**
139
+ * Compile JSP document to JavaScript function
140
+ */
141
+ toFn() {
142
+ const { app } = this;
143
+ const { nspKey } = app.options;
144
+ const js = this.toJS();
145
+ try {
146
+ const fn = Function(nspKey, `return ${js}`);
147
+ return fn(app);
148
+ }
149
+ catch (e) {
150
+ app.log("JspParser: " + js?.substring(0, 1000));
151
+ throw e;
152
+ }
153
+ }
154
+ }
155
+ const nameRE = `[A-Za-z][A-Za-z0-9]*`;
156
+ const stringRE = `"(?:\\\\[.]|[^\\\\"])*"|'(?:\\\\[.]|[^\\\\'])*'`;
157
+ const insideRE = `[^"']|${stringRE}`;
158
+ const tagRegExp = new RegExp(`(</?${nameRE}:(?:${insideRE})*?>)|(<%(?:${insideRE})*?%>)`, "s");
159
+ export const jspToJS = (app, src, option) => {
160
+ const root = new Element(app);
161
+ const tree = new Tree(root);
162
+ const { trimSpaces } = app.options;
163
+ const array = src.split(tagRegExp);
164
+ for (let i = 0; i < array.length; i++) {
165
+ const i3 = i % 3;
166
+ let str = array[i];
167
+ if (i3 === 1 && str) {
168
+ // taglib
169
+ const tagName = str.match(/^<\/?([^\s=/>]+)/)?.[1];
170
+ if (/^<\//.test(str)) {
171
+ tree.close(tagName);
172
+ continue;
173
+ }
174
+ const element = new Element(app, tagName, str);
175
+ if (/\/\s*>$/.test(str)) {
176
+ tree.append(element);
177
+ }
178
+ else {
179
+ tree.open(element);
180
+ }
181
+ }
182
+ else if (i3 === 2 && str) {
183
+ // <% scriptlet %>
184
+ const item = parseScriptlet(app, str);
185
+ tree.append(item);
186
+ }
187
+ else if (i3 === 0) {
188
+ // 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
+ tree.append(str);
196
+ }
197
+ }
198
+ if (!tree.isRoot()) {
199
+ throw new Error("missing closing tag: " + tree.getTagName());
200
+ }
201
+ return root.toJS(option);
202
+ };
@@ -0,0 +1,65 @@
1
+ import { parseEL } from "./parse-el.js";
2
+ /**
3
+ * Parser for Directive, Declaration, Scriptlet
4
+ * <%-- comment --%>
5
+ * <%@ directive %>
6
+ * <%! declaration(s) %>
7
+ * <% scriptlet %>
8
+ * <%= expression %>
9
+ */
10
+ export const parseScriptlet = (app, src) => new ScriptletParser(app, src);
11
+ const typeMap = {
12
+ "<%-": "comment",
13
+ "<%@": "directive",
14
+ "<%!": "declaration",
15
+ "<%=": "expression",
16
+ };
17
+ class ScriptletParser {
18
+ constructor(app, src) {
19
+ this.app = app;
20
+ this.src = src;
21
+ //
22
+ }
23
+ /**
24
+ * Compile <% scriptlet %> to JavaScript function instance
25
+ */
26
+ toFn() {
27
+ const { app } = this;
28
+ const { nspKey } = app.options;
29
+ const js = this.toJS();
30
+ const isComment = /^\/\/[^\n]*$/s.test(js);
31
+ if (isComment)
32
+ return () => null;
33
+ try {
34
+ const fn = Function(nspKey, `return ${js}`);
35
+ return fn(app);
36
+ }
37
+ catch (e) {
38
+ app.log("ScriptletParser: " + js?.substring(0, 1000));
39
+ throw e;
40
+ }
41
+ }
42
+ /**
43
+ * Transpile <% scriptlet %> to JavaScript source code
44
+ */
45
+ toJS(option) {
46
+ const { app } = this;
47
+ const { nspKey, vKey } = app.options;
48
+ let { src } = this;
49
+ const type = typeMap[src.substring(0, 3)] || "scriptlet";
50
+ if (type === "comment") {
51
+ src = src.replace(/\s\s+/sg, " ");
52
+ return `// ${src}`;
53
+ }
54
+ if (type === "expression") {
55
+ src = src.replace(/^<%=\s*/s, "");
56
+ src = src.replace(/\s*%>$/s, "");
57
+ src = parseEL(app, src).toJS(option);
58
+ return `${vKey} => (${src})`;
59
+ }
60
+ app.log(`${type} found: ${src?.substring(0, 1000)}`);
61
+ src = /`|\$\{/.test(src) ? JSON.stringify(src) : "`" + src + "`";
62
+ src = `${vKey} => ${nspKey}.emit("${type}", ${src}, ${vKey})`;
63
+ return src;
64
+ }
65
+ }
@@ -0,0 +1,114 @@
1
+ import { parseEL } from "./parse-el.js";
2
+ import { parseScriptlet } from "./parse-scriptlet.js";
3
+ /**
4
+ * escape special characters in Template Literal
5
+ */
6
+ const escapeTL = (str) => str.replace(/([\\`$])/g, "\\$1");
7
+ const TLStringify = (str) => ("`" + escapeTL(str) + "`");
8
+ const stringify = (str) => (/[\r\n"<>]/.test(str) ? TLStringify(str) : JSON.stringify(str));
9
+ /**
10
+ * Regular expression to match ${expression} in text
11
+ */
12
+ const stringRE = `"(?:\\\\[.]|[^\\\\"])*"|'(?:\\\\[.]|[^\\\\'])*'`;
13
+ const insideRE = `[^"']|${stringRE}`;
14
+ const bodyRE = `([$#][{](?:${insideRE})*?})|(<%=(?:${insideRE})*?%>)`;
15
+ const bodyRegExp = new RegExp(bodyRE, "s");
16
+ /**
17
+ * Parser for: text content
18
+ */
19
+ export const parseText = (app, src) => new TextParser(app, src);
20
+ class TextParser {
21
+ constructor(app, src) {
22
+ this.app = app;
23
+ this.src = src;
24
+ //
25
+ }
26
+ /**
27
+ * Transpile ${expression} and <% scriptlet %> to JavaScript source code
28
+ */
29
+ toJS(option) {
30
+ return textToJS(this.app, this.src, option);
31
+ }
32
+ /**
33
+ * Compile ${expression} and <% scriptlet %> to JavaScript function instance
34
+ */
35
+ toFn() {
36
+ const { app } = this;
37
+ const { nspKey, vKey } = app.options;
38
+ const js = this.toJS();
39
+ try {
40
+ const fn = Function(nspKey, vKey, `return ${js}`);
41
+ return (context) => fn(app, context);
42
+ }
43
+ catch (e) {
44
+ app.log("TextParser: " + js?.substring(0, 1000));
45
+ throw e;
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Transpile ${expression}, #{async expression} and <% scriptlet %> to JavaScript source code
51
+ */
52
+ const textToJS = (app, src, option) => {
53
+ const array = src.split(bodyRegExp);
54
+ const items = [];
55
+ for (let i = 0; i < array.length; i++) {
56
+ const i3 = i % 3;
57
+ let value = array[i];
58
+ if (!value)
59
+ continue;
60
+ if (i3 === 1) {
61
+ // ${expression}, #{async expression}
62
+ const isAsync = /^#/s.test(value);
63
+ value = value.replace(/^[$#]\{\s*/s, "");
64
+ value = value.replace(/\s*}$/s, "");
65
+ const item = parseEL(app, value);
66
+ if (isAsync) {
67
+ items.push({ toJS: (option) => `await ${item.toJS(option)}` });
68
+ }
69
+ else {
70
+ items.push(item);
71
+ }
72
+ }
73
+ else if (i3 === 2) {
74
+ // <% scriptlet %>
75
+ const item = parseScriptlet(app, value);
76
+ items.push(item);
77
+ }
78
+ else {
79
+ // text literal
80
+ items.push(value);
81
+ }
82
+ }
83
+ // empty string
84
+ if (!items.length)
85
+ return '""';
86
+ // single element
87
+ if (items.length === 1) {
88
+ const item = items[0];
89
+ if (typeof item === "string") {
90
+ return stringify(item);
91
+ }
92
+ else {
93
+ return "(" + item.toJS(option) + ")";
94
+ }
95
+ }
96
+ let hasAwait = false;
97
+ items.forEach((item, i) => {
98
+ if (typeof item === "string") {
99
+ items[i] = escapeTL(item);
100
+ }
101
+ else {
102
+ const js = item.toJS(option);
103
+ if (/^\(?await\s/.test(js))
104
+ hasAwait = true;
105
+ items[i] = "${" + js + "}";
106
+ }
107
+ });
108
+ let out = "`" + items.join("") + "`";
109
+ // wrap in async function if it has an await
110
+ if (hasAwait) {
111
+ out = `(async () => (${out}))()`;
112
+ }
113
+ return out;
114
+ };
package/src/taglib.js ADDED
@@ -0,0 +1,39 @@
1
+ import { toXML } from "to-xml";
2
+ export const addTagLib = (app, tagLibDef) => {
3
+ const { fnMap, tagMap } = app;
4
+ const { ns, fn, tag } = tagLibDef;
5
+ if (fn) {
6
+ for (const name in fn) {
7
+ fnMap.set(`${ns}:${name}`, fn[name]);
8
+ }
9
+ }
10
+ if (tag) {
11
+ for (const name in tag) {
12
+ tagMap.set(`${ns}:${name}`, tag[name]);
13
+ }
14
+ }
15
+ };
16
+ export const prepareTag = (app, name, attr, body) => {
17
+ const { tagMap, options } = app;
18
+ const tagFn = tagMap.get(name) || defaultTagFn;
19
+ const conf = options.conf[name];
20
+ const attrFn = !attr ? () => ({}) : (typeof attr !== "function") ? () => attr : attr;
21
+ const tagDef = { name, app, conf, attr: attrFn, body };
22
+ return tagFn(tagDef);
23
+ };
24
+ const defaultTagFn = (tagDef) => {
25
+ const { name } = tagDef;
26
+ // tagDef.app.log(`Unknown tag: ${name}`);
27
+ return (context) => {
28
+ const attr = tagDef.attr(context);
29
+ const body = tagDef.body(context);
30
+ const xml = {};
31
+ xml[name] = { "@": attr };
32
+ let tag = toXML(xml);
33
+ if (body === null) {
34
+ return tag;
35
+ }
36
+ tag = tag.replace(/\/>$/, `>`);
37
+ return tagDef.app.concat(tag, body, `</${name}>`);
38
+ };
39
+ };