@peter.naydenov/morph 3.1.4 → 3.2.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.
Files changed (61) hide show
  1. package/.mocharc.json +1 -0
  2. package/.opencode/command/speckit.analyze.md +184 -0
  3. package/.opencode/command/speckit.checklist.md +294 -0
  4. package/.opencode/command/speckit.clarify.md +181 -0
  5. package/.opencode/command/speckit.constitution.md +82 -0
  6. package/.opencode/command/speckit.implement.md +135 -0
  7. package/.opencode/command/speckit.plan.md +89 -0
  8. package/.opencode/command/speckit.specify.md +257 -0
  9. package/.opencode/command/speckit.tasks.md +137 -0
  10. package/.opencode/command/speckit.taskstoissues.md +28 -0
  11. package/.specify/memory/constitution.md +43 -0
  12. package/.specify/scripts/bash/check-prerequisites.sh +166 -0
  13. package/.specify/scripts/bash/common.sh +156 -0
  14. package/.specify/scripts/bash/create-new-feature.sh +305 -0
  15. package/.specify/scripts/bash/setup-plan.sh +61 -0
  16. package/.specify/scripts/bash/update-agent-context.sh +790 -0
  17. package/.specify/templates/agent-file-template.md +28 -0
  18. package/.specify/templates/checklist-template.md +40 -0
  19. package/.specify/templates/plan-template.md +104 -0
  20. package/.specify/templates/spec-template.md +115 -0
  21. package/.specify/templates/tasks-template.md +251 -0
  22. package/Changelog.md +17 -0
  23. package/README.md +154 -3
  24. package/dist/main.d.ts +100 -0
  25. package/dist/methods/_actionSupply.d.ts +18 -0
  26. package/dist/methods/_chopTemplates.d.ts +22 -0
  27. package/dist/methods/_defineData.d.ts +25 -0
  28. package/dist/methods/_defineType.d.ts +15 -0
  29. package/dist/methods/_readTemplate.d.ts +24 -0
  30. package/dist/methods/_renderHolder.d.ts +17 -0
  31. package/dist/methods/_setupActions.d.ts +19 -0
  32. package/dist/methods/build.d.ts +35 -0
  33. package/dist/methods/render.d.ts +25 -0
  34. package/dist/methods/settings.d.ts +7 -0
  35. package/dist/morph.cjs +1 -1
  36. package/dist/morph.esm.mjs +1 -1
  37. package/dist/morph.umd.js +1 -1
  38. package/morph.png +0 -0
  39. package/package.json +10 -7
  40. package/simple.js +17 -0
  41. package/specs/001-extend-templates/checklists/requirements.md +35 -0
  42. package/specs/001-extend-templates/contracts/set-method.json +39 -0
  43. package/specs/001-extend-templates/data-model.md +76 -0
  44. package/specs/001-extend-templates/plan.md +90 -0
  45. package/specs/001-extend-templates/quickstart.md +56 -0
  46. package/specs/001-extend-templates/research.md +18 -0
  47. package/specs/001-extend-templates/spec.md +139 -0
  48. package/specs/001-extend-templates/tasks.md +255 -0
  49. package/src/main.js +70 -32
  50. package/src/methods/_actionSupply.js +16 -0
  51. package/src/methods/_chopTemplates.js +22 -1
  52. package/src/methods/_defineData.js +33 -1
  53. package/src/methods/_defineType.js +13 -0
  54. package/src/methods/_readTemplate.js +46 -46
  55. package/src/methods/_renderHolder.js +15 -0
  56. package/src/methods/_setupActions.js +18 -8
  57. package/src/methods/build.js +98 -43
  58. package/src/methods/render.js +32 -11
  59. package/src/methods/settings.js +11 -0
  60. package/tsconfig.json +27 -0
  61. package/types/index.d.ts +0 -60
