Skip to content

Commit

Permalink
Merge pull request #8 from kawanet/0.2.2
Browse files Browse the repository at this point in the history
0.2.2
  • Loading branch information
kawanet committed Sep 1, 2023
2 parents e217cb8 + 58e481c commit 7af287e
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 49 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nsp-server-pages",
"description": "NSP JavaScript Server Pages for Node.js",
"version": "0.2.1",
"version": "0.2.2",
"author": "@kawanet",
"bugs": {
"url": "https://github.com/kawanet/nsp-server-pages/issues"
Expand Down
19 changes: 9 additions & 10 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type {NSP} from "../index.js";

import {load, mount} from "./mount.js";
import {bundle} from "./bundle.js";
import {catchFn} from "./catch.js";
import {concat} from "./concat.js";
import {FileLoader, JsLoader, JspLoader} from "./loaders.js";
import {load, mount} from "./mount.js";
import {JSP} from "./parser/jsp.js";
import {catchFn} from "./catch.js";
import {bundle} from "./bundle.js";
import {Store} from "./store.js";
import {addTagLib, prepareTag} from "./taglib.js";
import {concat} from "./concat.js";
import {StackStore} from "./stack-store.js";

export class App implements NSP.App {
protected loaders: NSP.LoaderFn[] = [];
Expand Down Expand Up @@ -93,17 +92,17 @@ export class App implements NSP.App {
return loader.load<T>(file);
}

store<P>(context: any, key: string): StackStore<P> {
store<P>(context: any, key: string): NSP.StackStore<P> {
if ("object" !== typeof context && context == null) {
throw new Error("Context must be an object");
}

const {storeKey} = this.options;
const map = (context[storeKey] ??= new Map()) as Map<string, StackStore<any>>;
const map = (context[storeKey] ??= new Map()) as Map<string, Store<any>>;

let value: StackStore<P> = map.get(key);
let value: Store<P> = map.get(key);
if (value == null) {
value = new StackStore<P>();
value = new Store<P>();
map.set(key, value);
}
return value;
Expand Down
4 changes: 2 additions & 2 deletions src/parser/jsp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {NSP} from "../../index.js";
import {Store} from "../store.js";
import {Scriptlet} from "./scriptlet.js";
import {StackStore} from "../stack-store.js";
import {Tag} from "./tag.js";

/**
Expand Down Expand Up @@ -48,7 +48,7 @@ const tagRegExp = new RegExp(`(</?${nameRE}:(?:${insideRE})*?>)|(<%(?:${insideRE

export const jspToJS = (app: NSP.App, src: string, option: NSP.ToJSOption): string => {
const root = new Tag(app);
const tree = new StackStore<Tag>(root);
const tree = new Store<Tag>(root);
const array = src.split(tagRegExp);

for (let i = 0; i < array.length; i++) {
Expand Down
33 changes: 0 additions & 33 deletions src/stack-store.ts

This file was deleted.

47 changes: 47 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {NSP} from "../types/index.js";

/**
* The Store class was implemented using a stack {stack: P[]} at first.
* Now it was changed to a linked list to allow access to the parent.
*/
export class Store<P> implements NSP.StackStore<P> {
item: Item<P>;

constructor(value?: P) {
this.item = {value} as Item<P>;
}

open(value?: P): void {
this.item = {parent: this.item, value};
}

close(): P {
const item = this.item;
this.item = item.parent;
return item.value;
}

get(): P {
return this.item.value;
}

set(value: P): void {
this.item.value = value;
}

find(test: (data: P) => boolean): P {
let item = this.item;

while (item) {
if (test(item.value)) {
return item.value;
}
item = item.parent;
}
}
}

interface Item<P> {
parent: Item<P>;
value: P;
}
30 changes: 28 additions & 2 deletions src/taglib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,45 @@ import {toXML} from "to-xml";
import type {NSP} from "../index.js";
import type {App} from "./app.js";

const isTagCon = (v: any): v is NSP.TagCon<any> => ("function" === typeof (v as NSP.TagCon<any>)?.prototype?.render);

const tagConToTagFn = <A, T>(Tag: NSP.TagCon<A, T>): NSP.TagFn<A, T> => {
return (tag) => {
return (context) => {
const result = new Tag(tag, context).render();
if (result) return result as (string | Promise<string>);
};
};
};

export function addTagLib(this: App, tagLibDef: NSP.TagLibDef): void {
const {fnMap, tagMap} = this;
const {ns, fn, tag} = tagLibDef;

if (fn) {
for (const name in fn) {
fnMap.set(`${ns}:${name}`, fn[name]);
const impl = fn[name];
if (typeof impl === "function") {
// FnFn is called with App instance as this
fnMap.set(`${ns}:${name}`, impl.bind(this));
} else if (impl != null) {
throw new Error(`Invalid taglib implementation: \${${ns}:${name}()}`);
}
}
}

if (tag) {
for (const name in tag) {
tagMap.set(`${ns}:${name}`, tag[name]);
const impl = tag[name];
if (isTagCon(impl)) {
// NSP.TagCon
tagMap.set(`${ns}:${name}`, tagConToTagFn(impl));
} else if (typeof impl === "function") {
// NSP.TagFn
tagMap.set(`${ns}:${name}`, impl);
} else if (impl != null) {
throw new Error(`Invalid taglib implementation: <${ns}:${name}>`);
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions test/300.function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,8 @@ describe(TITLE, () => {

assert.equal(nsp.parse('[${test:upper(bar)}]').toFn()(context), "[BAR]");
});

it("throws", () => {
assert.throws(() => nsp.addTagLib({ns: "test", fn: {invalid: [] as any}}));
});
});
31 changes: 31 additions & 0 deletions test/310.tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,35 @@ describe(TITLE, () => {
assert.equal(await nsp.parse(`<test:na>FOO</test:na>`).toFn()(ctx), `<test:na>FOO</test:na>`);
assert.equal(await nsp.parse(`<test:na><test:na/></test:na>`).toFn()(ctx), `<test:na><test:na/></test:na>`);
});

it("throws", () => {
assert.throws(() => nsp.addTagLib({ns: "test", tag: {invalid: [] as any}}));
});

it("VoidFn", async () => {
nsp.addTagLib({
ns: "void",
tag: {
syncFn: (_) => (_): void => {
// void
},
asyncFn: (_) => async (_) => {
// Promise<void>
},
syncClass: class {
render() {
// void
}
},
asyncClass: class {
async render() {
// Promise<void>
}
},
},
});

const src: string = `[<void:syncFn/>][<void:asyncFn/>][<void:syncClass/>][<void:asyncClass/>]`;
assert.equal(await nsp.parse(src).toFn()(ctx), `[][][][]`);
});
});
51 changes: 51 additions & 0 deletions test/350.tag-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {strict as assert} from "assert";
import {createNSP, NSP} from "../index.js";

const TITLE = "350.tag-class.ts";

interface TagAttr {
//
}

describe(TITLE, () => {
const nsp = createNSP();
const ctx = {};

class BodyTag implements NSP.TagClass {
constructor(private tag: NSP.TagDef<TagAttr>, private context: any) {
//
}

async render() {
const body = await this.tag.body(this.context);
return (body == null) ? "null" : (body === "") ? "empty" : ("string" === typeof body) ? body : typeof body;
}
}

nsp.addTagLib({ns: "test", tag: {body: BodyTag}});

it("empty tag", async () => {
assert.equal(await nsp.parse(`[<test:body/>]`).toFn()(ctx), "[null]");
});

it("empty body", async () => {
assert.equal(await nsp.parse(`[<test:body></test:body>]`).toFn()(ctx), "[empty]");
});

it("string body", async () => {
assert.equal(await nsp.parse(`[<test:body>FOO</test:body>]`).toFn()(ctx), "[FOO]");
});

it("nested", async () => {
assert.equal(await nsp.parse(`[<test:body><test:body/></test:body>]`).toFn()(ctx), "[null]");
assert.equal(await nsp.parse(`[<test:body><test:body></test:body></test:body>]`).toFn()(ctx), "[empty]");
assert.equal(await nsp.parse(`[<test:body><test:body>FOO</test:body></test:body>]`).toFn()(ctx), "[FOO]");
});

it("unregistered tag", async () => {
assert.equal(await nsp.parse(`<test:na/>`).toFn()(ctx), `<test:na/>`);
assert.equal(await nsp.parse(`<test:na></test:na>`).toFn()(ctx), `<test:na></test:na>`);
assert.equal(await nsp.parse(`<test:na>FOO</test:na>`).toFn()(ctx), `<test:na>FOO</test:na>`);
assert.equal(await nsp.parse(`<test:na><test:na/></test:na>`).toFn()(ctx), `<test:na><test:na/></test:na>`);
});
});
30 changes: 30 additions & 0 deletions test/360.function-this.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {strict as assert} from "assert";
import {createNSP, NSP} from "../index.js";

const TITLE = "360.function-this.ts";

describe(TITLE, () => {
const nsp = createNSP();
const ctx = {};

nsp.addTagLib({
ns: "test",
fn: {thisIsApp, thisIsNull}
});

function thisIsApp(this: NSP.App) {
return this instanceof nsp.constructor;
}

function thisIsNull(this: NSP.App) {
return this == null;
}

it("test:thisIsApp()", async () => {
assert.equal(nsp.parse('[${test:thisIsApp()}]').toFn()(ctx), "[true]");
});

it("test:thisIsNull()", async () => {
assert.equal(nsp.parse('[${test:thisIsNull()}]').toFn()(ctx), "[false]");
});
});
8 changes: 7 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export declare namespace NSP {

type TagFn<A, T = any> = (tag: TagDef<A, T>) => (NodeFn<T> | VoidFn<T>);

type TagCon<A, T = any> = { new(tag: NSP.TagDef<A>, context: T): TagClass };

type LoaderFn = (path: string) => Promise<NodeFn<any> | undefined>;

type Strings = string | Promise<string> | Strings[];
Expand Down Expand Up @@ -166,7 +168,11 @@ export declare namespace NSP {
/**
* tags
*/
tag?: { [name: string]: TagFn<any> };
tag?: { [name: string]: TagFn<any> | TagCon<any> };
}

interface TagClass {
render(): string | Promise<string> | void | Promise<void>;
}

interface StackStore<P> {
Expand Down

0 comments on commit 7af287e

Please sign in to comment.