web-signature 0.2.5 → 0.2.8

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.
@@ -66,6 +66,10 @@ interface Component$1 {
66
66
  * Optional content that is specified in the component tag.
67
67
  */
68
68
  content?: string;
69
+ /**
70
+ * Optional groups that the component belongs to.
71
+ */
72
+ groups: string[];
69
73
  /**
70
74
  * Defining component properties.
71
75
  */
@@ -74,6 +78,7 @@ interface Component$1 {
74
78
  * The properties of the component.
75
79
  */
76
80
  data: Record<string, string | number | boolean | null>;
81
+ $: Record<string, unknown>;
77
82
  /**
78
83
  * Returns the component as a string (template).
79
84
  * @returns {html | Promise<html>} The rendered component as a string (template).
@@ -112,10 +117,12 @@ interface Component$1 {
112
117
  declare abstract class Component implements Component$1 {
113
118
  abstract readonly name: string;
114
119
  content?: string;
120
+ groups: string[];
115
121
  options: Options;
116
122
  ref?: Ref;
117
123
  readonly props: Record<string, Prop$1>;
118
124
  readonly data: Record<string, string | number | boolean | null>;
125
+ $: Record<string, unknown>;
119
126
  abstract render(): html$1 | Promise<html$1>;
120
127
  onInit?(): void;
121
128
  onRender?(): void;
@@ -180,6 +187,11 @@ declare class Library {
180
187
  }>;
181
188
  }
182
189
 
190
+ declare abstract class Plugin {
191
+ readonly abstract modules: Record<string, () => Record<string, unknown>>;
192
+ abstract define(modules: Record<string, Record<string, unknown>>): Record<string, unknown>;
193
+ }
194
+
183
195
  type ResolvedLib = {
184
196
  components: string[];
185
197
  dependencies: Record<string, ResolvedLib>;
@@ -189,6 +201,8 @@ declare class Signature {
189
201
  private refs;
190
202
  private libs;
191
203
  private bank;
204
+ private $;
205
+ private $g;
192
206
  constructor();
193
207
  /**
194
208
  * Adds a component to the signature.
@@ -214,6 +228,7 @@ declare class Signature {
214
228
  * @return {Record<string, ResolvedLib>} A object of formatted libraries with their components and dependencies.
215
229
  */
216
230
  libraries(): Record<string, ResolvedLib>;
231
+ use(name: string, plugin: Plugin): void;
217
232
  /**
218
233
  * Contacts the Component.onContact method through its reference.
219
234
  * @param {string} name The name of the reference.
@@ -263,4 +278,4 @@ declare function unsafeHTML(value: any): {
263
278
 
264
279
  declare function export_default(): Signature;
265
280
 
266
- export { Component, Library, Prop, Signature, export_default as default, html, unsafeHTML };
281
+ export { Component, Library, Plugin, Prop, Signature, export_default as default, html, unsafeHTML };
package/bundle/index.js CHANGED
@@ -26,6 +26,8 @@ class Signature {
26
26
  refs = {};
27
27
  libs = {};
28
28
  bank = new Map();
29
+ $ = {};
30
+ $g = {};
29
31
  constructor() {
30
32
  }
31
33
  /**
@@ -99,6 +101,20 @@ class Signature {
99
101
  };
100
102
  return resolve(this.libs);
101
103
  }
104
+ use(name, plugin) {
105
+ if (this.$[name]) {
106
+ throw new Error(`Plugin with name ${name} already exists.`);
107
+ }
108
+ let modules = {};
109
+ Object.keys(plugin.modules).forEach((key) => {
110
+ modules[key] = plugin.modules[key]();
111
+ });
112
+ this.$[name] = {
113
+ plugin: plugin.define(modules),
114
+ modules
115
+ };
116
+ this.$g[name] = this.$[name].plugin;
117
+ }
102
118
  /**
103
119
  * Contacts the Component.onContact method through its reference.
104
120
  * @param {string} name The name of the reference.
@@ -153,6 +169,15 @@ class Signature {
153
169
  throw new Error(`Component '${component.name}' must render a single root element.`);
154
170
  }
155
171
  const newElement = template.content.firstElementChild;
172
+ // Preserve old ref attribute
173
+ let oldRef = ref.element.getAttribute("ref");
174
+ if (oldRef) {
175
+ newElement.setAttribute("ref", oldRef);
176
+ }
177
+ // Preserve si-group attribute
178
+ if (component.groups.length > 0) {
179
+ newElement.setAttribute("si-group", component.groups.join(" "));
180
+ }
156
181
  this.render(template.content);
157
182
  component.onRender?.(); // lifecycle hook
158
183
  ref.element.replaceWith(newElement);
@@ -244,7 +269,10 @@ class Signature {
244
269
  if (typeof value === "object" && value.type === "unsafeHTML") {
245
270
  let obj = document.createElement("div");
246
271
  obj.innerHTML = value.value;
247
- node.replaceWith(obj);
272
+ while (obj.firstChild) {
273
+ node.parentNode?.insertBefore(obj.firstChild, node);
274
+ }
275
+ node.remove();
248
276
  }
249
277
  else {
250
278
  node.replaceWith(document.createTextNode(String(value)));
@@ -283,6 +311,8 @@ class Signature {
283
311
  // Find all elements with the component name in the frame
284
312
  for (const el of Array.from(frame.querySelectorAll(com)).concat(Array.from(frame.querySelectorAll(`[si-component="${com}"]`)))) {
285
313
  const renderer = new component();
314
+ renderer.$ = this.$g; // injecting plugins
315
+ Object.freeze(renderer.$); // prevent plugins from being modified by components
286
316
  renderer.onInit?.(); // lifecycle hook
287
317
  if (el instanceof HTMLElement) {
288
318
  // Fill the renderer's content
@@ -408,6 +438,10 @@ class Signature {
408
438
  this.render(body.content);
409
439
  renderer.onRender?.(); // lifecycle hook
410
440
  const mountEl = body.firstElementChild;
441
+ // Processing group
442
+ if (renderer?.groups.length > 0) {
443
+ mountEl.setAttribute("si-group", renderer.groups.join(" "));
444
+ }
411
445
  // Processing ref
412
446
  if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
413
447
  let refName = el.getAttribute("ref");
@@ -440,12 +474,14 @@ class Signature {
440
474
 
441
475
  class Component {
442
476
  content;
477
+ groups = [];
443
478
  options = {
444
479
  generateRefIfNotSpecified: false,
445
480
  };
446
481
  ref;
447
482
  props = {};
448
483
  data = {};
484
+ $ = {};
449
485
  onInit() {
450
486
  }
451
487
  ;
@@ -584,8 +620,12 @@ function unsafeHTML(value) {
584
620
  return { type: "unsafeHTML", value: value };
585
621
  }
586
622
 
623
+ // todo: make description
624
+ class Plugin {
625
+ }
626
+
587
627
  function index () {
588
628
  return new Signature();
589
629
  }
590
630
 
591
- export { Component, Library, Prop, Signature, index as default, html, unsafeHTML };
631
+ export { Component, Library, Plugin, Prop, Signature, index as default, html, unsafeHTML };
@@ -1 +1 @@
1
- class e{element;instance;constructor(e,t){this.instance=e,this.element=t}}const t={"element-not-found":"Element not found for selector: #selector","prop-is-required":"Property '#prop' in component '#component' is required but not provided.","unsupported-type-for-property":"Unsupported type for property '#prop' in component '#component': #type","invalid-value-for-property":"Invalid value for property '#prop' in component '#component': #value (value: #attr)","multiple-root-elements":"Component '#component' must render a single root element. \n\t#elements","ref-collision":"Ref collision detected for ref '#ref' in component '#component'.",unknown:"An unknown error occurred.","unknown-from":"An unknown error occurred in component '#from'.","stack-overflow":"Stack Overflow detected: possible recursive component rendering.","render-async-failed":"Error during asynchronous rendering of the component #component."};let n=0;class r{components={};refs={};libs={};bank=new Map;constructor(){}add(e,t){const n="string"==typeof t?t:e.name;this.components[n]&&console.warn(new Error(`Component with name ${n} already exists.`)),this.components[n]=e}register(e,...t){this.libs[e.name]&&console.warn(new Error(`Library with name ${e.name} already exists.`));const n=e.list().filter(e=>!(e.name in t));this.libs[e.name]={name:e.name,version:e.version,author:e.author,components:n.map(e=>e.name),dependencies:e.libs};for(const t of n)this.add(t.component,`${e.name}-${t.name}`)}lib(e){return this.libs[e]}libraries(){const e=e=>{let t=e.name;return e.version&&(t+=`@${e.version}`),e.author&&(t+=`#${e.author}`),t},t=(n,r=new Set)=>{const o={};for(const[s,i]of Object.entries(n)){const n=e(i);r.has(n)||(r.add(n),o[n]={components:i.components,dependencies:t(i.dependencies,r)})}return o};return t(this.libs)}contactWith(e,...t){const n=this.refs[e];if(!n)throw new Error(`Ref with name ${e} does not exist.`);const r=n.instance;return r.onContact?.(...t)}updateRef(e){const t=this.refs[e];if(!t)throw new Error(`Ref with name ${e} does not exist.`);const n=t.instance;let r={strings:Object.assign([],{raw:[]}),values:[]};try{r=n.render()}catch(e){if(e instanceof Error)throw{id:"unknown-from",from:n.name,err:e}}const o=document.createElement("template");(e=>{r instanceof Promise?r.then(t=>{o.content.appendChild(this.templateToElement(t,n.name)),e()}).catch(e=>{throw{id:"unknown-from",from:n.name,err:e}}):"object"==typeof r&&(o.content.appendChild(this.templateToElement(r,n.name)),e())})(()=>{if(1!==o.content.children.length)throw new Error(`Component '${n.name}' must render a single root element.`);const e=o.content.firstElementChild;this.render(o.content),n.onRender?.(),t.element.replaceWith(e),t.element=e,n.onMount?.(e)})}contact(e,n){new Promise((t,r)=>{try{const t=document.querySelector(e);if(!t)return void r({id:"element-not-found",selector:e});const o=document.createElement("div");o.innerHTML=t.innerHTML,this.render(o),t.replaceChildren(...Array.from(o.childNodes)),n&&n()}catch(e){e instanceof Error?e instanceof RangeError&&e.message.includes("stack")?r({id:"stack-overflow",err:e}):r({id:"unknown",err:e}):r(e)}}).catch(e=>{let n=t[e.id];throw Object.keys(e).filter(e=>!(e in["id","err"])).forEach(t=>{n=n.replace(new RegExp(`#${t}`,"gm"),String(e[t]))}),window.SIGNATURE?.DEV_MODE&&console.log(e),e.id in["unknown","unknown-from","render-async-failed"]?console.error(`[${e.id}] ${n}`,e.err):console.error(`[${e.id}] ${n}`),"Page rendering was interrupted by Signature due to the above error."})}templateToString(e){let t="";for(let n=0;n<e.strings.length;n++)t+=e.strings[n],n<e.values.length&&(t+=`\x3c!--si-mark-${n}--\x3e`);return t}fillTemplate(e,t){let n;return this.bank.has(e.strings.join("@@"))?n=this.bank.get(e.strings.join("@@"))?.cloneNode(!0):(n=document.createElement("template"),n.innerHTML=t,this.bank.set(e.strings.join("@@"),n.cloneNode(!0))),(()=>{let t,r=document.createTreeWalker(n.content,NodeFilter.SHOW_COMMENT),o=[];for(;t=r.nextNode();)/si-mark-\d+/gm.test(t.nodeValue??"")&&o.push(t);for(const t of o){const n=e.values[Number((t.nodeValue??"").match(/si-mark-(\d+)/m)[1])];if("object"==typeof n&&"unsafeHTML"===n.type){let e=document.createElement("div");e.innerHTML=n.value,t.replaceWith(e)}else t.replaceWith(document.createTextNode(String(n)))}})(),(()=>{let t,r=document.createTreeWalker(n.content,NodeFilter.SHOW_ELEMENT);for(;t=r.nextNode();)for(const n of Array.from(t.attributes))if(/<!--si-mark-\d+-->/gm.test(n.value)){const r=n.value.match(/si-mark-(\d+)/m);if(r){const o=e.values[Number(r[1])];t.setAttribute(n.name,String(o))}}})(),n}templateToElement(e,t){const n=this.templateToString(e),r=this.fillTemplate(e,n);if(1!==r.content.children.length)throw{id:"multiple-root-elements",elements:r.innerHTML,component:t};return r.content.firstElementChild}render(t){for(const r of Object.keys(this.components)){const o=this.components[r];for(const s of Array.from(t.querySelectorAll(r)).concat(Array.from(t.querySelectorAll(`[si-component="${r}"]`)))){const t=new o;if(t.onInit?.(),s instanceof HTMLElement){t.content=s.innerHTML.trim();for(const e of Object.keys(t.props)){const n=s.getAttribute(e);if(null===n){if(t.props[e].required)throw{id:"prop-is-required",component:r,prop:e};t.data[e]=null}else if(""===n){if(t.props[e].required)throw{id:"prop-is-required",component:r,prop:e};t.props[e].isValid(n)&&(t.data[e]=null)}else{let o;switch(t.props[e].type){case"boolean":o=Boolean(n);break;case"number":o=Number(n);break;case"string":o=String(n);break;case"array":try{o=JSON.parse(n)}catch(t){throw{id:"invalid-value-for-property",component:r,prop:e,value:n,attr:n}}break;default:if(t.props[e].required)throw{id:"unsupported-type-for-property",component:r,prop:e,type:t.props[e].type}}if(void 0!==o){if(!t.props[e].isValid(o))throw{id:"invalid-value-for-property",component:r,prop:e,value:o,attr:n};if(t.props[e].validate&&!t.props[e].validate(o))throw{id:"invalid-value-for-property",component:r,prop:e,value:o,attr:n};t.data[e]=o,t.onPropParsed?.(t.props[e],o)}}}t.onPropsParsed?.()}const i=document.createElement("template");let a={strings:Object.assign([],{raw:[]}),values:[]};try{a=t.render()}catch(e){if(e instanceof Error)throw{id:"unknown-from",from:t.name,err:e}}(e=>{if(a instanceof Promise)try{a.then(t=>{i.appendChild(this.templateToElement(t,r)),e()}).catch(e=>{throw{id:"unknown-from",from:t.name,err:e}})}catch(e){throw{id:"render-async-failed",component:r,err:e}}else"object"==typeof a&&(i.appendChild(this.templateToElement(a,r)),e())})(()=>{this.render(i.content),t.onRender?.();const o=i.firstElementChild;if(s.hasAttribute("ref")||t.options.generateRefIfNotSpecified){let i=s.getAttribute("ref");if(null===i&&(i=""),n++,""===i&&(i=`r${n}${Math.random().toString(36).substring(2,15)}${n}`),this.refs[i])throw{id:"ref-collision",ref:i,component:r};this.refs[i]=new e(t,o),o.setAttribute("ref",i),t.ref={id:i,contact:(...e)=>this.contactWith(i,...e),update:()=>this.updateRef(i)}}s.replaceWith(i.firstElementChild),t.onMount?.(o)})}}}}class o{content;options={generateRefIfNotSpecified:!1};ref;props={};data={};onInit(){}onRender(){}onMount(e){}onContact(...e){}onPropsParsed(){}onPropParsed(e,t){}}class s{type;required=!0;validate;constructor(e,t=!0,n){this.type=e,this.required=t,this.validate=n||(()=>!0)}isValid(e){switch(this.type){case"boolean":return"boolean"==typeof e;case"number":return"number"==typeof e&&!isNaN(e);case"string":return"string"==typeof e;case"array":return Array.isArray(e);case"null":return null===e;default:return!1}}}class i{name;version;author;libs={};components={};constructor(e,t,n){this.name=e,this.author=t,this.version=n}add(e,t){const n="string"==typeof t?t:e.name;this.components[n]&&console.warn(new Error(`Component with name ${n} already exists.`)),this.components[n]=e}register(e,...t){this.libs[e.name]&&console.warn(new Error(`Library with name ${e.name} already exists in ${this.name}.`));const n=e.list().filter(e=>!(e.name in t));this.libs[e.name]={name:e.name,version:e.version,author:e.author,components:n.map(e=>e.name),dependencies:e.libs};for(const t of n)this.add(t.component,`${e.name}-${t.name}`)}get(e){return this.components[e]}lib(e){return this.libs[e]}list(){return Object.entries(this.components).map(([e,t])=>({component:t,name:e}))}}function a(e,...t){return{strings:e,values:t}}function c(e){return{type:"unsafeHTML",value:e}}function l(){return new r}export{o as Component,i as Library,s as Prop,r as Signature,l as default,a as html,c as unsafeHTML};
1
+ class e{element;instance;constructor(e,t){this.instance=e,this.element=t}}const t={"element-not-found":"Element not found for selector: #selector","prop-is-required":"Property '#prop' in component '#component' is required but not provided.","unsupported-type-for-property":"Unsupported type for property '#prop' in component '#component': #type","invalid-value-for-property":"Invalid value for property '#prop' in component '#component': #value (value: #attr)","multiple-root-elements":"Component '#component' must render a single root element. \n\t#elements","ref-collision":"Ref collision detected for ref '#ref' in component '#component'.",unknown:"An unknown error occurred.","unknown-from":"An unknown error occurred in component '#from'.","stack-overflow":"Stack Overflow detected: possible recursive component rendering.","render-async-failed":"Error during asynchronous rendering of the component #component."};let n=0;class r{components={};refs={};libs={};bank=new Map;$={};$g={};constructor(){}add(e,t){const n="string"==typeof t?t:e.name;this.components[n]&&console.warn(new Error(`Component with name ${n} already exists.`)),this.components[n]=e}register(e,...t){this.libs[e.name]&&console.warn(new Error(`Library with name ${e.name} already exists.`));const n=e.list().filter(e=>!(e.name in t));this.libs[e.name]={name:e.name,version:e.version,author:e.author,components:n.map(e=>e.name),dependencies:e.libs};for(const t of n)this.add(t.component,`${e.name}-${t.name}`)}lib(e){return this.libs[e]}libraries(){const e=e=>{let t=e.name;return e.version&&(t+=`@${e.version}`),e.author&&(t+=`#${e.author}`),t},t=(n,r=new Set)=>{const o={};for(const[s,i]of Object.entries(n)){const n=e(i);r.has(n)||(r.add(n),o[n]={components:i.components,dependencies:t(i.dependencies,r)})}return o};return t(this.libs)}use(e,t){if(this.$[e])throw new Error(`Plugin with name ${e} already exists.`);let n={};Object.keys(t.modules).forEach(e=>{n[e]=t.modules[e]()}),this.$[e]={plugin:t.define(n),modules:n},this.$g[e]=this.$[e].plugin}contactWith(e,...t){const n=this.refs[e];if(!n)throw new Error(`Ref with name ${e} does not exist.`);const r=n.instance;return r.onContact?.(...t)}updateRef(e){const t=this.refs[e];if(!t)throw new Error(`Ref with name ${e} does not exist.`);const n=t.instance;let r={strings:Object.assign([],{raw:[]}),values:[]};try{r=n.render()}catch(e){if(e instanceof Error)throw{id:"unknown-from",from:n.name,err:e}}const o=document.createElement("template");(e=>{r instanceof Promise?r.then(t=>{o.content.appendChild(this.templateToElement(t,n.name)),e()}).catch(e=>{throw{id:"unknown-from",from:n.name,err:e}}):"object"==typeof r&&(o.content.appendChild(this.templateToElement(r,n.name)),e())})(()=>{if(1!==o.content.children.length)throw new Error(`Component '${n.name}' must render a single root element.`);const e=o.content.firstElementChild;let r=t.element.getAttribute("ref");r&&e.setAttribute("ref",r),n.groups.length>0&&e.setAttribute("si-group",n.groups.join(" ")),this.render(o.content),n.onRender?.(),t.element.replaceWith(e),t.element=e,n.onMount?.(e)})}contact(e,n){new Promise((t,r)=>{try{const t=document.querySelector(e);if(!t)return void r({id:"element-not-found",selector:e});const o=document.createElement("div");o.innerHTML=t.innerHTML,this.render(o),t.replaceChildren(...Array.from(o.childNodes)),n&&n()}catch(e){e instanceof Error?e instanceof RangeError&&e.message.includes("stack")?r({id:"stack-overflow",err:e}):r({id:"unknown",err:e}):r(e)}}).catch(e=>{let n=t[e.id];throw Object.keys(e).filter(e=>!(e in["id","err"])).forEach(t=>{n=n.replace(new RegExp(`#${t}`,"gm"),String(e[t]))}),window.SIGNATURE?.DEV_MODE&&console.log(e),e.id in["unknown","unknown-from","render-async-failed"]?console.error(`[${e.id}] ${n}`,e.err):console.error(`[${e.id}] ${n}`),"Page rendering was interrupted by Signature due to the above error."})}templateToString(e){let t="";for(let n=0;n<e.strings.length;n++)t+=e.strings[n],n<e.values.length&&(t+=`\x3c!--si-mark-${n}--\x3e`);return t}fillTemplate(e,t){let n;return this.bank.has(e.strings.join("@@"))?n=this.bank.get(e.strings.join("@@"))?.cloneNode(!0):(n=document.createElement("template"),n.innerHTML=t,this.bank.set(e.strings.join("@@"),n.cloneNode(!0))),(()=>{let t,r=document.createTreeWalker(n.content,NodeFilter.SHOW_COMMENT),o=[];for(;t=r.nextNode();)/si-mark-\d+/gm.test(t.nodeValue??"")&&o.push(t);for(const t of o){const n=e.values[Number((t.nodeValue??"").match(/si-mark-(\d+)/m)[1])];if("object"==typeof n&&"unsafeHTML"===n.type){let e=document.createElement("div");for(e.innerHTML=n.value;e.firstChild;)t.parentNode?.insertBefore(e.firstChild,t);t.remove()}else t.replaceWith(document.createTextNode(String(n)))}})(),(()=>{let t,r=document.createTreeWalker(n.content,NodeFilter.SHOW_ELEMENT);for(;t=r.nextNode();)for(const n of Array.from(t.attributes))if(/<!--si-mark-\d+-->/gm.test(n.value)){const r=n.value.match(/si-mark-(\d+)/m);if(r){const o=e.values[Number(r[1])];t.setAttribute(n.name,String(o))}}})(),n}templateToElement(e,t){const n=this.templateToString(e),r=this.fillTemplate(e,n);if(1!==r.content.children.length)throw{id:"multiple-root-elements",elements:r.innerHTML,component:t};return r.content.firstElementChild}render(t){for(const r of Object.keys(this.components)){const o=this.components[r];for(const s of Array.from(t.querySelectorAll(r)).concat(Array.from(t.querySelectorAll(`[si-component="${r}"]`)))){const t=new o;if(t.$=this.$g,Object.freeze(t.$),t.onInit?.(),s instanceof HTMLElement){t.content=s.innerHTML.trim();for(const e of Object.keys(t.props)){const n=s.getAttribute(e);if(null===n){if(t.props[e].required)throw{id:"prop-is-required",component:r,prop:e};t.data[e]=null}else if(""===n){if(t.props[e].required)throw{id:"prop-is-required",component:r,prop:e};t.props[e].isValid(n)&&(t.data[e]=null)}else{let o;switch(t.props[e].type){case"boolean":o=Boolean(n);break;case"number":o=Number(n);break;case"string":o=String(n);break;case"array":try{o=JSON.parse(n)}catch(t){throw{id:"invalid-value-for-property",component:r,prop:e,value:n,attr:n}}break;default:if(t.props[e].required)throw{id:"unsupported-type-for-property",component:r,prop:e,type:t.props[e].type}}if(void 0!==o){if(!t.props[e].isValid(o))throw{id:"invalid-value-for-property",component:r,prop:e,value:o,attr:n};if(t.props[e].validate&&!t.props[e].validate(o))throw{id:"invalid-value-for-property",component:r,prop:e,value:o,attr:n};t.data[e]=o,t.onPropParsed?.(t.props[e],o)}}}t.onPropsParsed?.()}const i=document.createElement("template");let a={strings:Object.assign([],{raw:[]}),values:[]};try{a=t.render()}catch(e){if(e instanceof Error)throw{id:"unknown-from",from:t.name,err:e}}(e=>{if(a instanceof Promise)try{a.then(t=>{i.appendChild(this.templateToElement(t,r)),e()}).catch(e=>{throw{id:"unknown-from",from:t.name,err:e}})}catch(e){throw{id:"render-async-failed",component:r,err:e}}else"object"==typeof a&&(i.appendChild(this.templateToElement(a,r)),e())})(()=>{this.render(i.content),t.onRender?.();const o=i.firstElementChild;if(t?.groups.length>0&&o.setAttribute("si-group",t.groups.join(" ")),s.hasAttribute("ref")||t.options.generateRefIfNotSpecified){let i=s.getAttribute("ref");if(null===i&&(i=""),n++,""===i&&(i=`r${n}${Math.random().toString(36).substring(2,15)}${n}`),this.refs[i])throw{id:"ref-collision",ref:i,component:r};this.refs[i]=new e(t,o),o.setAttribute("ref",i),t.ref={id:i,contact:(...e)=>this.contactWith(i,...e),update:()=>this.updateRef(i)}}s.replaceWith(i.firstElementChild),t.onMount?.(o)})}}}}class o{content;groups=[];options={generateRefIfNotSpecified:!1};ref;props={};data={};$={};onInit(){}onRender(){}onMount(e){}onContact(...e){}onPropsParsed(){}onPropParsed(e,t){}}class s{type;required=!0;validate;constructor(e,t=!0,n){this.type=e,this.required=t,this.validate=n||(()=>!0)}isValid(e){switch(this.type){case"boolean":return"boolean"==typeof e;case"number":return"number"==typeof e&&!isNaN(e);case"string":return"string"==typeof e;case"array":return Array.isArray(e);case"null":return null===e;default:return!1}}}class i{name;version;author;libs={};components={};constructor(e,t,n){this.name=e,this.author=t,this.version=n}add(e,t){const n="string"==typeof t?t:e.name;this.components[n]&&console.warn(new Error(`Component with name ${n} already exists.`)),this.components[n]=e}register(e,...t){this.libs[e.name]&&console.warn(new Error(`Library with name ${e.name} already exists in ${this.name}.`));const n=e.list().filter(e=>!(e.name in t));this.libs[e.name]={name:e.name,version:e.version,author:e.author,components:n.map(e=>e.name),dependencies:e.libs};for(const t of n)this.add(t.component,`${e.name}-${t.name}`)}get(e){return this.components[e]}lib(e){return this.libs[e]}list(){return Object.entries(this.components).map(([e,t])=>({component:t,name:e}))}}function a(e,...t){return{strings:e,values:t}}function c(e){return{type:"unsafeHTML",value:e}}class l{}function p(){return new r}export{o as Component,i as Library,l as Plugin,s as Prop,r as Signature,p as default,a as html,c as unsafeHTML};
package/dist/Component.js CHANGED
@@ -1,11 +1,13 @@
1
1
  export default class Component {
2
2
  content;
3
+ groups = [];
3
4
  options = {
4
5
  generateRefIfNotSpecified: false,
5
6
  };
6
7
  ref;
7
8
  props = {};
8
9
  data = {};
10
+ $ = {};
9
11
  onInit() {
10
12
  }
11
13
  ;
package/dist/Plugin.js ADDED
@@ -0,0 +1,3 @@
1
+ // todo: make description
2
+ export default class Plugin {
3
+ }
package/dist/Signature.js CHANGED
@@ -6,6 +6,8 @@ export default class Signature {
6
6
  refs = {};
7
7
  libs = {};
8
8
  bank = new Map();
9
+ $ = {};
10
+ $g = {};
9
11
  constructor() {
10
12
  }
11
13
  /**
@@ -79,6 +81,20 @@ export default class Signature {
79
81
  };
80
82
  return resolve(this.libs);
81
83
  }
84
+ use(name, plugin) {
85
+ if (this.$[name]) {
86
+ throw new Error(`Plugin with name ${name} already exists.`);
87
+ }
88
+ let modules = {};
89
+ Object.keys(plugin.modules).forEach((key) => {
90
+ modules[key] = plugin.modules[key]();
91
+ });
92
+ this.$[name] = {
93
+ plugin: plugin.define(modules),
94
+ modules
95
+ };
96
+ this.$g[name] = this.$[name].plugin;
97
+ }
82
98
  /**
83
99
  * Contacts the Component.onContact method through its reference.
84
100
  * @param {string} name The name of the reference.
@@ -133,6 +149,15 @@ export default class Signature {
133
149
  throw new Error(`Component '${component.name}' must render a single root element.`);
134
150
  }
135
151
  const newElement = template.content.firstElementChild;
152
+ // Preserve old ref attribute
153
+ let oldRef = ref.element.getAttribute("ref");
154
+ if (oldRef) {
155
+ newElement.setAttribute("ref", oldRef);
156
+ }
157
+ // Preserve si-group attribute
158
+ if (component.groups.length > 0) {
159
+ newElement.setAttribute("si-group", component.groups.join(" "));
160
+ }
136
161
  this.render(template.content);
137
162
  component.onRender?.(); // lifecycle hook
138
163
  ref.element.replaceWith(newElement);
@@ -224,7 +249,10 @@ export default class Signature {
224
249
  if (typeof value === "object" && value.type === "unsafeHTML") {
225
250
  let obj = document.createElement("div");
226
251
  obj.innerHTML = value.value;
227
- node.replaceWith(obj);
252
+ while (obj.firstChild) {
253
+ node.parentNode?.insertBefore(obj.firstChild, node);
254
+ }
255
+ node.remove();
228
256
  }
229
257
  else {
230
258
  node.replaceWith(document.createTextNode(String(value)));
@@ -263,6 +291,8 @@ export default class Signature {
263
291
  // Find all elements with the component name in the frame
264
292
  for (const el of Array.from(frame.querySelectorAll(com)).concat(Array.from(frame.querySelectorAll(`[si-component="${com}"]`)))) {
265
293
  const renderer = new component();
294
+ renderer.$ = this.$g; // injecting plugins
295
+ Object.freeze(renderer.$); // prevent plugins from being modified by components
266
296
  renderer.onInit?.(); // lifecycle hook
267
297
  if (el instanceof HTMLElement) {
268
298
  // Fill the renderer's content
@@ -388,6 +418,10 @@ export default class Signature {
388
418
  this.render(body.content);
389
419
  renderer.onRender?.(); // lifecycle hook
390
420
  const mountEl = body.firstElementChild;
421
+ // Processing group
422
+ if (renderer?.groups.length > 0) {
423
+ mountEl.setAttribute("si-group", renderer.groups.join(" "));
424
+ }
391
425
  // Processing ref
392
426
  if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
393
427
  let refName = el.getAttribute("ref");
@@ -5,10 +5,12 @@ import type { Ref } from "./types/Component.js";
5
5
  export default abstract class Component implements component {
6
6
  abstract readonly name: string;
7
7
  content?: string;
8
+ groups: string[];
8
9
  options: Options;
9
10
  ref?: Ref;
10
11
  readonly props: Record<string, Prop>;
11
12
  readonly data: Record<string, string | number | boolean | null>;
13
+ $: Record<string, unknown>;
12
14
  abstract render(): html | Promise<html>;
13
15
  onInit?(): void;
14
16
  onRender?(): void;
@@ -0,0 +1,4 @@
1
+ export default abstract class Plugin {
2
+ readonly abstract modules: Record<string, () => Record<string, unknown>>;
3
+ abstract define(modules: Record<string, Record<string, unknown>>): Record<string, unknown>;
4
+ }
@@ -1,5 +1,6 @@
1
1
  import { ComponentConstructor } from "./Component.js";
2
2
  import Library, { LibMeta } from "./Library.js";
3
+ import Plugin from "./Plugin.js";
3
4
  type ResolvedLib = {
4
5
  components: string[];
5
6
  dependencies: Record<string, ResolvedLib>;
@@ -9,6 +10,8 @@ export default class Signature {
9
10
  private refs;
10
11
  private libs;
11
12
  private bank;
13
+ private $;
14
+ private $g;
12
15
  constructor();
13
16
  /**
14
17
  * Adds a component to the signature.
@@ -34,6 +37,7 @@ export default class Signature {
34
37
  * @return {Record<string, ResolvedLib>} A object of formatted libraries with their components and dependencies.
35
38
  */
36
39
  libraries(): Record<string, ResolvedLib>;
40
+ use(name: string, plugin: Plugin): void;
37
41
  /**
38
42
  * Contacts the Component.onContact method through its reference.
39
43
  * @param {string} name The name of the reference.
package/dist/d/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export { default as Component } from './Component.js';
5
5
  export { default as Prop } from './Prop.js';
6
6
  export { default as Library } from './Library.js';
7
7
  export { default as html, unsafeHTML } from './html.js';
8
+ export { default as Plugin } from './Plugin.js';
@@ -37,6 +37,10 @@ interface Component {
37
37
  * Optional content that is specified in the component tag.
38
38
  */
39
39
  content?: string;
40
+ /**
41
+ * Optional groups that the component belongs to.
42
+ */
43
+ groups: string[];
40
44
  /**
41
45
  * Defining component properties.
42
46
  */
@@ -45,6 +49,7 @@ interface Component {
45
49
  * The properties of the component.
46
50
  */
47
51
  data: Record<string, string | number | boolean | null>;
52
+ $: Record<string, unknown>;
48
53
  /**
49
54
  * Returns the component as a string (template).
50
55
  * @returns {html | Promise<html>} The rendered component as a string (template).
@@ -0,0 +1,5 @@
1
+ interface MetaPlugin {
2
+ plugin: Record<string, unknown>;
3
+ modules: Record<string, Record<string, unknown>>;
4
+ }
5
+ export type { MetaPlugin as default };
package/dist/index.js CHANGED
@@ -7,3 +7,4 @@ export { default as Component } from './Component.js';
7
7
  export { default as Prop } from './Prop.js';
8
8
  export { default as Library } from './Library.js';
9
9
  export { default as html, unsafeHTML } from './html.js';
10
+ export { default as Plugin } from './Plugin.js';
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-signature",
3
- "version": "0.2.5",
3
+ "version": "0.2.8",
4
4
  "description": "Primitive and fast framework for rendering web interfaces",
5
5
  "license": "ISC",
6
6
  "author": "PinBib",
package/src/Component.ts CHANGED
@@ -8,6 +8,8 @@ export default abstract class Component implements component {
8
8
  abstract readonly name: string;
9
9
  content?: string;
10
10
 
11
+ groups: string[] = [];
12
+
11
13
  options: Options = {
12
14
  generateRefIfNotSpecified: false,
13
15
  }
@@ -17,6 +19,8 @@ export default abstract class Component implements component {
17
19
  readonly props: Record<string, Prop> = {};
18
20
  readonly data: Record<string, string | number | boolean | null> = {};
19
21
 
22
+ public $: Record<string, unknown> = {};
23
+
20
24
  abstract render(): html | Promise<html>;
21
25
 
22
26
  onInit?(): void {
package/src/Plugin.ts ADDED
@@ -0,0 +1,6 @@
1
+ // todo: make description
2
+ export default abstract class Plugin {
3
+ readonly abstract modules: Record<string, () => Record<string, unknown>>;
4
+
5
+ public abstract define(modules: Record<string, Record<string, unknown>>): Record<string, unknown>;
6
+ }
package/src/Signature.ts CHANGED
@@ -3,7 +3,9 @@ import Ref from "./Ref.js";
3
3
  import ErrorUnion from "./types/Errors.js";
4
4
  import Errors from "./Errors.js";
5
5
  import Library, {LibMeta} from "./Library.js";
6
- import {html} from "./types/Component";
6
+ import {html} from "./types/Component.js";
7
+ import MetaPlugin from "./types/MetaPlugin.js";
8
+ import Plugin from "./Plugin.js";
7
9
 
8
10
  let _counter = 0;
9
11
 
@@ -16,7 +18,10 @@ export default class Signature {
16
18
  private components: Record<string, ComponentConstructor> = {};
17
19
  private refs: Record<string, Ref> = {};
18
20
  private libs: Record<string, LibMeta> = {};
19
- private bank: Map<string, HTMLTemplateElement> = new Map<string, HTMLTemplateElement>()
21
+ private bank: Map<string, HTMLTemplateElement> = new Map<string, HTMLTemplateElement>();
22
+
23
+ private $: Record<string, MetaPlugin> = {};
24
+ private $g: Record<string, unknown> = {};
20
25
 
21
26
  constructor() {
22
27
  }
@@ -109,6 +114,25 @@ export default class Signature {
109
114
  return resolve(this.libs);
110
115
  }
111
116
 
117
+ public use(name: string, plugin: Plugin): void {
118
+ if (this.$[name]) {
119
+ throw new Error(`Plugin with name ${name} already exists.`);
120
+ }
121
+
122
+ let modules: Record<string, Record<string, unknown>> = {};
123
+
124
+ Object.keys(plugin.modules).forEach((key) => {
125
+ modules[key] = plugin.modules[key]();
126
+ });
127
+
128
+ this.$[name] = {
129
+ plugin: plugin.define(modules),
130
+ modules
131
+ };
132
+
133
+ this.$g[name] = this.$[name].plugin;
134
+ }
135
+
112
136
  /**
113
137
  * Contacts the Component.onContact method through its reference.
114
138
  * @param {string} name The name of the reference.
@@ -172,6 +196,17 @@ export default class Signature {
172
196
 
173
197
  const newElement = template.content.firstElementChild as Element;
174
198
 
199
+ // Preserve old ref attribute
200
+ let oldRef = ref.element.getAttribute("ref");
201
+ if (oldRef) {
202
+ newElement.setAttribute("ref", oldRef);
203
+ }
204
+
205
+ // Preserve si-group attribute
206
+ if (component.groups.length > 0) {
207
+ newElement.setAttribute("si-group", component.groups.join(" "));
208
+ }
209
+
175
210
  this.render(template.content);
176
211
 
177
212
  component.onRender?.(); // lifecycle hook
@@ -289,7 +324,10 @@ export default class Signature {
289
324
  let obj = document.createElement("div");
290
325
  obj.innerHTML = value.value;
291
326
 
292
- node.replaceWith(obj);
327
+ while (obj.firstChild) {
328
+ node.parentNode?.insertBefore(obj.firstChild, node);
329
+ }
330
+ node.remove();
293
331
  } else {
294
332
  node.replaceWith(document.createTextNode(String(value)));
295
333
  }
@@ -338,6 +376,10 @@ export default class Signature {
338
376
  // Find all elements with the component name in the frame
339
377
  for (const el of Array.from(frame.querySelectorAll(com)).concat(Array.from(frame.querySelectorAll(`[si-component="${com}"]`)))) {
340
378
  const renderer: Component = new component();
379
+
380
+ renderer.$ = this.$g; // injecting plugins
381
+ Object.freeze(renderer.$); // prevent plugins from being modified by components
382
+
341
383
  renderer.onInit?.(); // lifecycle hook
342
384
 
343
385
  if (el instanceof HTMLElement) {
@@ -473,6 +515,11 @@ export default class Signature {
473
515
 
474
516
  const mountEl: Element = body.firstElementChild as Element;
475
517
 
518
+ // Processing group
519
+ if (renderer?.groups.length > 0) {
520
+ mountEl.setAttribute("si-group", renderer.groups.join(" "));
521
+ }
522
+
476
523
  // Processing ref
477
524
  if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
478
525
  let refName = el.getAttribute("ref");
package/src/index.ts CHANGED
@@ -8,4 +8,5 @@ export {default as Signature} from './Signature.js';
8
8
  export {default as Component} from './Component.js';
9
9
  export {default as Prop} from './Prop.js';
10
10
  export {default as Library} from './Library.js';
11
- export {default as html, unsafeHTML} from './html.js';
11
+ export {default as html, unsafeHTML} from './html.js';
12
+ export {default as Plugin} from './Plugin.js';
@@ -47,6 +47,11 @@ interface Component {
47
47
  */
48
48
  content?: string;
49
49
 
50
+ /**
51
+ * Optional groups that the component belongs to.
52
+ */
53
+ groups: string[];
54
+
50
55
  /**
51
56
  * Defining component properties.
52
57
  */
@@ -56,7 +61,8 @@ interface Component {
56
61
  * The properties of the component.
57
62
  */
58
63
  data: Record<string, string | number | boolean | null>;
59
-
64
+ // todo: make description
65
+ $: Record<string, unknown>;
60
66
 
61
67
  /**
62
68
  * Returns the component as a string (template).
@@ -0,0 +1,6 @@
1
+ interface MetaPlugin {
2
+ plugin: Record<string, unknown>;
3
+ modules: Record<string, Record<string, unknown>>;
4
+ }
5
+
6
+ export type {MetaPlugin as default};