package/dist/morph.umd.js CHANGED
@@ -1 +1 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@peter.naydenov/walk")):"function"==typeof define&&define.amd?define(["@peter.naydenov/walk"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).morph=t(e.walk)}(this,(function(e){"use strict";function t(e){return function t(n){const{TG_PRX:a,TG_SFX:o,TG_SIZE_P:l,TG_SIZE_S:s}=e;let i,c,u,f=[];if("string"!=typeof n)return r("notAString");if(0==n.length)return[];if(i=n.indexOf(a),0<i&&f.push(n.slice(0,i)),-1==i)return f.push(n),f;{if(u=n.indexOf(a,i+l),c=n.indexOf(o),-1==c)return r("missingClosing");if(c<i)return r("closedBeforeOpened");if(c+=s,-1!=u&&u<c)return r("newBeforeClosed");f.push(n.slice(i,c));let e=t(n.slice(c));return f.concat(e)}}}function r(e){switch(e){case"notAString":return"Error: Template is not a string.";case"missingClosing":return"Error: Placeholder with missing closing tag.";case"closedBeforeOpened":return"Error: Placeholder closing tag without starting one.";case"newBeforeClosed":return"Error: Nested placeholders. Close placeholder before open new one.";default:return"Error: Unknown template error."}}function*n(e,t){let r=function(e={}){let t=[],{type:r,limit:n,onLimit:a}=Object.assign({},{type:"FIFO",limit:!1,onLimit:"update"},e),o="LIFO"===r.toUpperCase(),l=!1;function s(e=1,r=0){let n=[];return r>0&&Array.from({length:r}).map((()=>t.pop())),1==e?t.pop():(Array.from({length:e}).map((()=>{let e=t.pop();null!=e&&n.push(e)})),n)}function i(e=1,r=0){let n=[],a=t.length-r;return e>1&&Array.from({length:e}).map((()=>{null!=t[a-1]&&n.push(t[a-1]),a--})),1==e?t[t.length-1]:n}function c(){}return c.prototype={pull:s,pullReverse:function(e=1,t=0){let r=s(e,t);return r instanceof Array?r.reverse():r},peek:i,peekReverse:function(e=1,t=0){const r=i(e,t);return r instanceof Array?r.reverse():r},getSize:()=>t.length,isEmpty:()=>0==t.length,reset:()=>(t=[],!0),debug:()=>[...t]},c.prototype.push=o?function(e){const r=e instanceof Array,o=r?e.length:1;let i=!1;if("full"!==a||!l){if(n&&r&&o>n&&(e=e.slice(0,-o+n)),n){const o=(r?e.length:1)+t.length;o>=n&&"full"===a&&(e=e.slice(0,-(o-n))),o>=n&&"update"===a&&(i=s(o-n))}return t=e instanceof Array?t.concat(e):t.concat([e]),l=!!n&&t.length===n,i||void 0}}:function(e){const r=e instanceof Array,o=r?e.length:1;let i=!1;if("full"!==a||!l){if(n&&r&&o>n&&(e=e.slice(o-n)),n){const o=(r?e.length:1)+t.length;o>=n&&"full"===a&&(e=e.slice(0,-(o-n))),o>=n&&"update"===a&&(i=s(o-n))}return t=r?e.reduce(((e,t)=>[t,...e]),t):[e].reduce(((e,t)=>[t,...e]),t),l=!!n&&t.length===n,i||void 0}},new c}({type:"LIFO"});for(let n=0;n<=t;n++)r.push(e[n]);for(;r&&!r.isEmpty();){let e=yield r.pull();e&&r.push(e)}}var a={TG_PRX:"{{",TG_SFX:"}}",TG_SIZE_P:2,TG_SIZE_S:2};function o(e){return null==e?"null":"string"==typeof e||"number"==typeof e||"boolean"==typeof e?"primitive":"function"==typeof e?"function":e instanceof Array?"array":e instanceof Object?"object":void 0}function l(e,r,n,o,l,...s){e instanceof Object&&Object.entries(e).forEach((([t,r])=>{r instanceof Object&&(e[t]=r.text),r instanceof Array&&(e[t]=r[0])}));const i="function"==typeof n[r];return e=function(e={}){return"string"==typeof e?{text:e}:e}(e),i?n[r]({theData:e,dependencies:l,full:o},...s):function(e,r){if(null==r)return null;const n=t(a)(e),o=a;return n.forEach(((e,t)=>{if(e.includes(o.TG_PRX)){const a=e.replace(o.TG_PRX,"").replace(o.TG_SFX,"").trim();r.hasOwnProperty(a)&&null!=r[a]&&(n[t]=r[a])}})),n.join("")}(n[r],e)}function s(r,s=!1,i={}){let{hasError:c,placeholders:u,chop:f,helpers:p,handshake:d,snippets:h}=function(e){const{template:r,helpers:n={},handshake:o}=e,{TG_PRX:l,TG_SFX:s}=a,i=[],c={},u="string"==typeof r?r.replace(/<!--[\s\S]*?-->/g,"").replace(/\s{2,}/g," "):r;let f=null;const p=t(a)(u);return"string"==typeof p?f=p:p.forEach(((e,t)=>{const r=RegExp(l+"\\s*(.*?)\\s*(?::\\s*(.*?)\\s*)?(?::\\s*(.*?)\\s*)?"+s,"g");if(e.includes(l)){const a=r.exec(e);if(!a)return;let o={index:t,data:(n=a[1],n||null),action:a[2]?a[2].split(",").map((e=>e.trim())):null,name:a[3]?a[3].trim():null};i.push(o),c[i.length-1]=o,o.name&&(c[o.name]=o)}var n})),i.forEach((e=>{e.action&&e.action.every((e=>"#"===e||"^^"===e||!(!e.startsWith("^")||"^^"===e)||(e.startsWith("?")&&(e=e.replace("?","")),e.startsWith("+")&&(e=e.replace("+","")),e.startsWith("[]")&&(e=e.replace("[]","")),e.startsWith(">")&&(e=e.replace(">","")),""===e||!!n[e]||(f=`Error: Missing helper: ${e}`,!1))))})),{hasError:f,placeholders:i,chop:p,helpers:n,handshake:o,snippets:c}}(r);if(c){function m(){return c}return s?[!1,m]:m}{const y=structuredClone(u);function g(t="render",r={},a={},...s){const c=structuredClone(f);let m=!1;if(!["render","debug","snippets"].includes(t)&&!t.startsWith("snippets"))return`Error: Wrong command "${t}". Available commands: render, debug, snippets.`;if(t.startsWith("snippets")&&t.includes(":")){m=!0;let e=t.split(":").slice(1)[0].trim().split(",").map((e=>e.trim()));u=e.map((e=>h[e]))}else"snippets"===t?m=!0:u=structuredClone(y);if("string"==typeof r)switch(r){case"raw":return c.join("");case"demo":if(!d)return"Error: No handshake data.";r=d;break;case"handshake":return d?structuredClone(d):"Error: No handshake data.";case"placeholders":return u.map((e=>c[e.index])).join(", ");default:return`Error: Wrong instruction "${r}". Available instructions: raw, demo, handshake, placeholders.`}const g=[],b={};let v=o(r),E={...i,...a};r=e({data:r});let k=e({data:r});return"null"===v?c.join(""):("array"!==v&&(r=[r]),"null"===v?c.join(""):(r.forEach((t=>{u.forEach((a=>{const{index:s,data:i,action:u}=a,f=!u&&i,d=structuredClone(b),h={dependencies:E,memory:d};let m=t;if(i&&i.includes("/")?m.hasOwnProperty(i)?m=m[i]:i.split("/").forEach((e=>{m=m.hasOwnProperty(e)?m[e]:[]})):"@all"===i||null===i||"@root"===i?m=t:i&&(m=m[i]),f){switch(o(m)){case"function":return void(c[s]=m());case"primitive":return void(c[s]=m);case"array":return void("primitive"===o(m[0])&&(c[s]=m[0]));case"object":return void(m.text&&(c[s]=m.text))}}else{let{dataDeepLevel:a,nestedData:i}=function(t,r){const n=[];let a=0;if(t instanceof Function)return{dataDeepLevel:0,nestedData:[[t()]]};if(null==t)return{dataDeepLevel:0,nestedData:[null]};if("string"==typeof t)return{dataDeepLevel:0,nestedData:[[t]]};const o=structuredClone(t);return r.includes("#")?(e({data:o,objectCallback:function({key:e,value:t,breadcrumbs:r}){return e===r?(n[0]=[t],t):(a=r.split("/").length-1,n[a]||(n[a]=[]),n[a].push(t),t)}}),{dataDeepLevel:a,nestedData:n}):(n.push([o]),{dataDeepLevel:0,nestedData:n})}(m,u),f=n(function(e,t=10){let r={},n=[...e],a=0,o=0,l=0;n.forEach((e=>{"#"===e&&l++})),l<t&&console.error(`Error: Not enough level markers (#) for data with depth level ${t}. Found ${l} level markers in actions: ${e.join(", ")}`);do{r[o]=[],o++}while(o<=t);return n.every((e=>"#"===e?(a++,!(a>t)):e.startsWith("?")?(r[a].push({type:"route",name:e.replace("?",""),level:a}),!0):e.startsWith("^")&&"^^"!==e?(r[a].push({type:"save",name:e.replace("^",""),level:a}),!0):"^^"===e?(r[a].push({type:"overwrite",name:"none",level:a}),!0):e.startsWith("+")?(r[a].push({type:"extendedRender",name:e.replace("+",""),level:a}),!0):e.startsWith("[]")?(r[a].push({type:"mix",name:e.replace("[]",""),level:a}),!0):e.startsWith(">")?(r[a].push({type:"data",name:e.replace(">",""),level:a}),!0):(""===e||r[a].push({type:"render",name:e,level:a}),!0))),r}(u,a),a);for(let n of f){let{type:a,name:s,level:c}=n;(i[c]||[]).forEach(((n,u)=>{let f=o(n);switch(a){case"route":switch(f){case"array":n.forEach(((e,t)=>{if(null==e)return;const r=o(e),a=p[s]({data:e,...h,full:e});null!=a&&("object"===r?n[t].text=l(e,a,p,k,E):n[t]=l(e,a,p,k,E))}));break;case"object":n.text=l(n,s,p,k,E)}break;case"save":b[s]=structuredClone(n);break;case"overwrite":t=structuredClone(n);break;case"data":switch(f){case"array":n.forEach(((e,t)=>n[t]=e instanceof Function?p[s]({data:e(),...h,full:e}):p[s]({data:e,...h,full:e})));break;case"object":i[c]=[p[s]({data:n,...h,full:r})];break;case"function":i[c]=[p[s]({data:n(),...h,full:r})];break;case"primitive":i[c]=p[s]({data:n,...h,full:r})}break;case"render":const d="function"==typeof p[s];switch(f){case"array":d?n.forEach(((e,t)=>{if(null==e)return;const r=o(e),a=p[s]({data:e,...h,full:k});null==a&&(n[t]=null),"object"===r?e.text=a:n[t]=a})):n.forEach(((e,t)=>{if(null==e)return;const r=o(e),a=l(e,s,p,k,E);null==a?n[t]=null:"object"===r?e.text=a:n[t]=a}));break;case"function":i[c]=p[s]({data:n(),...h,full:r});break;case"primitive":i[c]=d?p[s]({data:n,...h,full:r}):l(n,s,p,k,E);break;case"object":d?i[c][u].text=p[s]({data:n,...h,full:r}):n.text=l(n,s,p,k,E)}break;case"extendedRender":"function"==typeof p[s]&&i[0].forEach(((e,t)=>i[0][t]=p[s]({data:e,...h,full:e})));break;case"mix":if(""===s)switch(f){case"object":Object.keys(n).find((e=>e.includes("/")))?Object.entries(n).forEach((([e,t])=>i[c][e]=t.text)):i[c]=n.text;for(let y=c-1;y>=0;y--)i[y]=e({data:i[y],objectCallback:m});function m({value:e,breadcrumbs:t}){return i[c][t]?i[c][t]:e}break;case"array":n.forEach(((e,t)=>{if(t>0){let r=o(e);if(null==e)return;n[0]+="object"===r?`${e.text}`:`${e}`,n.toSpliced(t,1)}else{let t=o(e);if(n[0]="",null===t)return;n[0]="object"===t?`${e.text}`:`${e}`}})),n.length=1}else{let g=p[s]({data:n,...h,full:r}),v=o(g);switch(n.forEach(((e,t)=>n.splice(t,1))),n.length=0,v){case"primitive":n[0]=g;break;case"array":n.push(...g)}}}}))}if(i instanceof Array&&1===i.length&&i[0]instanceof Array&&(i=i[0]),null==i[0])return;let d=o(i[0]),y=i[0];switch(d){case"primitive":if(null==y)return;c[s]=y;break;case"object":if(null==y.text)return;c[s]=y.text;break;case"array":const e=o(y[0]);c[s]="object"===e?y.map((e=>e.text)).join(""):y.join("")}}})),m?g.push(u.map((e=>c[e.index])).join("<~>")):g.push(c.join(""))})),"array"===v?g:s?s.reduce(((e,t)=>"function"!=typeof t?e:t(e,E)),g.join("")):g.join("")))}return s?[!0,g]:g}}const i={default:{}};return{build:s,get:function(e){if(!(e instanceof Array))return function(){return'Error: Argument "location" is a string. Should be an array. E.g. ["templateName", "storageName"].'};const[t,r="default"]=e;return i[r]?i[r][t]?i[r][t]:function(){return`Error: Template "${t}" does not exist in storage "${r}".`}:function(){return`Error: Storage "${r}" does not exist.`}},add:function(e,t,...r){const[n,a="default"]=e;if(null==t)return void console.warn(`Warning: Template ${a}/${n} is not added to storage. The template is null.`);let o=t,l=!0;if(i[a]||(i[a]={}),"function"!=typeof t){let e=s(t,!0,...r);l=e[0],o=e[1]}l?i[a][n]=o:console.error(`Error: Template "${n}" looks broken and is not added to storage.`)},list:function(e=["default"]){return e.map((e=>i[e]?Object.keys(i[e]):[])).flat()},clear:function(){Object.keys(i).forEach((e=>{"default"!=e?delete i[e]:i.default={}}))},remove:function(e){const[t,r="default"]=e;return i[r]?i[r][t]?void delete i[r][t]:`Error: Template "${t}" does not exist in storage "${r}".`:`Error: Storage "${r}" does not exist.`}}}));
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@peter.naydenov/walk")):"function"==typeof define&&define.amd?define(["@peter.naydenov/walk"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).morph=t(e.walk)}(this,(function(e){"use strict";function t(e){return function t(n){const{TG_PRX:a,TG_SFX:o,TG_SIZE_P:l,TG_SIZE_S:s}=e;let i,c,u,f=[];if("string"!=typeof n)return r("notAString");if(0==n.length)return[];if(i=n.indexOf(a),0<i&&f.push(n.slice(0,i)),-1==i)return f.push(n),f;{if(u=n.indexOf(a,i+l),c=n.indexOf(o),-1==c)return r("missingClosing");if(c<i)return r("closedBeforeOpened");if(c+=s,-1!=u&&u<c)return r("newBeforeClosed");f.push(n.slice(i,c));let e=t(n.slice(c));return f.concat(e)}}}function r(e){switch(e){case"notAString":return"Error: Template is not a string.";case"missingClosing":return"Error: Placeholder with missing closing tag.";case"closedBeforeOpened":return"Error: Placeholder closing tag without starting one.";case"newBeforeClosed":return"Error: Nested placeholders. Close placeholder before open new one.";default:return"Error: Unknown template error."}}function*n(e,t){let r=function(e={}){let t=[],{type:r,limit:n,onLimit:a}=Object.assign({},{type:"FIFO",limit:!1,onLimit:"update"},e),o="LIFO"===r.toUpperCase(),l=!1;function s(e=1,r=0){let n=[];return r>0&&Array.from({length:r}).map((()=>t.pop())),1==e?t.pop():(Array.from({length:e}).map((()=>{let e=t.pop();null!=e&&n.push(e)})),n)}function i(e=1,r=0){let n=[],a=t.length-r;return e>1&&Array.from({length:e}).map((()=>{null!=t[a-1]&&n.push(t[a-1]),a--})),1==e?t[t.length-1]:n}function c(){}return c.prototype={pull:s,pullReverse:function(e=1,t=0){let r=s(e,t);return r instanceof Array?r.reverse():r},peek:i,peekReverse:function(e=1,t=0){const r=i(e,t);return r instanceof Array?r.reverse():r},getSize:()=>t.length,isEmpty:()=>0==t.length,reset:()=>(t=[],!0),debug:()=>[...t]},c.prototype.push=o?function(e){const r=e instanceof Array,o=r?e.length:1;let i=!1;if("full"!==a||!l){if(n&&r&&o>n&&(e=e.slice(0,-o+n)),n){const o=(r?e.length:1)+t.length;o>=n&&"full"===a&&(e=e.slice(0,-(o-n))),o>=n&&"update"===a&&(i=s(o-n))}return t=e instanceof Array?t.concat(e):t.concat([e]),l=!!n&&t.length===n,i||void 0}}:function(e){const r=e instanceof Array,o=r?e.length:1;let i=!1;if("full"!==a||!l){if(n&&r&&o>n&&(e=e.slice(o-n)),n){const o=(r?e.length:1)+t.length;o>=n&&"full"===a&&(e=e.slice(0,-(o-n))),o>=n&&"update"===a&&(i=s(o-n))}return t=r?e.reduce(((e,t)=>[t,...e]),t):[e].reduce(((e,t)=>[t,...e]),t),l=!!n&&t.length===n,i||void 0}},new c}({type:"LIFO"});for(let n=0;n<=t;n++)r.push(e[n]);for(;r&&!r.isEmpty();){let e=yield r.pull();e&&r.push(e)}}var a={TG_PRX:"{{",TG_SFX:"}}",TG_SIZE_P:2,TG_SIZE_S:2};function o(e){return null==e?"null":"string"==typeof e||"number"==typeof e||"boolean"==typeof e?"primitive":"function"==typeof e?"function":e instanceof Array?"array":e instanceof Object?"object":void 0}function l(e,r,n,o,l,...s){if(e instanceof Object&&Object.entries(e).forEach((([t,r])=>{r instanceof Object&&(e[t]=r.text),r instanceof Array&&(e[t]=r[0])})),!n[r])return`{{ Error: Helper '${r}' is not available}}`;const i="function"==typeof n[r];return e=function(e={}){return"string"==typeof e?{text:e}:e}(e),i?n[r]({theData:e,dependencies:l,full:o},...s):function(e,r){if(null==r)return null;const n=t(a)(e),o=a;return n.forEach(((e,t)=>{if(e.includes(o.TG_PRX)){const a=e.replace(o.TG_PRX,"").replace(o.TG_SFX,"").trim();r.hasOwnProperty(a)&&null!=r[a]&&(n[t]=r[a])}})),n.join("")}(n[r],e)}function s(r,i=!1,c={}){let{hasError:u,placeholders:f,chop:d,helpers:p,handshake:h,snippets:m}=function(e){const{template:r,helpers:n={},handshake:o}=e,{TG_PRX:l,TG_SFX:s}=a,i=[],c={},u="string"==typeof r?r.replace(/<!--[\s\S]*?-->/g,"").replace(/\s{2,}/g," "):r;let f=null;const d=t(a)(u);return"string"==typeof d?f=d:d.forEach(((e,t)=>{const r=RegExp(l+"\\s*(.*?)\\s*(?::\\s*(.*?)\\s*)?(?::\\s*(.*?)\\s*)?"+s,"g");if(e.includes(l)){const a=r.exec(e);if(!a)return;let o={index:t,data:(n=a[1],n||null),action:a[2]?a[2].split(",").map((e=>e.trim())):null,name:a[3]?a[3].trim():null};i.push(o),c[i.length-1]=o,o.name&&(c[o.name]=o)}var n})),{hasError:f,placeholders:i,chop:d,helpers:n,handshake:o,snippets:c}}(r);if(u){function y(){return u}return i?[!1,y]:y}{const g=structuredClone(f);function b(t="render",r={},a={},...i){const u=structuredClone(d);let y=!1;if("string"!=typeof t)return`Error: Wrong command "${t}". Available commands: render, debug, snippets, set, curry.`;if(!["render","debug","snippets","set","curry"].includes(t)&&!t.startsWith("snippets"))return`Error: Wrong command "${t}". Available commands: render, debug, snippets, set, curry.`;if(t.startsWith("snippets")&&t.includes(":")){y=!0;let e=t.split(":").slice(1)[0].trim().split(",").map((e=>e.trim()));f=e.map((e=>m[e]))}else if("snippets"===t)y=!0;else{if("set"===t){if("object"!=typeof r||!r)return"Error: 'set' command requires an object with placeholders, helpers, handshake.";const e={...p,...r.helpers||{}},t=h?{...h,...r.handshake||{}}:r.handshake||{},n=[...d];r.placeholders&&Object.entries(r.placeholders).forEach((([e,t])=>{if(isNaN(e)){let r=f.find((t=>t.name===e));n[r.index]=t}else{let r=f[e].index;n[r]=t}}));const a=s({template:n.join(""),helpers:e,handshake:t},!1,c);return"function"==typeof a?a:()=>a}if("curry"===t){return s({template:b("render",r,a,...i),helpers:p,handshake:h},!1,c)}f=structuredClone(g)}if("string"==typeof r)switch(r){case"raw":return u.join("");case"demo":if(!h)return"Error: No handshake data.";r=h;break;case"handshake":return h?structuredClone(h):"Error: No handshake data.";case"helpers":return Object.keys(p).join(", ");case"placeholders":return f.map((e=>u[e.index])).join(", ");case"count":return f.length;default:return`Error: Wrong instruction "${r}". Available instructions: raw, demo, handshake, helpers, placeholders, count.`}const v=[],k={};let E=o(r),j={...c,...a};r=e({data:r});let x=e({data:r});return"null"===E?u.join(""):("array"!==E&&(r=[r]),"null"===E?u.join(""):(r.forEach((t=>{f.forEach((a=>{const{index:s,data:i,action:c}=a,f=!c&&i,d=structuredClone(k),h={dependencies:j,memory:d};let m=t;if(i&&i.includes("/")?m.hasOwnProperty(i)?m=m[i]:i.split("/").forEach((e=>{m=m.hasOwnProperty(e)?m[e]:[]})):"@all"===i||null===i||"@root"===i?m=t:i&&(m=m[i]),f){switch(o(m)){case"function":return void(u[s]=m());case"primitive":return void(u[s]=m);case"array":return void("primitive"===o(m[0])&&(u[s]=m[0]));case"object":return void(m.text&&(u[s]=m.text))}}else{let{dataDeepLevel:a,nestedData:i}=function(t,r){const n=[];let a=0;if(t instanceof Function)return{dataDeepLevel:0,nestedData:[[t()]]};if(null==t)return{dataDeepLevel:0,nestedData:[null]};if("string"==typeof t)return{dataDeepLevel:0,nestedData:[[t]]};const o=structuredClone(t);return r.includes("#")?(e({data:o,objectCallback:function({key:e,value:t,breadcrumbs:r}){return e===r?(n[0]=[t],t):(a=r.split("/").length-1,n[a]||(n[a]=[]),n[a].push(t),t)}}),{dataDeepLevel:a,nestedData:n}):(n.push([o]),{dataDeepLevel:0,nestedData:n})}(m,c),f=n(function(e,t=10){let r={},n=[...e],a=0,o=0,l=0;n.forEach((e=>{"#"===e&&l++})),l<t&&console.error(`Error: Not enough level markers (#) for data with depth level ${t}. Found ${l} level markers in actions: ${e.join(", ")}`);do{r[o]=[],o++}while(o<=t);return n.every((e=>"#"===e?(a++,!(a>t)):e.startsWith("^")&&"^^"!==e?(r[a].push({type:"save",name:e.replace("^",""),level:a}),!0):"^^"===e?(r[a].push({type:"overwrite",name:"none",level:a}),!0):e.startsWith("+")?(r[a].push({type:"extendedRender",name:e.replace("+",""),level:a}),!0):e.startsWith("[]")?(r[a].push({type:"mix",name:e.replace("[]",""),level:a}),!0):e.startsWith(">")?(r[a].push({type:"data",name:e.replace(">",""),level:a}),!0):(""===e||r[a].push({type:"render",name:e,level:a}),!0))),r}(c,a),a);for(let n of f){let{type:a,name:s,level:c}=n;(i[c]||[]).forEach(((n,u)=>{let f=o(n);switch(a){case"route":switch(f){case"array":n.forEach(((e,t)=>{if(null==e)return;const r=o(e),a=p[s]({data:e,...h,full:e});null!=a&&("object"===r?n[t].text=l(e,a,p,x,j):n[t]=l(e,a,p,x,j))}));break;case"object":n.text=l(n,s,p,x,j)}break;case"save":k[s]=structuredClone(n);break;case"overwrite":t=structuredClone(n);break;case"data":switch(f){case"array":n.forEach(((e,t)=>n[t]=e instanceof Function?p[s]({data:e(),...h,full:e}):p[s]({data:e,...h,full:e})));break;case"object":i[c]=[p[s]({data:n,...h,full:r})];break;case"function":i[c]=[p[s]({data:n(),...h,full:r})];break;case"primitive":i[c]=p[s]({data:n,...h,full:r})}break;case"render":const d="function"==typeof p[s];switch(f){case"array":d?n.forEach(((e,t)=>{if(null==e)return;const r=o(e),a=p[s]({data:e,...h,full:x});null==a&&(n[t]=null),"object"===r?e.text=a:n[t]=a})):n.forEach(((e,t)=>{if(null==e)return;const r=o(e),a=l(e,s,p,x,j);null==a?n[t]=null:"object"===r?e.text=a:n[t]=a}));break;case"function":i[c]=p[s]({data:n(),...h,full:r});break;case"primitive":i[c]=d?p[s]({data:n,...h,full:r}):l(n,s,p,x,j);break;case"object":d?i[c][u].text=p[s]({data:n,...h,full:r}):n.text=l(n,s,p,x,j)}break;case"extendedRender":"function"==typeof p[s]&&i[0].forEach(((e,t)=>i[0][t]=p[s]({data:e,...h,full:e})));break;case"mix":if(""===s)switch(f){case"object":Object.keys(n).find((e=>e.includes("/")))?Object.entries(n).forEach((([e,t])=>i[c][e]=t.text)):i[c]=n.text;for(let y=c-1;y>=0;y--)i[y]=e({data:i[y],objectCallback:m});function m({value:e,breadcrumbs:t}){return i[c][t]?i[c][t]:e}break;case"array":n.forEach(((e,t)=>{if(t>0){let r=o(e);if(null==e)return;n[0]+="object"===r?`${e.text}`:`${e}`,n.toSpliced(t,1)}else{let t=o(e);if(n[0]="",null===t)return;n[0]="object"===t?`${e.text}`:`${e}`}})),n.length=1}else{let g=p[s]({data:n,...h,full:r}),b=o(g);switch(n.forEach(((e,t)=>n.splice(t,1))),n.length=0,b){case"primitive":n[0]=g;break;case"array":n.push(...g)}}}}))}if(i instanceof Array&&1===i.length&&i[0]instanceof Array&&(i=i[0]),null==i[0])return;let d=o(i[0]),y=i[0];switch(d){case"primitive":if(null==y)return;u[s]=y;break;case"object":if(null==y.text)return;u[s]=y.text;break;case"array":const e=o(y[0]);u[s]="object"===e?y.map((e=>e.text)).join(""):y.join("")}}})),y?v.push(f.map((e=>u[e.index])).join("<~>")):v.push(u.join(""))})),"array"===E?v:i?i.reduce(((e,t)=>"function"!=typeof t?e:t(e,j)),v.join("")):v.join("")))}return i?[!0,b]:b}}const i={default:{}};return{build:s,get:function(e){if(!(e instanceof Array))return function(){return'Error: Argument "location" is a string. Should be an array. E.g. ["templateName", "storageName"].'};const[t,r="default"]=e;return i[r]?i[r][t]?i[r][t]:function(){return`Error: Template "${t}" does not exist in storage "${r}".`}:function(){return`Error: Storage "${r}" does not exist.`}},add:function(e,t,...r){const[n,a="default"]=e;if(null==t)return void console.warn(`Warning: Template ${a}/${n} is not added to storage. The template is null.`);let o=t,l=!0;if(i[a]||(i[a]={}),"function"!=typeof t){let e=s(t,!0,...r);l=e[0],o=e[1]}l?i[a][n]=o:console.error(`Error: Template "${n}" looks broken and is not added to storage.`)},list:function(e=["default"]){return e.map((e=>i[e]?Object.keys(i[e]):[])).flat()},clear:function(){Object.keys(i).forEach((e=>{"default"!=e?delete i[e]:i.default={}}))},remove:function(e){const[t,r="default"]=e;return i[r]?i[r][t]?void delete i[r][t]:`Error: Template "${t}" does not exist in storage "${r}".`:`Error: Storage "${r}" does not exist.`}}}));
package/morph.png ADDED
Binary file
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@peter.naydenov/morph",
3
3
  "description": "Template engine",
