From 3f02e2ec21ee5fbf13305243aca0fd10eeff8dca Mon Sep 17 00:00:00 2001 From: Amphiluke Date: Sat, 23 Dec 2023 15:13:10 +0700 Subject: [PATCH] Allow skip adding an attribute. Improve ESM support --- .eslintrc.json | 4 + .npmrc | 1 + README.md | 111 +++-- dist/lindsvg.cjs | 443 +++++++++++++++++++ dist/lindsvg.esm.js | 62 +-- dist/lindsvg.esm.min.js | 4 +- dist/lindsvg.js | 62 +-- dist/lindsvg.min.js | 4 +- package-lock.json | 4 +- package.json | 15 +- rollup.config.mjs | 24 +- src/generator.mjs | 4 +- src/ls-error.mjs | 2 +- src/svg.mjs | 20 +- src/turtle.mjs | 34 +- src/validator.mjs | 2 +- test/install-test/node-cjs-test.cjs | 3 + test/install-test/node-esm-test.mjs | 3 + test/install-test/package-lock.json | 25 ++ test/install-test/package.json | 14 + test/{node-cjs-test.js => node-cjs-test.cjs} | 16 +- test/node-esm-test.mjs | 55 +-- test/params.mjs | 14 +- 23 files changed, 709 insertions(+), 217 deletions(-) create mode 100644 .npmrc create mode 100644 dist/lindsvg.cjs create mode 100644 test/install-test/node-cjs-test.cjs create mode 100644 test/install-test/node-esm-test.mjs create mode 100644 test/install-test/package-lock.json create mode 100644 test/install-test/package.json rename test/{node-cjs-test.js => node-cjs-test.cjs} (72%) diff --git a/.eslintrc.json b/.eslintrc.json index e7c8bfc..d764a19 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,6 +28,10 @@ "error", "1tbs" ], + "comma-dangle": [ + "error", + {"arrays": "always-multiline", "objects": "always-multiline"} + ], "comma-spacing": [ "error" ], diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..32fd3a7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +git-tag-version=false diff --git a/README.md b/README.md index 4723e68..0ae0db0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ Simple dependency-free module used to generate SVG images of deterministic L-systems. - ![Generated SVG tree](https://amphiluke.github.io/l-systems/img/autumn-tree.svg) ## Installation @@ -17,7 +16,7 @@ Installing the module is as simple as: npm install lindsvg ``` -Now you may get it in your scripts as usual: `require("lindsvg")`, or `import * as lindsvg from "lindsvg"` if you use such bundlers as webpack or rollup. +Now you may get it in your scripts as usual: `require("lindsvg")`, or `import * as lindsvg from "lindsvg"`. ### In a browser @@ -31,8 +30,8 @@ If you rather prefer using ES modules in a browser, just choose the “esm” bu ```html ``` @@ -54,32 +53,32 @@ All methods expect L-system parameters object as their first argument. These par ### Using “single-path” methods ```javascript -let {getSVGCode, getSVGData} = require("lindsvg"); +import {getSVGCode, getSVGData} from "lindsvg"; // L-system parameters let lsParams = { - axiom: "A---A", // The initial codeword (axiom) - rules: { // L-system production rules - F: "F", // Move forward a step with drawing a line - B: "B", // Move forward a step without drawing a line - A: "B-F+Z+F-BA", // Auxiliary rules... - Z: "F-FF-F--[--Z]F-FF-F--F-FF-F--" - }, - alpha: 0, // Initial angle in radians - theta: Math.PI / 3, // Angle increment in radians - step: 15, // The length of a “turtle” step - iterations: 7 // Total number of iterations + axiom: "A---A", // The initial codeword (axiom) + rules: { // L-system production rules + F: "F", // Move forward a step with drawing a line + B: "B", // Move forward a step without drawing a line + A: "B-F+Z+F-BA", // Auxiliary rules... + Z: "F-FF-F--[--Z]F-FF-F--F-FF-F--", + }, + alpha: 0, // Initial angle in radians + theta: Math.PI / 3, // Angle increment in radians + step: 15, // The length of a “turtle” step + iterations: 7, // Total number of iterations }; // Output SVG parameters (all of them are optional) let svgParams = { - width: 600, // Desired SVG element width - height: 600, // Desired SVG element height - padding: 5, // Additional space to extend the viewBox - pathAttributes: { // Name to value map for the element attributes - stroke: "green", - "stroke-width": "2" - } + width: 600, // Desired SVG element width + height: 600, // Desired SVG element height + padding: 5, // Additional space to extend the viewBox + pathAttributes: { // Name to value map for the element attributes + stroke: "green", + "stroke-width": "2", + }, }; // Get ready-to-render L-system’s SVG code as a string... @@ -95,35 +94,35 @@ An object returned by `getSVGData` contains [path data](https://www.w3.org/TR/SV Using “multi-path” methods (`getMultiPathSVGCode` and `getMultiPathSVGData`) allows you to specify different path attributes for every `` element separately, which may make branched L-systems (like plants) look “more naturally”. -For example, to generate the tree [demonstrated above](#lindsvg-demo-svg) (all but foliage) the following options were used: +For example, to generate the tree [demonstrated above](#lindsvg) (all but foliage) the following options were used: ```javascript -let {getMultiPathSVGCode, getMultiPathSVGData} = require("lindsvg"); +import {getMultiPathSVGCode, getMultiPathSVGData} from "lindsvg"; // L-system parameters let lsParams = { - axiom: "F-FFF-F+F+X", - rules: { - F: "F", - X: "FFF-[-F+F[Y]-[X]]+[+F+F[X]-[X]]", - Y: "FF-[-F+F]+[+F+FY]" - }, - alpha: 90 * Math.PI / 180, - theta: 14 * Math.PI / 180, - iterations: 6, - step: 12 + axiom: "F-FFF-F+F+X", + rules: { + F: "F", + X: "FFF-[-F+F[Y]-[X]]+[+F+F[X]-[X]]", + Y: "FF-[-F+F]+[+F+FY]", + }, + alpha: 90 * Math.PI / 180, + theta: 14 * Math.PI / 180, + iterations: 6, + step: 12, }; // Output SVG parameters let svgParams = { - width: 565, - height: 445, - padding: 10, - pathAttributes: { - stroke: "#514d3a", - "stroke-width": ["16", "11", "9", "7", "6", "5", "3", "2", "1"], - "stroke-linecap": ["square", "round" /* the rest items are equal to the last one */] - } + width: 565, + height: 445, + padding: 10, + pathAttributes: { + stroke: "#514d3a", + "stroke-width": ["16", "11", "9", "7", "6", "5", "3", "2", "1"], + "stroke-linecap": ["square", "round" /* the rest items are equal to the last one */], + }, }; // Get ready-to-render L-system’s SVG code as a string... @@ -135,6 +134,8 @@ let {multiPathData, minX, minY, width, height} = getMultiPathSVGData(lsParams); If an attribute array contains fewer elements than the maximum branching depth (e.g. see `stroke-linecap` in the example above), the missing items are implicitly made equal to the last one. So you don’t need to repeat the same value in the end of the list. +You may also use the special value `"n/a"` which prevents an attribute from being added on the corresponding `` element (e.g. when you need to add an attribute only to one or to a few ``s: `pathAttributes: {transform: ["skewY(-35)", "n/a"]}`). + The property `multiPathData` in the object returned by `getMultiPathSVGData` is a _list_ of path data for every `` element. The list is sorted in the order of increasing branch level (the deeper the branch the higher the index in the array). ### Error handling @@ -142,26 +143,22 @@ The property `multiPathData` in the object returned by `getMultiPathSVGData` is 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"); +import {getSVGCode} from "lindsvg"; +import {dump} from "js-yaml"; try { - console.log(getSVGCode(lsParams, svgParams)); + 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})); - } + // 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(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. - ## Demos Please, visit the project’s [demo web app](https://amphiluke.github.io/lindsvg/) (installable as a PWA and works offline too). You will find a few built-in L-system collections there, and will also be able to experiment with lindsvg while building your own L-systems. diff --git a/dist/lindsvg.cjs b/dist/lindsvg.cjs new file mode 100644 index 0000000..493a5cf --- /dev/null +++ b/dist/lindsvg.cjs @@ -0,0 +1,443 @@ +/*! +lindsvg v1.3.3 +https://amphiluke.github.io/lindsvg/ +(c) 2023 Amphiluke +*/ +'use strict'; + +let messages = { + 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", +}; + +let letterRE = /^[A-Z]$/; +function checkLetter(letter, msg = messages.LETTER) { + return letterRE.test(letter) || msg; +} + +let ruleRE = /^[A-Z+\-[\]]*$/; +function checkRule(rule, msg = messages.RULE) { + return ruleRE.test(rule) || msg; +} + +function checkRules(rules, letterMsg, ruleMsg) { + 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[letter] = result; + } + }); + return Object.keys(errors).length ? errors : true; +} + +function checkStep(step, msg = messages.STEP) { + return (Number.isFinite(step) && step > 0) || msg; +} + +function checkIterations(iterations, msg = messages.COUNT) { + return (Number.isInteger(iterations) && iterations > 0) || msg; +} + +function checkAngle(angle, msg = messages.NUMBER) { + return Number.isFinite(angle) || msg; +} + +function validate(lsParams) { + let errors = Object.create(null); + Object.entries(lsParams).forEach(([param, value]) => { + let result = true; + switch (param) { + case "axiom": + result = checkRule(value, messages.AXIOM); + break; + case "rules": + result = checkRules(value); + break; + case "alpha": + case "theta": + result = checkAngle(value, messages[param.toUpperCase()]); + break; + case "step": + result = checkStep(value); + break; + case "iterations": + result = checkIterations(value); + break; + } + if (result !== true) { + errors[param] = result; + } + }); + return Object.keys(errors).length ? errors : true; +} + +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: "", + B: "", + "+": "+", + "-": "-", + "[": "[", + "]": "]", +}; + +/** @type {LSParams} */ +let defaults = { + alpha: 0, + theta: 0, + step: 10, + iterations: 3, +}; + +/** + * Remove all the stuff which doesn’t affect the drawing process from the raw generated codeword + * @param {String} codeword - Raw L-system code + * @return {String} - Clean L-system code + */ +function cleanCodeword(codeword) { + // Remove auxiliary drawing-indifferent letters + let cleanCodeword = codeword.replace(/[^FB[\]+-]/g, ""); + do { + codeword = cleanCodeword; + // Remove useless brackets that don’t contain F commands or other brackets (preserving bracket balance!) + cleanCodeword = cleanCodeword.replace(/\[[^F[\]]*]/g, ""); + } while (cleanCodeword !== codeword); + return cleanCodeword; +} + +/** + * Generate L-system code + * @param {LSParams} lsParams - L-system parameters + * @return {String} - Clean L-system code + */ +function generateCodeword(lsParams) { + let validity = validate(lsParams); + if (validity !== true) { + throw new LSError(validity); + } + let {axiom: code, iterations} = {...defaults, ...lsParams}; + let rules = {...ctrlRules, ...lsParams.rules}; + for (; iterations > 0; iterations--) { + code = [...code].reduce((accumulator, letter) => accumulator + (rules[letter] || ""), ""); + } + return cleanCodeword(code); +} + +/** + * Split a codeword into “tokens” (group equal adjacent commands) + * @param {String} codeword - L-system code + * @return {String[]} + */ +function tokenizeCodeword(codeword) { + return codeword.match(/([FB[\]+-])\1*/g); // tokenize +} + +class Turtle { + constructor({x, y, step, alpha, theta}) { + this.stack = []; + this.x = this.minX = this.maxX = x; + this.y = this.minY = this.maxY = y; + this.step = step; + this.alpha = -alpha; // negate since Y axis is inverted + this.theta = theta; + } + + translate(stepCount = 1) { + this.x += stepCount * this.step * Math.cos(this.alpha); + this.y += stepCount * 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(factor) { + this.alpha += factor * this.theta; + } + + pushStack(repeatCount = 1) { + for (; repeatCount > 0; repeatCount--) { + this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); + } + } + + popStack(repeatCount) { + for (; repeatCount > 0; repeatCount--) { + ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); + } + } + + getDrawingRect() { + let minX = Math.floor(this.minX); + let minY = Math.floor(this.minY); + let maxX = Math.ceil(this.maxX); + let maxY = Math.ceil(this.maxY); + return {minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY}; + } +} + +function formatCoordinates(x, y) { + // Unary plus is used to remove insignificant trailing zeros + return `${+x.toFixed(4)} ${+y.toFixed(4)}`; +} + +/** + * Delete useless M commands which are followed by other M commands, and those in the end of path data + * @param {String} pathData - SVG path data + * @return {String} + */ +function dropUselessMoves(pathData) { + return pathData.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g, ""); +} + +/** + * Get the value of the d attribute + * @param {String[]} tokens - Tokenized codeword + * @param {Object} turtle - Turtle object to work with + * @return {String} + */ +function getPathData(tokens, turtle) { + let prevCommand; // used to avoid unnecessary repeating of the commands L and M + let pathData = tokens.reduce((accumulator, token) => { + let tokenLength = token.length; + switch (token[0]) { + case "F": + turtle.translate(tokenLength); + accumulator += (prevCommand === "L" ? " " : "L") + formatCoordinates(turtle.x, turtle.y); + prevCommand = "L"; + break; + case "B": + turtle.translate(tokenLength); + if (prevCommand === "M") { + // As the spec states, “If a moveto is followed by multiple pairs of coordinates, + // the subsequent pairs are treated as implicit lineto commands”. + // This is not what we want, so delete the preceding moveto command + accumulator = accumulator.slice(0, accumulator.lastIndexOf("M")); + } + accumulator += "M" + formatCoordinates(turtle.x, turtle.y); + prevCommand = "M"; + break; + case "+": + turtle.rotate(tokenLength); + break; + case "-": + turtle.rotate(-tokenLength); + break; + case "[": + turtle.pushStack(tokenLength); + break; + case "]": + turtle.popStack(tokenLength); + accumulator += `M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + } + return accumulator; + }, "M" + formatCoordinates(turtle.x, turtle.y)); + return dropUselessMoves(pathData); +} + +/** + * Get the values of the d attribute for each path element + * @param {String[]} tokens - Tokenized codeword + * @param {Object} turtle - Turtle object to work with + * @return {String[]} + */ +function getMultiPathData(tokens, turtle) { + let prevCommand; // used to avoid unnecessary repeating of the commands L and M + let branchLevel = 0; + let multiPathData = tokens.reduce((accumulator, token) => { + let pathData = accumulator[branchLevel] || ""; + let tokenLength = token.length; + switch (token[0]) { + case "F": + turtle.translate(tokenLength); + pathData += (prevCommand === "L" ? " " : "L") + formatCoordinates(turtle.x, turtle.y); + prevCommand = "L"; + break; + case "B": + turtle.translate(tokenLength); + if (prevCommand === "M") { + // As the spec states, “If a moveto is followed by multiple pairs of coordinates, + // the subsequent pairs are treated as implicit lineto commands”. + // This is not what we want, so delete the preceding moveto command + pathData = pathData.slice(0, pathData.lastIndexOf("M")); + } + pathData += "M" + formatCoordinates(turtle.x, turtle.y); + prevCommand = "M"; + break; + case "+": + turtle.rotate(tokenLength); + break; + case "-": + turtle.rotate(-tokenLength); + break; + case "[": + branchLevel += tokenLength; + turtle.pushStack(tokenLength); + pathData = `${accumulator[branchLevel] || ""}M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + case "]": + branchLevel -= tokenLength; + turtle.popStack(tokenLength); + pathData = `${accumulator[branchLevel] || ""}M${formatCoordinates(turtle.x, turtle.y)}`; + prevCommand = "M"; + break; + } + accumulator[branchLevel] = pathData; + return accumulator; + }, ["M" + formatCoordinates(turtle.x, turtle.y)]); + // Some L-systems can produce branching levels which contain no real draw commands (only moves and rotations). + // Such L-systems usually don’t have F commands in their axiom nor they have a production for F (example is + // the Penrose tiling). Having elements with only M commands is meaningless, so filtering them out + return multiPathData + .filter(pathData => pathData.includes("L")) + // also delete useless M commands (including those in the end of path data) + .map(dropUselessMoves); +} + +/** + * Get raw data required for SVG rendering + * @param {LSParams} lsParams - L-system parameters + * @return {{pathData: String, minX: Number, minY: Number, width: Number, height: Number}} + */ +function getSVGData(lsParams) { + let codeword = generateCodeword(lsParams); + let turtle = new Turtle({x: 0, y: 0, ...lsParams}); + let pathData = getPathData(tokenizeCodeword(codeword), turtle); + return { + pathData, + ...turtle.getDrawingRect(), + }; +} + +/** + * Get raw data required for rendering of a multi-path SVG + * @param {LSParams} lsParams - L-system parameters + * @return {{multiPathData: String[], minX: Number, minY: Number, width: Number, height: Number}} + */ +function getMultiPathSVGData(lsParams) { + let codeword = generateCodeword(lsParams); + let turtle = new Turtle({x: 0, y: 0, ...lsParams}); + let multiPathData = getMultiPathData(tokenizeCodeword(codeword), turtle); + return { + multiPathData, + ...turtle.getDrawingRect(), + }; +} + +function makeSVGConfig(svgParams, naturalWidth, naturalHeight) { + return { + width: svgParams.width || naturalWidth, + height: svgParams.height || naturalHeight, + padding: svgParams.padding || 0, + pathAttributes: { + // for backward compatibility with v1.1.0, also check fill and stroke as direct props of svgParams + fill: svgParams.fill || "none", + stroke: svgParams.stroke || "#000", + ...svgParams.pathAttributes, + }, + }; +} + +function makeSVGCode({viewBox, width, height, content}) { + return `${content}`; +} + +function makeAttrString(attrs, index) { + return Object.entries(attrs).reduce((accumulator, [name, value]) => { + if (Array.isArray(value)) { + value = value[Math.min(index, value.length - 1)]; + } + if (value === undefined || value.toLowerCase() === "n/a") { + return accumulator; + } + value = value.replace(/"/g, """); + return `${accumulator} ${name}="${value}"`; + }, ""); +} + +/** + * Get ready-to-render L-system’s SVG code + * @param {LSParams} lsParams - L-system parameters + * @param {SVGParams} svgParams - Output SVG parameters + * @return {String} + */ +function getSVGCode(lsParams, svgParams) { + let {pathData, minX, minY, width: naturalWidth, height: naturalHeight} = getSVGData(lsParams); + let {padding, width, height, pathAttributes} = makeSVGConfig(svgParams, naturalWidth, naturalHeight); + let pathAttrStr = makeAttrString(pathAttributes, 0); + return makeSVGCode({ + viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], + width, + height, + content: ``, + }); +} + +/** + * Get ready-to-render multi-path SVG code for an L-system + * @param {LSParams} lsParams - L-system parameters + * @param {SVGParams} svgParams - Output SVG parameters + * @return {String} + */ +function getMultiPathSVGCode(lsParams, svgParams) { + let {multiPathData, minX, minY, width: naturalWidth, height: naturalHeight} = getMultiPathSVGData(lsParams); + let {padding, width, height, pathAttributes} = makeSVGConfig(svgParams, naturalWidth, naturalHeight); + let content = multiPathData.reduce((accumulator, pathData, index) => { + let pathAttrStr = makeAttrString(pathAttributes, index); + return `${accumulator}`; + }, ""); + return makeSVGCode({ + viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], + width, + height, + content, + }); +} + +exports.getMultiPathSVGCode = getMultiPathSVGCode; +exports.getMultiPathSVGData = getMultiPathSVGData; +exports.getSVGCode = getSVGCode; +exports.getSVGData = getSVGData; diff --git a/dist/lindsvg.esm.js b/dist/lindsvg.esm.js index a3a0f08..9b86846 100644 --- a/dist/lindsvg.esm.js +++ b/dist/lindsvg.esm.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.3.2 +lindsvg v1.3.3 https://amphiluke.github.io/lindsvg/ (c) 2023 Amphiluke */ @@ -11,7 +11,7 @@ let messages = { 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" + NUMBER: "A valid finite number expected", }; let letterRE = /^[A-Z]$/; @@ -106,7 +106,7 @@ Object.defineProperty(LSError.prototype, "name", { configurable: true, enumerable: false, writable: true, - value: "LSError" + value: "LSError", }); /** @type {Rules} */ @@ -116,7 +116,7 @@ let ctrlRules = { "+": "+", "-": "-", "[": "[", - "]": "]" + "]": "]", }; /** @type {LSParams} */ @@ -124,7 +124,7 @@ let defaults = { alpha: 0, theta: 0, step: 10, - iterations: 3 + iterations: 3, }; /** @@ -170,7 +170,16 @@ function tokenizeCodeword(codeword) { return codeword.match(/([FB[\]+-])\1*/g); // tokenize } -let proto = { +class Turtle { + constructor({x, y, step, alpha, theta}) { + this.stack = []; + this.x = this.minX = this.maxX = x; + this.y = this.minY = this.maxY = y; + this.step = step; + this.alpha = -alpha; // negate since Y axis is inverted + this.theta = theta; + } + translate(stepCount = 1) { this.x += stepCount * this.step * Math.cos(this.alpha); this.y += stepCount * this.step * Math.sin(this.alpha); @@ -178,20 +187,24 @@ let proto = { 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(factor) { this.alpha += factor * this.theta; - }, + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); } - }, + } + popStack(repeatCount) { for (; repeatCount > 0; repeatCount--) { ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); } - }, + } + getDrawingRect() { let minX = Math.floor(this.minX); let minY = Math.floor(this.minY); @@ -199,17 +212,6 @@ let proto = { let maxY = Math.ceil(this.maxY); return {minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY}; } -}; - -function createTurtle({x, y, step, alpha, theta}) { - let turtle = Object.create(proto); - turtle.stack = []; - turtle.x = turtle.minX = turtle.maxX = x; - turtle.y = turtle.minY = turtle.maxY = y; - turtle.step = step; - turtle.alpha = -alpha; // negate since Y axis is inverted - turtle.theta = theta; - return turtle; } function formatCoordinates(x, y) { @@ -340,11 +342,11 @@ function getMultiPathData(tokens, turtle) { */ function getSVGData(lsParams) { let codeword = generateCodeword(lsParams); - let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let turtle = new Turtle({x: 0, y: 0, ...lsParams}); let pathData = getPathData(tokenizeCodeword(codeword), turtle); return { pathData, - ...turtle.getDrawingRect() + ...turtle.getDrawingRect(), }; } @@ -355,11 +357,11 @@ function getSVGData(lsParams) { */ function getMultiPathSVGData(lsParams) { let codeword = generateCodeword(lsParams); - let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let turtle = new Turtle({x: 0, y: 0, ...lsParams}); let multiPathData = getMultiPathData(tokenizeCodeword(codeword), turtle); return { multiPathData, - ...turtle.getDrawingRect() + ...turtle.getDrawingRect(), }; } @@ -372,8 +374,8 @@ function makeSVGConfig(svgParams, naturalWidth, naturalHeight) { // for backward compatibility with v1.1.0, also check fill and stroke as direct props of svgParams fill: svgParams.fill || "none", stroke: svgParams.stroke || "#000", - ...svgParams.pathAttributes - } + ...svgParams.pathAttributes, + }, }; } @@ -386,7 +388,7 @@ function makeAttrString(attrs, index) { if (Array.isArray(value)) { value = value[Math.min(index, value.length - 1)]; } - if (value === undefined) { + if (value === undefined || value.toLowerCase() === "n/a") { return accumulator; } value = value.replace(/"/g, """); @@ -408,7 +410,7 @@ function getSVGCode(lsParams, svgParams) { viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], width, height, - content: `` + content: ``, }); } @@ -429,7 +431,7 @@ function getMultiPathSVGCode(lsParams, svgParams) { viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], width, height, - content + content, }); } diff --git a/dist/lindsvg.esm.min.js b/dist/lindsvg.esm.min.js index fb6d8f0..4057a53 100644 --- a/dist/lindsvg.esm.min.js +++ b/dist/lindsvg.esm.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.3.2 +lindsvg v1.3.3 https://amphiluke.github.io/lindsvg/ (c) 2023 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 r(a,r,n){let h=Object.create(null);return Object.entries(a).forEach((([a,s])=>{let c=function(a,i=t.LETTER){return e.test(a)||i}(a,r);!0===c&&(c=i(s,n)),!0!==c&&(h[a]=c)})),!Object.keys(h).length||h}function n(e){let a=Object.create(null);return Object.entries(e).forEach((([e,n])=>{let h=!0;switch(e){case"axiom":h=i(n,t.AXIOM);break;case"rules":h=r(n);break;case"alpha":case"theta":h=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(n,t[e.toUpperCase()]);break;case"step":h=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(n);break;case"iterations":h=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(n)}!0!==h&&(a[e]=h)})),!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 s={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function l(t){let e=n(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...c,...t},r={...s,...t.rules};for(;i>0;i--)a=[...a].reduce(((t,e)=>t+(r[e]||"")),"");return function(t){let e=t.replace(/[^FB[\]+-]/g,"");do{t=e,e=e.replace(/\[[^F[\]]*]/g,"")}while(e!==t);return e}(a)}function o(t){return t.match(/([FB[\]+-])\1*/g)}let u={translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*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(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(t){for(;t>0;t--)({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 p({x:t,y:e,step:a,alpha:i,theta:r}){let n=Object.create(u);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}function m(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function f(t){return t.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g,"")}function x(t){let e=l(t),a=p({x:0,y:0,...t}),i=function(t,e){let a;return f(t.reduce(((t,i)=>{let r=i.length;switch(i[0]){case"F":e.translate(r),t+=("L"===a?" ":"L")+m(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+m(e.x,e.y),a="M";break;case"+":e.rotate(r);break;case"-":e.rotate(-r);break;case"[":e.pushStack(r);break;case"]":e.popStack(r),t+=`M${m(e.x,e.y)}`,a="M"}return t}),"M"+m(e.x,e.y)))}(o(e),a);return{pathData:i,...a.getDrawingRect()}}function d(t){let e=l(t),a=p({x:0,y:0,...t}),i=function(t,e){let a,i=0;return t.reduce(((t,r)=>{let n=t[i]||"",h=r.length;switch(r[0]){case"F":e.translate(h),n+=("L"===a?" ":"L")+m(e.x,e.y),a="L";break;case"B":e.translate(h),"M"===a&&(n=n.slice(0,n.lastIndexOf("M"))),n+="M"+m(e.x,e.y),a="M";break;case"+":e.rotate(h);break;case"-":e.rotate(-h);break;case"[":i+=h,e.pushStack(h),n=`${t[i]||""}M${m(e.x,e.y)}`,a="M";break;case"]":i-=h,e.popStack(h),n=`${t[i]||""}M${m(e.x,e.y)}`,a="M"}return t[i]=n,t}),["M"+m(e.x,e.y)]).filter((t=>t.includes("L"))).map(f)}(o(e),a);return{multiPathData:i,...a.getDrawingRect()}}function b(t,e,a){return{width:t.width||e,height:t.height||a,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function g({viewBox:t,width:e,height:a,content:i}){return`${i}`}function y(t,e){return Object.entries(t).reduce(((t,[a,i])=>(Array.isArray(i)&&(i=i[Math.min(e,i.length-1)]),void 0===i?t:`${t} ${a}="${i=i.replace(/"/g,""")}"`)),"")}function M(t,e){let{pathData:a,minX:i,minY:r,width:n,height:h}=x(t),{padding:s,width:c,height:l,pathAttributes:o}=b(e,n,h);return g({viewBox:[i-s,r-s,n+2*s,h+2*s],width:c,height:l,content:``})}function w(t,e){let{multiPathData:a,minX:i,minY:r,width:n,height:h}=d(t),{padding:s,width:c,height:l,pathAttributes:o}=b(e,n,h);return g({viewBox:[i-s,r-s,n+2*s,h+2*s],width:c,height:l,content:a.reduce(((t,e,a)=>`${t}`),"")})}export{w as getMultiPathSVGCode,d as getMultiPathSVGData,M as getSVGCode,x 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 h=Object.create(null);return Object.entries(a).forEach((([a,s])=>{let c=function(a,i=t.LETTER){return e.test(a)||i}(a,r);!0===c&&(c=i(s,n)),!0!==c&&(h[a]=c)})),!Object.keys(h).length||h}function n(e){let a=Object.create(null);return Object.entries(e).forEach((([e,n])=>{let h=!0;switch(e){case"axiom":h=i(n,t.AXIOM);break;case"rules":h=r(n);break;case"alpha":case"theta":h=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(n,t[e.toUpperCase()]);break;case"step":h=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(n);break;case"iterations":h=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(n)}!0!==h&&(a[e]=h)})),!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 s={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function o(t){let e=n(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...c,...t},r={...s,...t.rules};for(;i>0;i--)a=[...a].reduce(((t,e)=>t+(r[e]||"")),"");return function(t){let e=t.replace(/[^FB[\]+-]/g,"");do{t=e,e=e.replace(/\[[^F[\]]*]/g,"")}while(e!==t);return e}(a)}function l(t){return t.match(/([FB[\]+-])\1*/g)}class u{constructor({x:t,y:e,step:a,alpha:i,theta:r}){this.stack=[],this.x=this.minX=this.maxX=t,this.y=this.minY=this.maxY=e,this.step=a,this.alpha=-i,this.theta=r}translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*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(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})}popStack(t){for(;t>0;t--)({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 p(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function m(t){return t.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g,"")}function f(t){let e=o(t),a=new u({x:0,y:0,...t}),i=function(t,e){let a;return m(t.reduce(((t,i)=>{let r=i.length;switch(i[0]){case"F":e.translate(r),t+=("L"===a?" ":"L")+p(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+p(e.x,e.y),a="M";break;case"+":e.rotate(r);break;case"-":e.rotate(-r);break;case"[":e.pushStack(r);break;case"]":e.popStack(r),t+=`M${p(e.x,e.y)}`,a="M"}return t}),"M"+p(e.x,e.y)))}(l(e),a);return{pathData:i,...a.getDrawingRect()}}function x(t){let e=o(t),a=new u({x:0,y:0,...t}),i=function(t,e){let a,i=0;return t.reduce(((t,r)=>{let n=t[i]||"",h=r.length;switch(r[0]){case"F":e.translate(h),n+=("L"===a?" ":"L")+p(e.x,e.y),a="L";break;case"B":e.translate(h),"M"===a&&(n=n.slice(0,n.lastIndexOf("M"))),n+="M"+p(e.x,e.y),a="M";break;case"+":e.rotate(h);break;case"-":e.rotate(-h);break;case"[":i+=h,e.pushStack(h),n=`${t[i]||""}M${p(e.x,e.y)}`,a="M";break;case"]":i-=h,e.popStack(h),n=`${t[i]||""}M${p(e.x,e.y)}`,a="M"}return t[i]=n,t}),["M"+p(e.x,e.y)]).filter((t=>t.includes("L"))).map(m)}(l(e),a);return{multiPathData:i,...a.getDrawingRect()}}function d(t,e,a){return{width:t.width||e,height:t.height||a,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function b({viewBox:t,width:e,height:a,content:i}){return`${i}`}function g(t,e){return Object.entries(t).reduce(((t,[a,i])=>(Array.isArray(i)&&(i=i[Math.min(e,i.length-1)]),void 0===i||"n/a"===i.toLowerCase()?t:`${t} ${a}="${i=i.replace(/"/g,""")}"`)),"")}function w(t,e){let{pathData:a,minX:i,minY:r,width:n,height:h}=f(t),{padding:s,width:c,height:o,pathAttributes:l}=d(e,n,h);return b({viewBox:[i-s,r-s,n+2*s,h+2*s],width:c,height:o,content:``})}function y(t,e){let{multiPathData:a,minX:i,minY:r,width:n,height:h}=x(t),{padding:s,width:c,height:o,pathAttributes:l}=d(e,n,h);return b({viewBox:[i-s,r-s,n+2*s,h+2*s],width:c,height:o,content:a.reduce(((t,e,a)=>`${t}`),"")})}export{y as getMultiPathSVGCode,x as getMultiPathSVGData,w as getSVGCode,f as getSVGData}; diff --git a/dist/lindsvg.js b/dist/lindsvg.js index 6561754..4241471 100644 --- a/dist/lindsvg.js +++ b/dist/lindsvg.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.3.2 +lindsvg v1.3.3 https://amphiluke.github.io/lindsvg/ (c) 2023 Amphiluke */ @@ -17,7 +17,7 @@ https://amphiluke.github.io/lindsvg/ 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" + NUMBER: "A valid finite number expected", }; let letterRE = /^[A-Z]$/; @@ -112,7 +112,7 @@ https://amphiluke.github.io/lindsvg/ configurable: true, enumerable: false, writable: true, - value: "LSError" + value: "LSError", }); /** @type {Rules} */ @@ -122,7 +122,7 @@ https://amphiluke.github.io/lindsvg/ "+": "+", "-": "-", "[": "[", - "]": "]" + "]": "]", }; /** @type {LSParams} */ @@ -130,7 +130,7 @@ https://amphiluke.github.io/lindsvg/ alpha: 0, theta: 0, step: 10, - iterations: 3 + iterations: 3, }; /** @@ -176,7 +176,16 @@ https://amphiluke.github.io/lindsvg/ return codeword.match(/([FB[\]+-])\1*/g); // tokenize } - let proto = { + class Turtle { + constructor({x, y, step, alpha, theta}) { + this.stack = []; + this.x = this.minX = this.maxX = x; + this.y = this.minY = this.maxY = y; + this.step = step; + this.alpha = -alpha; // negate since Y axis is inverted + this.theta = theta; + } + translate(stepCount = 1) { this.x += stepCount * this.step * Math.cos(this.alpha); this.y += stepCount * this.step * Math.sin(this.alpha); @@ -184,20 +193,24 @@ https://amphiluke.github.io/lindsvg/ 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(factor) { this.alpha += factor * this.theta; - }, + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); } - }, + } + popStack(repeatCount) { for (; repeatCount > 0; repeatCount--) { ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); } - }, + } + getDrawingRect() { let minX = Math.floor(this.minX); let minY = Math.floor(this.minY); @@ -205,17 +218,6 @@ https://amphiluke.github.io/lindsvg/ let maxY = Math.ceil(this.maxY); return {minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY}; } - }; - - function createTurtle({x, y, step, alpha, theta}) { - let turtle = Object.create(proto); - turtle.stack = []; - turtle.x = turtle.minX = turtle.maxX = x; - turtle.y = turtle.minY = turtle.maxY = y; - turtle.step = step; - turtle.alpha = -alpha; // negate since Y axis is inverted - turtle.theta = theta; - return turtle; } function formatCoordinates(x, y) { @@ -346,11 +348,11 @@ https://amphiluke.github.io/lindsvg/ */ function getSVGData(lsParams) { let codeword = generateCodeword(lsParams); - let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let turtle = new Turtle({x: 0, y: 0, ...lsParams}); let pathData = getPathData(tokenizeCodeword(codeword), turtle); return { pathData, - ...turtle.getDrawingRect() + ...turtle.getDrawingRect(), }; } @@ -361,11 +363,11 @@ https://amphiluke.github.io/lindsvg/ */ function getMultiPathSVGData(lsParams) { let codeword = generateCodeword(lsParams); - let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let turtle = new Turtle({x: 0, y: 0, ...lsParams}); let multiPathData = getMultiPathData(tokenizeCodeword(codeword), turtle); return { multiPathData, - ...turtle.getDrawingRect() + ...turtle.getDrawingRect(), }; } @@ -378,8 +380,8 @@ https://amphiluke.github.io/lindsvg/ // for backward compatibility with v1.1.0, also check fill and stroke as direct props of svgParams fill: svgParams.fill || "none", stroke: svgParams.stroke || "#000", - ...svgParams.pathAttributes - } + ...svgParams.pathAttributes, + }, }; } @@ -392,7 +394,7 @@ https://amphiluke.github.io/lindsvg/ if (Array.isArray(value)) { value = value[Math.min(index, value.length - 1)]; } - if (value === undefined) { + if (value === undefined || value.toLowerCase() === "n/a") { return accumulator; } value = value.replace(/"/g, """); @@ -414,7 +416,7 @@ https://amphiluke.github.io/lindsvg/ viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], width, height, - content: `` + content: ``, }); } @@ -435,7 +437,7 @@ https://amphiluke.github.io/lindsvg/ viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], width, height, - content + content, }); } diff --git a/dist/lindsvg.min.js b/dist/lindsvg.min.js index c2c5636..88320b8 100644 --- a/dist/lindsvg.min.js +++ b/dist/lindsvg.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.3.2 +lindsvg v1.3.3 https://amphiluke.github.io/lindsvg/ (c) 2023 Amphiluke */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis: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 r(t,i,r){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=n(h,r)),!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=n(i,e.AXIOM);break;case"rules":s=r(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:"","+":"+","-":"-","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function l(t){let e=s(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...c,...t},n={...o,...t.rules};for(;i>0;i--)a=[...a].reduce(((t,e)=>t+(n[e]||"")),"");return function(t){let e=t.replace(/[^FB[\]+-]/g,"");do{t=e,e=e.replace(/\[[^F[\]]*]/g,"")}while(e!==t);return e}(a)}function u(t){return t.match(/([FB[\]+-])\1*/g)}let p={translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*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(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})},popStack(t){for(;t>0;t--)({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 f({x:t,y:e,step:a,alpha:i,theta:n}){let r=Object.create(p);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}function m(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function d(t){return t.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g,"")}function g(t){let e=l(t),a=f({x:0,y:0,...t}),i=function(t,e){let a;return d(t.reduce(((t,i)=>{let n=i.length;switch(i[0]){case"F":e.translate(n),t+=("L"===a?" ":"L")+m(e.x,e.y),a="L";break;case"B":e.translate(n),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+m(e.x,e.y),a="M";break;case"+":e.rotate(n);break;case"-":e.rotate(-n);break;case"[":e.pushStack(n);break;case"]":e.popStack(n),t+=`M${m(e.x,e.y)}`,a="M"}return t}),"M"+m(e.x,e.y)))}(u(e),a);return{pathData:i,...a.getDrawingRect()}}function x(t){let e=l(t),a=f({x:0,y:0,...t}),i=function(t,e){let a,i=0;return t.reduce(((t,n)=>{let r=t[i]||"",s=n.length;switch(n[0]){case"F":e.translate(s),r+=("L"===a?" ":"L")+m(e.x,e.y),a="L";break;case"B":e.translate(s),"M"===a&&(r=r.slice(0,r.lastIndexOf("M"))),r+="M"+m(e.x,e.y),a="M";break;case"+":e.rotate(s);break;case"-":e.rotate(-s);break;case"[":i+=s,e.pushStack(s),r=`${t[i]||""}M${m(e.x,e.y)}`,a="M";break;case"]":i-=s,e.popStack(s),r=`${t[i]||""}M${m(e.x,e.y)}`,a="M"}return t[i]=r,t}),["M"+m(e.x,e.y)]).filter((t=>t.includes("L"))).map(d)}(u(e),a);return{multiPathData:i,...a.getDrawingRect()}}function b(t,e,a){return{width:t.width||e,height:t.height||a,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function y({viewBox:t,width:e,height:a,content:i}){return`${i}`}function M(t,e){return Object.entries(t).reduce(((t,[a,i])=>(Array.isArray(i)&&(i=i[Math.min(e,i.length-1)]),void 0===i?t:`${t} ${a}="${i=i.replace(/"/g,""")}"`)),"")}t.getMultiPathSVGCode=function(t,e){let{multiPathData:a,minX:i,minY:n,width:r,height:s}=x(t),{padding:h,width:o,height:c,pathAttributes:l}=b(e,r,s);return y({viewBox:[i-h,n-h,r+2*h,s+2*h],width:o,height:c,content:a.reduce(((t,e,a)=>`${t}`),"")})},t.getMultiPathSVGData=x,t.getSVGCode=function(t,e){let{pathData:a,minX:i,minY:n,width:r,height:s}=g(t),{padding:h,width:o,height:c,pathAttributes:l}=b(e,r,s);return y({viewBox:[i-h,n-h,r+2*h,s+2*h],width:o,height:c,content:``})},t.getSVGData=g})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis: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"},i=/^[A-Z]$/;let a=/^[A-Z+\-[\]]*$/;function n(t,i=e.RULE){return a.test(t)||i}function r(t,a,r){let s=Object.create(null);return Object.entries(t).forEach((([t,h])=>{let o=function(t,a=e.LETTER){return i.test(t)||a}(t,a);!0===o&&(o=n(h,r)),!0!==o&&(s[t]=o)})),!Object.keys(s).length||s}function s(t){let i=Object.create(null);return Object.entries(t).forEach((([t,a])=>{let s=!0;switch(t){case"axiom":s=n(a,e.AXIOM);break;case"rules":s=r(a);break;case"alpha":case"theta":s=function(t,i=e.NUMBER){return Number.isFinite(t)||i}(a,e[t.toUpperCase()]);break;case"step":s=function(t,i=e.STEP){return Number.isFinite(t)&&t>0||i}(a);break;case"iterations":s=function(t,i=e.COUNT){return Number.isInteger(t)&&t>0||i}(a)}!0!==s&&(i[t]=s)})),!Object.keys(i).length||i}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:"","+":"+","-":"-","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function l(t){let e=s(t);if(!0!==e)throw new h(e);let{axiom:i,iterations:a}={...c,...t},n={...o,...t.rules};for(;a>0;a--)i=[...i].reduce(((t,e)=>t+(n[e]||"")),"");return function(t){let e=t.replace(/[^FB[\]+-]/g,"");do{t=e,e=e.replace(/\[[^F[\]]*]/g,"")}while(e!==t);return e}(i)}function u(t){return t.match(/([FB[\]+-])\1*/g)}class p{constructor({x:t,y:e,step:i,alpha:a,theta:n}){this.stack=[],this.x=this.minX=this.maxX=t,this.y=this.minY=this.maxY=e,this.step=i,this.alpha=-a,this.theta=n}translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*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(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})}popStack(t){for(;t>0;t--)({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())}getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),i=Math.ceil(this.maxX),a=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:i,maxY:a,width:i-t,height:a-e}}}function f(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function m(t){return t.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g,"")}function d(t){let e=l(t),i=new p({x:0,y:0,...t}),a=function(t,e){let i;return m(t.reduce(((t,a)=>{let n=a.length;switch(a[0]){case"F":e.translate(n),t+=("L"===i?" ":"L")+f(e.x,e.y),i="L";break;case"B":e.translate(n),"M"===i&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+f(e.x,e.y),i="M";break;case"+":e.rotate(n);break;case"-":e.rotate(-n);break;case"[":e.pushStack(n);break;case"]":e.popStack(n),t+=`M${f(e.x,e.y)}`,i="M"}return t}),"M"+f(e.x,e.y)))}(u(e),i);return{pathData:a,...i.getDrawingRect()}}function g(t){let e=l(t),i=new p({x:0,y:0,...t}),a=function(t,e){let i,a=0;return t.reduce(((t,n)=>{let r=t[a]||"",s=n.length;switch(n[0]){case"F":e.translate(s),r+=("L"===i?" ":"L")+f(e.x,e.y),i="L";break;case"B":e.translate(s),"M"===i&&(r=r.slice(0,r.lastIndexOf("M"))),r+="M"+f(e.x,e.y),i="M";break;case"+":e.rotate(s);break;case"-":e.rotate(-s);break;case"[":a+=s,e.pushStack(s),r=`${t[a]||""}M${f(e.x,e.y)}`,i="M";break;case"]":a-=s,e.popStack(s),r=`${t[a]||""}M${f(e.x,e.y)}`,i="M"}return t[a]=r,t}),["M"+f(e.x,e.y)]).filter((t=>t.includes("L"))).map(m)}(u(e),i);return{multiPathData:a,...i.getDrawingRect()}}function x(t,e,i){return{width:t.width||e,height:t.height||i,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function b({viewBox:t,width:e,height:i,content:a}){return`${a}`}function y(t,e){return Object.entries(t).reduce(((t,[i,a])=>(Array.isArray(a)&&(a=a[Math.min(e,a.length-1)]),void 0===a||"n/a"===a.toLowerCase()?t:`${t} ${i}="${a=a.replace(/"/g,""")}"`)),"")}t.getMultiPathSVGCode=function(t,e){let{multiPathData:i,minX:a,minY:n,width:r,height:s}=g(t),{padding:h,width:o,height:c,pathAttributes:l}=x(e,r,s);return b({viewBox:[a-h,n-h,r+2*h,s+2*h],width:o,height:c,content:i.reduce(((t,e,i)=>`${t}`),"")})},t.getMultiPathSVGData=g,t.getSVGCode=function(t,e){let{pathData:i,minX:a,minY:n,width:r,height:s}=d(t),{padding:h,width:o,height:c,pathAttributes:l}=x(e,r,s);return b({viewBox:[a-h,n-h,r+2*h,s+2*h],width:o,height:c,content:``})},t.getSVGData=d})); diff --git a/package-lock.json b/package-lock.json index f40b821..d4cf507 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lindsvg", - "version": "1.3.2", + "version": "1.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lindsvg", - "version": "1.3.2", + "version": "1.3.3", "license": "MIT", "devDependencies": { "@rollup/plugin-terser": "^0.4.4", diff --git a/package.json b/package.json index 8a068d4..1210814 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,17 @@ { "name": "lindsvg", - "version": "1.3.2", + "version": "1.3.3", "description": "Lindenmayer System [Scalable] Vector Graphics", - "main": "dist/lindsvg.js", - "module": "dist/lindsvg.esm.js", - "unpkg": "dist/lindsvg.js", + "main": "./dist/lindsvg.js", + "module": "./dist/lindsvg.esm.js", + "unpkg": "./dist/lindsvg.js", + "exports": { + ".": { + "import": "./dist/lindsvg.esm.js", + "require": "./dist/lindsvg.cjs" + } + }, + "type": "module", "files": [ "dist", "src" diff --git a/rollup.config.mjs b/rollup.config.mjs index ea5a89c..96c5b89 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,4 +1,4 @@ -import pkg from "./package.json" assert {type: "json"}; +import pkg from "./package.json" with {type: "json"}; import terser from "@rollup/plugin-terser"; let config = { @@ -9,32 +9,36 @@ let config = { ${pkg.name} v${pkg.version} ${pkg.homepage} (c) ${new Date().getUTCFullYear()} ${pkg.author} -*/` +*/`, }, plugins: [ terser({ - output: {comments: /^!/} - }) - ] + output: {comments: /^!/}, + }), + ], }; export default [ { input: config.input, - output: {file: "dist/lindsvg.esm.js", format: "esm", ...config.output} + output: {file: "dist/lindsvg.esm.js", format: "esm", ...config.output}, }, { input: config.input, output: {file: "dist/lindsvg.esm.min.js", format: "esm", ...config.output}, - plugins: config.plugins + plugins: config.plugins, }, { input: config.input, - output: {file: "dist/lindsvg.js", format: "umd", ...config.output} + output: {file: "dist/lindsvg.js", format: "umd", ...config.output}, }, { input: config.input, output: {file: "dist/lindsvg.min.js", format: "umd", ...config.output}, - plugins: config.plugins - } + plugins: config.plugins, + }, + { + input: config.input, + output: {file: "dist/lindsvg.cjs", format: "cjs", ...config.output}, + }, ]; diff --git a/src/generator.mjs b/src/generator.mjs index 1f46091..50d850e 100644 --- a/src/generator.mjs +++ b/src/generator.mjs @@ -8,7 +8,7 @@ let ctrlRules = { "+": "+", "-": "-", "[": "[", - "]": "]" + "]": "]", }; /** @type {LSParams} */ @@ -16,7 +16,7 @@ let defaults = { alpha: 0, theta: 0, step: 10, - iterations: 3 + iterations: 3, }; /** diff --git a/src/ls-error.mjs b/src/ls-error.mjs index 9b9addb..422b8e0 100644 --- a/src/ls-error.mjs +++ b/src/ls-error.mjs @@ -25,7 +25,7 @@ Object.defineProperty(LSError.prototype, "name", { configurable: true, enumerable: false, writable: true, - value: "LSError" + value: "LSError", }); export {LSError}; diff --git a/src/svg.mjs b/src/svg.mjs index b966639..b152c3d 100644 --- a/src/svg.mjs +++ b/src/svg.mjs @@ -1,5 +1,5 @@ import {generateCodeword, tokenizeCodeword} from "./generator.mjs"; -import {createTurtle} from "./turtle.mjs"; +import {Turtle} from "./turtle.mjs"; function formatCoordinates(x, y) { // Unary plus is used to remove insignificant trailing zeros @@ -129,11 +129,11 @@ function getMultiPathData(tokens, turtle) { */ export function getSVGData(lsParams) { let codeword = generateCodeword(lsParams); - let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let turtle = new Turtle({x: 0, y: 0, ...lsParams}); let pathData = getPathData(tokenizeCodeword(codeword), turtle); return { pathData, - ...turtle.getDrawingRect() + ...turtle.getDrawingRect(), }; } @@ -144,11 +144,11 @@ export function getSVGData(lsParams) { */ export function getMultiPathSVGData(lsParams) { let codeword = generateCodeword(lsParams); - let turtle = createTurtle({x: 0, y: 0, ...lsParams}); + let turtle = new Turtle({x: 0, y: 0, ...lsParams}); let multiPathData = getMultiPathData(tokenizeCodeword(codeword), turtle); return { multiPathData, - ...turtle.getDrawingRect() + ...turtle.getDrawingRect(), }; } @@ -161,8 +161,8 @@ function makeSVGConfig(svgParams, naturalWidth, naturalHeight) { // for backward compatibility with v1.1.0, also check fill and stroke as direct props of svgParams fill: svgParams.fill || "none", stroke: svgParams.stroke || "#000", - ...svgParams.pathAttributes - } + ...svgParams.pathAttributes, + }, }; } @@ -175,7 +175,7 @@ function makeAttrString(attrs, index) { if (Array.isArray(value)) { value = value[Math.min(index, value.length - 1)]; } - if (value === undefined) { + if (value === undefined || value.toLowerCase() === "n/a") { return accumulator; } value = value.replace(/"/g, """); @@ -197,7 +197,7 @@ export function getSVGCode(lsParams, svgParams) { viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], width, height, - content: `` + content: ``, }); } @@ -218,6 +218,6 @@ export function getMultiPathSVGCode(lsParams, svgParams) { viewBox: [minX - padding, minY - padding, naturalWidth + 2 * padding, naturalHeight + 2 * padding], width, height, - content + content, }); } diff --git a/src/turtle.mjs b/src/turtle.mjs index 698cd08..2367e52 100644 --- a/src/turtle.mjs +++ b/src/turtle.mjs @@ -1,4 +1,13 @@ -let proto = { +export class Turtle { + constructor({x, y, step, alpha, theta}) { + this.stack = []; + this.x = this.minX = this.maxX = x; + this.y = this.minY = this.maxY = y; + this.step = step; + this.alpha = -alpha; // negate since Y axis is inverted + this.theta = theta; + } + translate(stepCount = 1) { this.x += stepCount * this.step * Math.cos(this.alpha); this.y += stepCount * this.step * Math.sin(this.alpha); @@ -6,20 +15,24 @@ let proto = { 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(factor) { this.alpha += factor * this.theta; - }, + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); } - }, + } + popStack(repeatCount) { for (; repeatCount > 0; repeatCount--) { ({x: this.x, y: this.y, alpha: this.alpha} = this.stack.pop()); } - }, + } + getDrawingRect() { let minX = Math.floor(this.minX); let minY = Math.floor(this.minY); @@ -27,15 +40,4 @@ let proto = { let maxY = Math.ceil(this.maxY); return {minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY}; } -}; - -export function createTurtle({x, y, step, alpha, theta}) { - let turtle = Object.create(proto); - turtle.stack = []; - turtle.x = turtle.minX = turtle.maxX = x; - turtle.y = turtle.minY = turtle.maxY = y; - turtle.step = step; - turtle.alpha = -alpha; // negate since Y axis is inverted - turtle.theta = theta; - return turtle; } diff --git a/src/validator.mjs b/src/validator.mjs index 99edfec..b0ced72 100644 --- a/src/validator.mjs +++ b/src/validator.mjs @@ -6,7 +6,7 @@ let messages = { 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" + NUMBER: "A valid finite number expected", }; let letterRE = /^[A-Z]$/; diff --git a/test/install-test/node-cjs-test.cjs b/test/install-test/node-cjs-test.cjs new file mode 100644 index 0000000..d47eee6 --- /dev/null +++ b/test/install-test/node-cjs-test.cjs @@ -0,0 +1,3 @@ +let lindsvg = require("lindsvg"); + +console.dir(lindsvg); diff --git a/test/install-test/node-esm-test.mjs b/test/install-test/node-esm-test.mjs new file mode 100644 index 0000000..153701b --- /dev/null +++ b/test/install-test/node-esm-test.mjs @@ -0,0 +1,3 @@ +import * as lindsvg from "lindsvg"; + +console.dir(lindsvg); diff --git a/test/install-test/package-lock.json b/test/install-test/package-lock.json new file mode 100644 index 0000000..b68467a --- /dev/null +++ b/test/install-test/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "lindsvg-install-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lindsvg-install-test", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "lindsvg": "file:../../lindsvg-1.3.3.tgz" + } + }, + "node_modules/lindsvg": { + "version": "1.3.3", + "resolved": "file:../../lindsvg-1.3.3.tgz", + "integrity": "sha512-ZVyUCjg96eoY4onY34/kzxnme8IR8hrrYgPb95Y2n7OkPiDqmMIapuqyka3dO4mOveFqRKgRT8QJQpXdOQHrvQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + } + } + } +} diff --git a/test/install-test/package.json b/test/install-test/package.json new file mode 100644 index 0000000..1df1d93 --- /dev/null +++ b/test/install-test/package.json @@ -0,0 +1,14 @@ +{ + "name": "lindsvg-install-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Amphiluke", + "license": "MIT", + "dependencies": { + "lindsvg": "file:../../lindsvg-1.3.3.tgz" + } +} diff --git a/test/node-cjs-test.js b/test/node-cjs-test.cjs similarity index 72% rename from test/node-cjs-test.js rename to test/node-cjs-test.cjs index c4aaed1..f114b36 100644 --- a/test/node-cjs-test.js +++ b/test/node-cjs-test.cjs @@ -1,25 +1,21 @@ -let {getSVGCode, getMultiPathSVGCode} = require("../dist/lindsvg.js"); -let {promisify} = require("util"); -let {writeFile, unlink} = require("fs"); -let {join} = require("path"); +let {getSVGCode, getMultiPathSVGCode} = require("../dist/lindsvg.cjs"); +let {writeFile, unlink} = require("node:fs/promises"); +let {join} = require("node:path"); let {dump} = require("js-yaml"); -let asyncWriteFile = promisify(writeFile); -let asyncUnlink = promisify(unlink); - async function runTest() { let {singlePathLSParams, multiPathLSParams, lsInvalidParams, singlePathSVGParams, multiPathSVGParams} = await import("./params.mjs"); let singlePathSVG = join(__dirname, "svg", "single-path.svg"); let multiPathSVG = join(__dirname, "svg", "multi-path.svg"); - await Promise.allSettled([asyncUnlink(singlePathSVG), asyncUnlink(multiPathSVG)]); + await Promise.allSettled([unlink(singlePathSVG), unlink(multiPathSVG)]); let singlePathSVGCode = getSVGCode(singlePathLSParams, singlePathSVGParams); - await asyncWriteFile(singlePathSVG, singlePathSVGCode); + await writeFile(singlePathSVG, singlePathSVGCode); let multiPathSVGCode = getMultiPathSVGCode(multiPathLSParams, multiPathSVGParams); - await asyncWriteFile(multiPathSVG, multiPathSVGCode); + await writeFile(multiPathSVG, multiPathSVGCode); try { console.log("Below, an exception is expected"); diff --git a/test/node-esm-test.mjs b/test/node-esm-test.mjs index eba6df4..0094796 100644 --- a/test/node-esm-test.mjs +++ b/test/node-esm-test.mjs @@ -1,44 +1,33 @@ import {getSVGCode, getMultiPathSVGCode} from "../src/lindsvg.mjs"; -import {promisify} from "util"; -import {writeFile, unlink} from "fs"; -import {join, dirname} from "path"; -import {fileURLToPath} from "url"; - -import {createRequire} from "module"; -let require = createRequire(import.meta.url); -let {dump} = require("js-yaml"); +import {writeFile, unlink} from "node:fs/promises"; +import {join, dirname} from "node:path"; +import {fileURLToPath} from "node:url"; +import {dump} from "js-yaml"; let __dirname = dirname(fileURLToPath(import.meta.url)); -let asyncWriteFile = promisify(writeFile); -let asyncUnlink = promisify(unlink); - -async function runTest() { - let {singlePathLSParams, multiPathLSParams, lsInvalidParams, singlePathSVGParams, multiPathSVGParams} = await import("./params.mjs"); +let {singlePathLSParams, multiPathLSParams, lsInvalidParams, singlePathSVGParams, multiPathSVGParams} = await import("./params.mjs"); - let singlePathSVG = join(__dirname, "svg", "single-path.svg"); - let multiPathSVG = join(__dirname, "svg", "multi-path.svg"); +let singlePathSVG = join(__dirname, "svg", "single-path.svg"); +let multiPathSVG = join(__dirname, "svg", "multi-path.svg"); - await Promise.allSettled([asyncUnlink(singlePathSVG), asyncUnlink(multiPathSVG)]); +await Promise.allSettled([unlink(singlePathSVG), unlink(multiPathSVG)]); - let singlePathSVGCode = getSVGCode(singlePathLSParams, singlePathSVGParams); - await asyncWriteFile(singlePathSVG, singlePathSVGCode); +let singlePathSVGCode = getSVGCode(singlePathLSParams, singlePathSVGParams); +await writeFile(singlePathSVG, singlePathSVGCode); - let multiPathSVGCode = getMultiPathSVGCode(multiPathLSParams, multiPathSVGParams); - await asyncWriteFile(multiPathSVG, multiPathSVGCode); +let multiPathSVGCode = getMultiPathSVGCode(multiPathLSParams, multiPathSVGParams); +await writeFile(multiPathSVG, multiPathSVGCode); - try { - console.log("Below, an exception is expected"); - console.log(getMultiPathSVGCode(lsInvalidParams, multiPathSVGParams)); - } 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(dump(errorJSON, {indent: 4})); - } +try { + console.log("Below, an exception is expected"); + console.log(getMultiPathSVGCode(lsInvalidParams, multiPathSVGParams)); +} 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(dump(errorJSON, {indent: 4})); } } - -runTest(); diff --git a/test/params.mjs b/test/params.mjs index 1336e42..225c5e5 100644 --- a/test/params.mjs +++ b/test/params.mjs @@ -7,7 +7,7 @@ export let singlePathLSParams = { alpha: 90 * Math.PI / 180, theta: 20 * Math.PI / 180, iterations: 7, - step: 2 + step: 2, }; export let multiPathLSParams = { @@ -20,7 +20,7 @@ export let multiPathLSParams = { alpha: 90 * Math.PI / 180, theta: 10 * Math.PI / 180, iterations: 7, - step: 5 + step: 5, }; export let lsInvalidParams = { @@ -32,7 +32,7 @@ export let lsInvalidParams = { alpha: (90 * Math.PI / 180).toString(), theta: Infinity, iterations: 7.1, - step: -2 + step: -2, }; export let singlePathSVGParams = { @@ -41,8 +41,8 @@ export let singlePathSVGParams = { padding: 10, pathAttributes: { // fill: "skyblue", - stroke: "#69983a" - } + stroke: "#69983a", + }, }; export let multiPathSVGParams = { @@ -54,6 +54,6 @@ export let multiPathSVGParams = { "rgba(131, 163, 90, 0.5)", "rgba(164, 184, 102, 0.5)", "rgba(192, 200, 97, 0.5)"], "stroke-width": ["11", "5", "3", "1"], "stroke-linecap": ["square", "square", "round"], - transform: ["skewY(-35)", ""] - } + transform: ["skewY(-35)", "n/a"], + }, };