From 7b1a34261ff4b6514e87e55a2db591bb4c5e3c9e Mon Sep 17 00:00:00 2001 From: Saneef Ansari Date: Mon, 23 Oct 2023 02:23:20 +0530 Subject: [PATCH] refactor: break helpers.js in smaller files - Moves utils related tests next to the source files --- lib/helpers.js | 92 -------------------- lib/img2picture.js | 21 +++-- lib/utils/file.js | 20 +++++ lib/utils/image.js | 22 +++++ lib/utils/number.js | 15 ++++ lib/utils/object.js | 36 ++++++++ tests/helpers.js => lib/utils/object.test.js | 4 +- lib/utils/url.js | 39 +++++++++ lib/utils/url.test.js | 15 ++++ package.json | 2 +- 10 files changed, 160 insertions(+), 106 deletions(-) delete mode 100644 lib/helpers.js create mode 100644 lib/utils/file.js create mode 100644 lib/utils/image.js create mode 100644 lib/utils/number.js create mode 100644 lib/utils/object.js rename tests/helpers.js => lib/utils/object.test.js (57%) create mode 100644 lib/utils/url.js create mode 100644 lib/utils/url.test.js diff --git a/lib/helpers.js b/lib/helpers.js deleted file mode 100644 index dced58e..0000000 --- a/lib/helpers.js +++ /dev/null @@ -1,92 +0,0 @@ -// @ts-check - -/** - * Determines whether the specified string is an HTTP(S) URL. - * - * @param {string} str The string - * @return {boolean} `True` if the specified path is an URL. - */ -function isUrl(str) { - return /^https?:\/\//.test(str); -} - -/** - * Checks if the file path matches a whitelist of extensions - * - * @param {string} path The path - * @param {Array} extensions The extensions - * @return {boolean} `True` if matches, `False` otherwise. - */ -function isAllowedExtension(path, extensions) { - const lowerCaseExtensions = extensions.map((x) => x.toLowerCase()); - - return Boolean( - lowerCaseExtensions.find((ext) => path.toLowerCase().endsWith(ext)), - ); -} - -/** - * Parse string with comma separated numbers into array of numbers - * - * @param {string} str The string to parse - * @return {Array} An array of numbers - */ -function parseStringToNumbers(str) { - return str.split(",").map((s) => Number(s)); -} - -/** - * Generate Widths for Image - * - * @param {number} min The minimum - * @param {number} max The maximum - * @param {number} step The step - * @return {Array} Widths - */ -function generateWidths(min, max, step) { - const sizes = []; - for (let i = min; i < max; i += step) { - sizes.push(i); - } - - return sizes; -} - -/** - * Converts object to HTML attributes string - * - * @param {object} obj The object - * @return {string} - */ -function objectToAttributes(obj) { - return Object.keys(obj).reduce((acc, key) => { - // Ignore empty class attribute - if (key === "class" && !obj[key]) return acc; - - return `${acc} ${key}="${obj[key]}"`; - }, ""); -} - -/** - * Removes object properties. - * - * @param {object} obj The object - * @param {Array} props The properties - * @return {} { description_of_the_return_value } - */ -function removeObjectProperties(obj, props = []) { - const copy = { ...obj }; - - props.forEach((key) => delete copy[key]); - - return copy; -} - -module.exports = { - isUrl, - isAllowedExtension, - parseStringToNumbers, - generateWidths, - objectToAttributes, - removeObjectProperties, -}; diff --git a/lib/img2picture.js b/lib/img2picture.js index 5d8b419..817d198 100644 --- a/lib/img2picture.js +++ b/lib/img2picture.js @@ -3,17 +3,16 @@ const cheerio = require("cheerio"); const Image = require("@11ty/eleventy-img"); const path = require("path"); const debug = require("debug")("img2picture"); -const { removeObjectProperties } = require("./helpers"); - -/** @typedef { import('@11ty/eleventy-img').ImageFormatWithAliases } ImageFormatWithAliases */ - const { - isUrl, - isAllowedExtension, - generateWidths, - parseStringToNumbers, + removeObjectProperties, objectToAttributes, -} = require("./helpers.js"); +} = require("./utils/object"); +const { parseStringToNumbers } = require("./utils/number"); +const { isRemoteUrl, getPathFromUrl } = require("./utils/url"); +const { generateWidths } = require("./utils/image"); +const { isAllowedExtension } = require("./utils/file"); + +/** @typedef { import('@11ty/eleventy-img').ImageFormatWithAliases } ImageFormatWithAliases */ /** * @typedef {object} Img2PictureOptions @@ -183,7 +182,7 @@ async function generateImage(attrs, options) { const widths = imgAttrWidths ? parseStringToNumbers(imgAttrWidths) : generateWidths(minWidth, maxWidth, widthStep); - const filePath = isUrl(src) ? src : path.join(eleventyInputDir, src); + const filePath = isRemoteUrl(src) ? src : path.join(eleventyInputDir, src); const filenameFormatFn = typeof filenameFormat === "function" ? filenameFormat : filenameFormatter; @@ -236,7 +235,7 @@ async function replaceImages(content, options) { const src = $(el).attr("src"); if (src) { - return !isUrl(src); + return !isRemoteUrl(src); } return false; diff --git a/lib/utils/file.js b/lib/utils/file.js new file mode 100644 index 0000000..bef1808 --- /dev/null +++ b/lib/utils/file.js @@ -0,0 +1,20 @@ +// @ts-check + +/** + * Checks if the file path matches a whitelist of extensions + * + * @param {string} path The path + * @param {Array} extensions The extensions + * @return {boolean} `True` if matches, `False` otherwise. + */ +function isAllowedExtension(path, extensions) { + const lowerCaseExtensions = extensions.map((x) => x.toLowerCase()); + + return Boolean( + lowerCaseExtensions.find((ext) => path.toLowerCase().endsWith(ext)), + ); +} + +module.exports = { + isAllowedExtension, +}; diff --git a/lib/utils/image.js b/lib/utils/image.js new file mode 100644 index 0000000..abbd1bb --- /dev/null +++ b/lib/utils/image.js @@ -0,0 +1,22 @@ +// @ts-check + +/** + * Generate Widths for Image + * + * @param {number} min The minimum + * @param {number} max The maximum + * @param {number} step The step + * @return {Array} Widths + */ +function generateWidths(min, max, step) { + const sizes = []; + for (let i = min; i < max; i += step) { + sizes.push(i); + } + + return sizes; +} + +module.exports = { + generateWidths, +}; diff --git a/lib/utils/number.js b/lib/utils/number.js new file mode 100644 index 0000000..9927f74 --- /dev/null +++ b/lib/utils/number.js @@ -0,0 +1,15 @@ +// @ts-check + +/** + * Parse string with comma separated numbers into array of numbers + * + * @param {string} str The string to parse + * @return {Array} An array of numbers + */ +function parseStringToNumbers(str) { + return str.split(",").map((s) => Number(s)); +} + +module.exports = { + parseStringToNumbers, +}; diff --git a/lib/utils/object.js b/lib/utils/object.js new file mode 100644 index 0000000..86fde06 --- /dev/null +++ b/lib/utils/object.js @@ -0,0 +1,36 @@ +// @ts-check + +/** + * Converts object to HTML attributes string + * + * @param {object} obj The object + * @return {string} + */ +function objectToAttributes(obj) { + return Object.keys(obj).reduce((acc, key) => { + // Ignore empty class attribute + if (key === "class" && !obj[key]) return acc; + + return `${acc} ${key}="${obj[key]}"`; + }, ""); +} + +/** + * Removes object properties. + * + * @param {object} obj The object + * @param {Array} props The properties + * @return {object} Shallow cloned object without properties + */ +function removeObjectProperties(obj, props = []) { + const copy = { ...obj }; + + props.forEach((key) => delete copy[key]); + + return copy; +} + +module.exports = { + objectToAttributes, + removeObjectProperties, +}; diff --git a/tests/helpers.js b/lib/utils/object.test.js similarity index 57% rename from tests/helpers.js rename to lib/utils/object.test.js index f92bd0f..6fa2b48 100644 --- a/tests/helpers.js +++ b/lib/utils/object.test.js @@ -1,8 +1,8 @@ const test = require("ava"); -const { removeObjectProperties } = require("../lib/helpers"); +const { removeObjectProperties } = require("./object"); -test("Should remove name and age properties from object", async (t) => { +test("removeObjectProperties: should remove name and age properties from object", async (t) => { const obj = { name: "Jane Doe", age: 20, diff --git a/lib/utils/url.js b/lib/utils/url.js new file mode 100644 index 0000000..40e80ce --- /dev/null +++ b/lib/utils/url.js @@ -0,0 +1,39 @@ +// @ts-check + +/** + * Determines whether the specified string is an HTTP(S) URL. + * + * @param {string} url The string + * @return {boolean} `True` if the specified path is an URL. + * + * Code from https://github.com/11ty/eleventy-img/blob/a2eb5d0e0e4cf3ce2dd330a7ac09ece676bfb7cd/img.js + */ +function isRemoteUrl(url) { + try { + const validUrl = new URL(url); + + if ( + validUrl.protocol.startsWith("https:") || + validUrl.protocol.startsWith("http:") + ) { + return true; + } + + return false; + } catch { + // Invalid url OR local path + return false; + } +} + +function getPathFromUrl(url) { + try { + const validUrl = new URL(url); + return validUrl.pathname; + } catch {} +} + +module.exports = { + isRemoteUrl, + getPathFromUrl, +}; diff --git a/lib/utils/url.test.js b/lib/utils/url.test.js new file mode 100644 index 0000000..3a42f19 --- /dev/null +++ b/lib/utils/url.test.js @@ -0,0 +1,15 @@ +const test = require("ava"); + +const { isRemoteUrl } = require("./url"); + +test("isRemoteUrl: should return true for valid URLs", async (t) => { + t.true(isRemoteUrl("https://example.com")); + t.true(isRemoteUrl("https://example.com/image.jpeg")); + t.true(isRemoteUrl("https://example.com/image.jpeg")); +}); + +test("isRemoteUrl: should return false for invalid URLs", async (t) => { + t.false(isRemoteUrl("ftp://example.com")); + t.false(isRemoteUrl("//example.com/image.jpeg")); + t.false(isRemoteUrl("ssh://example.com/image.jpeg")); +}); diff --git a/package.json b/package.json index 8395940..23d6cf9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "url": "git+https://github.com:saneef/eleventy-plugin-img2picture.git" }, "scripts": { - "lint": "eslint lib/**.js tests/**.js", "test": "ava --timeout 1m --no-worker-threads", "prepare": "husky install" }, @@ -48,6 +47,7 @@ "failFast": false, "files": [ "tests/**/*", + "**/*.test.js", "!tests/utils.js" ], "ignoredByWatcher": [