@jsenv/urls 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/urls",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,13 +19,12 @@
19
19
  "type": "module",
20
20
  "exports": {
21
21
  ".": {
22
- "import": "./main.js"
22
+ "import": "./src/urls.js"
23
23
  },
24
24
  "./*": "./*"
25
25
  },
26
26
  "files": [
27
- "/src/",
28
- "/main.js"
27
+ "/src/"
29
28
  ],
30
29
  "scripts": {
31
30
  "test": "node --conditions=development ./scripts/test.mjs"
package/src/data_url.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  /* eslint-env browser, node */
7
7
 
8
- export const DataUrl = {
8
+ export const DATA_URL = {
9
9
  parse: (string) => {
10
10
  const afterDataProtocol = string.slice("data:".length)
11
11
  const commaIndex = afterDataProtocol.indexOf(",")
package/src/urls.js ADDED
@@ -0,0 +1,42 @@
1
+ export { DATA_URL } from "./data_url.js"
2
+ export { generateInlineContentUrl } from "./inline_content_url_generator.js"
3
+ export {
4
+ stringifyUrlTrace,
5
+ stringifyUrlSite,
6
+ humanizeUrl,
7
+ showSourceLocation,
8
+ } from "./url_trace.js"
9
+ export {
10
+ asUrlWithoutSearch,
11
+ isValidUrl,
12
+ normalizeUrl,
13
+ injectQueryParamsIntoSpecifier,
14
+ injectQueryParams,
15
+ setUrlExtension,
16
+ setUrlFilename,
17
+ ensurePathnameTrailingSlash,
18
+ asUrlUntilPathname,
19
+ } from "./url_utils.js"
20
+ export {
21
+ startsWithWindowsDriveLetter,
22
+ windowsFilePathToUrl,
23
+ replaceBackSlashesWithSlashes,
24
+ } from "./windows_file_path_utils.js"
25
+ export { getCallerPosition } from "./caller_position.js"
26
+
27
+ export { fileSystemPathToUrl } from "./filesystem_path_to_url.js"
28
+ export { isFileSystemPath } from "./is_filesystem_path.js"
29
+ export { resolveDirectoryUrl } from "./resolve_directory_url.js"
30
+ export { moveUrl } from "./move_url.js"
31
+ export { resolveUrl } from "./resolve_url.js"
32
+ export { urlIsInsideOf } from "./url_is_inside_of.js"
33
+ export { urlToBasename } from "./url_to_basename.js"
34
+ export { urlToExtension } from "./url_to_extension.js"
35
+ export { urlToFilename } from "./url_to_filename.js"
36
+ export { urlToFileSystemPath } from "./url_to_filesystem_path.js"
37
+ export { urlToOrigin } from "./url_to_origin.js"
38
+ export { urlToParentUrl } from "./url_to_parent_url.js"
39
+ export { urlToPathname } from "./url_to_pathname.js"
40
+ export { urlToRelativeUrl } from "./url_to_relative_url.js"
41
+ export { urlToRessource } from "./url_to_ressource.js"
42
+ export { urlToScheme } from "./url_to_scheme.js"
package/main.js DELETED
@@ -1,44 +0,0 @@
1
- export { DataUrl } from "./src/data_url.js"
2
- export { generateInlineContentUrl } from "./src/inline_content_url_generator.js"
3
- export {
4
- stringifyUrlTrace,
5
- stringifyUrlSite,
6
- humanizeUrl,
7
- showSourceLocation,
8
- } from "./src/url_trace.js"
9
- export {
10
- asUrlWithoutSearch,
11
- isValidUrl,
12
- normalizeUrl,
13
- injectQueryParamsIntoSpecifier,
14
- injectQueryParams,
15
- setUrlExtension,
16
- setUrlFilename,
17
- ensurePathnameTrailingSlash,
18
- asUrlUntilPathname,
19
- } from "./src/url_utils.js"
20
- export {
21
- startsWithWindowsDriveLetter,
22
- windowsFilePathToUrl,
23
- replaceBackSlashesWithSlashes,
24
- } from "./src/windows_file_path_utils.js"
25
- export { getCallerPosition } from "./src/caller_position.js"
26
-
27
- export { URL_META } from "./src/meta/url_meta.js"
28
-
29
- export { fileSystemPathToUrl } from "./src/filesystem_path_to_url.js"
30
- export { isFileSystemPath } from "./src/is_filesystem_path.js"
31
- export { resolveDirectoryUrl } from "./src/resolve_directory_url.js"
32
- export { moveUrl } from "./src/move_url.js"
33
- export { resolveUrl } from "./src/resolve_url.js"
34
- export { urlIsInsideOf } from "./src/url_is_inside_of.js"
35
- export { urlToBasename } from "./src/url_to_basename.js"
36
- export { urlToExtension } from "./src/url_to_extension.js"
37
- export { urlToFilename } from "./src/url_to_filename.js"
38
- export { urlToFileSystemPath } from "./src/url_to_filesystem_path.js"
39
- export { urlToOrigin } from "./src/url_to_origin.js"
40
- export { urlToParentUrl } from "./src/url_to_parent_url.js"
41
- export { urlToPathname } from "./src/url_to_pathname.js"
42
- export { urlToRelativeUrl } from "./src/url_to_relative_url.js"
43
- export { urlToRessource } from "./src/url_to_ressource.js"
44
- export { urlToScheme } from "./src/url_to_scheme.js"
@@ -1,28 +0,0 @@
1
- import { applyPatternMatching } from "./pattern_matching.js"
2
-
3
- export const applyAliases = ({ url, aliases }) => {
4
- let aliasFullMatchResult
5
- const aliasMatchingKey = Object.keys(aliases).find((key) => {
6
- const aliasMatchResult = applyPatternMatching({
7
- pattern: key,
8
- url,
9
- })
10
- if (aliasMatchResult.matched) {
11
- aliasFullMatchResult = aliasMatchResult
12
- return true
13
- }
14
- return false
15
- })
16
- if (!aliasMatchingKey) {
17
- return url
18
- }
19
- const { matchGroups } = aliasFullMatchResult
20
- const alias = aliases[aliasMatchingKey]
21
- const parts = alias.split("*")
22
- const newUrl = parts.reduce((previous, value, index) => {
23
- return `${previous}${value}${
24
- index === parts.length - 1 ? "" : matchGroups[index]
25
- }`
26
- }, "")
27
- return newUrl
28
- }
@@ -1,28 +0,0 @@
1
- import { isPlainObject } from "./assertions.js"
2
-
3
- export const asFlatAssociations = (associations) => {
4
- if (!isPlainObject(associations)) {
5
- throw new TypeError(
6
- `associations must be a plain object, got ${associations}`,
7
- )
8
- }
9
- const flatAssociations = {}
10
- Object.keys(associations).forEach((key) => {
11
- const valueMap = associations[key]
12
- if (!isPlainObject(valueMap)) {
13
- throw new TypeError(
14
- `all associations value must be objects, found "${key}": ${valueMap}`,
15
- )
16
- }
17
- Object.keys(valueMap).forEach((pattern) => {
18
- const value = valueMap[pattern]
19
- const previousValue = flatAssociations[pattern]
20
- flatAssociations[pattern] = previousValue
21
- ? { ...previousValue, [key]: value }
22
- : {
23
- [key]: value,
24
- }
25
- })
26
- })
27
- return flatAssociations
28
- }
@@ -1,57 +0,0 @@
1
- export const assertSpecifierMetaMap = (value, checkComposition = true) => {
2
- if (!isPlainObject(value)) {
3
- throw new TypeError(`specifierMetaMap must be a plain object, got ${value}`)
4
- }
5
- if (checkComposition) {
6
- const plainObject = value
7
- Object.keys(plainObject).forEach((key) => {
8
- assertUrlLike(key, "specifierMetaMap key")
9
- const value = plainObject[key]
10
- if (value !== null && !isPlainObject(value)) {
11
- throw new TypeError(
12
- `specifierMetaMap value must be a plain object or null, got ${value} under key ${key}`,
13
- )
14
- }
15
- })
16
- }
17
- }
18
-
19
- export const assertUrlLike = (value, name = "url") => {
20
- if (typeof value !== "string") {
21
- throw new TypeError(`${name} must be a url string, got ${value}`)
22
- }
23
- if (isWindowsPathnameSpecifier(value)) {
24
- throw new TypeError(
25
- `${name} must be a url but looks like a windows pathname, got ${value}`,
26
- )
27
- }
28
- if (!hasScheme(value)) {
29
- throw new TypeError(
30
- `${name} must be a url and no scheme found, got ${value}`,
31
- )
32
- }
33
- }
34
-
35
- export const isPlainObject = (value) => {
36
- if (value === null) {
37
- return false
38
- }
39
- if (typeof value === "object") {
40
- if (Array.isArray(value)) {
41
- return false
42
- }
43
- return true
44
- }
45
- return false
46
- }
47
-
48
- const isWindowsPathnameSpecifier = (specifier) => {
49
- const firstChar = specifier[0]
50
- if (!/[a-zA-Z]/.test(firstChar)) return false
51
- const secondChar = specifier[1]
52
- if (secondChar !== ":") return false
53
- const thirdChar = specifier[2]
54
- return thirdChar === "/" || thirdChar === "\\"
55
- }
56
-
57
- const hasScheme = (specifier) => /^[a-zA-Z]+:/.test(specifier)
@@ -1,25 +0,0 @@
1
- import { assertUrlLike, isPlainObject } from "./assertions.js"
2
- import { asFlatAssociations } from "./as_flat_associations.js"
3
- import { applyPatternMatching } from "./pattern_matching.js"
4
-
5
- export const applyAssociations = ({ url, associations }) => {
6
- assertUrlLike(url)
7
- const flatAssociations = asFlatAssociations(associations)
8
- return Object.keys(flatAssociations).reduce((previousValue, pattern) => {
9
- const { matched } = applyPatternMatching({
10
- pattern,
11
- url,
12
- })
13
- if (matched) {
14
- const value = flatAssociations[pattern]
15
- if (isPlainObject(previousValue) && isPlainObject(value)) {
16
- return {
17
- ...previousValue,
18
- ...value,
19
- }
20
- }
21
- return value
22
- }
23
- return previousValue
24
- }, {})
25
- }
@@ -1,245 +0,0 @@
1
- /*
2
- * Link to things doing pattern matching:
3
- * https://git-scm.com/docs/gitignore
4
- * https://github.com/kaelzhang/node-ignore
5
- */
6
-
7
- import { assertUrlLike } from "./assertions.js"
8
-
9
- /** @module jsenv_url_meta **/
10
- /**
11
- * An object representing the result of applying a pattern to an url
12
- * @typedef {Object} MatchResult
13
- * @property {boolean} matched Indicates if url matched pattern
14
- * @property {number} patternIndex Index where pattern stopped matching url, otherwise pattern.length
15
- * @property {number} urlIndex Index where url stopped matching pattern, otherwise url.length
16
- * @property {Array} matchGroups Array of strings captured during pattern matching
17
- */
18
-
19
- /**
20
- * Apply a pattern to an url
21
- * @param {Object} applyPatternMatchingParams
22
- * @param {string} applyPatternMatchingParams.pattern "*", "**" and trailing slash have special meaning
23
- * @param {string} applyPatternMatchingParams.url a string representing an url
24
- * @return {MatchResult}
25
- */
26
- export const applyPatternMatching = ({ url, pattern }) => {
27
- assertUrlLike(pattern, "pattern")
28
- assertUrlLike(url, "url")
29
- const { matched, patternIndex, index, groups } = applyMatching(pattern, url)
30
- const matchGroups = []
31
- let groupIndex = 0
32
- groups.forEach((group) => {
33
- if (group.name) {
34
- matchGroups[group.name] = group.string
35
- } else {
36
- matchGroups[groupIndex] = group.string
37
- groupIndex++
38
- }
39
- })
40
- return {
41
- matched,
42
- patternIndex,
43
- urlIndex: index,
44
- matchGroups,
45
- }
46
- }
47
-
48
- const applyMatching = (pattern, string) => {
49
- const groups = []
50
- let patternIndex = 0
51
- let index = 0
52
- let remainingPattern = pattern
53
- let remainingString = string
54
- let restoreIndexes = true
55
-
56
- const consumePattern = (count) => {
57
- const subpattern = remainingPattern.slice(0, count)
58
- remainingPattern = remainingPattern.slice(count)
59
- patternIndex += count
60
- return subpattern
61
- }
62
- const consumeString = (count) => {
63
- const substring = remainingString.slice(0, count)
64
- remainingString = remainingString.slice(count)
65
- index += count
66
- return substring
67
- }
68
- const consumeRemainingString = () => {
69
- return consumeString(remainingString.length)
70
- }
71
-
72
- let matched
73
- const iterate = () => {
74
- const patternIndexBefore = patternIndex
75
- const indexBefore = index
76
- matched = matchOne()
77
- if (matched === undefined) {
78
- consumePattern(1)
79
- consumeString(1)
80
- iterate()
81
- return
82
- }
83
- if (matched === false && restoreIndexes) {
84
- patternIndex = patternIndexBefore
85
- index = indexBefore
86
- }
87
- }
88
- const matchOne = () => {
89
- // pattern consumed and string consumed
90
- if (remainingPattern === "" && remainingString === "") {
91
- return true // string fully matched pattern
92
- }
93
- // pattern consumed, string not consumed
94
- if (remainingPattern === "" && remainingString !== "") {
95
- return false // fails because string longer than expected
96
- }
97
- // -- from this point pattern is not consumed --
98
- // string consumed, pattern not consumed
99
- if (remainingString === "") {
100
- if (remainingPattern === "**") {
101
- // trailing "**" is optional
102
- consumePattern(2)
103
- return true
104
- }
105
- if (remainingPattern === "*") {
106
- groups.push({ string: "" })
107
- }
108
- return false // fail because string shorter than expected
109
- }
110
- // -- from this point pattern and string are not consumed --
111
- // fast path trailing slash
112
- if (remainingPattern === "/") {
113
- if (remainingString[0] === "/") {
114
- // trailing slash match remaining
115
- consumePattern(1)
116
- groups.push({ string: consumeRemainingString() })
117
- return true
118
- }
119
- return false
120
- }
121
- // fast path trailing '**'
122
- if (remainingPattern === "**") {
123
- consumePattern(2)
124
- consumeRemainingString()
125
- return true
126
- }
127
- // pattern leading **
128
- if (remainingPattern.slice(0, 2) === "**") {
129
- consumePattern(2) // consumes "**"
130
- if (remainingPattern[0] === "/") {
131
- consumePattern(1) // consumes "/"
132
- }
133
- // pattern ending with ** always match remaining string
134
- if (remainingPattern === "") {
135
- consumeRemainingString()
136
- return true
137
- }
138
- const skipResult = skipUntilMatch({
139
- pattern: remainingPattern,
140
- string: remainingString,
141
- canSkipSlash: true,
142
- })
143
- groups.push(...skipResult.groups)
144
- consumePattern(skipResult.patternIndex)
145
- consumeRemainingString()
146
- restoreIndexes = false
147
- return skipResult.matched
148
- }
149
- if (remainingPattern[0] === "*") {
150
- consumePattern(1) // consumes "*"
151
- if (remainingPattern === "") {
152
- // matches everything except '/'
153
- const slashIndex = remainingString.indexOf("/")
154
- if (slashIndex === -1) {
155
- groups.push({ string: consumeRemainingString() })
156
- return true
157
- }
158
- groups.push({ string: consumeString(slashIndex) })
159
- return false
160
- }
161
- // the next char must not the one expected by remainingPattern[0]
162
- // because * is greedy and expect to skip at least one char
163
- if (remainingPattern[0] === remainingString[0]) {
164
- groups.push({ string: "" })
165
- patternIndex = patternIndex - 1
166
- return false
167
- }
168
- const skipResult = skipUntilMatch({
169
- pattern: remainingPattern,
170
- string: remainingString,
171
- canSkipSlash: false,
172
- })
173
- groups.push(skipResult.group, ...skipResult.groups)
174
- consumePattern(skipResult.patternIndex)
175
- consumeString(skipResult.index)
176
- restoreIndexes = false
177
- return skipResult.matched
178
- }
179
- if (remainingPattern[0] !== remainingString[0]) {
180
- return false
181
- }
182
- return undefined
183
- }
184
- iterate()
185
-
186
- return {
187
- matched,
188
- patternIndex,
189
- index,
190
- groups,
191
- }
192
- }
193
-
194
- const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
195
- let index = 0
196
- let remainingString = string
197
- let longestMatchRange = null
198
- const tryToMatch = () => {
199
- const matchAttempt = applyMatching(pattern, remainingString)
200
- if (matchAttempt.matched) {
201
- return {
202
- matched: true,
203
- patternIndex: matchAttempt.patternIndex,
204
- index: index + matchAttempt.index,
205
- groups: matchAttempt.groups,
206
- group: {
207
- string:
208
- remainingString === ""
209
- ? string
210
- : string.slice(0, -remainingString.length),
211
- },
212
- }
213
- }
214
- const matchAttemptIndex = matchAttempt.index
215
- const matchRange = {
216
- patternIndex: matchAttempt.patternIndex,
217
- index,
218
- length: matchAttemptIndex,
219
- groups: matchAttempt.groups,
220
- }
221
- if (!longestMatchRange || longestMatchRange.length < matchRange.length) {
222
- longestMatchRange = matchRange
223
- }
224
- const nextIndex = matchAttemptIndex + 1
225
- const canSkip =
226
- nextIndex < remainingString.length &&
227
- (canSkipSlash || remainingString[0] !== "/")
228
- if (canSkip) {
229
- // search against the next unattempted string
230
- index += nextIndex
231
- remainingString = remainingString.slice(nextIndex)
232
- return tryToMatch()
233
- }
234
- return {
235
- matched: false,
236
- patternIndex: longestMatchRange.patternIndex,
237
- index: longestMatchRange.index + longestMatchRange.length,
238
- groups: longestMatchRange.groups,
239
- group: {
240
- string: string.slice(0, longestMatchRange.index),
241
- },
242
- }
243
- }
244
- return tryToMatch()
245
- }
@@ -1,25 +0,0 @@
1
- import { assertUrlLike } from "./assertions.js"
2
-
3
- export const resolveAssociations = (associations, baseUrl) => {
4
- assertUrlLike(baseUrl, "baseUrl")
5
- const associationsResolved = {}
6
- Object.keys(associations).forEach((key) => {
7
- const valueMap = associations[key]
8
- const valueMapResolved = {}
9
- Object.keys(valueMap).forEach((pattern) => {
10
- const value = valueMap[pattern]
11
- const patternResolved = normalizeUrlPattern(pattern, baseUrl)
12
- valueMapResolved[patternResolved] = value
13
- })
14
- associationsResolved[key] = valueMapResolved
15
- })
16
- return associationsResolved
17
- }
18
-
19
- const normalizeUrlPattern = (urlPattern, baseUrl) => {
20
- // starts with a scheme
21
- if (/^[a-zA-Z]{2,}:/.test(urlPattern)) {
22
- return urlPattern
23
- }
24
- return String(new URL(urlPattern, baseUrl))
25
- }
@@ -1,47 +0,0 @@
1
- import { assertUrlLike, isPlainObject } from "./assertions.js"
2
- import { asFlatAssociations } from "./as_flat_associations.js"
3
- import { applyPatternMatching } from "./pattern_matching.js"
4
-
5
- export const urlChildMayMatch = ({ url, associations, predicate }) => {
6
- assertUrlLike(url, "url")
7
- // the function was meants to be used on url ending with '/'
8
- if (!url.endsWith("/")) {
9
- throw new Error(`url should end with /, got ${url}`)
10
- }
11
- if (typeof predicate !== "function") {
12
- throw new TypeError(`predicate must be a function, got ${predicate}`)
13
- }
14
- const flatAssociations = asFlatAssociations(associations)
15
- // for full match we must create an object to allow pattern to override previous ones
16
- let fullMatchMeta = {}
17
- let someFullMatch = false
18
- // for partial match, any meta satisfying predicate will be valid because
19
- // we don't know for sure if pattern will still match for a file inside pathname
20
- const partialMatchMetaArray = []
21
- Object.keys(flatAssociations).forEach((pattern) => {
22
- const value = flatAssociations[pattern]
23
- const matchResult = applyPatternMatching({
24
- pattern,
25
- url,
26
- })
27
- if (matchResult.matched) {
28
- someFullMatch = true
29
- if (isPlainObject(fullMatchMeta) && isPlainObject(value)) {
30
- fullMatchMeta = {
31
- ...fullMatchMeta,
32
- ...value,
33
- }
34
- } else {
35
- fullMatchMeta = value
36
- }
37
- } else if (someFullMatch === false && matchResult.urlIndex >= url.length) {
38
- partialMatchMetaArray.push(value)
39
- }
40
- })
41
- if (someFullMatch) {
42
- return Boolean(predicate(fullMatchMeta))
43
- }
44
- return partialMatchMetaArray.some((partialMatchMeta) =>
45
- predicate(partialMatchMeta),
46
- )
47
- }
@@ -1,13 +0,0 @@
1
- import { resolveAssociations } from "./resolve_associations.js"
2
- import { applyAssociations } from "./associations.js"
3
- import { applyAliases } from "./aliases.js"
4
- import { applyPatternMatching } from "./pattern_matching.js"
5
- import { urlChildMayMatch } from "./url_child_may_match.js"
6
-
7
- export const URL_META = {
8
- resolveAssociations,
9
- applyAssociations,
10
- urlChildMayMatch,
11
- applyPatternMatching,
12
- applyAliases,
13
- }