@jsenv/url-meta
Advanced tools
Comparing version 6.0.3 to 6.1.0
@@ -26,36 +26,107 @@ const assertUrlLike = (value, name = "url") => { | ||
// https://git-scm.com/docs/gitignore | ||
/* | ||
* | ||
* Link to things doing pattern matching: | ||
* https://git-scm.com/docs/gitignore | ||
* https://github.com/kaelzhang/node-ignore | ||
*/ | ||
/** @module jsenv_url_meta **/ | ||
/** | ||
* An object representing the result of applying a pattern to an url | ||
* @typedef {Object} MatchResult | ||
* @property {boolean} matched Indicates if url matched pattern | ||
* @property {number} patternIndex Index where pattern stopped matching url, otherwise pattern.length | ||
* @property {number} urlIndex Index where url stopped matching pattern, otherwise url.length | ||
* @property {Array} matchGroups Array of strings captured during pattern matching | ||
*/ | ||
/** | ||
* Apply a pattern to an url | ||
* @param {Object} applyPatternMatchingParams | ||
* @param {string} applyPatternMatchingParams.pattern "*", "**" and trailing slash have special meaning | ||
* @param {string} applyPatternMatchingParams.url a string representing an url | ||
* @return {MatchResult} | ||
*/ | ||
const applyPatternMatching = ({ | ||
pattern, | ||
url, | ||
...rest | ||
} = {}) => { | ||
url | ||
}) => { | ||
assertUrlLike(pattern, "pattern"); | ||
assertUrlLike(url, "url"); | ||
if (Object.keys(rest).length) { | ||
throw new Error(`received more parameters than expected. | ||
--- name of unexpected parameters --- | ||
${Object.keys(rest)} | ||
--- name of expected parameters --- | ||
pattern, url`); | ||
} | ||
return applyMatching(pattern, url); | ||
const { | ||
matched, | ||
patternIndex, | ||
index, | ||
groups | ||
} = applyMatching(pattern, url); | ||
const matchGroups = []; | ||
let groupIndex = 0; | ||
groups.forEach(group => { | ||
if (group.name) { | ||
matchGroups[group.name] = group.string; | ||
} else { | ||
matchGroups[groupIndex] = group.string; | ||
groupIndex++; | ||
} | ||
}); | ||
return { | ||
matched, | ||
patternIndex, | ||
urlIndex: index, | ||
matchGroups | ||
}; | ||
}; | ||
const applyMatching = (pattern, string) => { | ||
const groups = []; | ||
let patternIndex = 0; | ||
let index = 0; | ||
let remainingPattern = pattern; | ||
let remainingString = string; // eslint-disable-next-line no-constant-condition | ||
let remainingString = string; | ||
let restoreIndexes = true; | ||
while (true) { | ||
const consumePattern = count => { | ||
const subpattern = remainingPattern.slice(0, count); | ||
remainingPattern = remainingPattern.slice(count); | ||
patternIndex += count; | ||
return subpattern; | ||
}; | ||
const consumeString = count => { | ||
const substring = remainingString.slice(0, count); | ||
remainingString = remainingString.slice(count); | ||
index += count; | ||
return substring; | ||
}; | ||
const consumeRemainingString = () => { | ||
return consumeString(remainingString.length); | ||
}; | ||
let matched; | ||
const iterate = () => { | ||
const patternIndexBefore = patternIndex; | ||
const indexBefore = index; | ||
matched = matchOne(); | ||
if (matched === undefined) { | ||
consumePattern(1); | ||
consumeString(1); | ||
iterate(); | ||
return; | ||
} | ||
if (matched === false && restoreIndexes) { | ||
patternIndex = patternIndexBefore; | ||
index = indexBefore; | ||
} | ||
}; | ||
const matchOne = () => { | ||
// pattern consumed and string consumed | ||
if (remainingPattern === "" && remainingString === "") { | ||
// pass because string fully matched pattern | ||
return pass({ | ||
patternIndex, | ||
index | ||
}); | ||
return true; // string fully matched pattern | ||
} // pattern consumed, string not consumed | ||
@@ -65,8 +136,4 @@ | ||
if (remainingPattern === "" && remainingString !== "") { | ||
// fails because string longer than expected | ||
return fail({ | ||
patternIndex, | ||
index | ||
}); | ||
} // from this point pattern is not consumed | ||
return false; // fails because string longer than expected | ||
} // -- from this point pattern is not consumed -- | ||
// string consumed, pattern not consumed | ||
@@ -76,16 +143,16 @@ | ||
if (remainingString === "") { | ||
// pass because trailing "**" is optional | ||
if (remainingPattern === "**") { | ||
return pass({ | ||
patternIndex: patternIndex + 2, | ||
index | ||
// trailing "**" is optional | ||
consumePattern(2); | ||
return true; | ||
} | ||
if (remainingPattern === "*") { | ||
groups.push({ | ||
string: "" | ||
}); | ||
} // fail because string shorted than expected | ||
} | ||
return fail({ | ||
patternIndex, | ||
index | ||
}); | ||
} // from this point pattern and string are not consumed | ||
return false; // fail because string shorter than expected | ||
} // -- from this point pattern and string are not consumed -- | ||
// fast path trailing slash | ||
@@ -95,14 +162,12 @@ | ||
if (remainingPattern === "/") { | ||
// pass because trailing slash matches remaining | ||
if (remainingString[0] === "/") { | ||
return pass({ | ||
patternIndex: patternIndex + 1, | ||
index: string.length | ||
// trailing slash match remaining | ||
consumePattern(1); | ||
groups.push({ | ||
string: consumeRemainingString() | ||
}); | ||
return true; | ||
} | ||
return fail({ | ||
patternIndex, | ||
index | ||
}); | ||
return false; | ||
} // fast path trailing '**' | ||
@@ -112,7 +177,5 @@ | ||
if (remainingPattern === "**") { | ||
// pass because trailing ** matches remaining | ||
return pass({ | ||
patternIndex: patternIndex + 2, | ||
index: string.length | ||
}); | ||
consumePattern(2); | ||
consumeRemainingString(); | ||
return true; | ||
} // pattern leading ** | ||
@@ -122,10 +185,6 @@ | ||
if (remainingPattern.slice(0, 2) === "**") { | ||
// consumes "**" | ||
remainingPattern = remainingPattern.slice(2); | ||
patternIndex += 2; | ||
consumePattern(2); // consumes "**" | ||
if (remainingPattern[0] === "/") { | ||
// consumes "/" | ||
remainingPattern = remainingPattern.slice(1); | ||
patternIndex += 1; | ||
consumePattern(1); // consumes "/" | ||
} // pattern ending with ** always match remaining string | ||
@@ -135,6 +194,4 @@ | ||
if (remainingPattern === "") { | ||
return pass({ | ||
patternIndex, | ||
index: string.length | ||
}); | ||
consumeRemainingString(); | ||
return true; | ||
} | ||
@@ -144,47 +201,40 @@ | ||
pattern: remainingPattern, | ||
string: remainingString | ||
string: remainingString, | ||
canSkipSlash: true | ||
}); | ||
if (!skipResult.matched) { | ||
return fail({ | ||
patternIndex: patternIndex + skipResult.patternIndex, | ||
index: index + skipResult.index | ||
}); | ||
} | ||
return pass({ | ||
patternIndex: pattern.length, | ||
index: string.length | ||
}); | ||
groups.push(...skipResult.groups); | ||
consumePattern(skipResult.patternIndex); | ||
consumeRemainingString(); | ||
restoreIndexes = false; | ||
return skipResult.matched; | ||
} | ||
if (remainingPattern[0] === "*") { | ||
// consumes "*" | ||
remainingPattern = remainingPattern.slice(1); | ||
patternIndex += 1; // la c'est plus délicat, il faut que remainingString | ||
// ne soit composé que de truc !== '/' | ||
consumePattern(1); // consumes "*" | ||
if (remainingPattern === "") { | ||
// matches everything except '/' | ||
const slashIndex = remainingString.indexOf("/"); | ||
if (slashIndex > -1) { | ||
return fail({ | ||
patternIndex, | ||
index: index + slashIndex | ||
if (slashIndex === -1) { | ||
groups.push({ | ||
string: consumeRemainingString() | ||
}); | ||
return true; | ||
} | ||
return pass({ | ||
patternIndex, | ||
index: string.length | ||
groups.push({ | ||
string: consumeString(slashIndex) | ||
}); | ||
return false; | ||
} // the next char must not the one expected by remainingPattern[0] | ||
// because * is greedy and expect to skip one char | ||
// because * is greedy and expect to skip at least one char | ||
if (remainingPattern[0] === remainingString[0]) { | ||
return fail({ | ||
patternIndex: patternIndex - "*".length, | ||
index | ||
groups.push({ | ||
string: "" | ||
}); | ||
patternIndex = patternIndex - 1; | ||
return false; | ||
} | ||
@@ -195,32 +245,25 @@ | ||
string: remainingString, | ||
skippablePredicate: remainingString => remainingString[0] !== "/" | ||
canSkipSlash: false | ||
}); | ||
if (!skipResult.matched) { | ||
return fail({ | ||
patternIndex: patternIndex + skipResult.patternIndex, | ||
index: index + skipResult.index | ||
}); | ||
} | ||
return pass({ | ||
patternIndex: pattern.length, | ||
index: string.length | ||
}); | ||
groups.push(skipResult.group, ...skipResult.groups); | ||
consumePattern(skipResult.patternIndex); | ||
consumeString(skipResult.index); | ||
restoreIndexes = false; | ||
return skipResult.matched; | ||
} | ||
if (remainingPattern[0] !== remainingString[0]) { | ||
return fail({ | ||
patternIndex, | ||
index | ||
}); | ||
} // consumes next char | ||
return false; | ||
} | ||
return undefined; | ||
}; | ||
remainingPattern = remainingPattern.slice(1); | ||
remainingString = remainingString.slice(1); | ||
patternIndex += 1; | ||
index += 1; | ||
continue; | ||
} | ||
iterate(); | ||
return { | ||
matched, | ||
patternIndex, | ||
index, | ||
groups | ||
}; | ||
}; | ||
@@ -231,76 +274,61 @@ | ||
string, | ||
skippablePredicate = () => true | ||
canSkipSlash | ||
}) => { | ||
let index = 0; | ||
let remainingString = string; | ||
let bestMatch = null; // eslint-disable-next-line no-constant-condition | ||
let longestMatchRange = null; | ||
while (true) { | ||
const tryToMatch = () => { | ||
const matchAttempt = applyMatching(pattern, remainingString); | ||
if (matchAttempt.matched) { | ||
bestMatch = matchAttempt; | ||
break; | ||
return { | ||
matched: true, | ||
patternIndex: matchAttempt.patternIndex, | ||
index: index + matchAttempt.index, | ||
groups: matchAttempt.groups, | ||
group: { | ||
string: remainingString === "" ? string : string.slice(0, -remainingString.length) | ||
} | ||
}; | ||
} | ||
const skippable = skippablePredicate(remainingString); | ||
bestMatch = fail({ | ||
patternIndex: bestMatch ? Math.max(bestMatch.patternIndex, matchAttempt.patternIndex) : matchAttempt.patternIndex, | ||
index: index + matchAttempt.index | ||
}); | ||
const matchAttemptIndex = matchAttempt.index; | ||
const matchRange = { | ||
patternIndex: matchAttempt.patternIndex, | ||
index, | ||
length: matchAttemptIndex, | ||
groups: matchAttempt.groups | ||
}; | ||
if (!skippable) { | ||
break; | ||
} // search against the next unattempted string | ||
if (!longestMatchRange || longestMatchRange.length < matchRange.length) { | ||
longestMatchRange = matchRange; | ||
} | ||
const nextIndex = matchAttemptIndex + 1; | ||
const canSkip = nextIndex < remainingString.length && (canSkipSlash || remainingString[0] !== "/"); | ||
remainingString = remainingString.slice(matchAttempt.index + 1); | ||
index += matchAttempt.index + 1; | ||
if (remainingString === "") { | ||
bestMatch = { ...bestMatch, | ||
index: string.length | ||
}; | ||
break; | ||
if (canSkip) { | ||
// search against the next unattempted string | ||
index += nextIndex; | ||
remainingString = remainingString.slice(nextIndex); | ||
return tryToMatch(); | ||
} | ||
continue; | ||
} | ||
return bestMatch; | ||
}; | ||
const pass = ({ | ||
patternIndex, | ||
index | ||
}) => { | ||
return { | ||
matched: true, | ||
index, | ||
patternIndex | ||
return { | ||
matched: false, | ||
patternIndex: longestMatchRange.patternIndex, | ||
index: longestMatchRange.index + longestMatchRange.length, | ||
groups: longestMatchRange.groups, | ||
group: { | ||
string: string.slice(0, longestMatchRange.index) | ||
} | ||
}; | ||
}; | ||
}; | ||
const fail = ({ | ||
patternIndex, | ||
index | ||
}) => { | ||
return { | ||
matched: false, | ||
index, | ||
patternIndex | ||
}; | ||
return tryToMatch(); | ||
}; | ||
const normalizeStructuredMetaMap = (structuredMetaMap, baseUrl, ...rest) => { | ||
const normalizeStructuredMetaMap = (structuredMetaMap, baseUrl) => { | ||
assertUrlLike(baseUrl, "url"); | ||
if (rest.length) { | ||
throw new Error(`received more arguments than expected. | ||
--- number of arguments received --- | ||
${2 + rest.length} | ||
--- number of arguments expected --- | ||
2`); | ||
} | ||
const structuredMetaMapNormalized = {}; | ||
@@ -345,3 +373,3 @@ Object.keys(structuredMetaMap).forEach(metaProperty => { | ||
const structuredMetaMapToMetaMap = (structuredMetaMap, ...rest) => { | ||
const structuredMetaMapToMetaMap = structuredMetaMap => { | ||
if (!isPlainObject(structuredMetaMap)) { | ||
@@ -351,10 +379,2 @@ throw new TypeError(`structuredMetaMap must be a plain object, got ${structuredMetaMap}`); | ||
if (rest.length) { | ||
throw new Error(`received more arguments than expected. | ||
--- number of arguments received --- | ||
${1 + rest.length} | ||
--- number of arguments expected --- | ||
1`); | ||
} | ||
const metaMap = {}; | ||
@@ -384,4 +404,3 @@ Object.keys(structuredMetaMap).forEach(metaProperty => { | ||
structuredMetaMap, | ||
predicate, | ||
...rest | ||
predicate | ||
}) => { | ||
@@ -398,10 +417,2 @@ assertUrlLike(url, "url"); // the function was meants to be used on url ending with '/' | ||
if (Object.keys(rest).length) { | ||
throw new Error(`received more parameters than expected. | ||
--- name of unexpected parameters --- | ||
${Object.keys(rest)} | ||
--- name of expected parameters --- | ||
url, structuredMetaMap, predicate`); | ||
} | ||
const metaMap = structuredMetaMapToMetaMap(structuredMetaMap); // for full match we must create an object to allow pattern to override previous ones | ||
@@ -416,6 +427,3 @@ | ||
const meta = metaMap[pattern]; | ||
const { | ||
matched, | ||
index | ||
} = applyPatternMatching({ | ||
const matchResult = applyPatternMatching({ | ||
pattern, | ||
@@ -425,3 +433,3 @@ url | ||
if (matched) { | ||
if (matchResult.matched) { | ||
someFullMatch = true; | ||
@@ -431,3 +439,3 @@ fullMatchMeta = { ...fullMatchMeta, | ||
}; | ||
} else if (someFullMatch === false && index >= url.length) { | ||
} else if (someFullMatch === false && matchResult.urlIndex >= url.length) { | ||
partialMatchMetaArray.push(meta); | ||
@@ -446,15 +454,5 @@ } | ||
url, | ||
structuredMetaMap, | ||
...rest | ||
} = {}) => { | ||
structuredMetaMap | ||
}) => { | ||
assertUrlLike(url); | ||
if (Object.keys(rest).length) { | ||
throw new Error(`received more parameters than expected. | ||
--- name of unexpected parameters --- | ||
${Object.keys(rest)} | ||
--- name of expected parameters --- | ||
url, structuredMetaMap`); | ||
} | ||
const metaMap = structuredMetaMapToMetaMap(structuredMetaMap); | ||
@@ -461,0 +459,0 @@ return Object.keys(metaMap).reduce((previousMeta, pattern) => { |
{ | ||
"name": "@jsenv/url-meta", | ||
"version": "6.0.3", | ||
"version": "6.1.0", | ||
"description": "Associate data to urls using patterns", | ||
@@ -10,4 +10,10 @@ "license": "MIT", | ||
}, | ||
"author": { | ||
"name": "dmail", | ||
"email": "dmaillard06@gmail.com", | ||
"url": "https://twitter.com/damienmaillard" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
"access": "public", | ||
"registry": "https://registry.npmjs.org" | ||
}, | ||
@@ -32,4 +38,4 @@ "engines": { | ||
"scripts": { | ||
"dev": "node ./script/dev/start_dev_server.mjs", | ||
"eslint": "node ./node_modules/eslint/bin/eslint.js . --ext=.html,.js,.mjs,.cjs", | ||
"dev": "node ./script/dev/dev.mjs", | ||
"eslint": "npx eslint . --ext=.html,.js,.mjs,.cjs", | ||
"importmap": "node ./script/importmap/importmap.mjs", | ||
@@ -46,13 +52,13 @@ "test": "node --unhandled-rejections=strict ./script/test/test.mjs", | ||
"devDependencies": { | ||
"@jsenv/assert": "2.4.1", | ||
"@jsenv/assert": "2.5.2", | ||
"@jsenv/babel-preset": "1.1.2", | ||
"@jsenv/core": "25.1.1", | ||
"@jsenv/core": "25.4.7", | ||
"@jsenv/eslint-config": "16.0.9", | ||
"@jsenv/file-size-impact": "12.1.1", | ||
"@jsenv/github-release-package": "1.2.3", | ||
"@jsenv/importmap-eslint-resolver": "5.2.2", | ||
"@jsenv/file-size-impact": "12.1.7", | ||
"@jsenv/github-release-package": "1.3.4", | ||
"@jsenv/importmap-eslint-resolver": "5.2.5", | ||
"@jsenv/importmap-node-module": "5.1.0", | ||
"@jsenv/package-publish": "1.6.2", | ||
"@jsenv/performance-impact": "2.2.1", | ||
"eslint": "8.6.0", | ||
"@jsenv/package-publish": "1.7.2", | ||
"@jsenv/performance-impact": "2.2.7", | ||
"eslint": "8.7.0", | ||
"eslint-plugin-html": "6.2.0", | ||
@@ -63,2 +69,2 @@ "eslint-plugin-import": "2.25.4", | ||
} | ||
} | ||
} |
@@ -1,13 +0,5 @@ | ||
# url-meta | ||
# url-meta [![npm package](https://img.shields.io/npm/v/@jsenv/url-meta.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/url-meta) [![github ci](https://github.com/jsenv/url-meta/workflows/main/badge.svg)](https://github.com/jsenv/url-meta/actions?workflow=main) [![codecov coverage](https://codecov.io/gh/jsenv/url-meta/branch/master/graph/badge.svg)](https://codecov.io/gh/jsenv/url-meta) | ||
Associate data to urls using patterns. | ||
_@jsenv/url-meta_ allows to associate value to urls using pattern matching. | ||
[![npm package](https://img.shields.io/npm/v/@jsenv/url-meta.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/url-meta) | ||
[![github ci](https://github.com/jsenv/url-meta/workflows/main/badge.svg)](https://github.com/jsenv/url-meta/actions?workflow=main) | ||
[![codecov coverage](https://codecov.io/gh/jsenv/url-meta/branch/master/graph/badge.svg)](https://codecov.io/gh/jsenv/url-meta) | ||
# Presentation | ||
`@jsenv/url-meta` allows to associate value to urls using pattern matching. | ||
```js | ||
@@ -53,16 +45,2 @@ import { urlToMeta } from "@jsenv/url-meta" | ||
# Pattern matching example | ||
Table showing if a pattern matches when applied to `"file:///directory/file.js"` | ||
| pattern | matches? | | ||
| ---------------------------- | -------- | | ||
| `file:///directory/*.js` | true | | ||
| `file:///directory/**/*.js` | true | | ||
| `file:///**/*.js` | true | | ||
| `file:///directory` | false | | ||
| `file:///directory/` | true | | ||
| `file:///directory/file.js` | true | | ||
| `file:///directory/file.jsx` | false | | ||
# metaMap and structuredMetaMap | ||
@@ -77,3 +55,2 @@ | ||
} | ||
const structuredMetaMap = { | ||
@@ -87,7 +64,7 @@ visible: { | ||
_structuredMetaMap_ allows to group patterns per property which are easier to read and compose. For this reason it's the object structure used by our API. | ||
_structuredMetaMap_ allows to group patterns per property which are easier to read and compose. For this reason it's the object structure used by _@jsenv/url-meta_ API. | ||
# applyPatternMatching | ||
`applyPatternMatching` is a function returning a `matchResult` indicating if and how a `pattern` matches an `url`. | ||
_applyPatternMatching_ is a function returning a _matchResult_ indicating if and how a _pattern_ matches an _url_. | ||
@@ -101,3 +78,2 @@ ```js | ||
}) | ||
matchResult.matched // true | ||
@@ -108,23 +84,42 @@ ``` | ||
`pattern` parameter is a string looking like an url but where `*` and `**` can be used so that one specifier can match several url. This parameter is **required**. | ||
_pattern_ parameter is a string looking like an url but where `*` and `**` can be used so that one specifier can match several url. | ||
This parameter is **required**. | ||
## url | ||
`url` parameter is a string representing a url. This parameter is **required**. | ||
_url_ parameter is a string representing a url. | ||
This parameter is **required**. | ||
## matchResult | ||
`matchResult` represents if and how a `pattern` matches an `url`. | ||
_matchResult_ represents if and how a _pattern_ matches an _url_. | ||
### Matching example | ||
```js | ||
import { applyPatternMatching } from "@jsenv/url-meta" | ||
const fullMatch = applyPatternMatching({ | ||
pattern: "file:///**/*", | ||
url: "file://Users/directory/file.js", | ||
url: "file:///Users/directory/file.js", | ||
}) | ||
fullMatch // { matched: true, index: 31, patternIndex: 12 } | ||
console.log(JSON.stringify(fullMatch, null, " ")) | ||
``` | ||
fullMatch object indicates `pattern` fully matched `url`. | ||
```json | ||
{ | ||
"matched": true, | ||
"patternIndex": 12, | ||
"urlIndex": 31, | ||
"matchGroups": ["file.js"] | ||
} | ||
``` | ||
### Partial matching example | ||
```js | ||
import { applyPatternMatching } from "@jsenv/url-meta" | ||
const partialMatch = applyPatternMatching({ | ||
@@ -134,10 +129,17 @@ pattern: "file:///*.js", | ||
}) | ||
partialMatch // { matched: false, index: 14, patternIndex: 14 } | ||
console.log(JSON.stringify(partialMatch, null, " ")) | ||
``` | ||
partialMatch object indicates `pattern` matched `url` until comparing `url[14]` with `pattern[14]`. | ||
```json | ||
{ | ||
"matched": false, | ||
"patternIndex": 12, | ||
"urlIndex": 15, | ||
"matchGroups": ["file"] | ||
} | ||
``` | ||
# normalizeStructuredMetaMap | ||
`normalizeStructuredMetaMap` is a function resolving a `structuredMetaMap` against an `url`. | ||
_normalizeStructuredMetaMap_ is a function resolving _structuredMetaMap_ keys against an _url_. | ||
@@ -170,3 +172,3 @@ ```js | ||
`urlCanContainsMetaMatching` is a function designed to ignore directory content that would never have specific metas. | ||
_urlCanContainsMetaMatching_ is a function designed to ignore directory content that would never have specific metas. | ||
@@ -200,3 +202,3 @@ ```js | ||
`urlToMeta` is a function returning an object being the composition of all meta where `pattern` matched the `url`. | ||
_urlToMeta_ is a function returning an object being the composition of all meta where _pattern_ matched the _url_. | ||
@@ -214,9 +216,7 @@ ```js | ||
} | ||
const urlA = "file:///src/file.js" | ||
const urlB = "file:///src/file.json" | ||
console.log( | ||
`${urlA}: ${JSON.stringify( | ||
urlToMeta({ url: urlA, specifierMetaMap }), | ||
urlToMeta({ url: urlA, structuredMetaMap }), | ||
null, | ||
@@ -228,3 +228,3 @@ " ", | ||
`${urlB}: ${JSON.stringify( | ||
urlToMeta({ url: urlB, specifierMetaMap }), | ||
urlToMeta({ url: urlB, structuredMetaMap }), | ||
null, | ||
@@ -241,3 +241,3 @@ " ", | ||
"insideSrc": true, | ||
"extensionIsJs": true, | ||
"extensionIsJs": true | ||
} | ||
@@ -244,0 +244,0 @@ file:///src/file.json: { |
@@ -1,20 +0,51 @@ | ||
// https://git-scm.com/docs/gitignore | ||
// https://github.com/kaelzhang/node-ignore | ||
/* | ||
* | ||
* Link to things doing pattern matching: | ||
* https://git-scm.com/docs/gitignore | ||
* https://github.com/kaelzhang/node-ignore | ||
*/ | ||
import { assertUrlLike } from "./internal/assertUrlLike.js" | ||
export const applyPatternMatching = ({ pattern, url, ...rest } = {}) => { | ||
/** @module jsenv_url_meta **/ | ||
/** | ||
* An object representing the result of applying a pattern to an url | ||
* @typedef {Object} MatchResult | ||
* @property {boolean} matched Indicates if url matched pattern | ||
* @property {number} patternIndex Index where pattern stopped matching url, otherwise pattern.length | ||
* @property {number} urlIndex Index where url stopped matching pattern, otherwise url.length | ||
* @property {Array} matchGroups Array of strings captured during pattern matching | ||
*/ | ||
/** | ||
* Apply a pattern to an url | ||
* @param {Object} applyPatternMatchingParams | ||
* @param {string} applyPatternMatchingParams.pattern "*", "**" and trailing slash have special meaning | ||
* @param {string} applyPatternMatchingParams.url a string representing an url | ||
* @return {MatchResult} | ||
*/ | ||
export const applyPatternMatching = ({ pattern, url }) => { | ||
assertUrlLike(pattern, "pattern") | ||
assertUrlLike(url, "url") | ||
if (Object.keys(rest).length) { | ||
throw new Error(`received more parameters than expected. | ||
--- name of unexpected parameters --- | ||
${Object.keys(rest)} | ||
--- name of expected parameters --- | ||
pattern, url`) | ||
const { matched, patternIndex, index, groups } = applyMatching(pattern, url) | ||
const matchGroups = [] | ||
let groupIndex = 0 | ||
groups.forEach((group) => { | ||
if (group.name) { | ||
matchGroups[group.name] = group.string | ||
} else { | ||
matchGroups[groupIndex] = group.string | ||
groupIndex++ | ||
} | ||
}) | ||
return { | ||
matched, | ||
patternIndex, | ||
urlIndex: index, | ||
matchGroups, | ||
} | ||
return applyMatching(pattern, url) | ||
} | ||
const applyMatching = (pattern, string) => { | ||
const groups = [] | ||
let patternIndex = 0 | ||
@@ -24,232 +55,193 @@ let index = 0 | ||
let remainingString = string | ||
let restoreIndexes = true | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
const consumePattern = (count) => { | ||
const subpattern = remainingPattern.slice(0, count) | ||
remainingPattern = remainingPattern.slice(count) | ||
patternIndex += count | ||
return subpattern | ||
} | ||
const consumeString = (count) => { | ||
const substring = remainingString.slice(0, count) | ||
remainingString = remainingString.slice(count) | ||
index += count | ||
return substring | ||
} | ||
const consumeRemainingString = () => { | ||
return consumeString(remainingString.length) | ||
} | ||
let matched | ||
const iterate = () => { | ||
const patternIndexBefore = patternIndex | ||
const indexBefore = index | ||
matched = matchOne() | ||
if (matched === undefined) { | ||
consumePattern(1) | ||
consumeString(1) | ||
iterate() | ||
return | ||
} | ||
if (matched === false && restoreIndexes) { | ||
patternIndex = patternIndexBefore | ||
index = indexBefore | ||
} | ||
} | ||
const matchOne = () => { | ||
// pattern consumed and string consumed | ||
if (remainingPattern === "" && remainingString === "") { | ||
// pass because string fully matched pattern | ||
return pass({ | ||
patternIndex, | ||
index, | ||
}) | ||
return true // string fully matched pattern | ||
} | ||
// pattern consumed, string not consumed | ||
if (remainingPattern === "" && remainingString !== "") { | ||
// fails because string longer than expected | ||
return fail({ | ||
patternIndex, | ||
index, | ||
}) | ||
return false // fails because string longer than expected | ||
} | ||
// from this point pattern is not consumed | ||
// -- from this point pattern is not consumed -- | ||
// string consumed, pattern not consumed | ||
if (remainingString === "") { | ||
// pass because trailing "**" is optional | ||
if (remainingPattern === "**") { | ||
return pass({ | ||
patternIndex: patternIndex + 2, | ||
index, | ||
}) | ||
// trailing "**" is optional | ||
consumePattern(2) | ||
return true | ||
} | ||
// fail because string shorted than expected | ||
return fail({ | ||
patternIndex, | ||
index, | ||
}) | ||
if (remainingPattern === "*") { | ||
groups.push({ string: "" }) | ||
} | ||
return false // fail because string shorter than expected | ||
} | ||
// from this point pattern and string are not consumed | ||
// -- from this point pattern and string are not consumed -- | ||
// fast path trailing slash | ||
if (remainingPattern === "/") { | ||
// pass because trailing slash matches remaining | ||
if (remainingString[0] === "/") { | ||
return pass({ | ||
patternIndex: patternIndex + 1, | ||
index: string.length, | ||
}) | ||
// trailing slash match remaining | ||
consumePattern(1) | ||
groups.push({ string: consumeRemainingString() }) | ||
return true | ||
} | ||
return fail({ | ||
patternIndex, | ||
index, | ||
}) | ||
return false | ||
} | ||
// fast path trailing '**' | ||
if (remainingPattern === "**") { | ||
// pass because trailing ** matches remaining | ||
return pass({ | ||
patternIndex: patternIndex + 2, | ||
index: string.length, | ||
}) | ||
consumePattern(2) | ||
consumeRemainingString() | ||
return true | ||
} | ||
// pattern leading ** | ||
if (remainingPattern.slice(0, 2) === "**") { | ||
// consumes "**" | ||
remainingPattern = remainingPattern.slice(2) | ||
patternIndex += 2 | ||
consumePattern(2) // consumes "**" | ||
if (remainingPattern[0] === "/") { | ||
// consumes "/" | ||
remainingPattern = remainingPattern.slice(1) | ||
patternIndex += 1 | ||
consumePattern(1) // consumes "/" | ||
} | ||
// pattern ending with ** always match remaining string | ||
if (remainingPattern === "") { | ||
return pass({ | ||
patternIndex, | ||
index: string.length, | ||
}) | ||
consumeRemainingString() | ||
return true | ||
} | ||
const skipResult = skipUntilMatch({ | ||
pattern: remainingPattern, | ||
string: remainingString, | ||
canSkipSlash: true, | ||
}) | ||
if (!skipResult.matched) { | ||
return fail({ | ||
patternIndex: patternIndex + skipResult.patternIndex, | ||
index: index + skipResult.index, | ||
}) | ||
} | ||
return pass({ | ||
patternIndex: pattern.length, | ||
index: string.length, | ||
}) | ||
groups.push(...skipResult.groups) | ||
consumePattern(skipResult.patternIndex) | ||
consumeRemainingString() | ||
restoreIndexes = false | ||
return skipResult.matched | ||
} | ||
if (remainingPattern[0] === "*") { | ||
// consumes "*" | ||
remainingPattern = remainingPattern.slice(1) | ||
patternIndex += 1 | ||
// la c'est plus délicat, il faut que remainingString | ||
// ne soit composé que de truc !== '/' | ||
consumePattern(1) // consumes "*" | ||
if (remainingPattern === "") { | ||
// matches everything except '/' | ||
const slashIndex = remainingString.indexOf("/") | ||
if (slashIndex > -1) { | ||
return fail({ | ||
patternIndex, | ||
index: index + slashIndex, | ||
}) | ||
if (slashIndex === -1) { | ||
groups.push({ string: consumeRemainingString() }) | ||
return true | ||
} | ||
return pass({ | ||
patternIndex, | ||
index: string.length, | ||
}) | ||
groups.push({ string: consumeString(slashIndex) }) | ||
return false | ||
} | ||
// the next char must not the one expected by remainingPattern[0] | ||
// because * is greedy and expect to skip one char | ||
// because * is greedy and expect to skip at least one char | ||
if (remainingPattern[0] === remainingString[0]) { | ||
return fail({ | ||
patternIndex: patternIndex - "*".length, | ||
index, | ||
}) | ||
groups.push({ string: "" }) | ||
patternIndex = patternIndex - 1 | ||
return false | ||
} | ||
const skipResult = skipUntilMatch({ | ||
pattern: remainingPattern, | ||
string: remainingString, | ||
skippablePredicate: (remainingString) => remainingString[0] !== "/", | ||
canSkipSlash: false, | ||
}) | ||
if (!skipResult.matched) { | ||
return fail({ | ||
patternIndex: patternIndex + skipResult.patternIndex, | ||
index: index + skipResult.index, | ||
}) | ||
} | ||
return pass({ | ||
patternIndex: pattern.length, | ||
index: string.length, | ||
}) | ||
groups.push(skipResult.group, ...skipResult.groups) | ||
consumePattern(skipResult.patternIndex) | ||
consumeString(skipResult.index) | ||
restoreIndexes = false | ||
return skipResult.matched | ||
} | ||
if (remainingPattern[0] !== remainingString[0]) { | ||
return fail({ | ||
patternIndex, | ||
index, | ||
}) | ||
return false | ||
} | ||
return undefined | ||
} | ||
iterate() | ||
// consumes next char | ||
remainingPattern = remainingPattern.slice(1) | ||
remainingString = remainingString.slice(1) | ||
patternIndex += 1 | ||
index += 1 | ||
continue | ||
return { | ||
matched, | ||
patternIndex, | ||
index, | ||
groups, | ||
} | ||
} | ||
const skipUntilMatch = ({ | ||
pattern, | ||
string, | ||
skippablePredicate = () => true, | ||
}) => { | ||
const skipUntilMatch = ({ pattern, string, canSkipSlash }) => { | ||
let index = 0 | ||
let remainingString = string | ||
let bestMatch = null | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
let longestMatchRange = null | ||
const tryToMatch = () => { | ||
const matchAttempt = applyMatching(pattern, remainingString) | ||
if (matchAttempt.matched) { | ||
bestMatch = matchAttempt | ||
break | ||
return { | ||
matched: true, | ||
patternIndex: matchAttempt.patternIndex, | ||
index: index + matchAttempt.index, | ||
groups: matchAttempt.groups, | ||
group: { | ||
string: | ||
remainingString === "" | ||
? string | ||
: string.slice(0, -remainingString.length), | ||
}, | ||
} | ||
} | ||
const skippable = skippablePredicate(remainingString) | ||
bestMatch = fail({ | ||
patternIndex: bestMatch | ||
? Math.max(bestMatch.patternIndex, matchAttempt.patternIndex) | ||
: matchAttempt.patternIndex, | ||
index: index + matchAttempt.index, | ||
}) | ||
if (!skippable) { | ||
break | ||
const matchAttemptIndex = matchAttempt.index | ||
const matchRange = { | ||
patternIndex: matchAttempt.patternIndex, | ||
index, | ||
length: matchAttemptIndex, | ||
groups: matchAttempt.groups, | ||
} | ||
// search against the next unattempted string | ||
remainingString = remainingString.slice(matchAttempt.index + 1) | ||
index += matchAttempt.index + 1 | ||
if (remainingString === "") { | ||
bestMatch = { | ||
...bestMatch, | ||
index: string.length, | ||
} | ||
break | ||
if (!longestMatchRange || longestMatchRange.length < matchRange.length) { | ||
longestMatchRange = matchRange | ||
} | ||
continue | ||
const nextIndex = matchAttemptIndex + 1 | ||
const canSkip = | ||
nextIndex < remainingString.length && | ||
(canSkipSlash || remainingString[0] !== "/") | ||
if (canSkip) { | ||
// search against the next unattempted string | ||
index += nextIndex | ||
remainingString = remainingString.slice(nextIndex) | ||
return tryToMatch() | ||
} | ||
return { | ||
matched: false, | ||
patternIndex: longestMatchRange.patternIndex, | ||
index: longestMatchRange.index + longestMatchRange.length, | ||
groups: longestMatchRange.groups, | ||
group: { | ||
string: string.slice(0, longestMatchRange.index), | ||
}, | ||
} | ||
} | ||
return bestMatch | ||
return tryToMatch() | ||
} | ||
const pass = ({ patternIndex, index }) => { | ||
return { | ||
matched: true, | ||
index, | ||
patternIndex, | ||
} | ||
} | ||
const fail = ({ patternIndex, index }) => { | ||
return { | ||
matched: false, | ||
index, | ||
patternIndex, | ||
} | ||
} |
@@ -8,3 +8,2 @@ import { isPlainObject } from "./isPlainObject.js" | ||
} | ||
if (checkComposition) { | ||
@@ -11,0 +10,0 @@ const plainObject = value |
import { isPlainObject } from "./isPlainObject.js" | ||
export const structuredMetaMapToMetaMap = (structuredMetaMap, ...rest) => { | ||
export const structuredMetaMapToMetaMap = (structuredMetaMap) => { | ||
if (!isPlainObject(structuredMetaMap)) { | ||
@@ -9,10 +9,2 @@ throw new TypeError( | ||
} | ||
if (rest.length) { | ||
throw new Error(`received more arguments than expected. | ||
--- number of arguments received --- | ||
${1 + rest.length} | ||
--- number of arguments expected --- | ||
1`) | ||
} | ||
const metaMap = {} | ||
@@ -19,0 +11,0 @@ Object.keys(structuredMetaMap).forEach((metaProperty) => { |
import { assertUrlLike } from "./internal/assertUrlLike.js" | ||
export const normalizeStructuredMetaMap = ( | ||
structuredMetaMap, | ||
baseUrl, | ||
...rest | ||
) => { | ||
export const normalizeStructuredMetaMap = (structuredMetaMap, baseUrl) => { | ||
assertUrlLike(baseUrl, "url") | ||
if (rest.length) { | ||
throw new Error(`received more arguments than expected. | ||
--- number of arguments received --- | ||
${2 + rest.length} | ||
--- number of arguments expected --- | ||
2`) | ||
} | ||
const structuredMetaMapNormalized = {} | ||
@@ -18,0 +6,0 @@ Object.keys(structuredMetaMap).forEach((metaProperty) => { |
@@ -9,3 +9,2 @@ import { assertUrlLike } from "./internal/assertUrlLike.js" | ||
predicate, | ||
...rest | ||
}) => { | ||
@@ -20,12 +19,3 @@ assertUrlLike(url, "url") | ||
} | ||
if (Object.keys(rest).length) { | ||
throw new Error(`received more parameters than expected. | ||
--- name of unexpected parameters --- | ||
${Object.keys(rest)} | ||
--- name of expected parameters --- | ||
url, structuredMetaMap, predicate`) | ||
} | ||
const metaMap = structuredMetaMapToMetaMap(structuredMetaMap) | ||
// for full match we must create an object to allow pattern to override previous ones | ||
@@ -37,10 +27,9 @@ let fullMatchMeta = {} | ||
const partialMatchMetaArray = [] | ||
Object.keys(metaMap).forEach((pattern) => { | ||
const meta = metaMap[pattern] | ||
const { matched, index } = applyPatternMatching({ | ||
const matchResult = applyPatternMatching({ | ||
pattern, | ||
url, | ||
}) | ||
if (matched) { | ||
if (matchResult.matched) { | ||
someFullMatch = true | ||
@@ -51,11 +40,9 @@ fullMatchMeta = { | ||
} | ||
} else if (someFullMatch === false && index >= url.length) { | ||
} else if (someFullMatch === false && matchResult.urlIndex >= url.length) { | ||
partialMatchMetaArray.push(meta) | ||
} | ||
}) | ||
if (someFullMatch) { | ||
return Boolean(predicate(fullMatchMeta)) | ||
} | ||
return partialMatchMetaArray.some((partialMatchMeta) => | ||
@@ -62,0 +49,0 @@ predicate(partialMatchMeta), |
@@ -5,12 +5,4 @@ import { assertUrlLike } from "./internal/assertUrlLike.js" | ||
export const urlToMeta = ({ url, structuredMetaMap, ...rest } = {}) => { | ||
export const urlToMeta = ({ url, structuredMetaMap }) => { | ||
assertUrlLike(url) | ||
if (Object.keys(rest).length) { | ||
throw new Error(`received more parameters than expected. | ||
--- name of unexpected parameters --- | ||
${Object.keys(rest)} | ||
--- name of expected parameters --- | ||
url, structuredMetaMap`) | ||
} | ||
const metaMap = structuredMetaMapToMetaMap(structuredMetaMap) | ||
@@ -17,0 +9,0 @@ return Object.keys(metaMap).reduce((previousMeta, pattern) => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
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
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
98027
1
1182