diff --git a/.eslintignore b/.eslintignore index 7773828..47c8315 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -dist/ \ No newline at end of file +dist/ +test/ diff --git a/README.md b/README.md index ce8d0a1..f193bfa 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,25 @@ let {pathData, minX, minY, width, height} = getSVGData(lsParams); An object returned by `getSVGData` contains [path data](https://www.w3.org/TR/SVG11/paths.html#PathData) needed to draw the L-system, and also the drawing boundaries that are essential for the `viewBox` attribute. +In case of invalid input L-system parameters, the methods throw a custom exception. You may use it to get a detailed explanation of which parameter(s) failed to pass validation, and format the message as you wish. + +```javascript +let {getSVGCode} = require("lindsvg"); +let yaml = require("js-yaml"); + +try { + console.log(getSVGCode(lsParams, svgParams)); +} catch (error) { + // Log the original message + console.error(error); + if (error.name === "LSError") { + // Get a JSON representation of the error list and format it as YAML + let errorJSON = error.toJSON(); + console.log(yaml.dump(errorJSON, {indent: 4})); + } +} +``` + ### Compatibility note lindsvg utilizes the ECMAScript 2018 syntax. If you want to use the module in environments that do not support ES 2018, please transpile the sources with babel or whatever for your needs. diff --git a/dist/lindsvg.esm.js b/dist/lindsvg.esm.js index 08ac793..e37e8f6 100644 --- a/dist/lindsvg.esm.js +++ b/dist/lindsvg.esm.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.0.0 +lindsvg v1.1.0 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ @@ -25,17 +25,17 @@ function checkRule(rule, msg = messages.RULE) { } function checkRules(rules, letterMsg, ruleMsg) { - let errors = new Map(); + let errors = Object.create(null); Object.entries(rules).forEach(([letter, rule]) => { let result = checkLetter(letter, letterMsg); if (result === true) { result = checkRule(rule, ruleMsg); } if (result !== true) { - errors.set(letter, result); + errors[letter] = result; } }); - return errors.size ? errors : true; + return Object.keys(errors).length ? errors : true; } function checkStep(step, msg = messages.STEP) { @@ -51,7 +51,7 @@ function checkAngle(angle, msg = messages.NUMBER) { } function validate(lsParams) { - let errors = new Map(); + let errors = Object.create(null); Object.entries(lsParams).forEach(([param, value]) => { let result = true; switch (param) { @@ -73,21 +73,42 @@ function validate(lsParams) { break; } if (result !== true) { - errors.set(param, result); + errors[param] = result; } }); - return errors.size ? errors : true; + return Object.keys(errors).length ? errors : true; } -function formatErrors(errors) { - return [...errors].reduce((accumulator, [param, error]) => { - if (error instanceof Map) { - return `${accumulator}\n${param}:${formatErrors(error).replace(/\n/g, "\n ")}`; - } - return `${accumulator}\n${param}: ${error}`; - }, ""); +class LSError extends Error { + /** + * LSError constructor + * @param {Object} errors - Error map “parameter->message(s)” + * @constructor + */ + constructor(errors) { + let message = JSON.stringify(errors, null, 2); + super(message); + // Using JSON.parse for deep cloning + Object.defineProperty(this, "lsErrors", {value: JSON.parse(message)}); + } + + /** + * Get raw object representation of the errors + * @return {Object} + */ + toJSON() { + return JSON.parse(JSON.stringify(this.lsErrors)); + } } +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString +Object.defineProperty(LSError.prototype, "name", { + configurable: true, + enumerable: false, + writable: true, + value: "LSError" +}); + /** @type {Rules} */ let ctrlRules = { F: "", @@ -114,7 +135,7 @@ let defaults = { function generate(lsParams) { let validity = validate(lsParams); if (validity !== true) { - throw new Error(formatErrors(validity)); + throw new LSError(validity); } let {axiom: code, iterations} = {...defaults, ...lsParams}; let rules = {...ctrlRules, ...lsParams.rules}; diff --git a/dist/lindsvg.esm.min.js b/dist/lindsvg.esm.min.js index 6924e67..0c24bfb 100644 --- a/dist/lindsvg.esm.min.js +++ b/dist/lindsvg.esm.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.0.0 +lindsvg v1.1.0 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ -let t={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},e=/^[A-Z]$/;let a=/^[A-Z+\-[\]]*$/;function i(e,i=t.RULE){return a.test(e)||i}function n(a,n,r){let s=new Map;return Object.entries(a).forEach(([a,h])=>{let c=function(a,i=t.LETTER){return e.test(a)||i}(a,n);!0===c&&(c=i(h,r)),!0!==c&&s.set(a,c)}),!s.size||s}function r(e){let a=new Map;return Object.entries(e).forEach(([e,r])=>{let s=!0;switch(e){case"axiom":s=i(r,t.AXIOM);break;case"rules":s=n(r);break;case"alpha":case"theta":s=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(r,t[e.toUpperCase()]);break;case"step":s=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(r);break;case"iterations":s=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(r)}!0!==s&&a.set(e,s)}),!a.size||a}let s={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},h={alpha:0,theta:0,step:10,iterations:3};function c(t){let e=r(t);if(!0!==e)throw new Error(function t(e){return[...e].reduce((e,[a,i])=>i instanceof Map?`${e}\n${a}:${t(i).replace(/\n/g,"\n ")}`:`${e}\n${a}: ${i}`,"")}(e));let{axiom:a,iterations:i}={...h,...t},n={...s,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(n[e]||""),"");return a}let o={translate(){this.x+=this.step*Math.cos(this.alpha),this.y+=this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)},rotate(t){this.alpha+=t*this.theta},pushStack(){this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(){({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())},getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}};function l(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function u(t){let e=c(t),a=function({x:t,y:e,step:a,alpha:i,theta:n}){let r=Object.create(o);return r.stack=[],r.x=r.minX=r.maxX=t,r.y=r.minY=r.maxY=e,r.step=a,r.alpha=-i,r.theta=n,r}({x:0,y:0,...t});return{pathData:function(t,e){let a;return[...t].reduce((t,i)=>{switch(i){case"F":e.translate(),t+=("L"===a?" ":"L")+l(e.x,e.y),a="L";break;case"B":e.translate(),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+l(e.x,e.y),a="M";break;case"+":e.rotate(1);break;case"-":e.rotate(-1);break;case"[":e.pushStack();break;case"]":e.popStack(),t+=`M${l(e.x,e.y)}`,a="M"}return t},"M"+l(e.x,e.y))}(function(t){return t.replace(/[^FB[\]+-]/g,"")}(e),a),...a.getDrawingRect()}}function p(t,e){let{pathData:a,minX:i,minY:n,width:r,height:s}=u(t);e={width:r,height:s,padding:0,fill:"none",stroke:"#000",...e};let{padding:h}=e;return``}export{p as getSVGCode,u as getSVGData}; +let t={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},e=/^[A-Z]$/;let a=/^[A-Z+\-[\]]*$/;function i(e,i=t.RULE){return a.test(e)||i}function r(a,r,n){let s=Object.create(null);return Object.entries(a).forEach(([a,h])=>{let l=function(a,i=t.LETTER){return e.test(a)||i}(a,r);!0===l&&(l=i(h,n)),!0!==l&&(s[a]=l)}),!Object.keys(s).length||s}function n(e){let a=Object.create(null);return Object.entries(e).forEach(([e,n])=>{let s=!0;switch(e){case"axiom":s=i(n,t.AXIOM);break;case"rules":s=r(n);break;case"alpha":case"theta":s=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(n,t[e.toUpperCase()]);break;case"step":s=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(n);break;case"iterations":s=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(n)}!0!==s&&(a[e]=s)}),!Object.keys(a).length||a}class s extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(s.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let h={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},l={alpha:0,theta:0,step:10,iterations:3};let o={translate(){this.x+=this.step*Math.cos(this.alpha),this.y+=this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)},rotate(t){this.alpha+=t*this.theta},pushStack(){this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(){({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())},getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}};function c(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function u(t){let e=function(t){let e=n(t);if(!0!==e)throw new s(e);let{axiom:a,iterations:i}={...l,...t},r={...h,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(r[e]||""),"");return a}(t),a=function({x:t,y:e,step:a,alpha:i,theta:r}){let n=Object.create(o);return n.stack=[],n.x=n.minX=n.maxX=t,n.y=n.minY=n.maxY=e,n.step=a,n.alpha=-i,n.theta=r,n}({x:0,y:0,...t});return{pathData:function(t,e){let a;return[...t].reduce((t,i)=>{switch(i){case"F":e.translate(),t+=("L"===a?" ":"L")+c(e.x,e.y),a="L";break;case"B":e.translate(),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+c(e.x,e.y),a="M";break;case"+":e.rotate(1);break;case"-":e.rotate(-1);break;case"[":e.pushStack();break;case"]":e.popStack(),t+=`M${c(e.x,e.y)}`,a="M"}return t},"M"+c(e.x,e.y))}(function(t){return t.replace(/[^FB[\]+-]/g,"")}(e),a),...a.getDrawingRect()}}function p(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=u(t);e={width:n,height:s,padding:0,fill:"none",stroke:"#000",...e};let{padding:h}=e;return``}export{p as getSVGCode,u as getSVGData}; diff --git a/dist/lindsvg.js b/dist/lindsvg.js index 2c1f7a7..17b216b 100644 --- a/dist/lindsvg.js +++ b/dist/lindsvg.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.0.0 +lindsvg v1.1.0 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ @@ -31,17 +31,17 @@ https://amphiluke.github.io/l-systems/ } function checkRules(rules, letterMsg, ruleMsg) { - let errors = new Map(); + let errors = Object.create(null); Object.entries(rules).forEach(([letter, rule]) => { let result = checkLetter(letter, letterMsg); if (result === true) { result = checkRule(rule, ruleMsg); } if (result !== true) { - errors.set(letter, result); + errors[letter] = result; } }); - return errors.size ? errors : true; + return Object.keys(errors).length ? errors : true; } function checkStep(step, msg = messages.STEP) { @@ -57,7 +57,7 @@ https://amphiluke.github.io/l-systems/ } function validate(lsParams) { - let errors = new Map(); + let errors = Object.create(null); Object.entries(lsParams).forEach(([param, value]) => { let result = true; switch (param) { @@ -79,21 +79,42 @@ https://amphiluke.github.io/l-systems/ break; } if (result !== true) { - errors.set(param, result); + errors[param] = result; } }); - return errors.size ? errors : true; + return Object.keys(errors).length ? errors : true; } - function formatErrors(errors) { - return [...errors].reduce((accumulator, [param, error]) => { - if (error instanceof Map) { - return `${accumulator}\n${param}:${formatErrors(error).replace(/\n/g, "\n ")}`; - } - return `${accumulator}\n${param}: ${error}`; - }, ""); + class LSError extends Error { + /** + * LSError constructor + * @param {Object} errors - Error map “parameter->message(s)” + * @constructor + */ + constructor(errors) { + let message = JSON.stringify(errors, null, 2); + super(message); + // Using JSON.parse for deep cloning + Object.defineProperty(this, "lsErrors", {value: JSON.parse(message)}); + } + + /** + * Get raw object representation of the errors + * @return {Object} + */ + toJSON() { + return JSON.parse(JSON.stringify(this.lsErrors)); + } } + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString + Object.defineProperty(LSError.prototype, "name", { + configurable: true, + enumerable: false, + writable: true, + value: "LSError" + }); + /** @type {Rules} */ let ctrlRules = { F: "", @@ -120,7 +141,7 @@ https://amphiluke.github.io/l-systems/ function generate(lsParams) { let validity = validate(lsParams); if (validity !== true) { - throw new Error(formatErrors(validity)); + throw new LSError(validity); } let {axiom: code, iterations} = {...defaults, ...lsParams}; let rules = {...ctrlRules, ...lsParams.rules}; diff --git a/dist/lindsvg.min.js b/dist/lindsvg.min.js index 7283be8..c770b30 100644 --- a/dist/lindsvg.min.js +++ b/dist/lindsvg.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.0.0 +lindsvg v1.1.0 https://amphiluke.github.io/l-systems/ (c) 2020 Amphiluke */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).lindsvg={})}(this,(function(t){"use strict";let e={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},a=/^[A-Z]$/;let i=/^[A-Z+\-[\]]*$/;function n(t,a=e.RULE){return i.test(t)||a}function s(t,i,s){let r=new Map;return Object.entries(t).forEach(([t,h])=>{let o=function(t,i=e.LETTER){return a.test(t)||i}(t,i);!0===o&&(o=n(h,s)),!0!==o&&r.set(t,o)}),!r.size||r}function r(t){let a=new Map;return Object.entries(t).forEach(([t,i])=>{let r=!0;switch(t){case"axiom":r=n(i,e.AXIOM);break;case"rules":r=s(i);break;case"alpha":case"theta":r=function(t,a=e.NUMBER){return Number.isFinite(t)||a}(i,e[t.toUpperCase()]);break;case"step":r=function(t,a=e.STEP){return Number.isFinite(t)&&t>0||a}(i);break;case"iterations":r=function(t,a=e.COUNT){return Number.isInteger(t)&&t>0||a}(i)}!0!==r&&a.set(t,r)}),!a.size||a}let h={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},o={alpha:0,theta:0,step:10,iterations:3};function c(t){let e=r(t);if(!0!==e)throw new Error(function t(e){return[...e].reduce((e,[a,i])=>i instanceof Map?`${e}\n${a}:${t(i).replace(/\n/g,"\n ")}`:`${e}\n${a}: ${i}`,"")}(e));let{axiom:a,iterations:i}={...o,...t},n={...h,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(n[e]||""),"");return a}let l={translate(){this.x+=this.step*Math.cos(this.alpha),this.y+=this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)},rotate(t){this.alpha+=t*this.theta},pushStack(){this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(){({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())},getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}};function u(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function p(t){let e=c(t),a=function({x:t,y:e,step:a,alpha:i,theta:n}){let s=Object.create(l);return s.stack=[],s.x=s.minX=s.maxX=t,s.y=s.minY=s.maxY=e,s.step=a,s.alpha=-i,s.theta=n,s}({x:0,y:0,...t});return{pathData:function(t,e){let a;return[...t].reduce((t,i)=>{switch(i){case"F":e.translate(),t+=("L"===a?" ":"L")+u(e.x,e.y),a="L";break;case"B":e.translate(),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+u(e.x,e.y),a="M";break;case"+":e.rotate(1);break;case"-":e.rotate(-1);break;case"[":e.pushStack();break;case"]":e.popStack(),t+=`M${u(e.x,e.y)}`,a="M"}return t},"M"+u(e.x,e.y))}(function(t){return t.replace(/[^FB[\]+-]/g,"")}(e),a),...a.getDrawingRect()}}t.getSVGCode=function(t,e){let{pathData:a,minX:i,minY:n,width:s,height:r}=p(t);e={width:s,height:r,padding:0,fill:"none",stroke:"#000",...e};let{padding:h}=e;return``},t.getSVGData=p,Object.defineProperty(t,"__esModule",{value:!0})})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).lindsvg={})}(this,(function(t){"use strict";let e={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},a=/^[A-Z]$/;let i=/^[A-Z+\-[\]]*$/;function r(t,a=e.RULE){return i.test(t)||a}function n(t,i,n){let s=Object.create(null);return Object.entries(t).forEach(([t,h])=>{let o=function(t,i=e.LETTER){return a.test(t)||i}(t,i);!0===o&&(o=r(h,n)),!0!==o&&(s[t]=o)}),!Object.keys(s).length||s}function s(t){let a=Object.create(null);return Object.entries(t).forEach(([t,i])=>{let s=!0;switch(t){case"axiom":s=r(i,e.AXIOM);break;case"rules":s=n(i);break;case"alpha":case"theta":s=function(t,a=e.NUMBER){return Number.isFinite(t)||a}(i,e[t.toUpperCase()]);break;case"step":s=function(t,a=e.STEP){return Number.isFinite(t)&&t>0||a}(i);break;case"iterations":s=function(t,a=e.COUNT){return Number.isInteger(t)&&t>0||a}(i)}!0!==s&&(a[t]=s)}),!Object.keys(a).length||a}class h extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(h.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let o={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},l={alpha:0,theta:0,step:10,iterations:3};let c={translate(){this.x+=this.step*Math.cos(this.alpha),this.y+=this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)},rotate(t){this.alpha+=t*this.theta},pushStack(){this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(){({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())},getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}};function u(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function p(t){let e=function(t){let e=s(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...l,...t},r={...o,...t.rules};for(;i>0;i--)a=[...a].reduce((t,e)=>t+(r[e]||""),"");return a}(t),a=function({x:t,y:e,step:a,alpha:i,theta:r}){let n=Object.create(c);return n.stack=[],n.x=n.minX=n.maxX=t,n.y=n.minY=n.maxY=e,n.step=a,n.alpha=-i,n.theta=r,n}({x:0,y:0,...t});return{pathData:function(t,e){let a;return[...t].reduce((t,i)=>{switch(i){case"F":e.translate(),t+=("L"===a?" ":"L")+u(e.x,e.y),a="L";break;case"B":e.translate(),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+u(e.x,e.y),a="M";break;case"+":e.rotate(1);break;case"-":e.rotate(-1);break;case"[":e.pushStack();break;case"]":e.popStack(),t+=`M${u(e.x,e.y)}`,a="M"}return t},"M"+u(e.x,e.y))}(function(t){return t.replace(/[^FB[\]+-]/g,"")}(e),a),...a.getDrawingRect()}}t.getSVGCode=function(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=p(t);e={width:n,height:s,padding:0,fill:"none",stroke:"#000",...e};let{padding:h}=e;return``},t.getSVGData=p,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/package-lock.json b/package-lock.json index 89f91f4..db29e41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lindsvg", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -25,15 +25,15 @@ } }, "@types/estree": { - "version": "0.0.41", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.41.tgz", - "integrity": "sha512-rIAmXyJlqw4KEBO7+u9gxZZSQHaCNnIzYrnNmYVpgfJhxTqO0brCX0SYpqUTkVI5mwwUwzmtspLBGBKroMeynA==", + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.42.tgz", + "integrity": "sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ==", "dev": true }, "@types/node": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.4.tgz", - "integrity": "sha512-Lue/mlp2egZJoHXZr4LndxDAd7i/7SQYhV0EjWfb/a4/OZ6tuVwMCVPiwkU5nsEipxEf7hmkSU7Em5VQ8P5NGA==", + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz", + "integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg==", "dev": true }, "acorn": { @@ -815,9 +815,9 @@ } }, "rollup": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.28.0.tgz", - "integrity": "sha512-v2J/DmQi9+Nf6frGqzwZRvbiuTTrqH0yzoUF4Eybf8sONT4UpLZzJYnYzW96Zm9X1+4SJmijfnFBWCzHDAXYnQ==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.29.0.tgz", + "integrity": "sha512-V63Iz0dSdI5qPPN5HmCN6OBRzBFhMqNWcvwgq863JtSCTU6Vdvqq6S2fYle/dSCyoPrBkIP3EIr1RVs3HTRqqg==", "dev": true, "requires": { "@types/estree": "*", @@ -826,16 +826,16 @@ } }, "rollup-plugin-terser": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.1.3.tgz", - "integrity": "sha512-FuFuXE5QUJ7snyxHLPp/0LFXJhdomKlIx/aK7Tg88Yubsx/UU/lmInoJafXJ4jwVVNcORJ1wRUC5T9cy5yk0wA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.2.0.tgz", + "integrity": "sha512-jQI+nYhtDBc9HFRBz8iGttQg7li9klmzR62RG2W2nN6hJ/FI2K2ItYQ7kJ7/zn+vs+BP1AEccmVRjRN989I+Nw==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "jest-worker": "^24.6.0", - "rollup-pluginutils": "^2.8.1", + "@babel/code-frame": "^7.5.5", + "jest-worker": "^24.9.0", + "rollup-pluginutils": "^2.8.2", "serialize-javascript": "^2.1.2", - "terser": "^4.1.0" + "terser": "^4.6.2" } }, "rollup-pluginutils": { @@ -1037,9 +1037,9 @@ } }, "terser": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.5.1.tgz", - "integrity": "sha512-lH9zLIbX8PRBEFCTvfHGCy0s9HEKnNso1Dx9swSopF3VUnFLB8DpQ61tHxoofovNC/sG0spajJM3EIIRSTByiQ==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.2.tgz", + "integrity": "sha512-6FUjJdY2i3WZAtYBtnV06OOcOfzl+4hSKYE9wgac8rkLRBToPDDrBB2AcHwQD/OKDxbnvhVy2YgOPWO2SsKWqg==", "dev": true, "requires": { "commander": "^2.20.0", diff --git a/package.json b/package.json index 96715c8..e9ec982 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lindsvg", - "version": "1.0.0", + "version": "1.1.0", "description": "Lindenmayer System [Scalable] Vector Graphics", "main": "dist/lindsvg.js", "module": "dist/lindsvg.esm.js", @@ -29,8 +29,8 @@ "homepage": "https://amphiluke.github.io/l-systems/", "devDependencies": { "eslint": "^6.8.0", - "rollup": "^1.28.0", - "rollup-plugin-terser": "^5.1.3" + "rollup": "^1.29.0", + "rollup-plugin-terser": "^5.2.0" }, "engines": { "node": ">=8.3.0" diff --git a/src/generator.mjs b/src/generator.mjs index 6408bf8..0a8804e 100644 --- a/src/generator.mjs +++ b/src/generator.mjs @@ -1,4 +1,5 @@ -import {validate, formatErrors} from "./validator.mjs"; +import {validate} from "./validator.mjs"; +import {LSError} from "./ls-error.mjs"; /** @type {Rules} */ let ctrlRules = { @@ -26,7 +27,7 @@ let defaults = { export function generate(lsParams) { let validity = validate(lsParams); if (validity !== true) { - throw new Error(formatErrors(validity)); + throw new LSError(validity); } let {axiom: code, iterations} = {...defaults, ...lsParams}; let rules = {...ctrlRules, ...lsParams.rules}; diff --git a/src/ls-error.mjs b/src/ls-error.mjs new file mode 100644 index 0000000..9b9addb --- /dev/null +++ b/src/ls-error.mjs @@ -0,0 +1,31 @@ +class LSError extends Error { + /** + * LSError constructor + * @param {Object} errors - Error map “parameter->message(s)” + * @constructor + */ + constructor(errors) { + let message = JSON.stringify(errors, null, 2); + super(message); + // Using JSON.parse for deep cloning + Object.defineProperty(this, "lsErrors", {value: JSON.parse(message)}); + } + + /** + * Get raw object representation of the errors + * @return {Object} + */ + toJSON() { + return JSON.parse(JSON.stringify(this.lsErrors)); + } +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString +Object.defineProperty(LSError.prototype, "name", { + configurable: true, + enumerable: false, + writable: true, + value: "LSError" +}); + +export {LSError}; diff --git a/src/validator.mjs b/src/validator.mjs index fc51377..99edfec 100644 --- a/src/validator.mjs +++ b/src/validator.mjs @@ -20,17 +20,17 @@ export function checkRule(rule, msg = messages.RULE) { } export function checkRules(rules, letterMsg, ruleMsg) { - let errors = new Map(); + let errors = Object.create(null); Object.entries(rules).forEach(([letter, rule]) => { let result = checkLetter(letter, letterMsg); if (result === true) { result = checkRule(rule, ruleMsg); } if (result !== true) { - errors.set(letter, result); + errors[letter] = result; } }); - return errors.size ? errors : true; + return Object.keys(errors).length ? errors : true; } export function checkStep(step, msg = messages.STEP) { @@ -46,7 +46,7 @@ export function checkAngle(angle, msg = messages.NUMBER) { } export function validate(lsParams) { - let errors = new Map(); + let errors = Object.create(null); Object.entries(lsParams).forEach(([param, value]) => { let result = true; switch (param) { @@ -68,17 +68,8 @@ export function validate(lsParams) { break; } if (result !== true) { - errors.set(param, result); + errors[param] = result; } }); - return errors.size ? errors : true; -} - -export function formatErrors(errors) { - return [...errors].reduce((accumulator, [param, error]) => { - if (error instanceof Map) { - return `${accumulator}\n${param}:${formatErrors(error).replace(/\n/g, "\n ")}`; - } - return `${accumulator}\n${param}: ${error}`; - }, ""); + return Object.keys(errors).length ? errors : true; } diff --git a/test/browser-esm-test.html b/test/browser-esm-test.html index bd15e24..20d3908 100644 --- a/test/browser-esm-test.html +++ b/test/browser-esm-test.html @@ -15,11 +15,25 @@
+