web-signature 0.2.6 → 0.2.9

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/README.MD CHANGED
@@ -31,12 +31,17 @@ npm run dev
31
31
 
32
32
  * [Signature](#signature)
33
33
  * [Quick Start](#quick-start)
34
+ * [Try online](#try-online)
34
35
  * [Introduction](#introduction)
35
36
  * [Briefly about Signature](#briefly-about-signature)
36
37
  * [About Component](#about-component)
37
38
  * [Quick about the Library](#quick-about-the-library)
38
39
  * [See more here](#see-more-here)
39
40
 
41
+ ## Try online
42
+
43
+ You can also try Signature online using [StackBlitz](https://stackblitz.com/fork/signature-starter)
44
+
40
45
  # Introduction
41
46
 
42
47
  Signature uses four classes as its basis:
@@ -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,10 @@ interface Component$1 {
74
78
  * The properties of the component.
75
79
  */
76
80
  data: Record<string, string | number | boolean | null>;
81
+ /**
82
+ * Plugins are stored here.
83
+ */
84
+ $: Record<string, unknown>;
77
85
  /**
78
86
  * Returns the component as a string (template).
79
87
  * @returns {html | Promise<html>} The rendered component as a string (template).
@@ -112,10 +120,12 @@ interface Component$1 {
112
120
  declare abstract class Component implements Component$1 {
113
121
  abstract readonly name: string;
114
122
  content?: string;
123
+ groups: string[];
115
124
  options: Options;
116
125
  ref?: Ref;
117
126
  readonly props: Record<string, Prop$1>;
118
127
  readonly data: Record<string, string | number | boolean | null>;
128
+ $: Record<string, unknown>;
119
129
  abstract render(): html$1 | Promise<html$1>;
120
130
  onInit?(): void;
121
131
  onRender?(): void;
@@ -180,6 +190,19 @@ declare class Library {
180
190
  }>;
181
191
  }
182
192
 
193
+ declare abstract class Plugin {
194
+ /**
195
+ * Modules are mini plugins that are required for the plugin to work, they are processed and stored before the plugin itself is installed and stored in a safe place.
196
+ */
197
+ readonly abstract modules: Record<string, () => Record<string, unknown>>;
198
+ /**
199
+ * The function returns an object that will be installed as a plugin in Signature.$
200
+ * @param modules
201
+ * @returns {Record<string, unknown>} The object that will be installed as a plugin
202
+ */
203
+ abstract define(modules: Record<string, Record<string, unknown>>): Record<string, unknown>;
204
+ }
205
+
183
206
  type ResolvedLib = {
184
207
  components: string[];
185
208
  dependencies: Record<string, ResolvedLib>;
@@ -189,6 +212,8 @@ declare class Signature {
189
212
  private refs;
190
213
  private libs;
191
214
  private bank;
215
+ private $;
216
+ private $g;
192
217
  constructor();
193
218
  /**
194
219
  * Adds a component to the signature.
@@ -214,6 +239,13 @@ declare class Signature {
214
239
  * @return {Record<string, ResolvedLib>} A object of formatted libraries with their components and dependencies.
215
240
  */
216
241
  libraries(): Record<string, ResolvedLib>;
242
+ /**
243
+ * Registers a plugin in the signature.
244
+ * @import {Plugin} from "./Plugin.js";
245
+ * @param {string} name The name of the plugin.
246
+ * @param {Plugin} plugin The plugin to register.
247
+ */
248
+ use(name: string, plugin: Plugin): void;
217
249
  /**
218
250
  * Contacts the Component.onContact method through its reference.
219
251
  * @param {string} name The name of the reference.
@@ -252,15 +284,17 @@ declare class Prop<T extends keyof TypesMap> implements Prop$1 {
252
284
  isValid(value: TypesMap[keyof TypesMap]): boolean;
253
285
  }
254
286
 
255
- declare function html(strings: TemplateStringsArray, ...values: any[]): {
287
+ type HTMLTemplate = {
256
288
  strings: TemplateStringsArray;
257
289
  values: any[];
258
290
  };
259
- declare function unsafeHTML(value: any): {
291
+ declare function html(strings: TemplateStringsArray, ...values: any[]): HTMLTemplate;
292
+ type unsafeHTMLTemplate = {
260
293
  type: "unsafeHTML";
261
294
  value: any;
262
295
  };
296
+ declare function unsafeHTML(value: any): unsafeHTMLTemplate;
263
297
 
264
298
  declare function export_default(): Signature;
265
299
 
266
- export { Component, Library, Prop, Signature, export_default as default, html, unsafeHTML };
300
+ 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,26 @@ class Signature {
99
101
  };
100
102
  return resolve(this.libs);
101
103
  }
104
+ /**
105
+ * Registers a plugin in the signature.
106
+ * @import {Plugin} from "./Plugin.js";
107
+ * @param {string} name The name of the plugin.
108
+ * @param {Plugin} plugin The plugin to register.
109
+ */
110
+ use(name, plugin) {
111
+ if (this.$[name]) {
112
+ throw new Error(`Plugin with name ${name} already exists.`);
113
+ }
114
+ let modules = {};
115
+ Object.keys(plugin.modules).forEach((key) => {
116
+ modules[key] = plugin.modules[key]();
117
+ });
118
+ this.$[name] = {
119
+ plugin: plugin.define(modules),
120
+ modules
121
+ };
122
+ this.$g[name] = this.$[name].plugin;
123
+ }
102
124
  /**
103
125
  * Contacts the Component.onContact method through its reference.
104
126
  * @param {string} name The name of the reference.
@@ -153,6 +175,15 @@ class Signature {
153
175
  throw new Error(`Component '${component.name}' must render a single root element.`);
154
176
  }
155
177
  const newElement = template.content.firstElementChild;
178
+ // Preserve old ref attribute
179
+ let oldRef = ref.element.getAttribute("ref");
180
+ if (oldRef) {
181
+ newElement.setAttribute("ref", oldRef);
182
+ }
183
+ // Preserve si-group attribute
184
+ if (component.groups.length > 0) {
185
+ newElement.setAttribute("si-group", component.groups.join(" "));
186
+ }
156
187
  this.render(template.content);
157
188
  component.onRender?.(); // lifecycle hook
158
189
  ref.element.replaceWith(newElement);
@@ -286,6 +317,8 @@ class Signature {
286
317
  // Find all elements with the component name in the frame
287
318
  for (const el of Array.from(frame.querySelectorAll(com)).concat(Array.from(frame.querySelectorAll(`[si-component="${com}"]`)))) {
288
319
  const renderer = new component();
320
+ renderer.$ = this.$g; // injecting plugins
321
+ Object.freeze(renderer.$); // prevent plugins from being modified by components
289
322
  renderer.onInit?.(); // lifecycle hook
290
323
  if (el instanceof HTMLElement) {
291
324
  // Fill the renderer's content
@@ -411,6 +444,10 @@ class Signature {
411
444
  this.render(body.content);
412
445
  renderer.onRender?.(); // lifecycle hook
413
446
  const mountEl = body.firstElementChild;
447
+ // Processing group
448
+ if (renderer?.groups.length > 0) {
449
+ mountEl.setAttribute("si-group", renderer.groups.join(" "));
450
+ }
414
451
  // Processing ref
415
452
  if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
416
453
  let refName = el.getAttribute("ref");
@@ -433,6 +470,14 @@ class Signature {
433
470
  update: () => this.updateRef(refName)
434
471
  };
435
472
  }
473
+ // Copying another attributes from the original element to the new one, with exceptions
474
+ for (const attr of Array.from(el.attributes)) {
475
+ const name = attr.name;
476
+ if (renderer.props[name] !== undefined || name === "ref" || name === "si-component" || name === "si-group")
477
+ continue;
478
+ if (!mountEl.hasAttribute(name))
479
+ mountEl.setAttribute(name, attr.value);
480
+ }
436
481
  el.replaceWith(body.firstElementChild);
437
482
  renderer.onMount?.(mountEl); // lifecycle hook
438
483
  });
@@ -443,12 +488,14 @@ class Signature {
443
488
 
444
489
  class Component {
445
490
  content;
491
+ groups = [];
446
492
  options = {
447
493
  generateRefIfNotSpecified: false,
448
494
  };
449
495
  ref;
450
496
  props = {};
451
497
  data = {};
498
+ $ = {};
452
499
  onInit() {
453
500
  }
454
501
  ;
@@ -586,9 +633,13 @@ function html(strings, ...values) {
586
633
  function unsafeHTML(value) {
587
634
  return { type: "unsafeHTML", value: value };
588
635
  }
636
+ // todo: make a loop function
637
+
638
+ class Plugin {
639
+ }
589
640
 
590
641
  function index () {
591
642
  return new Signature();
592
643
  }
593
644
 
594
- export { Component, Library, Prop, Signature, index as default, html, unsafeHTML };
645
+ 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");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.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)}}for(const e of Array.from(s.attributes)){const n=e.name;void 0===t.props[n]&&"ref"!==n&&"si-component"!==n&&"si-group"!==n&&(o.hasAttribute(n)||o.setAttribute(n,e.value))}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,2 @@
1
+ export default class Plugin {
2
+ }
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,26 @@ export default class Signature {
79
81
  };
80
82
  return resolve(this.libs);
81
83
  }
84
+ /**
85
+ * Registers a plugin in the signature.
86
+ * @import {Plugin} from "./Plugin.js";
87
+ * @param {string} name The name of the plugin.
88
+ * @param {Plugin} plugin The plugin to register.
89
+ */
90
+ use(name, plugin) {
91
+ if (this.$[name]) {
92
+ throw new Error(`Plugin with name ${name} already exists.`);
93
+ }
94
+ let modules = {};
95
+ Object.keys(plugin.modules).forEach((key) => {
96
+ modules[key] = plugin.modules[key]();
97
+ });
98
+ this.$[name] = {
99
+ plugin: plugin.define(modules),
100
+ modules
101
+ };
102
+ this.$g[name] = this.$[name].plugin;
103
+ }
82
104
  /**
83
105
  * Contacts the Component.onContact method through its reference.
84
106
  * @param {string} name The name of the reference.
@@ -133,6 +155,15 @@ export default class Signature {
133
155
  throw new Error(`Component '${component.name}' must render a single root element.`);
134
156
  }
135
157
  const newElement = template.content.firstElementChild;
158
+ // Preserve old ref attribute
159
+ let oldRef = ref.element.getAttribute("ref");
160
+ if (oldRef) {
161
+ newElement.setAttribute("ref", oldRef);
162
+ }
163
+ // Preserve si-group attribute
164
+ if (component.groups.length > 0) {
165
+ newElement.setAttribute("si-group", component.groups.join(" "));
166
+ }
136
167
  this.render(template.content);
137
168
  component.onRender?.(); // lifecycle hook
138
169
  ref.element.replaceWith(newElement);
@@ -266,6 +297,8 @@ export default class Signature {
266
297
  // Find all elements with the component name in the frame
267
298
  for (const el of Array.from(frame.querySelectorAll(com)).concat(Array.from(frame.querySelectorAll(`[si-component="${com}"]`)))) {
268
299
  const renderer = new component();
300
+ renderer.$ = this.$g; // injecting plugins
301
+ Object.freeze(renderer.$); // prevent plugins from being modified by components
269
302
  renderer.onInit?.(); // lifecycle hook
270
303
  if (el instanceof HTMLElement) {
271
304
  // Fill the renderer's content
@@ -391,6 +424,10 @@ export default class Signature {
391
424
  this.render(body.content);
392
425
  renderer.onRender?.(); // lifecycle hook
393
426
  const mountEl = body.firstElementChild;
427
+ // Processing group
428
+ if (renderer?.groups.length > 0) {
429
+ mountEl.setAttribute("si-group", renderer.groups.join(" "));
430
+ }
394
431
  // Processing ref
395
432
  if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
396
433
  let refName = el.getAttribute("ref");
@@ -413,6 +450,14 @@ export default class Signature {
413
450
  update: () => this.updateRef(refName)
414
451
  };
415
452
  }
453
+ // Copying another attributes from the original element to the new one, with exceptions
454
+ for (const attr of Array.from(el.attributes)) {
455
+ const name = attr.name;
456
+ if (renderer.props[name] !== undefined || name === "ref" || name === "si-component" || name === "si-group")
457
+ continue;
458
+ if (!mountEl.hasAttribute(name))
459
+ mountEl.setAttribute(name, attr.value);
460
+ }
416
461
  el.replaceWith(body.firstElementChild);
417
462
  renderer.onMount?.(mountEl); // lifecycle hook
418
463
  });
@@ -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,12 @@
1
+ export default abstract class Plugin {
2
+ /**
3
+ * Modules are mini plugins that are required for the plugin to work, they are processed and stored before the plugin itself is installed and stored in a safe place.
4
+ */
5
+ readonly abstract modules: Record<string, () => Record<string, unknown>>;
6
+ /**
7
+ * The function returns an object that will be installed as a plugin in Signature.$
8
+ * @param modules
9
+ * @returns {Record<string, unknown>} The object that will be installed as a plugin
10
+ */
11
+ abstract define(modules: Record<string, Record<string, unknown>>): Record<string, unknown>;
12
+ }
@@ -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,13 @@ 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
+ /**
41
+ * Registers a plugin in the signature.
42
+ * @import {Plugin} from "./Plugin.js";
43
+ * @param {string} name The name of the plugin.
44
+ * @param {Plugin} plugin The plugin to register.
45
+ */
46
+ use(name: string, plugin: Plugin): void;
37
47
  /**
38
48
  * Contacts the Component.onContact method through its reference.
39
49
  * @param {string} name The name of the reference.
package/dist/d/html.d.ts CHANGED
@@ -1,8 +1,12 @@
1
- export default function html(strings: TemplateStringsArray, ...values: any[]): {
1
+ type HTMLTemplate = {
2
2
  strings: TemplateStringsArray;
3
3
  values: any[];
4
4
  };
5
- export declare function unsafeHTML(value: any): {
5
+ export default function html(strings: TemplateStringsArray, ...values: any[]): HTMLTemplate;
6
+ type unsafeHTMLTemplate = {
6
7
  type: "unsafeHTML";
7
8
  value: any;
8
9
  };
10
+ export declare function unsafeHTML(value: any): unsafeHTMLTemplate;
11
+ export declare function joinTemplates(...templates: HTMLTemplate[]): HTMLTemplate;
12
+ export {};
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,10 @@ interface Component {
45
49
  * The properties of the component.
46
50
  */
47
51
  data: Record<string, string | number | boolean | null>;
52
+ /**
53
+ * Plugins are stored here.
54
+ */
55
+ $: Record<string, unknown>;
48
56
  /**
49
57
  * Returns the component as a string (template).
50
58
  * @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/html.js CHANGED
@@ -4,3 +4,16 @@ export default function html(strings, ...values) {
4
4
  export function unsafeHTML(value) {
5
5
  return { type: "unsafeHTML", value: value };
6
6
  }
7
+ export function joinTemplates(...templates) {
8
+ let strings = [];
9
+ let values = [];
10
+ templates.forEach((t) => {
11
+ strings.push(...t.strings);
12
+ values.push(...t.values);
13
+ });
14
+ return {
15
+ strings: strings,
16
+ values: values
17
+ };
18
+ }
19
+ // todo: make a loop function
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.6",
3
+ "version": "0.2.9",
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,13 @@
1
+ export default abstract class Plugin {
2
+ /**
3
+ * Modules are mini plugins that are required for the plugin to work, they are processed and stored before the plugin itself is installed and stored in a safe place.
4
+ */
5
+ readonly abstract modules: Record<string, () => Record<string, unknown>>;
6
+
7
+ /**
8
+ * The function returns an object that will be installed as a plugin in Signature.$
9
+ * @param modules
10
+ * @returns {Record<string, unknown>} The object that will be installed as a plugin
11
+ */
12
+ public abstract define(modules: Record<string, Record<string, unknown>>): Record<string, unknown>;
13
+ }
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,31 @@ export default class Signature {
109
114
  return resolve(this.libs);
110
115
  }
111
116
 
117
+ /**
118
+ * Registers a plugin in the signature.
119
+ * @import {Plugin} from "./Plugin.js";
120
+ * @param {string} name The name of the plugin.
121
+ * @param {Plugin} plugin The plugin to register.
122
+ */
123
+ public use(name: string, plugin: Plugin): void {
124
+ if (this.$[name]) {
125
+ throw new Error(`Plugin with name ${name} already exists.`);
126
+ }
127
+
128
+ let modules: Record<string, Record<string, unknown>> = {};
129
+
130
+ Object.keys(plugin.modules).forEach((key) => {
131
+ modules[key] = plugin.modules[key]();
132
+ });
133
+
134
+ this.$[name] = {
135
+ plugin: plugin.define(modules),
136
+ modules
137
+ };
138
+
139
+ this.$g[name] = this.$[name].plugin;
140
+ }
141
+
112
142
  /**
113
143
  * Contacts the Component.onContact method through its reference.
114
144
  * @param {string} name The name of the reference.
@@ -172,6 +202,17 @@ export default class Signature {
172
202
 
173
203
  const newElement = template.content.firstElementChild as Element;
174
204
 
205
+ // Preserve old ref attribute
206
+ let oldRef = ref.element.getAttribute("ref");
207
+ if (oldRef) {
208
+ newElement.setAttribute("ref", oldRef);
209
+ }
210
+
211
+ // Preserve si-group attribute
212
+ if (component.groups.length > 0) {
213
+ newElement.setAttribute("si-group", component.groups.join(" "));
214
+ }
215
+
175
216
  this.render(template.content);
176
217
 
177
218
  component.onRender?.(); // lifecycle hook
@@ -341,6 +382,10 @@ export default class Signature {
341
382
  // Find all elements with the component name in the frame
342
383
  for (const el of Array.from(frame.querySelectorAll(com)).concat(Array.from(frame.querySelectorAll(`[si-component="${com}"]`)))) {
343
384
  const renderer: Component = new component();
385
+
386
+ renderer.$ = this.$g; // injecting plugins
387
+ Object.freeze(renderer.$); // prevent plugins from being modified by components
388
+
344
389
  renderer.onInit?.(); // lifecycle hook
345
390
 
346
391
  if (el instanceof HTMLElement) {
@@ -476,6 +521,11 @@ export default class Signature {
476
521
 
477
522
  const mountEl: Element = body.firstElementChild as Element;
478
523
 
524
+ // Processing group
525
+ if (renderer?.groups.length > 0) {
526
+ mountEl.setAttribute("si-group", renderer.groups.join(" "));
527
+ }
528
+
479
529
  // Processing ref
480
530
  if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
481
531
  let refName = el.getAttribute("ref");
@@ -506,6 +556,15 @@ export default class Signature {
506
556
  };
507
557
  }
508
558
 
559
+ // Copying another attributes from the original element to the new one, with exceptions
560
+ for (const attr of Array.from(el.attributes)) {
561
+ const name: string = attr.name;
562
+
563
+ if (renderer.props[name] !== undefined || name === "ref" || name === "si-component" || name === "si-group") continue;
564
+
565
+ if (!mountEl.hasAttribute(name)) mountEl.setAttribute(name, attr.value);
566
+ }
567
+
509
568
  el.replaceWith(body.firstElementChild as Element);
510
569
 
511
570
  renderer.onMount?.(mountEl); // lifecycle hook
package/src/html.ts CHANGED
@@ -1,10 +1,31 @@
1
- export default function html(strings: TemplateStringsArray, ...values: any[]): {
1
+ type HTMLTemplate = {
2
2
  strings: TemplateStringsArray,
3
3
  values: any[]
4
- } {
4
+ };
5
+
6
+ export default function html(strings: TemplateStringsArray, ...values: any[]): HTMLTemplate {
5
7
  return {strings, values};
6
8
  }
7
9
 
8
- export function unsafeHTML(value: any): { type: "unsafeHTML", value: any } {
10
+ type unsafeHTMLTemplate = { type: "unsafeHTML", value: any };
11
+
12
+ export function unsafeHTML(value: any): unsafeHTMLTemplate {
9
13
  return {type: "unsafeHTML", value: value};
10
- }
14
+ }
15
+
16
+ export function joinTemplates(...templates: HTMLTemplate[]): HTMLTemplate {
17
+ let strings: string[] = [];
18
+ let values: any[] = [];
19
+
20
+ templates.forEach((t) => {
21
+ strings.push(...t.strings);
22
+ values.push(...t.values);
23
+ });
24
+
25
+ return {
26
+ strings: strings as unknown as TemplateStringsArray,
27
+ values: values
28
+ } as HTMLTemplate;
29
+ }
30
+
31
+ // todo: make a loop function
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
  */
@@ -57,6 +62,10 @@ interface Component {
57
62
  */
58
63
  data: Record<string, string | number | boolean | null>;
59
64
 
65
+ /**
66
+ * Plugins are stored here.
67
+ */
68
+ $: Record<string, unknown>;
60
69
 
61
70
  /**
62
71
  * 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};