pawa-ssr 1.0.0
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/LICENSE +21 -0
- package/README.md +13 -0
- package/classes.js +56 -0
- package/index.js +448 -0
- package/package.json +29 -0
- package/pawaComponent.js +71 -0
- package/pawaElement.js +103 -0
- package/power.js +108 -0
- package/utils.js +151 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Allwell Oriso-owubo (Allisboy)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# pawajs-ssr
|
|
2
|
+
pawajs ssr (server side rendering) for javascript
|
|
3
|
+
|
|
4
|
+
# Directives
|
|
5
|
+
server-for, server-if,server-else,server-else-if
|
|
6
|
+
|
|
7
|
+
>>>html
|
|
8
|
+
<div server-if="user.value.name">
|
|
9
|
+
<h1>@(user.value.name)</h1>
|
|
10
|
+
</div>
|
|
11
|
+
>>>
|
|
12
|
+
|
|
13
|
+
* Notice pawajs ssr uses @() instead of @{} and pawajs ssr doesn't use client's hooks
|
package/classes.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
|
|
2
|
+
export const ComponentProps=(some,message)=>{
|
|
3
|
+
|
|
4
|
+
return({
|
|
5
|
+
Array:()=>{
|
|
6
|
+
|
|
7
|
+
if (Array.isArray(some)) {
|
|
8
|
+
return true
|
|
9
|
+
}else{
|
|
10
|
+
throw new Error(message ?message + ' / Not type of an Array ': `${some} must be an array`);
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
String:()=>{
|
|
15
|
+
if (typeof some === 'string') {
|
|
16
|
+
return true
|
|
17
|
+
}else{
|
|
18
|
+
throw new Error(message? message + ' / Not type of a String' :`${some} must be a string`);
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
Number:()=>{
|
|
23
|
+
if (typeof some === 'number') {
|
|
24
|
+
return true
|
|
25
|
+
}else{
|
|
26
|
+
throw new Error(message? message+' / Not type of a Number ': `${some} must be a number`);
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
Object:()=>{
|
|
31
|
+
if (typeof some === 'object') {
|
|
32
|
+
return true
|
|
33
|
+
}else{
|
|
34
|
+
throw new Error(message? message+' / Not type of an Object ' :`${some} must be an object`);
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
Function:()=>{
|
|
39
|
+
if (typeof some === 'function') {
|
|
40
|
+
return true
|
|
41
|
+
}else{
|
|
42
|
+
throw new Error(message? message+' / Not type of a Function ': `${some} must be a function`);
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
Boolean:()=>{
|
|
47
|
+
if (typeof some === 'boolean') {
|
|
48
|
+
return true
|
|
49
|
+
}else{
|
|
50
|
+
throw new Error(message? message+' / Not type of a Boolean ' :`${some} must be a Boolean`);
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import {Attr, DOMParser,parseHTML,Node, HTMLElement} from 'linkedom'
|
|
2
|
+
import PawaElement from './pawaElement.js'
|
|
3
|
+
import { If,Else,ElseIf,For } from './power.js';
|
|
4
|
+
import PawaComponent from './pawaComponent.js';
|
|
5
|
+
import { sanitizeTemplate,propsValidator, evaluateExpr } from './utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @type{null|{_formerContext:stateContext,_hasRun:boolean,_prop:object,_name:string,_insert:object,_transportContext}}
|
|
9
|
+
*/
|
|
10
|
+
// export let stateContext=null;
|
|
11
|
+
// let formerContext=null;
|
|
12
|
+
export const components=new Map()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export const $state=(arg)=>{
|
|
17
|
+
return {
|
|
18
|
+
value:arg
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// under consideration
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @type{string}
|
|
26
|
+
*/
|
|
27
|
+
export const allServerAttr=['server-if','server-else','server-else-if','server-for']
|
|
28
|
+
|
|
29
|
+
export const RegisterComponent=(...arg)=>{
|
|
30
|
+
|
|
31
|
+
arg.forEach(func=>{
|
|
32
|
+
if (typeof func === 'function') {
|
|
33
|
+
components.set(func.name.toUpperCase(),func)
|
|
34
|
+
}else{
|
|
35
|
+
console.warn('must be a function')
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const compoBeforeCall = new Set()
|
|
42
|
+
const compoAfterCall=new Set()
|
|
43
|
+
const renderBeforePawa=new Set()
|
|
44
|
+
const renderAfterPawa=new Set()
|
|
45
|
+
const renderBeforeChild=new Set()
|
|
46
|
+
const attrPlugin=new Set()
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {{
|
|
49
|
+
* attribute?:{register:Array<string>,plugin:(el:HTMLElement,attr:object)=>void},
|
|
50
|
+
* component?:{
|
|
51
|
+
* beforeCall?:(stateContext:PawaComponent,app:object)=>void,
|
|
52
|
+
* afterCall?:(stateContext:PawaComponent,el:HTMLElement)=>void
|
|
53
|
+
* },
|
|
54
|
+
* renderSystem?:{
|
|
55
|
+
* beforePawa?:(el:HTMLElement,context:object)=>void,
|
|
56
|
+
* afterPawa?:(el:PawaElement)=>void,
|
|
57
|
+
* beforeChildRender?:(el:PawaElement)=>void
|
|
58
|
+
* }
|
|
59
|
+
* }} PluginObject
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* @param {Array<()=>PluginObject>} func
|
|
63
|
+
*/
|
|
64
|
+
export const PluginSystem=(...func)=>{
|
|
65
|
+
func.forEach(fn=>{
|
|
66
|
+
/**
|
|
67
|
+
* @type {PluginObject}
|
|
68
|
+
*/
|
|
69
|
+
const getPlugin=fn()
|
|
70
|
+
// attributes plugin or extension
|
|
71
|
+
if (getPlugin?.attribute) {
|
|
72
|
+
const attr=getPlugin.attribute
|
|
73
|
+
if(attr.register === null){
|
|
74
|
+
console.error('attribute register must be giving is an array of attributes to add into pawajs attribute rendering')
|
|
75
|
+
}
|
|
76
|
+
if(Array.isArray(attr.register)){
|
|
77
|
+
attr.register.forEach(attr=>{
|
|
78
|
+
if(pawaAttributes.has(attr)){
|
|
79
|
+
console.warn('attribute already exist in pawajs Attributes',attr)
|
|
80
|
+
throw Error('attribute already exist ',attr)
|
|
81
|
+
}else{
|
|
82
|
+
pawaAttributes.add(attr)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}else{
|
|
86
|
+
console.warn('pawa attribute plugin register must be an array')
|
|
87
|
+
}
|
|
88
|
+
if(attr.plugin === null){
|
|
89
|
+
console.error('attribute plugin function must be giving, is a function of attributes to run the plugin pawajs attribute rendering')
|
|
90
|
+
}else{
|
|
91
|
+
if(attr.plugin instanceof Function){
|
|
92
|
+
attrPlugin.add(attr.plugin)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
if (getPlugin?.component) {
|
|
99
|
+
if (getPlugin.component?.beforeCall && typeof getPlugin.component?.beforeCall === 'function') {
|
|
100
|
+
compoBeforeCall.add(getPlugin.component.beforeCall)
|
|
101
|
+
}
|
|
102
|
+
if (getPlugin.component?.afterCall && typeof getPlugin.component?.afterCall === 'function') {
|
|
103
|
+
compoAfterCall.add(getPlugin.component.afterCall)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (getPlugin?.renderSystem) {
|
|
107
|
+
if (getPlugin.renderSystem?.beforePawa && typeof getPlugin.renderSystem?.beforePawa === 'function') {
|
|
108
|
+
renderBeforePawa.add(getPlugin.renderSystem?.beforePawa)
|
|
109
|
+
}
|
|
110
|
+
if (getPlugin.renderSystem?.afterPawa && typeof getPlugin.renderSystem?.afterPawa === 'function') {
|
|
111
|
+
renderAfterPawa.add(getPlugin.renderSystem?.afterPawa)
|
|
112
|
+
}
|
|
113
|
+
if (getPlugin.renderSystem?.beforeChildRender && typeof getPlugin.renderSystem?.beforeChildRender === 'function') {
|
|
114
|
+
renderAfterPawa.add(getPlugin.renderSystem?.beforeChildRender)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
*
|
|
123
|
+
* @param {PawaElement|HTMLElement} el
|
|
124
|
+
* @returns
|
|
125
|
+
*/
|
|
126
|
+
const component=(el)=>{
|
|
127
|
+
if(el._running){
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const slot=el._slots
|
|
132
|
+
const slots={}
|
|
133
|
+
let stateContext=null
|
|
134
|
+
Array.from(slot.children).forEach(prop =>{
|
|
135
|
+
if (prop.getAttribute('prop')) {
|
|
136
|
+
slots[prop.getAttribute('prop')]=prop.innerHTML
|
|
137
|
+
}else{
|
|
138
|
+
console.warn('sloting props must have prop attribute')
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
const children=el._componentChildren
|
|
142
|
+
const component =el._component
|
|
143
|
+
stateContext=component
|
|
144
|
+
const insert=(arg={})=>{
|
|
145
|
+
Object.assign(stateContext.context,arg)
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
*
|
|
149
|
+
* @param {object} props
|
|
150
|
+
* @returns {object}
|
|
151
|
+
*/
|
|
152
|
+
stateContext._prop={children,...el._props}
|
|
153
|
+
stateContext._name=el._componentName
|
|
154
|
+
const useValidateProps=(props={}) => {
|
|
155
|
+
if (!stateContext) {
|
|
156
|
+
console.warn('must be used inside of a component')
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return propsValidator(props,stateContext._prop,stateContext._name)
|
|
161
|
+
}
|
|
162
|
+
const app = {
|
|
163
|
+
children,
|
|
164
|
+
app:{
|
|
165
|
+
insert,
|
|
166
|
+
useValidateProps
|
|
167
|
+
},
|
|
168
|
+
...slots,
|
|
169
|
+
...el._props
|
|
170
|
+
}
|
|
171
|
+
for (const fn of compoBeforeCall) {
|
|
172
|
+
try {
|
|
173
|
+
fn(stateContext,app)
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(error.message,error.stack)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const {document}=parseHTML()
|
|
179
|
+
const comment=document.createComment('componet')
|
|
180
|
+
el.replaceWith(comment)
|
|
181
|
+
const div=document.createElement('div')
|
|
182
|
+
let compo
|
|
183
|
+
try{
|
|
184
|
+
compo=sanitizeTemplate(component.component(app))
|
|
185
|
+
}catch(error){
|
|
186
|
+
console.error(error.message,error.stack)
|
|
187
|
+
}
|
|
188
|
+
if (component?._insert) {
|
|
189
|
+
Object.assign(el._context,component._insert)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
div.innerHTML=compo
|
|
193
|
+
if(Object.entries(el._restProps).length > 0){
|
|
194
|
+
const findElement=div.querySelector('[--]') || div.querySelector('[rest]')
|
|
195
|
+
if (findElement) {
|
|
196
|
+
for (const [key,value] of Object.entries(el._restProps)) {
|
|
197
|
+
findElement.setAttribute(value.name,value.value)
|
|
198
|
+
findElement.removeAttribute('--')
|
|
199
|
+
findElement.removeAttribute('rest')
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const fn of compoAfterCall) {
|
|
204
|
+
try {
|
|
205
|
+
fn(stateContext,div?.firstElementChild)
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(error.message,error.stack)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const newElement=div.firstElementChild
|
|
211
|
+
if (newElement) {
|
|
212
|
+
comment.replaceWith(newElement)
|
|
213
|
+
render(newElement,el._context)
|
|
214
|
+
}
|
|
215
|
+
comment.remove()
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.log(error.message,error.stack);
|
|
218
|
+
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const textContentHandler=(el)=>{
|
|
222
|
+
if (el._running) {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
const nodesMap = new Map();
|
|
226
|
+
|
|
227
|
+
// Get all text nodes and store their original content
|
|
228
|
+
const textNodes = el.childNodes.filter(node => node.nodeType === 3);
|
|
229
|
+
textNodes.forEach(node => {
|
|
230
|
+
nodesMap.set(node, node.nodeValue);
|
|
231
|
+
});
|
|
232
|
+
const evaluate = () => {
|
|
233
|
+
try {
|
|
234
|
+
textNodes.forEach(textNode => {
|
|
235
|
+
// Always use original content from map for evaluation
|
|
236
|
+
let value = nodesMap.get(textNode);
|
|
237
|
+
const regex = /@\((.*?)\)/g;
|
|
238
|
+
|
|
239
|
+
value = value.replace(regex, (match, expression) => {
|
|
240
|
+
const func = evaluateExpr(expression,el._context)
|
|
241
|
+
return String(func);
|
|
242
|
+
});
|
|
243
|
+
textNode.nodeValue = value;
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
});
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.warn(`error at ${el} textcontent`)
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
evaluate()
|
|
252
|
+
}
|
|
253
|
+
const attributeHandler=(el,attr)=>{
|
|
254
|
+
if (el._hasForOrIf()) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if(el._running){
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
const removableAttributes=new Set()
|
|
261
|
+
removableAttributes.add('disabled')
|
|
262
|
+
const evaluate=()=>{
|
|
263
|
+
try {
|
|
264
|
+
const regex = /@\((.*?)\)/g;
|
|
265
|
+
|
|
266
|
+
let value = attr.value;
|
|
267
|
+
const keys = Object.keys(el._context);
|
|
268
|
+
const resolvePath = (path, obj) => {
|
|
269
|
+
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
|
270
|
+
};
|
|
271
|
+
const values = keys.map((key) => resolvePath(key, el._context));
|
|
272
|
+
|
|
273
|
+
value = value.replace(regex, (match, expression) => {
|
|
274
|
+
return evaluateExpr(expression,el._context)
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (removableAttributes.has(attr.name)) {
|
|
278
|
+
if (value) {
|
|
279
|
+
el.setAttribute(attr.name, '');
|
|
280
|
+
} else {
|
|
281
|
+
el.removeAttribute(attr.name)
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
el.setAttribute(attr.name, value);
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.log(error.message,error.stack)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
evaluate()
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* @param {HTMLElement} el
|
|
294
|
+
*/
|
|
295
|
+
const innerHtml = (el,context) => {
|
|
296
|
+
if (el.getAttribute('client')) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
const {document}=parseHTML()
|
|
300
|
+
const nodesMap = new Map();
|
|
301
|
+
|
|
302
|
+
// Get all text nodes and store original value
|
|
303
|
+
const textNodes = Array.from(el.childNodes).filter(
|
|
304
|
+
(node) => node.nodeType === 3
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
textNodes.forEach((node) => {
|
|
308
|
+
nodesMap.set(node, node.nodeValue);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const evaluate = () => {
|
|
312
|
+
try {
|
|
313
|
+
|
|
314
|
+
textNodes.forEach((textNode) => {
|
|
315
|
+
const originalValue = nodesMap.get(textNode);
|
|
316
|
+
const regex = /@html\((.*?)\)/g;
|
|
317
|
+
let match;
|
|
318
|
+
let hasHtml = false;
|
|
319
|
+
const fragments = [];
|
|
320
|
+
|
|
321
|
+
let lastIndex = 0;
|
|
322
|
+
|
|
323
|
+
while ((match = regex.exec(originalValue))) {
|
|
324
|
+
const before = originalValue.slice(lastIndex, match.index);
|
|
325
|
+
if (before) fragments.push(document.createTextNode(before));
|
|
326
|
+
|
|
327
|
+
let expression = match[1];
|
|
328
|
+
let htmlString = '';
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
|
|
333
|
+
htmlString = evaluateExpr(expression,context)
|
|
334
|
+
} catch (e) {
|
|
335
|
+
htmlString = `<span style="color:red;">[Invalid Expression]</span>`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const temp = document.createElement('div');
|
|
339
|
+
temp.innerHTML = sanitizeTemplate(htmlString);
|
|
340
|
+
fragments.push(...temp.childNodes);
|
|
341
|
+
hasHtml = true;
|
|
342
|
+
|
|
343
|
+
lastIndex = regex.lastIndex;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const after = originalValue.slice(lastIndex);
|
|
347
|
+
if (after) fragments.push(document.createTextNode(after));
|
|
348
|
+
|
|
349
|
+
if (hasHtml) {
|
|
350
|
+
const parent = textNode.parentNode;
|
|
351
|
+
parent.insertBefore(document.createDocumentFragment(), textNode);
|
|
352
|
+
fragments.forEach((frag) => parent.insertBefore(frag, textNode));
|
|
353
|
+
parent.removeChild(textNode);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.warn(`Error while evaluating innerHTML for`, el, error);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Helper to resolve nested properties
|
|
362
|
+
const resolvePath = (path, obj) => {
|
|
363
|
+
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
evaluate();
|
|
367
|
+
};
|
|
368
|
+
const directives={
|
|
369
|
+
'server-if':If,
|
|
370
|
+
'server-else':Else,
|
|
371
|
+
'server-else-if':ElseIf,
|
|
372
|
+
'server-for':For
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
*
|
|
377
|
+
* @param {PawaElement | HTMLElement} el
|
|
378
|
+
* @param {object} contexts
|
|
379
|
+
*/
|
|
380
|
+
export const render=(el,contexts={})=>{
|
|
381
|
+
const context={
|
|
382
|
+
...contexts
|
|
383
|
+
}
|
|
384
|
+
for (const fn of renderBeforePawa) {
|
|
385
|
+
try {
|
|
386
|
+
fn(el,context)
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error(error.message,error.stack)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
innerHtml(el,context)
|
|
392
|
+
PawaElement.Element(el,context)
|
|
393
|
+
|
|
394
|
+
if(el.childNodes.some(node=>node.nodeType === 3 && node.nodeValue.includes('@('))){
|
|
395
|
+
textContentHandler(el)
|
|
396
|
+
}
|
|
397
|
+
for (const fn of renderAfterPawa) {
|
|
398
|
+
try {
|
|
399
|
+
fn(el)
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.error(error.message,error.stack)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
el.attributes.forEach(attr=>{
|
|
405
|
+
if (directives[attr.name]) {
|
|
406
|
+
directives[attr.name](el,attr)
|
|
407
|
+
}else if(attr.value.includes('@(')){
|
|
408
|
+
attributeHandler(el,attr)
|
|
409
|
+
}else {
|
|
410
|
+
attrPlugin.forEach((plugins) => {
|
|
411
|
+
plugins(el,attr)
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
})
|
|
416
|
+
if (el._componentName) {
|
|
417
|
+
component(el)
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
for (const fn of renderBeforeChild) {
|
|
421
|
+
try {
|
|
422
|
+
fn(el)
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.error(error.message,error.stack)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if(!el._running){
|
|
428
|
+
Array.from(el.children).forEach(child=>{
|
|
429
|
+
render(child,el._context)
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export const startApp=(html,context={})=>{
|
|
435
|
+
const app=new DOMParser()
|
|
436
|
+
const {document}=parseHTML()
|
|
437
|
+
const body= app.parseFromString(html,'text/html')
|
|
438
|
+
const div=body.firstElementChild
|
|
439
|
+
const element=document.createElement('div')
|
|
440
|
+
element.appendChild(div)
|
|
441
|
+
render(div,context);
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
element:div,
|
|
446
|
+
toString:()=>element.innerHTML
|
|
447
|
+
}
|
|
448
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pawa-ssr",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "pawajs ssr libary",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Allisboy/pawajs-ssr.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ssr",
|
|
15
|
+
"html",
|
|
16
|
+
"js",
|
|
17
|
+
"pawajs"
|
|
18
|
+
],
|
|
19
|
+
"author": "Allwell Oriso-owubo",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Allisboy/pawajs-ssr/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/Allisboy/pawajs-ssr#readme",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"linkedom": "^0.18.11",
|
|
27
|
+
"vm2": "^3.9.19"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/pawaComponent.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a Pawa component instance.
|
|
3
|
+
* @class
|
|
4
|
+
*/
|
|
5
|
+
class PawaComponent {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Function} func - The component function that defines rendering logic.
|
|
8
|
+
*/
|
|
9
|
+
constructor(func) {
|
|
10
|
+
/**
|
|
11
|
+
* Default props for the component (currently unused).
|
|
12
|
+
* @type {Object}
|
|
13
|
+
*/
|
|
14
|
+
this.prop = {};
|
|
15
|
+
/**
|
|
16
|
+
* The component function.
|
|
17
|
+
* @type {Function}
|
|
18
|
+
*/
|
|
19
|
+
this.component = func;
|
|
20
|
+
/**
|
|
21
|
+
* Tracks whether the component has been rendered.
|
|
22
|
+
* @type {boolean}
|
|
23
|
+
*/
|
|
24
|
+
this._hasRun = false;
|
|
25
|
+
/**
|
|
26
|
+
* Data to inject into the rendering context.
|
|
27
|
+
* @type {Object}
|
|
28
|
+
*/
|
|
29
|
+
this._insert = {};
|
|
30
|
+
/**
|
|
31
|
+
* Lifecycle hooks for mount, unmount, and effects.
|
|
32
|
+
* @type {{effect: Array, isMount: Array, isUnMount: Array}}
|
|
33
|
+
*/
|
|
34
|
+
this._hook = {
|
|
35
|
+
effect: [],
|
|
36
|
+
isMount: [],
|
|
37
|
+
isUnMount: [],
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Map for non-reactive internal state (currently unused).
|
|
41
|
+
* @type {Map}
|
|
42
|
+
*/
|
|
43
|
+
this._innerState = new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Map for reactive state created via $state.
|
|
46
|
+
* @type {Map}
|
|
47
|
+
*/
|
|
48
|
+
this._stateMap = new Map();
|
|
49
|
+
/**
|
|
50
|
+
* Stores the previous context for scoping.
|
|
51
|
+
* @type {Object|null}
|
|
52
|
+
*/
|
|
53
|
+
this._formerContext = null;
|
|
54
|
+
/**
|
|
55
|
+
* @type {boolean}
|
|
56
|
+
*/
|
|
57
|
+
this._isAction=false
|
|
58
|
+
/**
|
|
59
|
+
* @type{object}
|
|
60
|
+
*/
|
|
61
|
+
this._action={}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns the component function.
|
|
65
|
+
* @returns {Function}
|
|
66
|
+
*/
|
|
67
|
+
getComponent() {
|
|
68
|
+
return this.component;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export default PawaComponent
|
package/pawaElement.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { HTMLElement, parseHTML } from "linkedom"
|
|
2
|
+
import { allServerAttr, components } from "./index.js"
|
|
3
|
+
import PawaComponent from "./pawaComponent.js"
|
|
4
|
+
import { evaluateExpr, splitAndAdd } from "./utils.js"
|
|
5
|
+
|
|
6
|
+
class PawaElement {
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param {HTMLElement} element
|
|
10
|
+
* @param {object} context
|
|
11
|
+
*/
|
|
12
|
+
constructor(element,context) {
|
|
13
|
+
const {document}=parseHTML()
|
|
14
|
+
/**
|
|
15
|
+
* @type{PawaElement|HTMLElement}
|
|
16
|
+
*/
|
|
17
|
+
this._el=element
|
|
18
|
+
this._slots=document.createDocumentFragment()
|
|
19
|
+
this._context=context
|
|
20
|
+
this._props={}
|
|
21
|
+
this._component=null
|
|
22
|
+
this._componentName=''
|
|
23
|
+
this._running=false
|
|
24
|
+
this._hasForOrIf=this.hasForOrIf
|
|
25
|
+
/**
|
|
26
|
+
* @typedef{object}
|
|
27
|
+
* @property{any}
|
|
28
|
+
* Object of Html Attributes for Rest Attributes
|
|
29
|
+
*/
|
|
30
|
+
this._restProps={}
|
|
31
|
+
this._componentChildren=null
|
|
32
|
+
this.getComponent()
|
|
33
|
+
this.setProps()
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param {HTMLElement} el
|
|
38
|
+
* @param {object} context
|
|
39
|
+
* @returns {PawaElement}
|
|
40
|
+
*/
|
|
41
|
+
static Element(el,context){
|
|
42
|
+
const pawa=new PawaElement(el,context)
|
|
43
|
+
Object.assign(el,pawa)
|
|
44
|
+
return el
|
|
45
|
+
}
|
|
46
|
+
hasForOrIf(){
|
|
47
|
+
if (this._el.getAttribute('server-if') || this._el.getAttribute('server-for') || this._el.getAttribute('server-else') || this._el.getAttribute('server-else-if')) {
|
|
48
|
+
return true
|
|
49
|
+
}else{
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getComponent(){
|
|
55
|
+
if (components.has(splitAndAdd(this._el.tagName.toUpperCase())) && this._el.getAttribute('client') === null) {
|
|
56
|
+
this._componentName=splitAndAdd(this._el.tagName.toUpperCase())
|
|
57
|
+
this._component=new PawaComponent(components.get(splitAndAdd(this._el.tagName.toUpperCase())))
|
|
58
|
+
Array.from(this._el.children).forEach(slot =>{
|
|
59
|
+
|
|
60
|
+
if (slot.tagName === 'TEMPLATE' && slot.getAttribute('prop')) {
|
|
61
|
+
|
|
62
|
+
this._slots.appendChild(slot)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
this._componentChildren=this._el.innerHTML
|
|
66
|
+
}else{
|
|
67
|
+
if(this._el.getAttribute('client')){
|
|
68
|
+
this._el.removeAttribute('client')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//set Component props
|
|
74
|
+
setProps(){
|
|
75
|
+
if (this._componentName) {
|
|
76
|
+
|
|
77
|
+
this._el.attributes.forEach(attr=>{
|
|
78
|
+
if(!allServerAttr.includes(attr.name)){
|
|
79
|
+
if (attr.name.startsWith('-') || attr.name.startsWith('r-')) {
|
|
80
|
+
let name=''
|
|
81
|
+
if (attr.name.startsWith('r-')) {
|
|
82
|
+
name=attr.name.slice(2)
|
|
83
|
+
} else {
|
|
84
|
+
name=attr.name.slice(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this._restProps[name]={name:name,value:attr.value}
|
|
88
|
+
|
|
89
|
+
} else {
|
|
90
|
+
try {
|
|
91
|
+
const func = evaluateExpr(attr.value,this._context)
|
|
92
|
+
const name=attr.name
|
|
93
|
+
this._props[name]=func
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.log(error.message,error.stack)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export default PawaElement
|
package/power.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { render } from "./index.js";
|
|
2
|
+
import { convertToNumber,evaluateExpr } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const If = (el, attr) => {
|
|
7
|
+
if (el._running) return;
|
|
8
|
+
el._running = true;
|
|
9
|
+
|
|
10
|
+
const nextSibling = el.nextElementSibling || null;
|
|
11
|
+
if (nextSibling && (nextSibling.getAttribute('server-else') !== null || nextSibling.getAttribute('server-else-if'))) {
|
|
12
|
+
|
|
13
|
+
nextSibling.setAttribute('data-if', attr.value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = evaluateExpr(attr.value, el._context);
|
|
17
|
+
if (result) {
|
|
18
|
+
const newElement = el.cloneNode(true);
|
|
19
|
+
newElement.removeAttribute('server-if');
|
|
20
|
+
newElement.setAttribute('s-data-if', convertToNumber(attr.value));
|
|
21
|
+
el.replaceWith(newElement);
|
|
22
|
+
render(newElement, el._context);
|
|
23
|
+
} else {
|
|
24
|
+
el.remove();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const Else = (el, attr) => {
|
|
29
|
+
if (el._running) return;
|
|
30
|
+
el._running = true;
|
|
31
|
+
|
|
32
|
+
const value = el.getAttribute('data-if') || '';
|
|
33
|
+
el.removeAttribute('data-if');
|
|
34
|
+
const result = evaluateExpr(value, el._context);
|
|
35
|
+
if (result) {
|
|
36
|
+
el.remove();
|
|
37
|
+
} else {
|
|
38
|
+
const newElement = el.cloneNode(true);
|
|
39
|
+
newElement.removeAttribute('server-else');
|
|
40
|
+
newElement.setAttribute('s-data-else',convertToNumber(value));
|
|
41
|
+
el.replaceWith(newElement);
|
|
42
|
+
render(newElement, el._context);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const ElseIf = (el, attr) => {
|
|
47
|
+
if (el._running) return;
|
|
48
|
+
el._running = true;
|
|
49
|
+
|
|
50
|
+
const nextSibling = el.nextElementSibling || null;
|
|
51
|
+
const prevCondition = el.getAttribute('data-if') || '';
|
|
52
|
+
el.removeAttribute('data-if');
|
|
53
|
+
|
|
54
|
+
if (nextSibling && (nextSibling.getAttribute('server-else') !== null || nextSibling.getAttribute('server-else-if'))) {
|
|
55
|
+
|
|
56
|
+
nextSibling.setAttribute('data-if', attr.value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const prevResult = evaluateExpr(prevCondition, el._context);
|
|
60
|
+
const currentResult = evaluateExpr(attr.value, el._context);
|
|
61
|
+
|
|
62
|
+
if (prevResult) {
|
|
63
|
+
el.remove();
|
|
64
|
+
} else if (currentResult) {
|
|
65
|
+
const newElement = el.cloneNode(true);
|
|
66
|
+
newElement.removeAttribute('server-else-if');
|
|
67
|
+
newElement.setAttribute('s-data-else-if', convertToNumber(attr.value));
|
|
68
|
+
el.replaceWith(newElement);
|
|
69
|
+
render(newElement, el._context);
|
|
70
|
+
} else {
|
|
71
|
+
el.remove();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const For=(el,attr)=>{
|
|
76
|
+
if(el._running){
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
el._running=true
|
|
80
|
+
const value=attr.value
|
|
81
|
+
const split=value.split(' in ')
|
|
82
|
+
const arrayName=split[1]
|
|
83
|
+
const arrayItems=split[0].split(',')
|
|
84
|
+
const arrayItem=arrayItems[0]
|
|
85
|
+
const indexes=arrayItems[1]
|
|
86
|
+
const array=evaluateExpr(arrayName,el._context)
|
|
87
|
+
if(Array.isArray(array)){
|
|
88
|
+
array.forEach((item,index)=>{
|
|
89
|
+
const context=el._context
|
|
90
|
+
const itemContext = {
|
|
91
|
+
[arrayItem]: item,
|
|
92
|
+
[indexes]: index,
|
|
93
|
+
...context
|
|
94
|
+
}
|
|
95
|
+
const newElement=el.cloneNode(true)
|
|
96
|
+
newElement.removeAttribute('server-for')
|
|
97
|
+
newElement.setAttribute('s-data-for',convertToNumber(attr.value))
|
|
98
|
+
|
|
99
|
+
el.parentElement.insertBefore(newElement,el)
|
|
100
|
+
render(newElement,itemContext)
|
|
101
|
+
if (newElement.getAttribute('server-key')) {
|
|
102
|
+
newElement.setAttribute('s-data-loop-key',convertToNumber(newElement.getAttribute('server-key')))
|
|
103
|
+
newElement.removeAttribute('server-key')
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
el.remove()
|
|
108
|
+
}
|
package/utils.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {VM} from 'vm2'
|
|
2
|
+
export const splitAndAdd=(string) => {
|
|
3
|
+
const strings=string.split('-')
|
|
4
|
+
let newString=''
|
|
5
|
+
strings.forEach(str=>{
|
|
6
|
+
newString+=str
|
|
7
|
+
})
|
|
8
|
+
return newString.toUpperCase()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const matchRoute = (pattern, path) => {
|
|
12
|
+
// Remove trailing slashes for consistency
|
|
13
|
+
const cleanPattern = pattern.replace(/\/$/, '');
|
|
14
|
+
const cleanPath = path.replace(/\/$/, '');
|
|
15
|
+
|
|
16
|
+
const patternParts = cleanPattern.split('/');
|
|
17
|
+
const pathParts = cleanPath.split('/');
|
|
18
|
+
|
|
19
|
+
if (patternParts.length !== pathParts.length) {
|
|
20
|
+
|
|
21
|
+
return [false, {}];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const params = {};
|
|
25
|
+
|
|
26
|
+
const match = patternParts.every((part, index) => {
|
|
27
|
+
if (part.startsWith(':')) {
|
|
28
|
+
// This is a parameter
|
|
29
|
+
const paramName = part.slice(1);
|
|
30
|
+
params[paramName] = pathParts[index];
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return part === pathParts[index];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return [match, params];
|
|
37
|
+
}
|
|
38
|
+
export const sanitizeTemplate = (temp) => {
|
|
39
|
+
if (typeof temp !== 'string') return '';
|
|
40
|
+
return temp.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, '');
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Safely evaluates a JavaScript expression in a sandbox.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} expr - The expression to evaluate.
|
|
46
|
+
* @param {object} context - The context to expose inside the sandbox.
|
|
47
|
+
* @returns {any} - The result of the evaluated expression or null on error.
|
|
48
|
+
*/
|
|
49
|
+
export const evaluateExpr = (expr, context = {}) => {
|
|
50
|
+
try {
|
|
51
|
+
const vm = new VM({
|
|
52
|
+
timeout: 50,
|
|
53
|
+
sandbox: { ...context },
|
|
54
|
+
require:false
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return vm.run(expr);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.warn(`Evaluation failed for: ${expr}`, err.message);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
export const propsValidator=(obj={},propsAttri,name)=>{
|
|
64
|
+
let newObj={}
|
|
65
|
+
|
|
66
|
+
const jsTypes=['Array','String','Number']
|
|
67
|
+
for (const[key,value] of Object.entries(obj)) {
|
|
68
|
+
const propsValue=propsAttri[key]
|
|
69
|
+
if(typeof value === 'object'){
|
|
70
|
+
if(propsAttri[key] || propsAttri[key] === 0){
|
|
71
|
+
const checker=ComponentProps(propsAttri[key],value?.err,name)
|
|
72
|
+
if (value.type) {
|
|
73
|
+
checker[value.type.name]()
|
|
74
|
+
}
|
|
75
|
+
}else{
|
|
76
|
+
if (value.strict) {
|
|
77
|
+
console.warn(`props ${key} at ${name} component props is needed`)
|
|
78
|
+
throw new Error(` ${key} props is needed`|| `${key} props undefined ${name}`);
|
|
79
|
+
}else{
|
|
80
|
+
if (value.default || value.default === 0) {
|
|
81
|
+
propsAttri[key]=value.default
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {...propsAttri}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const convertToNumber=(str)=>{
|
|
92
|
+
let hash = 0;
|
|
93
|
+
for (let i = 0; i < str.length; i++) {
|
|
94
|
+
hash=(hash * 31 + str.charCodeAt(i)) | 0
|
|
95
|
+
}
|
|
96
|
+
return hash
|
|
97
|
+
}
|
|
98
|
+
export const ComponentProps=(some,message,name)=>{
|
|
99
|
+
|
|
100
|
+
return({
|
|
101
|
+
Array:()=>{
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(some)) {
|
|
104
|
+
return true
|
|
105
|
+
}else{
|
|
106
|
+
throw new Error(message ?message + ' / Not type of an Array ': `${some} must be an array at ${name} component`);
|
|
107
|
+
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
String:()=>{
|
|
111
|
+
if (typeof some === 'string') {
|
|
112
|
+
return true
|
|
113
|
+
}else{
|
|
114
|
+
throw new Error(message? message + ' / Not type of a String' :`${some} must be a string at ${name} component`);
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
Number:()=>{
|
|
119
|
+
if (typeof some === 'number') {
|
|
120
|
+
return true
|
|
121
|
+
}else{
|
|
122
|
+
throw new Error(message? message+' / Not type of a Number ': `${some} must be a number at ${name} component`);
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
Object:()=>{
|
|
127
|
+
if (typeof some === 'object') {
|
|
128
|
+
return true
|
|
129
|
+
}else{
|
|
130
|
+
throw new Error(message? message+' / Not type of an Object ' :`${some} must be an object at ${name} component`);
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
Function:()=>{
|
|
135
|
+
if (typeof some === 'function') {
|
|
136
|
+
return true
|
|
137
|
+
}else{
|
|
138
|
+
throw new Error(message? message+' / Not type of a Function ': `${some} must be a function at ${name} component`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
Boolean:()=>{
|
|
142
|
+
if (typeof some === 'boolean') {
|
|
143
|
+
return true
|
|
144
|
+
}else{
|
|
145
|
+
throw new Error(message? message+' / Not type of a Boolean ' :`${some} must be a Boolean at ${name} component`);
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
}
|