4
- "version": "3.1.4",
4
+ "version": "3.2.0",
5
5
  "license": "MIT",
6
6
  "author": "Peter Naydenov",
7
7
  "main": "./src/main.js",
8
8
  "type": "module",
9
- "types": "./types/index.d.ts",
9
+ "types": "./dist/main.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
12
  "import": "./src/main.js",
@@ -20,20 +20,23 @@
20
20
  "scripts": {
21
21
  "test": "mocha test",
22
22
  "cover": "c8 mocha",
23
- "build": "rollup -c"
23
+ "build": "npm run build:types && rollup -c",
24
+ "build:types": "tsc"
24
25
  },
25
26
  "dependencies": {
26
27
  "@peter.naydenov/stack": "^3.0.0",
27
28
  "@peter.naydenov/walk": "^5.0.2"
28
29
  },
29
30
  "devDependencies": {
30
- "@rollup/plugin-commonjs": "^28.0.6",
31
+ "@rollup/plugin-commonjs": "^29.0.0",
31
32
  "@rollup/plugin-node-resolve": "^16.0.3",
32
33
  "@rollup/plugin-terser": "^0.4.4",
33
34
  "c8": "^10.1.3",
34
- "chai": "6.2.0",
35
- "mocha": "11.7.4",
36
- "rollup": "^4.52.4"
35
+ "chai": "6.2.1",
36
+ "esm": "^3.2.25",
37
+ "mocha": "11.7.5",
38
+ "rollup": "^4.53.3",
39
+ "typescript": "^5.9.3"
37
40
  },
