@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.
- package/.mocharc.json +1 -0
- package/.opencode/command/speckit.analyze.md +184 -0
- package/.opencode/command/speckit.checklist.md +294 -0
- package/.opencode/command/speckit.clarify.md +181 -0
- package/.opencode/command/speckit.constitution.md +82 -0
- package/.opencode/command/speckit.implement.md +135 -0
- package/.opencode/command/speckit.plan.md +89 -0
- package/.opencode/command/speckit.specify.md +257 -0
- package/.opencode/command/speckit.tasks.md +137 -0
- package/.opencode/command/speckit.taskstoissues.md +28 -0
- package/.specify/memory/constitution.md +43 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +305 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +790 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/Changelog.md +17 -0
- package/README.md +154 -3
- package/dist/main.d.ts +100 -0
- package/dist/methods/_actionSupply.d.ts +18 -0
- package/dist/methods/_chopTemplates.d.ts +22 -0
- package/dist/methods/_defineData.d.ts +25 -0
- package/dist/methods/_defineType.d.ts +15 -0
- package/dist/methods/_readTemplate.d.ts +24 -0
- package/dist/methods/_renderHolder.d.ts +17 -0
- package/dist/methods/_setupActions.d.ts +19 -0
- package/dist/methods/build.d.ts +35 -0
- package/dist/methods/render.d.ts +25 -0
- package/dist/methods/settings.d.ts +7 -0
- package/dist/morph.cjs +1 -1
- package/dist/morph.esm.mjs +1 -1
- package/dist/morph.umd.js +1 -1
- package/morph.png +0 -0
- package/package.json +10 -7
- package/simple.js +17 -0
- package/specs/001-extend-templates/checklists/requirements.md +35 -0
- package/specs/001-extend-templates/contracts/set-method.json +39 -0
- package/specs/001-extend-templates/data-model.md +76 -0
- package/specs/001-extend-templates/plan.md +90 -0
- package/specs/001-extend-templates/quickstart.md +56 -0
- package/specs/001-extend-templates/research.md +18 -0
- package/specs/001-extend-templates/spec.md +139 -0
- package/specs/001-extend-templates/tasks.md +255 -0
- package/src/main.js +70 -32
- package/src/methods/_actionSupply.js +16 -0
- package/src/methods/_chopTemplates.js +22 -1
- package/src/methods/_defineData.js +33 -1
- package/src/methods/_defineType.js +13 -0
- package/src/methods/_readTemplate.js +46 -46
- package/src/methods/_renderHolder.js +15 -0
- package/src/methods/_setupActions.js +18 -8
- package/src/methods/build.js +98 -43
- package/src/methods/render.js +32 -11
- package/src/methods/settings.js +11 -0
- package/tsconfig.json +27 -0
- 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.
|
|
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": "./
|
|
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": "^
|
|
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.
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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%.
|