Skip to content

Commit

Permalink
feat: simple virtual dom creator
Browse files Browse the repository at this point in the history
To create XML documents use a virtual dom then render it to a string
  • Loading branch information
blacha committed Mar 17, 2020
1 parent 395324c commit 2d191d9
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 0 deletions.
22 changes: 22 additions & 0 deletions packages/lambda-shared/src/__test__/vdom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as o from 'ospec';
import { V, VNodeType, VToString } from '../vdom';

o.spec('VDom', () => {
o('should create text nodes', () => {
const res = V('div', {}, ['text']);
o(res.children[0]).deepEquals({ type: VNodeType.Raw, text: 'text' });

o(VToString(res)).deepEquals('<div>\n text\n</div>');
});

o('should create nodes', () => {
const res = V('div', {}, [V('span', { style: 'color:red' }, 'text')]);
o(res.children[0]).deepEquals({
type: VNodeType.Node,
tag: 'span',
attrs: { style: 'color:red' },
children: [{ type: VNodeType.Raw, text: 'text' }],
});
o(VToString(res)).deepEquals('<div>\n <span style="color:red">\n text\n </span>\n</div>');
});
});
1 change: 1 addition & 0 deletions packages/lambda-shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export { LambdaHttpResponse, LambdaType } from './lambda.response.http';
export { LogConfig, LogType } from './log';
export { getXyzFromPath, PathData } from './path';
export { LambdaSession } from './session';
export { V, VToString } from './vdom';

export * from './file';
108 changes: 108 additions & 0 deletions packages/lambda-shared/src/vdom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
export type VNodeInput = string | number | VNode;
/**
* Two types of virtual nodes are required raw text and actual elements
*/
export enum VNodeType {
Raw = 'raw',
Node = 'node',
}

export type VNode = VNodeElement | VNodeText;
/** Virtual node */
export interface VNodeElement {
type: VNodeType.Node;
/** XML tag to use eg "div" */
tag: string;
attrs: Record<string, string>;
children: VNode[];
}

/** Text virtual node */
export interface VNodeText {
type: VNodeType.Raw;
/** Value of the text */
text: string;
}

/**
* Validate and convert all children to VNodes
*
* @param children input to validate and convert
*/
function normalizeChildren(children?: VNodeInput[] | VNodeInput): VNode[] {
if (children == null) {
return [];
}
if (Array.isArray(children)) {
const childNodes: VNode[] = [];
for (const c of children) {
if (c == null) continue;
if (typeof c == 'string' || typeof c == 'number') {
childNodes.push({ type: VNodeType.Raw, text: String(c) });
continue;
}

childNodes.push(c);
}
return childNodes;
} else if (typeof children == 'string' || typeof children == 'number') {
return [{ type: VNodeType.Raw, text: String(children) }];
}
return [children];
}

/**
* Create a virtual dom node
*
* @example
* ```typescript
* V('div', {style: 'color:red'},'Hello World')
* ```
*
* @param tag DOM tag to use
* @param attrs DOM attributes
* @param children DOM children
*/
export function V(tag: string, attrs?: Record<string, any>, children?: VNodeInput[] | VNodeInput): VNodeElement {
return {
type: VNodeType.Node,
tag,
attrs: attrs ?? {},
children: normalizeChildren(children),
};
}

function VDomToStringAttrs(n: VNodeElement): string {
const keys = Object.keys(n.attrs);
if (keys.length == 0) {
return '';
}
let out = '';
for (const key of keys) {
const val = n.attrs[key];
if (val == null) continue;
out += ` ${key}="${val}"`;
}
return out;
}

function VDomToStringChildren(n: VNodeElement, level = 0): string {
if (n.children.length == 0) return '';
const lastIndent = ' '.repeat(level);
const indent = lastIndent + ' ';
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return `\n${indent}${n.children.map(c => VToString(c, level + 1)).join(`\n${indent}`)}\n${lastIndent}`;
}

/**
* Convert a virtual dom node to text
* @param node Root virtual node
* @param level current indentation level
*/
export function VToString(node: VNode, level = 0): string {
if (node.type == VNodeType.Raw) {
return node.text;
}
const attrs = VDomToStringAttrs(node);
return `<${node.tag}${attrs}>${VDomToStringChildren(node, level)}</${node.tag}>`;
}

0 comments on commit 2d191d9

Please sign in to comment.