hokey-cokey
Advanced tools
Comparing version 1.1.0 to 2.0.0
// RegExp to find named variables in several formats e.g. `:a`, `${b}`, `{{c}}` or `{d}` | ||
const R_NAMED = /:[a-z][a-z0-9]*|\$\{[a-z][a-z0-9]*\}|\{\{[a-z][a-z0-9]*\}\}|\{[a-z][a-z0-9]*\}/gi; | ||
const R_NAMED = /\*|:[a-z][a-z0-9]*|\$\{[a-z][a-z0-9]*\}|\{\{[a-z][a-z0-9]*\}\}|\{[a-z][a-z0-9]*\}/gi; | ||
// Find actual name within template parameter e.g. `${name}` → `name` | ||
// Find actual name within template placeholder e.g. `${name}` → `name` | ||
const R_NAME = /[a-z0-9]+/i; | ||
// Cache of templates. | ||
const templates = {}; | ||
const cache = {}; | ||
/** | ||
* Split up a template into an array of separator → param → separator → param → separator | ||
* i.e. odd numbers are separators (possibly zero length) and even numbers are params. | ||
* Split up a template into an array of separator → placeholder → separator → placeholder → separator | ||
* i.e. odd numbers are separators (possibly zero length) and even numbers are placeholders. | ||
* | ||
* @param {string} template The template including template parameters, e.g. `:name-${country}/{city}` | ||
* @returns {string[]} Array of strings alternating separator and param. | ||
* @param {string} template The template including template placeholders, e.g. `:name-${country}/{city}` | ||
* @returns {string[]} Array of strings alternating separator and placeholder. | ||
*/ | ||
function split(template) { | ||
// Checks. | ||
if (typeof template !== "string") throw new TypeError("split(): template must be string"); | ||
if (typeof template !== "string") throw new TypeError("split(): template: Must be string"); | ||
// Create if not already in cache. | ||
if (!templates.hasOwnProperty(template)) { | ||
if (!cache.hasOwnProperty(template)) { | ||
// Vars. | ||
const splits = []; | ||
// Match each param. | ||
let match, last; | ||
while ((match = R_NAMED.exec(template))) { | ||
// Match each placeholder. | ||
let m, l; | ||
while ((m = R_NAMED.exec(template))) { | ||
// Add separator. | ||
splits.push(template.substring(last, match.index)); | ||
// Add param. | ||
splits.push(match[0]); | ||
splits.push(template.substring(l, m.index)); | ||
// Add placeholder. | ||
splits.push(m[0]); | ||
// Save last index. | ||
last = R_NAMED.lastIndex; | ||
l = R_NAMED.lastIndex; | ||
} | ||
// Add last separator. | ||
splits.push(template.substring(last)); | ||
splits.push(template.substring(l)); | ||
// Freeze. | ||
templates[template] = Object.freeze(splits); | ||
cache[template] = Object.freeze(splits); | ||
} | ||
// Return. | ||
return templates[template]; | ||
return cache[template]; | ||
} | ||
/** | ||
* Get the actual clean name (e.g. `city`) from a parameter (e.g. `{city}`) | ||
* Get the actual clean name (e.g. `city`) from a placeholder (e.g. `{city}`) | ||
* | ||
* @param {string} param The input param, e.g. `${country}` or `{{country}}` | ||
* @returns {string} The param name, e.g. `country` | ||
* @param {string} placeholder The input placeholder, e.g. `${country}` or `{{country}}` | ||
* @returns {string} The placeholder name, e.g. `country` | ||
* | ||
* @internal | ||
*/ | ||
function cleanParam(param) { | ||
return R_NAME.exec(param)[0]; | ||
function clean(placeholder) { | ||
return R_NAME.exec(placeholder)[0]; | ||
} | ||
/** | ||
* Get list of params named in a template string. | ||
* Get list of placeholders named in a template string. | ||
* | ||
* @param {string} template The template including template parameters, e.g. `:name-${country}/{city}` | ||
* @returns {string[]} Array of clean string names of found params, e.g. `["name", "country", "city"]` | ||
* @param {string} template The template including template placeholders, e.g. `:name-${country}/{city}` | ||
* @returns {string[]} Array of clean string names of found placeholders, e.g. `["name", "country", "city"]` | ||
*/ | ||
function params(template) { | ||
function placeholders(template) { | ||
// Checks. | ||
if (typeof template !== "string") throw new TypeError("params(): template must be string"); | ||
if (typeof template !== "string") throw new TypeError("placeholders(): template: Must be string"); | ||
// Get chunks, filter out separators (zero-index even numbers), and map to clean names. | ||
const names = split(template) | ||
.filter((c, i) => i % 2) | ||
.map(cleanParam); | ||
// Vars. | ||
const chunks = split(template); | ||
const names = []; | ||
let num = 0; | ||
// Loop through chunks and save cleaned names to names. | ||
for (let i = 1; i < chunks.length; i += 2) | ||
// Push num++ for * placeholders or clean name otherwise. | ||
names.push(chunks[i] === "*" ? "" + num++ : clean(chunks[i])); | ||
// Return names (frozen). | ||
@@ -82,12 +87,37 @@ return Object.freeze(names); | ||
* | ||
* @param {string} template The template including template parameters, e.g. `:name-${country}/{city}` | ||
* @param {string|string[]|Function} templates The template including template placeholders, e.g. `:name-${country}/{city}`, an iterable object (e.g. array) that returns a set of templates to be tested, or function or generator that returns those. | ||
* @param {string} target The string containing values, e.g. `Dave-UK/Manchester` | ||
* @return {Object|false} An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }`, or false if target didn't match template. | ||
*/ | ||
function extract(template, target) { | ||
function match(templates, target) { | ||
// Call templates() first if it's function. | ||
if (typeof templates === "function") templates = templates(target); | ||
// Checks. | ||
if (typeof template !== "string") throw new TypeError("extract(): template must be string"); | ||
if (typeof target !== "string") throw new TypeError("extract(): target must be string"); | ||
if (typeof templates !== "string" && !isIterable(templates)) | ||
throw new TypeError("match(): template: Must be string or iterable object"); | ||
if (typeof target !== "string") throw new TypeError("match(): target: Must be string"); | ||
// Get separators and params from template. | ||
// What is templates? | ||
if (typeof templates === "string") { | ||
// Templates is string. | ||
const values = matchInternal(templates, target); | ||
if (values) return values; | ||
} else { | ||
// Templates is iterable object. | ||
// Loop through templates and attempt to match each. | ||
for (const template of templates) { | ||
if (typeof template !== "string") throw new TypeError("match(): template: Must yield string"); | ||
const values = matchInternal(template, target); | ||
if (values) return values; | ||
} | ||
} | ||
// Didn't match any template. | ||
return false; | ||
} | ||
// Internal match function (called several times). | ||
function matchInternal(template, target) { | ||
// Get separators and placeholders from template. | ||
const chunks = split(template); | ||
@@ -100,13 +130,14 @@ if (chunks.length < 2) return template === target ? {} : false; // Return early if empty. | ||
// Loop through the params in the chunks. | ||
// Loop through the placeholders in the chunks. | ||
const values = {}; | ||
for (let i = 1; i < chunks.length; i += 2) { | ||
let num = 0; | ||
for (let j = 1; j < chunks.length; j += 2) { | ||
// Increment cursor position. | ||
cursor += chunks[i - 1].length; | ||
cursor += chunks[j - 1].length; | ||
// Check next separator length. | ||
const separator = chunks[i + 1]; | ||
const isLast = i >= chunks.length - 2; | ||
const separator = chunks[j + 1]; | ||
const isLast = j >= chunks.length - 2; | ||
if (!isLast && !separator.length) | ||
throw new Error("extract(): template parameters must be separated by at least one character"); | ||
throw new SyntaxError("match(): template: Placeholders must be separated by at least one character"); | ||
@@ -117,4 +148,5 @@ // Get position of next separator in target. | ||
// Save param value to values. | ||
const name = cleanParam(chunks[i]); | ||
// Save placeholder value to values. | ||
const placeholder = chunks[j]; | ||
const name = placeholder === "*" ? "" + num++ : clean(placeholder); // num++ for * placeholders or clean name otherwise. | ||
const value = target.substring(cursor, next); | ||
@@ -134,15 +166,20 @@ values[name] = value; | ||
// Is a value an iterable object? | ||
function isIterable(obj) { | ||
return typeof obj === "object" && obj !== null && typeof obj[Symbol.iterator] === "function"; | ||
} | ||
/** | ||
* Turn ":year-:month" and `{ year: "2016"... }` etc into "2016-06..." etc. | ||
* | ||
* @param {string} template The template including template parameters, e.g. `:name-${country}/{city}` | ||
* @param {Object|string|Function} values An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }` (functions are called, everything else converted to string), or a function or string to use for all parameters. | ||
* @param {string} template The template including template placeholders, e.g. `:name-${country}/{city}` | ||
* @param {Object|string|Function} values An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }` (functions are called, everything else converted to string), or a function or string to use for all placeholders. | ||
* @return {string} The rendered string, e.g. `Dave-UK/Manchester` | ||
* | ||
* @throws {ReferenceError} If a param in the template string is not specified in values. | ||
* @throws {ReferenceError} If a placeholder in the template string is not specified in values. | ||
*/ | ||
function render(template, values) { | ||
// Checks. | ||
if (typeof template !== "string") throw new TypeError("render(): template must be string"); | ||
if (typeof values === "undefined") throw new TypeError("render(): values must be defined"); | ||
if (typeof template !== "string") throw new TypeError("render(): template: Must be string"); | ||
if (typeof values === "undefined") throw new TypeError("render(): values: Must be defined"); | ||
@@ -154,7 +191,8 @@ // Vars. | ||
// Render. | ||
if (chunks.length > 1) | ||
if (chunks.length > 1) { | ||
let num = 0; | ||
for (let i = 1; i < chunks.length; i += 2) { | ||
// Vars. | ||
const param = chunks[i]; | ||
const name = cleanParam(param); | ||
const placeholder = chunks[i]; | ||
const name = placeholder === "*" ? "" + num++ : clean(placeholder); // num++ for * placeholders or clean name otherwise. | ||
let value; | ||
@@ -168,3 +206,3 @@ | ||
// Object use named key. | ||
if (!values.hasOwnProperty(name)) throw new Error(`render(): values.${name} must be set`); | ||
if (!values.hasOwnProperty(name)) throw new ReferenceError(`render(): values.${name}: Must be defined`); | ||
const v = values[name]; | ||
@@ -178,4 +216,5 @@ value = typeof v === "function" ? v() : v; | ||
// Update output. | ||
output = output.replace(param, value); | ||
output = output.replace(placeholder, value); | ||
} | ||
} | ||
@@ -188,4 +227,4 @@ // Return output. | ||
module.exports.split = split; | ||
module.exports.params = params; | ||
module.exports.extract = extract; | ||
module.exports.placeholders = placeholders; | ||
module.exports.match = match; | ||
module.exports.render = render; |
{ | ||
"name": "hokey-cokey", | ||
"description": "Render values into templates OR extract values out of templates. That's what it's all about.", | ||
"version": "1.1.0", | ||
"version": "2.0.0", | ||
"license": "0BSD", | ||
@@ -48,4 +48,3 @@ "repository": { | ||
"parserOptions": { | ||
"sourceType": "module", | ||
"ecmaVersion": 2017 | ||
"sourceType": "module" | ||
}, | ||
@@ -52,0 +51,0 @@ "extends": [ |
103
README.md
@@ -1,2 +0,2 @@ | ||
# Hokey Cokey! Simple string template rendering and parameter value extracting | ||
# Hokey Cokey! Simple string template rendering and matching | ||
@@ -9,21 +9,19 @@ [![Travis CI](https://travis-ci.com/dhoulb/hokey-cokey.svg?branch=master)](https://travis-ci.com/dhoulb/hokey-cokey) | ||
Hokey Cokey is a quick tool for extracting parameters from, and inserting parameters into, template strings: | ||
Hokey Cokey is a quick tool for matching placeholders from, and inserting placeholders into, template strings. Useful for cases where RegExps are too fussy and you want something chunky and robust without having to worry about edge cases. | ||
**Extract:** | ||
- Match a target string against a template e.g. `places/france/paris` and `places/:country/{city}` | ||
- Return an object listing found values e.g. `{ country: "france", city: "paris" }` | ||
**Match:** Match a target string against a template and return a list of found values | ||
e.g. `places/:country/{city}` + `places/france/paris` = `{ country: "france", city: "paris" }` | ||
**Render:** | ||
- Take a template string and merge in some values e.g. `places/${country}/:city` and `{ country: "france", city: "paris" }` | ||
- Return the rendered string after variables have been inserted e.g. `places/france/paris` | ||
**Render:** Take a template string, merge in some values, and return the rendered string. | ||
e.g. `places/${country}/:city` + `{ country: "france", city: "paris" }` = `places/france/paris` | ||
**Things to know:** | ||
- Works with parameters in `:express`, `{jsx}`, `{{handlebars}}` or `${es6}` format. | ||
- Doesn't do anything complicated (like RegExp patterns, `*` glob patterns, or optional parameters). | ||
- Parameter names must match the simple format `[a-zA-Z][a-zA-Z0-9]`. | ||
- When extracting values the template must have one character between each template parameter. | ||
- Template destructuring is cached internally for performance. | ||
- Well unit-tested and 100% covered. | ||
- Useful for cases where RegExps are too fussy and you want something chunky and robust. | ||
- Works with named placeholders in multiple formats: `:express`, `{jsx}`, `{{handlebars}}` or `${es6}` | ||
- Named placeholders must match the format `[a-zA-Z][a-zA-Z0-9]` | ||
- Works with `*` placeholder (matched values are zero-indexed) | ||
- Doesn't do anything complicated like RegExp patterns or optional placeholders | ||
- Template parsing is cached for performance | ||
- Fully unit-tested, 100% covered, all inputs are validated, throws friendly error messages | ||
## Installation | ||
@@ -37,26 +35,48 @@ | ||
### `extract(template: string, target: string)` Extract values from a string | ||
### `match(templates: string|string[], target: string)` | ||
Extracts a set of values out of a target string based on a template string. | ||
Matches a template string against a target string and return an object containing values corresponding to the placeholders. | ||
- `template` must be a string containing one or more parameters in any allowed format | ||
- Returns an object in `parameter: value` format | ||
- `templates` can be... | ||
- String containing one or more placeholders in any allowed format | ||
- Array (or any iterable object) with multiple template strings to attempt to match | ||
- Function that returns a template string (called with `target`) | ||
- Generator function that yields multiple template strings (called with `target`) | ||
- `target` must be the string to match against `templates` | ||
- Returns an object in `placeholder: value` format | ||
```js | ||
const { extract } = require('hokey-cokey'); | ||
const { match } = require('hokey-cokey'); | ||
// Extract the values. | ||
extract("places/{country}/{city}", "places/france/paris"); // { country: "france", city: "paris" } | ||
extract("places/:country/:city", "places/france/paris"); // { country: "france", city: "paris" } | ||
// Match named placeholders in template. | ||
match("places/{country}/{city}", "places/france/paris"); // { country: "france", city: "paris" } | ||
match("places/:country/:city", "places/france/paris"); // { country: "france", city: "paris" } | ||
// Match numbered placeholders in template. | ||
match("*-*-*", "A-B-C"); // { "0": "A", "1": "B", "2": "C } | ||
// Match several possible templates. | ||
const templates = [ | ||
"${dog}===${cat}", | ||
"{name}@{domain}", | ||
"places/:country/:city", | ||
]; | ||
match(templates, "places/france/paris"); // { country: "france", city: "paris" } | ||
``` | ||
### `render(template, values)` Inject values into a template | ||
Template must have one character between each placeholder or an error will be thrown: | ||
```js | ||
match("{placeholders}{with}{no}{gap}", "abcd"); // throws SyntaxError "template: Placeholders must be separated by at least one character" | ||
``` | ||
### `render(template: string, values: Object|Function|string)` | ||
Render a set of values into a template string. | ||
- `template` must be a string containing one or more parameters in any allowed format | ||
- `template` must be a string containing one or more placeholders in any allowed format | ||
- `values` can be: | ||
- An object containing string or function keys (keys must correspond to parameters in the template or render will fail) | ||
- A function called for each parameter (receives the parameter name) | ||
- A single string used for all parameters | ||
- Object containing string or function keys | ||
- Function called for each placeholder (receives the placeholder name) | ||
- String used for all placeholders | ||
- Returns the rendered string | ||
@@ -67,3 +87,3 @@ | ||
// Render the template with an object. | ||
// Render named values into template. | ||
render("blogs-:category-:slug", { category: "cheeses", slug: "stilton" }); // blogs-cheeses-stilton | ||
@@ -73,17 +93,26 @@ render("blogs-:category-:slug", "Arrrrgh"); // blogs-Arrrrgh-Arrrrgh | ||
// Render numbered values into template (using an array works!) | ||
render("*-*-*", ["A", "B", "C"]); // A-B-C | ||
``` | ||
### `params(template)` Get parameter names from template string | ||
If using an object with values the keys _must_ correspond to placeholders in the template or render will fail: | ||
Parse a template string and return an array of found variables that were found. | ||
```js | ||
render("{name}-{date}", { name: "Dave" }); // Throws ReferenceError "values.date: Must be defined" | ||
render("*-*", { "0": "Dave" }); // Throws ReferenceError "values.1: Must be defined" | ||
``` | ||
- `template` must be a string containing one or more parameters in any allowed format | ||
- Returns an array of string parameter names that were found in the template | ||
### `placeholders(template: string)` | ||
Parse a template string and return an array of found placeholders. | ||
- `template` must be a string containing one or more placeholders in any allowed format | ||
- Returns an array of string placeholder names that were found in the template | ||
```js | ||
const { params } = require("hokey-cokey"); | ||
const { placeholders } = require("hokey-cokey"); | ||
// Extract the parameter names. | ||
params("{username}@{domain}"); // ["username", "domain"] | ||
params(":name // ${age}"); // ["name", "age"] | ||
// Extract the placeholder names. | ||
placeholders("{username}@{domain}"); // ["username", "domain"] | ||
placeholders(":name // ${age}"); // ["name", "age"] | ||
``` |
@@ -9,2 +9,3 @@ const { render } = require("../"); | ||
expect(render("/:a/:b", "123")).toBe("/123/123"); | ||
expect(render("/*/*", "123")).toBe("/123/123"); | ||
}); | ||
@@ -15,2 +16,3 @@ test("Function values", () => { | ||
expect(render("/:a/:b", p => p)).toBe("/a/b"); | ||
expect(render("/*/*", p => p)).toBe("/0/1"); | ||
}); | ||
@@ -21,3 +23,8 @@ test("Object values with string properties", () => { | ||
expect(render("/:a/:b", { a: "1", b: "2" })).toBe("/1/2"); | ||
expect(render("/*/*", { "0": "A", "1": "B" })).toBe("/A/B"); | ||
}); | ||
test("Array values with string items", () => { | ||
expect(render("/*/", ["A"])).toBe("/A/"); | ||
expect(render("/*/*", ["A", "B"])).toBe("/A/B"); | ||
}); | ||
test("Object values with non-string properties", () => { | ||
@@ -27,21 +34,27 @@ expect(render("/:a/:b", { a: 1, b: 2 })).toBe("/1/2"); | ||
expect(render("/:a/:b", { a: null, b: undefined })).toBe("/null/undefined"); | ||
expect(render("/*/*", { "0": null, "1": undefined })).toBe("/null/undefined"); | ||
}); | ||
test("Object values with function properties", () => { | ||
expect(render("/:a/:b", { a: () => "1", b: () => "2" })).toBe("/1/2"); | ||
expect(render("/*/*", { "0": () => "A", "1": () => "B" })).toBe("/A/B"); | ||
}); | ||
test("TypeError for wrong input", () => { | ||
expect(() => render(123, {})).toThrow("render(): template must be string"); | ||
expect(() => render(123, {})).toThrow("render(): template: Must be string"); | ||
expect(() => render(123, {})).toThrow(TypeError); | ||
expect(() => render(null, {})).toThrow(TypeError); | ||
expect(() => render(String, {})).toThrow(TypeError); | ||
expect(() => render("/a", undefined)).toThrow("render(): values must be defined"); | ||
expect(() => render("/a", undefined)).toThrow("render(): values: Must be defined"); | ||
}); | ||
test("TypeError for missing parameters", () => { | ||
expect(() => render("/:a", {})).toThrow(Error); | ||
expect(() => render("/:a", {})).toThrow("render(): values.a must be set"); | ||
expect(() => render("/:a/:b", { a: "abc" })).toThrow(Error); | ||
expect(() => render("/:a/:b", { a: "abc" })).toThrow("render(): values.b must be set"); | ||
expect(() => render("/a/:a", {})).toThrow(Error); | ||
expect(() => render("/a/:a", {})).toThrow("render(): values.a must be set"); | ||
test("ReferenceError for missing parameters", () => { | ||
expect(() => render("/:a", {})).toThrow(ReferenceError); | ||
expect(() => render("/:a", {})).toThrow("render(): values.a: Must be defined"); | ||
expect(() => render("/:a/:b", { a: "abc" })).toThrow(ReferenceError); | ||
expect(() => render("/:a/:b", { a: "abc" })).toThrow("render(): values.b: Must be defined"); | ||
expect(() => render("/a/:a", {})).toThrow(ReferenceError); | ||
expect(() => render("/a/:a", {})).toThrow("render(): values.a: Must be defined"); | ||
expect(() => render("/*/", [])).toThrow(ReferenceError); | ||
expect(() => render("/*/", [])).toThrow("render(): values.0: Must be defined"); | ||
expect(() => render("/*/*", ["A"])).toThrow(ReferenceError); | ||
expect(() => render("/*/*", ["A"])).toThrow("render(): values.1: Must be defined"); | ||
}); | ||
}); |
@@ -10,2 +10,5 @@ const { split } = require("../"); | ||
expect(split("/:a{b}${c}{{d}}/")).toEqual(["/", ":a", "", "{b}", "", "${c}", "", "{{d}}", "/"]); | ||
expect(split("*")).toEqual(["", "*", ""]); | ||
expect(split("*/*")).toEqual(["", "*", "/", "*", ""]); | ||
expect(split("/:a{b}**/")).toEqual(["/", ":a", "", "{b}", "", "*", "", "*", "/"]); | ||
}); | ||
@@ -12,0 +15,0 @@ test("Splits correctly using cache", () => { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
296086
399
115
1