nsp-server-pages 0.0.1

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