38
41
  "repository": {
39
42
  "type": "git",
package/simple.js ADDED
@@ -0,0 +1,17 @@
1
+ import morph from './src/main.js'
2
+
3
+ const template = morph.build({
4
+ template: 'Hello {{name}}!',
5
+ helpers: {},
6
+ handshake: { name: 'World' }
7
+ });
8
+
9
+ const extensions = {
10
+ placeholders: { 0: '{{ greeting }}' }
11
+ };
12
+
13
+ const newTemplate = template('set', extensions);
14
+
15
+ const result = newTemplate('render', { greeting: 'Hi' });
16
+
17
+ console.log(result);
@@ -0,0 +1,35 @@
1
+ # Specification Quality Checklist: Extend Templates
2
+
3
+ **Purpose**: Validate specification completeness and quality before proceeding to planning
4
+ **Created**: 2025-11-30
5
+ **Feature**: specs/001-extend-templates/spec.md
6
+
7
+ ## Content Quality
8
+
9
+ - [x] No implementation details (languages, frameworks, APIs)
10
+ - [x] Focused on user value and business needs
11
+ - [x] Written for non-technical stakeholders
12
+ - [x] All mandatory sections completed
13
+
14
+ ## Requirement Completeness
15
+
16
+ - [x] No [NEEDS CLARIFICATION] markers remain
17
+ - [x] Requirements are testable and unambiguous
18
+ - [x] Success criteria are measurable
19
+ - [x] Success criteria are technology-agnostic (no implementation details)
20
+ - [x] All acceptance scenarios are defined
21
+ - [x] Edge cases are identified
22
+ - [x] Scope is clearly bounded
23
+ - [x] Dependencies and assumptions identified
24
+
25
+ ## Feature Readiness
26
+
27
+ - [x] All functional requirements have clear acceptance criteria
28
+ - [x] User scenarios cover primary flows
29
+ - [x] Feature meets measurable outcomes defined in Success Criteria
30
+ - [x] No implementation details leak into specification
31
+
32
+ ## Notes
33
+
34
+ All items pass. Specification is ready for planning phase.</content>
35
+ <parameter name="filePath">specs/001-extend-templates/checklists/requirements.md
@@ -0,0 +1,39 @@
1
+ {
2
+ "method": "set",
3
+ "description": "Extend a template with new placeholders, helpers, and handshake data",
4
+ "parameters": {
5
+ "placeholders": {
6
+ "type": "object",
7
+ "description": "Object with keys as placeholder identifiers (string/number) and values as new placeholder definitions (string)",
8
+ "example": {
9
+ "0": "{{ newField: action }}",
10
+ "header": "{{ title: formatTitle }}"
11
+ }
12
+ },
13
+ "helpers": {
14
+ "type": "object",
15
+ "description": "Object with helper names and implementations (functions or strings)",
16
+ "example": {
17
+ "formatTitle": "function(data) { return data.toUpperCase(); }",
18
+ "action": "function(data) { return '<div>' + data + '</div>'; }"
19
+ }
20
+ },
21
+ "handshake": {
22
+ "type": "object",
23
+ "description": "Object with demo data keys and values",
24
+ "example": {
25
+ "title": "Example Title",
26
+ "newField": "example data"
27
+ }
28
+ }
29
+ },
30
+ "returns": {
31
+ "type": "function",
32
+ "description": "New template function with extensions applied"
33
+ },
34
+ "errors": {
35
+ "invalidPlaceholder": "If placeholder definition is malformed",
36
+ "invalidHelper": "If helper is not function or string"
37
+ }
38
+ }</content>
39
+ <parameter name="filePath">specs/001-extend-templates/contracts/set-method.json
@@ -0,0 +1,76 @@
1
+ # Data Model: Extend Templates
2
+
3
+ **Feature**: Extend Templates
4
+ **Date**: 2025-11-30
5
+
6
+ ## Entities
7
+
8
+ ### Template
9
+ Core entity representing a Morph template.
10
+
11
+ **Fields**:
12
+ - `template`: string - The template string with placeholders
13
+ - `helpers`: object - Key-value pairs of helper functions or sub-templates
14
+ - `handshake`: object - Key-value pairs of demo data
15
+
16
+ **Relationships**:
17
+ - Contains multiple Placeholders (derived from template string)
18
+ - Contains multiple Helpers
19
+ - Contains Handshake data
20
+
21
+ **Validation Rules**:
22
+ - `template` must be non-empty string
23
+ - `helpers` must be object or undefined
24
+ - `handshake` must be object or undefined
25
+
26
+ ### Placeholder
27
+ Represents a dynamic part in the template.
28
+
29
+ **Fields**:
30
+ - `key`: string|number - Position (number) or name (string) identifier
31
+ - `definition`: string - The placeholder syntax (e.g., "{{ name: action }}")
32
+
33
+ **Relationships**:
34
+ - Belongs to Template
35
+
36
+ **Validation Rules**:
37
+ - `key` must be string or number
38
+ - `definition` must be valid template syntax
39
+
40
+ ### Helper
41
+ Represents a helper function or sub-template.
42
+
43
+ **Fields**:
44
+ - `name`: string - Helper identifier
45
+ - `value`: function|string - The helper implementation
46
+
47
+ **Relationships**:
48
+ - Belongs to Template
49
+
50
+ **Validation Rules**:
51
+ - `name` must be non-empty string
52
+ - `value` must be function or string
53
+
54
+ ### Handshake
55
+ Represents demo data for testing.
56
+
57
+ **Fields**:
58
+ - `key`: string - Data identifier
59
+ - `value`: any - The demo data
60
+
61
+ **Relationships**:
62
+ - Belongs to Template
63
+
64
+ **Validation Rules**:
65
+ - `key` must be non-empty string
66
+ - `value` can be any type
67
+
68
+ ## State Transitions
69
+
70
+ N/A - Static data structures, no lifecycle states.
71
+
72
+ ## Data Flow
73
+
74
+ 1. Original Template → 'set' command → Extended Template
75
+ 2. Extensions merged: placeholders replace, helpers merge (new overwrite old), handshake merge</content>
76
+ <parameter name="filePath">specs/001-extend-templates/data-model.md
@@ -0,0 +1,90 @@
1
+ # Implementation Plan: Extend Templates
2
+
3
+ **Branch**: `001-extend-templates` | **Date**: 2025-11-30 | **Spec**: specs/001-extend-templates/spec.md
4
+ **Input**: Feature specification from /specs/001-extend-templates/spec.md
5
+
6
+ **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
7
+
8
+ ## Summary
9
+
10
+ Extend Morph templates with 'set' command to modify placeholders, helpers, handshake without altering originals. Technical approach: Add 'set' method to template functions, merge extensions with originals, remove routing actions.
11
+
12
+ ## Technical Context
13
+
14
+ **Language/Version**: JavaScript (ES6+)
15
+
16
+ **Primary Dependencies**: None (existing library)
17
+
18
+ **Storage**: N/A
19
+
20
+ **Testing**: Existing test suite
21
+
22
+ **Target Platform**: Browser and Node.js
23
+
24
+ **Project Type**: Library
25
+
26
+ **Performance Goals**: Template extensions created and tested in under 5 minutes
27
+
28
+ **Constraints**: Maintain backward compatibility, no breaking changes
29
+
30
+ **Scale/Scope**: Small feature addition to existing library
31
+
32
+ ## Constitution Check
33
+
34
+ *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
35
+
36
+ - I. Logic-Less Templating: PASS - Extensions maintain logic-less syntax.
37
+ - II. Builtin Storage: PASS - No impact.
38
+ - III. Action System (NON-NEGOTIABLE): PASS - 'set' is a command, not action; removing inconsistent routing actions improves system.
39
+ - IV. Demo and Handshake: PASS - Extending handshake aligns.
40
+ - V. Extensibility: PASS - Adds extensibility via 'set'.
41
+ - VI. Debuggability: PASS - Empty placeholders support partial render principle.
42
+
43
+ ## Project Structure
44
+
45
+ ### Documentation (this feature)
46
+
47
+ ```text
48
+ specs/[###-feature]/
49
+ ├── plan.md # This file (/speckit.plan command output)
50
+ ├── research.md # Phase 0 output (/speckit.plan command)
51
+ ├── data-model.md # Phase 1 output (/speckit.plan command)
52
+ ├── quickstart.md # Phase 1 output (/speckit.plan command)
53
+ ├── contracts/ # Phase 1 output (/speckit.plan command)
54
+ └── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
55
+ ```
56
+
57
+ ### Source Code (repository root)
58
+
59
+ ```text
60
+ src/
61
+ ├── methods/
62
+ │ ├── _actionSupply.js
63
+ │ ├── _chopTemplates.js
64
+ │ ├── _defineData.js
65
+ │ ├── _defineType.js
66
+ │ ├── _readTemplate.js
67
+ │ ├── _renderHolder.js
68
+ │ ├── _setupActions.js
69
+ │ ├── build.js
70
+ │ ├── render.js
71
+ │ └── settings.js
72
+ ├── main.js
73
+ └── ...
74
+
75
+ tests/
76
+ ├── 01-chop.test.js
77
+ ├── 02-build.test.js
78
+ ├── 03-storage.test.js
79
+ ├── 04-commands.js
80
+ ├── 05-data.test.js
81
+ ├── 06-deps-injection.js
82
+ ├── 07-errors.test.js
83
+ └── ...
84
+ ```
85
+
86
+ **Structure Decision**: Single project library structure. Source in src/, tests in test/. Feature will modify build.js and render.js primarily.
87
+
88
+ ## Complexity Tracking
89
+
90
+ No constitution violations to justify.
@@ -0,0 +1,56 @@
1
+ # Quickstart: Extend Templates
2
+
3
+ **Feature**: Extend Templates
4
+ **Date**: 2025-11-30
5
+
6
+ ## Overview
7
+
8
+ The `set` command allows you to create extended versions of existing templates by modifying placeholders, adding helpers, and providing alternative handshake data.
9
+
10
+ ## Basic Usage
11
+
12
+ ```javascript
13
+ import morph from '@peter.naydenov/morph';
14
+
15
+ // Create a base template
16
+ const baseTemplate = morph.build({
17
+ template: `Hello {{ name }}!`,
18
+ helpers: {},
19
+ handshake: { name: 'World' }
20
+ });
21
+
22
+ // Extend with new placeholder and helper
23
+ const extendedTemplate = baseTemplate('set', {
24
+ placeholders: {
25
+ 0: '{{ greeting: formatGreeting }}' // Replace position 0
26
+ },
27
+ helpers: {
28
+ formatGreeting: (data) => data.toUpperCase()
29
+ },
30
+ handshake: {
31
+ greeting: 'Hello'
32
+ }
33
+ });
34
+
35
+ // Render the extended template
36
+ const result = extendedTemplate('render', { greeting: 'Hi' });
37
+ console.log(result); // "HI!"
38
+ ```
39
+
40
+ ## Key Points
41
+
42
+ - `set` creates a new template function, leaving the original unchanged
43
+ - Placeholders can be identified by position (number) or name (string)
44
+ - New helpers overwrite existing ones with the same name
45
+ - Handshake data is merged
46
+ - Empty placeholders `{{}}` remain untouched if no data provided
47
+
48
+ ## Error Handling
49
+
50
+ If a required helper is missing during render:
51
+
52
+ ```javascript
53
+ // Will return: "Helper 'missingHelper' is not available"
54
+ const result = template('render', { data: 'value' });
55
+ ```</content>
56
+ <parameter name="filePath">specs/001-extend-templates/quickstart.md
@@ -0,0 +1,18 @@
1
+ # Research: Extend Templates
2
+
3
+ **Feature**: Extend Templates
4
+ **Date**: 2025-11-30
5
+
6
+ No unknowns identified in technical context. All implementation details known from existing Morph library codebase.
7
+
8
+ - Language/Version: JavaScript (ES6+) - already in use
9
+ - Dependencies: None new - extending existing library
10
+ - Storage: N/A - in-memory operations
11
+ - Testing: Existing test suite - will extend
12
+ - Platform: Browser/Node.js - already supported
13
+ - Performance: Under 5 minutes - developer time, no system perf impact
14
+ - Constraints: Backward compatibility - standard for library updates
15
+ - Scale: Small feature - fits within existing architecture
16
+
17
+ **Decision**: No research required, proceed to design phase.</content>
18
+ <parameter name="filePath">specs/001-extend-templates/research.md
@@ -0,0 +1,139 @@
1
+ # Feature Specification: Extend Templates
2
+
3
+ **Feature Branch**: `001-extend-templates`
4
+ **Created**: 2025-11-30
5
+ **Status**: Draft
6
+ **Input**: User description: "- Empty placeholders - {{}}
7
+ - Extend templates: Change of placeholder definitions, add/overwrite helpers, provide alternative handshake data. template ('set',{ plachholders:{nameOrPosition: newPlaceholderDefinition}, helpers: { helpers}, handshake:{ new Handshake}},). Result is a new function(template), ready to receive data;
8
+ - Building templates - will not have 'fail' response, because we could build templates without having all helpers available. They can come latter.
9
+ - 'Set' creates a new template. Current template is not changed;
10
+ - What if we trying to render a template that don't have a helper? - it will return error as a render result. Will explain that renderFunction isn't available;
11
+ - Routing actions will be removed. Too unconsistant with other actions. It's can be solved with normal render functions by providing a list of possible templates. It's something that is possible even now;
12
+ - Update 'tests' according new features;
13
+ - Check jsdoc and update it according code changes. Extend jsdoc where possible. Update .d.ts type definitions; "
14
+
15
+ ## Out of Scope
16
+
17
+ - Modifying the original template in place; 'set' always creates a new template.
18
+ - Adding new action types to the system beyond 'set' and existing ones.
19
+
20
+ ## Clarifications
21
+
22
+ ### Session 2025-11-30
23
+
24
+ - Q: What should happen with empty placeholders {{}}? → A: Leave empty placeholders {{}} untouched in output when no data is provided.
25
+ - Q: What should be the exact error message format when a helper is missing? → A: Helper '<name>' is not available
26
+ - Q: How are placeholders identified in the 'set' command? → A: By position (number) or name (string); new placeholders replace old ones at that position/name.
27
+
28
+ ## User Scenarios & Testing *(mandatory)*
29
+
30
+ ### User Story 1 - Create Template Variants (Priority: P1)
31
+
32
+ As a developer using the Morph library, I want to modify placeholder definitions in an existing template so I can create variants for different data structures without duplicating template code.
33
+
34
+ **Why this priority**: Enables template reuse and reduces code duplication.
35
+
36
+ **Independent Test**: Can be tested by creating a base template, extending it with modified placeholders, and rendering with different data structures.
37
+
38
+ **Acceptance Scenarios**:
39
+
40
+ 1. **Given** a template with placeholders, **When** I use 'set' command to change placeholder definitions, **Then** the new template renders with modified behavior.
41
+ 2. **Given** a template, **When** I extend it with new placeholders, **Then** the new template includes both original and new placeholders.
42
+
43
+ ---
44
+
45
+ ### User Story 2 - Add/Overwrite Helpers (Priority: P1)
46
+
47
+ As a developer, I want to add new helpers or overwrite existing ones in a template so I can extend functionality dynamically.
48
+
49
+ **Why this priority**: Allows dynamic extension of template capabilities. Mixing templates with different sets of helpers. Extend templates without breaking existing functionality and minimal adiditional code.
50
+
51
+ **Independent Test**: Can be tested by extending a template with new helpers and verifying they are used in rendering.
52
+
53
+ **Acceptance Scenarios**:
54
+
55
+ 1. **Given** a template with helpers, **When** I use 'set' to add new helpers, **Then** the new template includes the additional helpers.
56
+ 2. **Given** a template, **When** I overwrite an existing helper, **Then** the new template uses the updated helper.
57
+
58
+ ---
59
+
60
+ ### User Story 3 - Provide Alternative Handshake Data (Priority: P2)
61
+
62
+ As a developer, I want to provide alternative demo data for a template so I can test different scenarios. Helpfull for testing component behavior with different data.
63
+
64
+ **Why this priority**: Improves testing and prototyping capabilities.
65
+
66
+ **Independent Test**: Can be tested by extending a template with new handshake data and rendering in demo mode.
67
+
68
+ **Acceptance Scenarios**:
69
+
70
+ 1. **Given** a template with handshake data, **When** I use 'set' to provide new handshake, **Then** demo rendering uses the new data.
71
+
72
+ ---
73
+
74
+ ### User Story 4 - Handle Missing Helpers Gracefully (Priority: P2)
75
+
76
+ As a developer, I want clear error messages when trying to render a template with missing helpers so I can debug issues easily.
77
+
78
+ **Why this priority**: Improves developer experience and debugging.
79
+
80
+ **Independent Test**: Can be tested by attempting to render with missing helpers and verifying error message.
81
+
82
+ **Acceptance Scenarios**:
83
+
84
+ 1. **Given** a template requiring a helper, **When** I render without the helper, **Then** I receive an error explaining the missing function.
85
+
86
+ ---
87
+
88
+ ### User Story 5 - Remove Routing Actions (Priority: P3)
89
+
90
+ As a developer, I want routing actions removed so I can use consistent action patterns with normal render functions.
91
+
92
+ **Why this priority**: Simplifies the action system and removes inconsistency.
93
+
94
+ **Independent Test**: Can be tested by verifying routing actions are no longer available and alternatives work.
95
+
96
+ **Acceptance Scenarios**:
97
+
98
+ 1. **Given** the library, **When** I check available actions, **Then** routing actions are not present.
99
+
100
+ ---
101
+
102
+ ## Edge Cases
103
+
104
+ - Rendering with empty placeholders {{}} should produce {{}} - no touched placeholder, as when data is not provided;
105
+ - Building templates with partial helpers should succeed. If helpers are not available we getting an error as a render result.
106
+ - Extending templates with invalid placeholder definitions should fail gracefully.
107
+
108
+
109
+
110
+ ## Requirements *(mandatory)*
111
+
112
+ ### Functional Requirements
113
+
114
+ - **FR-001**: System MUST support empty placeholders {{}} that remain untouched when no data is provided.
115
+ - **FR-002**: System MUST support 'set' command for extending templates with new placeholders, helpers, and handshake data. Placeholders identified by position (number) or name (string); new placeholders replace existing ones. Helpers from original template preserved, new helpers can overwrite. Handshake data merged similarly.
116
+ - **FR-003**: System MUST return a new template function from 'set' command that incorporates existing elements with the extensions. Where extensions have higher priority.
117
+ - **FR-004**: System MUST allow building templates without all required helpers present.
118
+ - **FR-005**: System MUST return error messages in the format "Helper '<name>' is not available" when rendering with missing helpers (can list multiple if applicable).
119
+ - **FR-006**: System MUST remove routing actions from the action system.
120
+ - **FR-007**: Tests MUST be updated to cover new template extension features.
121
+ - **FR-008**: JSDoc comments MUST be updated and extended according to code changes.
122
+ - **FR-009**: TypeScript .d.ts definitions MUST be updated to reflect new features.
123
+
124
+ ### Key Entities *(include if feature involves data)*
125
+
126
+ - **Template**: The core template object with placeholders, helpers, and handshake.
127
+ - **Helper**: Functions or sub-templates used in actions.
128
+ - **Handshake**: Demo data for testing templates.
129
+ - **Placeholder**: Dynamic parts of the template defined by {{ }}.
130
+
131
+ ## Success Criteria *(mandatory)*
132
+
133
+ ### Measurable Outcomes
134
+
135
+ - **SC-001**: Developers can create and test template extensions in under 5 minutes.
136
+ - **SC-002**: 100% of template extension operations complete without errors.
137
+ - **SC-003**: Error messages for missing helpers are clear and include the helper name.
138
+ - **SC-004**: All routing actions are successfully removed without breaking existing functionality.
139
+ - **SC-005**: Test coverage for new features reaches 90%.