projen-pipelines 0.3.21 → 0.3.23

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.
Files changed (123) hide show
  1. package/.jsii +7 -4
  2. package/API.md +420 -0
  3. package/lib/assign-approver/base.js +3 -4
  4. package/lib/assign-approver/github.js +4 -4
  5. package/lib/awscdk/base.js +48 -6
  6. package/lib/awscdk/bash.js +2 -4
  7. package/lib/awscdk/github.js +13 -6
  8. package/lib/awscdk/gitlab.js +11 -6
  9. package/lib/drift/base.js +8 -4
  10. package/lib/drift/bash.js +3 -4
  11. package/lib/drift/detect-drift.js +3 -2
  12. package/lib/drift/github.js +5 -4
  13. package/lib/drift/gitlab.js +5 -4
  14. package/lib/drift/step.js +2 -4
  15. package/lib/release.js +5 -2
  16. package/lib/steps/amplify-deploy.step.js +5 -4
  17. package/lib/steps/artifact-steps.js +5 -6
  18. package/lib/steps/aws-assume-role.step.js +3 -4
  19. package/lib/steps/github-summary.step.js +3 -4
  20. package/lib/steps/package-manager-setup.step.js +4 -6
  21. package/lib/steps/registries.js +7 -8
  22. package/lib/steps/set-env.step.js +2 -1
  23. package/lib/steps/step.js +9 -10
  24. package/lib/versioning/computation.js +6 -8
  25. package/lib/versioning/config.js +3 -6
  26. package/lib/versioning/outputs.js +13 -16
  27. package/lib/versioning/setup.js +9 -9
  28. package/lib/versioning/strategy.js +4 -4
  29. package/lib/versioning/version-info.js +18 -9
  30. package/node_modules/@babel/code-frame/package.json +2 -2
  31. package/node_modules/@babel/helper-validator-identifier/package.json +1 -1
  32. package/node_modules/@nodable/entities/README.md +52 -1
  33. package/node_modules/@nodable/entities/package.json +4 -1
  34. package/node_modules/@nodable/entities/src/EntityDecoder.js +105 -4
  35. package/node_modules/@nodable/entities/src/EntityEncoder.js +79 -34
  36. package/node_modules/@nodable/entities/src/all-entities.js +53 -0
  37. package/node_modules/@nodable/entities/src/entities.js +0 -38
  38. package/node_modules/@nodable/entities/src/entityTries.js +48 -37
  39. package/node_modules/@nodable/entities/src/index.d.ts +93 -2
  40. package/node_modules/@nodable/entities/src/index.js +2 -2
  41. package/node_modules/anynum/LICENSE +21 -0
  42. package/node_modules/anynum/README.md +142 -0
  43. package/node_modules/anynum/anynum.js +135 -0
  44. package/node_modules/anynum/digitTable.js +116 -0
  45. package/node_modules/anynum/package.json +42 -0
  46. package/node_modules/dotgitignore/node_modules/brace-expansion/README.md +23 -1
  47. package/node_modules/dotgitignore/node_modules/brace-expansion/index.js +265 -86
  48. package/node_modules/dotgitignore/node_modules/brace-expansion/package.json +1 -1
  49. package/node_modules/fast-xml-builder/CHANGELOG.md +12 -0
  50. package/node_modules/fast-xml-builder/README.md +4 -1
  51. package/node_modules/fast-xml-builder/lib/fxb.cjs +1 -1
  52. package/node_modules/fast-xml-builder/lib/fxb.min.js +1 -1
  53. package/node_modules/fast-xml-builder/lib/fxb.min.js.map +1 -1
  54. package/node_modules/fast-xml-builder/package.json +6 -10
  55. package/node_modules/fast-xml-builder/src/fxb.js +34 -24
  56. package/node_modules/fast-xml-builder/src/orderedJs2Xml.js +18 -17
  57. package/node_modules/fast-xml-builder/src/util.js +10 -3
  58. package/node_modules/fast-xml-parser/CHANGELOG.md +34 -0
  59. package/node_modules/fast-xml-parser/README.md +10 -6
  60. package/node_modules/fast-xml-parser/lib/fxbuilder.min.js +1 -1
  61. package/node_modules/fast-xml-parser/lib/fxbuilder.min.js.map +1 -1
  62. package/node_modules/fast-xml-parser/lib/fxp.cjs +1 -1
  63. package/node_modules/fast-xml-parser/lib/fxp.d.cts +3 -1
  64. package/node_modules/fast-xml-parser/lib/fxp.min.js +1 -1
  65. package/node_modules/fast-xml-parser/lib/fxp.min.js.map +1 -1
  66. package/node_modules/fast-xml-parser/lib/fxparser.min.js +1 -1
  67. package/node_modules/fast-xml-parser/lib/fxparser.min.js.map +1 -1
  68. package/node_modules/fast-xml-parser/package.json +8 -10
  69. package/node_modules/fast-xml-parser/src/fxp.d.ts +5 -1
  70. package/node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js +16 -1
  71. package/node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js +2 -1
  72. package/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js +36 -3
  73. package/node_modules/fast-xml-parser/src/xmlparser/xmlNode.js +14 -0
  74. package/node_modules/get-pkg-repo/node_modules/yargs/package.json +27 -26
  75. package/node_modules/get-pkg-repo/node_modules/yargs/yargs +5 -9
  76. package/node_modules/get-pkg-repo/node_modules/yargs/yargs.cjs +15 -0
  77. package/node_modules/hasown/CHANGELOG.md +7 -0
  78. package/node_modules/hasown/index.d.ts +0 -1
  79. package/node_modules/hasown/package.json +4 -5
  80. package/node_modules/is-unsafe/LICENSE +21 -0
  81. package/node_modules/is-unsafe/README.md +599 -0
  82. package/node_modules/is-unsafe/package.json +56 -0
  83. package/node_modules/is-unsafe/src/contexts/html.js +102 -0
  84. package/node_modules/is-unsafe/src/contexts/log.js +98 -0
  85. package/node_modules/is-unsafe/src/contexts/nosql.js +114 -0
  86. package/node_modules/is-unsafe/src/contexts/redos.js +60 -0
  87. package/node_modules/is-unsafe/src/contexts/shell.js +105 -0
  88. package/node_modules/is-unsafe/src/contexts/sql-strict.js +47 -0
  89. package/node_modules/is-unsafe/src/contexts/sql.js +95 -0
  90. package/node_modules/is-unsafe/src/contexts/svg.js +81 -0
  91. package/node_modules/is-unsafe/src/contexts/xml.js +79 -0
  92. package/node_modules/is-unsafe/src/index.cjs +27 -0
  93. package/node_modules/is-unsafe/src/index.d.ts +95 -0
  94. package/node_modules/is-unsafe/src/index.js +265 -0
  95. package/node_modules/path-expression-matcher/README.md +82 -7
  96. package/node_modules/path-expression-matcher/lib/pem.cjs +1 -1
  97. package/node_modules/path-expression-matcher/lib/pem.d.cts +94 -3
  98. package/node_modules/path-expression-matcher/lib/pem.min.js +1 -1
  99. package/node_modules/path-expression-matcher/lib/pem.min.js.map +1 -1
  100. package/node_modules/path-expression-matcher/package.json +2 -1
  101. package/node_modules/path-expression-matcher/src/ExpressionSet.js +18 -2
  102. package/node_modules/path-expression-matcher/src/Matcher.js +109 -16
  103. package/node_modules/path-expression-matcher/src/index.d.ts +67 -1
  104. package/node_modules/path-expression-matcher/src/index.js +3 -2
  105. package/node_modules/semver/classes/range.js +24 -4
  106. package/node_modules/semver/classes/semver.js +19 -2
  107. package/node_modules/semver/package.json +2 -2
  108. package/node_modules/semver/ranges/subset.js +2 -2
  109. package/node_modules/strnum/CHANGELOG.md +14 -0
  110. package/node_modules/strnum/README.md +9 -0
  111. package/node_modules/strnum/package.json +7 -3
  112. package/node_modules/strnum/strnum.js +14 -2
  113. package/node_modules/xml-naming/LICENSE +21 -0
  114. package/node_modules/xml-naming/README.md +99 -1
  115. package/node_modules/xml-naming/package.json +4 -2
  116. package/node_modules/xml-naming/src/index.d.ts +34 -0
  117. package/node_modules/xml-naming/src/index.js +134 -22
  118. package/node_modules/yargs/build/index.cjs +1 -1
  119. package/node_modules/yargs/package.json +6 -4
  120. package/node_modules/yargs/yargs +5 -9
  121. package/node_modules/yargs/yargs.cjs +15 -0
  122. package/package.json +8 -9
  123. package/node_modules/yargs/browser.d.ts +0 -5
@@ -98,6 +98,9 @@ class Range {
98
98
  }
99
99
 
100
100
  parseRange (range) {
101
+ // strip build metadata so it can't bleed into the version
102
+ range = range.replace(BUILDSTRIPRE, '')
103
+
101
104
  // memoize range parsing for performance.
102
105
  // this is a very hot path, and fully deterministic.
103
106
  const memoOpts =
@@ -223,6 +226,7 @@ const debug = require('../internal/debug')
223
226
  const SemVer = require('./semver')
224
227
  const {
225
228
  safeRe: re,
229
+ src,
226
230
  t,
227
231
  comparatorTrimReplace,
228
232
  tildeTrimReplace,
@@ -230,6 +234,9 @@ const {
230
234
  } = require('../internal/re')
231
235
  const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = require('../internal/constants')
232
236
 
237
+ // unbounded global build-metadata stripper used by parseRange
238
+ const BUILDSTRIPRE = new RegExp(src[t.BUILD], 'g')
239
+
233
240
  const isNullSet = c => c.value === '<0.0.0-0'
234
241
  const isAny = c => c.value === ''
235
242
 
@@ -270,6 +277,11 @@ const parseComparator = (comp, options) => {
270
277
 
271
278
  const isX = id => !id || id.toLowerCase() === 'x' || id === '*'
272
279
 
280
+ const invalidXRangeOrder = (M, m, p) => (
281
+ (isX(M) && !isX(m)) ||
282
+ (isX(m) && p && !isX(p))
283
+ )
284
+
273
285
  // ~, ~> --> * (any, kinda silly)
274
286
  // ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0-0
275
287
  // ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0-0
@@ -287,6 +299,10 @@ const replaceTildes = (comp, options) => {
287
299
 
288
300
  const replaceTilde = (comp, options) => {
289
301
  const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE]
302
+ // if we're including prereleases in the match, then the lower bound is
303
+ // -0, the lowest possible prerelease value, just like x-ranges and carets.
304
+ // this keeps `~1.2` equivalent to the `1.2.x` x-range it's documented as.
305
+ const z = options.includePrerelease ? '-0' : ''
290
306
  return comp.replace(r, (_, M, m, p, pr) => {
291
307
  debug('tilde', comp, _, M, m, p, pr)
292
308
  let ret
@@ -294,10 +310,10 @@ const replaceTilde = (comp, options) => {
294
310
  if (isX(M)) {
295
311
  ret = ''
296
312
  } else if (isX(m)) {
297
- ret = `>=${M}.0.0 <${+M + 1}.0.0-0`
313
+ ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0`
298
314
  } else if (isX(p)) {
299
315
  // ~1.2 == >=1.2.0 <1.3.0-0
300
- ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`
316
+ ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0`
301
317
  } else if (pr) {
302
318
  debug('replaceTilde pr', pr)
303
319
  ret = `>=${M}.${m}.${p}-${pr
@@ -366,10 +382,10 @@ const replaceCaret = (comp, options) => {
366
382
  if (M === '0') {
367
383
  if (m === '0') {
368
384
  ret = `>=${M}.${m}.${p
369
- }${z} <${M}.${m}.${+p + 1}-0`
385
+ } <${M}.${m}.${+p + 1}-0`
370
386
  } else {
371
387
  ret = `>=${M}.${m}.${p
372
- }${z} <${M}.${+m + 1}.0-0`
388
+ } <${M}.${+m + 1}.0-0`
373
389
  }
374
390
  } else {
375
391
  ret = `>=${M}.${m}.${p
@@ -395,6 +411,10 @@ const replaceXRange = (comp, options) => {
395
411
  const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE]
396
412
  return comp.replace(r, (ret, gtlt, M, m, p, pr) => {
397
413
  debug('xRange', comp, ret, gtlt, M, m, p, pr)
414
+ if (invalidXRangeOrder(M, m, p)) {
415
+ return comp
416
+ }
417
+
398
418
  const xM = isX(M)
399
419
  const xm = xM || isX(m)
400
420
  const xp = xm || isX(p)
@@ -6,6 +6,22 @@ const { safeRe: re, t } = require('../internal/re')
6
6
 
7
7
  const parseOptions = require('../internal/parse-options')
8
8
  const { compareIdentifiers } = require('../internal/identifiers')
9
+
10
+ const isPrereleaseIdentifier = (prerelease, identifier) => {
11
+ const identifiers = identifier.split('.')
12
+ if (identifiers.length > prerelease.length) {
13
+ return false
14
+ }
15
+
16
+ for (let i = 0; i < identifiers.length; i++) {
17
+ if (compareIdentifiers(prerelease[i], identifiers[i]) !== 0) {
18
+ return false
19
+ }
20
+ }
21
+
22
+ return true
23
+ }
24
+
9
25
  class SemVer {
10
26
  constructor (version, options) {
11
27
  options = parseOptions(options)
@@ -309,8 +325,9 @@ class SemVer {
309
325
  if (identifierBase === false) {
310
326
  prerelease = [identifier]
311
327
  }
312
- if (compareIdentifiers(this.prerelease[0], identifier) === 0) {
313
- if (isNaN(this.prerelease[1])) {
328
+ if (isPrereleaseIdentifier(this.prerelease, identifier)) {
329
+ const prereleaseBase = this.prerelease[identifier.split('.').length]
330
+ if (isNaN(prereleaseBase)) {
314
331
  this.prerelease = prerelease
315
332
  }
316
333
  } else {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "semver",
3
- "version": "7.8.0",
3
+ "version": "7.8.5",
4
4
  "description": "The semantic version parser used by npm.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -14,7 +14,7 @@
14
14
  "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\""
15
15
  },
16
16
  "devDependencies": {
17
- "@npmcli/eslint-config": "^6.0.0",
17
+ "@npmcli/eslint-config": "^7.0.0",
18
18
  "@npmcli/template-oss": "5.0.0",
19
19
  "benchmark": "^2.1.4",
20
20
  "tap": "^16.0.0"
@@ -174,7 +174,7 @@ const simpleSubset = (sub, dom, options) => {
174
174
  if (higher === c && higher !== gt) {
175
175
  return false
176
176
  }
177
- } else if (gt.operator === '>=' && !satisfies(gt.semver, String(c), options)) {
177
+ } else if (gt.operator === '>=' && !c.test(gt.semver)) {
178
178
  return false
179
179
  }
180
180
  }
@@ -192,7 +192,7 @@ const simpleSubset = (sub, dom, options) => {
192
192
  if (lower === c && lower !== lt) {
193
193
  return false
194
194
  }
195
- } else if (lt.operator === '<=' && !satisfies(lt.semver, String(c), options)) {
195
+ } else if (lt.operator === '<=' && !c.test(lt.semver)) {
196
196
  return false
197
197
  }
198
198
  }
@@ -1,4 +1,18 @@
1
1
 
2
+ **2.4.2 / 2026-08-14**
3
+ - fix: trimZeros to avoid quadratic complexity
4
+
5
+ **2.4.1 / 2026-06-18**
6
+ - deps: update anynum to 1.0.1
7
+
8
+ **2.4.0 / 2026-06-09**
9
+ - support unicode numerals using 'anynum'
10
+
11
+ **2.3.0 / 2026-05-07**
12
+ - support octal and binary, test with @byspec/numbers
13
+ - test with @byspec/numbers
14
+
15
+
2
16
  **2.2.3 / 2026-04-07**
3
17
  - remove unnecessary files from npm package
4
18
 
@@ -1,6 +1,10 @@
1
1
  # strnum
2
2
  Parse string into Number based on configuration
3
3
 
4
+ [![strnum downloads](https://img.shields.io/npm/dw/strnum.svg)](https://npm-compare.com/strnum)
5
+ [![strnum version](https://img.shields.io/npm/v/strnum.svg)](https://www.npmjs.com/package/strnum)
6
+ [![strnum license](https://img.shields.io/npm/l/strnum.svg)](https://github.com/NaturalIntelligence/strnum)
7
+
4
8
  ## Users
5
9
 
6
10
  <a href="https://github.com/aws-amplify" target="_blank"><img src="https://avatars.githubusercontent.com/u/41077760?s=100&v=4"></a>
@@ -86,6 +90,10 @@ toNumber("1.0e-2"); //0.01)
86
90
 
87
91
  toNumber("+1212121212"); // 1212121212
88
92
  toNumber("+1212121212", { skipLike: /\+[0-9]{10}/} )); //"+1212121212"
93
+
94
+ toNumber("1e1000", { unicode: true, infinity: "original" }); //"1e1000"
95
+ toNumber("1000", { unicode: true }); //1000
96
+ toNumber("1000", { unicode: false }); //"1000"
89
97
  ```
90
98
 
91
99
  Supported Options
@@ -95,6 +103,7 @@ leadingZeros: true, //when number with leading zeros like 08 should be parsed. 0
95
103
  eNotation: true, //when number with eNotation or number parsed in eNotation should be considered
96
104
  skipLike: /regex/ //when string should not be parsed when it matches the specified regular expression
97
105
  infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal))
106
+ unicode: false, //when number with unicode numerals should be parsed
98
107
  ```
99
108
 
100
109
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strnum",
3
- "version": "2.3.0",
3
+ "version": "2.4.2",
4
4
  "description": "Parse String to Number based on configuration",
5
5
  "type": "module",
6
6
  "main": "strnum.js",
@@ -11,7 +11,8 @@
11
11
  "string",
12
12
  "number",
13
13
  "parse",
14
- "convert"
14
+ "convert",
15
+ "unicode"
15
16
  ],
16
17
  "repository": {
17
18
  "type": "git",
@@ -28,5 +29,8 @@
28
29
  "devDependencies": {
29
30
  "@byspec/numbers": "^0.1.1",
30
31
  "jasmine": "^5.6.0"
32
+ },
33
+ "dependencies": {
34
+ "anynum": "^1.0.1"
31
35
  }
32
- }
36
+ }
@@ -3,6 +3,8 @@ const binRegex = /^0b[01]+$/;
3
3
  const octRegex = /^0o[0-7]+$/;
4
4
  const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/;
5
5
 
6
+ import anynum from "anynum";
7
+
6
8
  const consider = {
7
9
  hex: true,
8
10
  binary: false,
@@ -12,6 +14,7 @@ const consider = {
12
14
  eNotation: true,
13
15
  //skipLike: /regex/,
14
16
  infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal))
17
+ unicode: false,
15
18
  };
16
19
 
17
20
  export default function toNumber(str, options = {}) {
@@ -23,7 +26,12 @@ export default function toNumber(str, options = {}) {
23
26
  if (trimmedStr.length === 0) return str;
24
27
  else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str;
25
28
  else if (trimmedStr === "0") return 0;
26
- else if (options.hex && hexRegex.test(trimmedStr)) {
29
+
30
+ if (options.unicode) {
31
+ trimmedStr = anynum(trimmedStr);
32
+ if (trimmedStr === "0") return 0; // re-check after normalization
33
+ }
34
+ if (options.hex && hexRegex.test(trimmedStr)) {
27
35
  return parse_int(trimmedStr, 16);
28
36
  } else if (options.binary && binRegex.test(trimmedStr)) {
29
37
  return parse_int(trimmedStr, 2);
@@ -120,7 +128,11 @@ function resolveEnotation(str, trimmedStr, options) {
120
128
  */
121
129
  function trimZeros(numStr) {
122
130
  if (numStr && numStr.indexOf(".") !== -1) {//float
123
- numStr = numStr.replace(/0+$/, ""); //remove ending zeros
131
+ //remove ending zeros without the O(n^2) backtracking that /0+$/ hits
132
+ //when the string doesn't end in 0 but has a long internal zero-run
133
+ let end = numStr.length;
134
+ while (end > 0 && numStr.charCodeAt(end - 1) === 48 /* '0' */) end--;
135
+ numStr = numStr.slice(0, end);
124
136
  if (numStr === ".") numStr = "0";
125
137
  else if (numStr[0] === ".") numStr = "0" + numStr;
126
138
  else if (numStr[numStr.length - 1] === ".") numStr = numStr.substring(0, numStr.length - 1);
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Natural Intelligence
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,4 +1,7 @@
1
1
  # xml-naming
2
+ [![xml-naming downloads](https://img.shields.io/npm/dw/xml-naming.svg)](https://npm-compare.com/xml-naming)
3
+ [![xml-naming version](https://img.shields.io/npm/v/xml-naming.svg)](https://www.npmjs.com/package/xml-naming)
4
+ [![xml-naming license](https://img.shields.io/npm/l/xml-naming.svg)](https://github.com/NaturalIntelligence/xml-naming)
2
5
 
3
6
  Validates XML name productions as defined in the [XML 1.0](https://www.w3.org/TR/xml/) and [XML 1.1](https://www.w3.org/TR/xml11/) specifications.
4
7
 
@@ -71,6 +74,88 @@ name('\uD800\uDC00', { xmlVersion: '1.1' }) // true
71
74
 
72
75
  ---
73
76
 
77
+ ### ASCII-only fast path
78
+
79
+ All validators, `validate`, `validateAll`, and `sanitize` also accept `{ asciiOnly: true }`.
80
+ When set, matching is restricted to the ASCII subset of the NameStartChar/NameChar
81
+ productions and skips unicode-aware regex matching entirely — no `\u00C0-\uFFFD`-style
82
+ ranges, and (for XML 1.1) no `/u` regex flag. Unicode-aware regexes are measurably slower
83
+ than plain ASCII matching in JS engines, so this is a real performance win when you know
84
+ your input is ASCII-only, which is the common case for HTML/SVG ids and most XML tags.
85
+
86
+ **This is opt-in and defaults to `false`** for backward compatibility: turning it on
87
+ changes behavior, since it rejects legitimate non-ASCII XML names that would otherwise be
88
+ valid. Only enable it when you control the input and know it's ASCII (e.g. internal
89
+ identifiers, machine-generated names), not for validating arbitrary user- or
90
+ externally-supplied XML/SVG content.
91
+
92
+ ```js
93
+ import { name, sanitize } from 'xml-naming';
94
+
95
+ name('café', { asciiOnly: true }) // false — 'é' is not ASCII, even though it's
96
+ name('café') // true a valid XML 1.0/1.1 NameChar
97
+
98
+ sanitize('café', 'name', { asciiOnly: true }) // 'caf_' — non-ASCII replaced too
99
+ sanitize('café', 'name') // 'café' — left untouched by default
100
+ ```
101
+
102
+ ---
103
+
104
+ ### Memoized validator (`createValidator`)
105
+
106
+ Real documents tend to reuse a small vocabulary of tag/attribute names across many
107
+ siblings (`id`, `class`, `href`, ... repeated across hundreds of elements). Calling the
108
+ plain boolean validators re-runs the regex on every call, even for names seen before.
109
+
110
+ `createValidator(production, opts)` returns a memoized validator function: `xmlVersion`
111
+ and `asciiOnly` are fixed at creation time (so the regex is resolved once, not per call),
112
+ and repeated inputs after the first are served from an internal cache instead of
113
+ re-matching the regex.
114
+
115
+ ```js
116
+ import { createValidator } from 'xml-naming';
117
+
118
+ const isQName = createValidator('qName', { xmlVersion: '1.0' });
119
+
120
+ isQName('sku'); // false → regex test (cache miss), result cached
121
+ isQName('sku'); // false → cache hit, no regex run
122
+ ```
123
+
124
+ Use one instance per document/parse (or reuse across a session — your choice), rather
125
+ than creating one per call:
126
+
127
+ ```js
128
+ // e.g. inside a parser, once per parse call:
129
+ const isValidTag = createValidator('qName', { asciiOnly: true });
130
+
131
+ for (const tagName of tagNames) {
132
+ if (!isValidTag(tagName)) throw new Error(`Invalid tag name: ${tagName}`);
133
+ }
134
+ ```
135
+
136
+ Because the validator is a plain function, this loop already gets short-circuit
137
+ behaviour (via `break`/`throw` on first failure) and zero extra allocation on the happy
138
+ path — no separate "bulk" API is needed for that.
139
+
140
+ **Cache bound:** the internal cache is capped by `maxCacheSize` (default `2048`). Once
141
+ the cap is reached, new distinct strings are still validated correctly, they're just no
142
+ longer cached — existing cached entries keep being served. This keeps memory bounded
143
+ even against high-cardinality or adversarial input (e.g. externally-supplied names that
144
+ never repeat), without the cost of a full LRU or the perf cliff of reset-and-refill.
145
+
146
+ Call `.reset()` on the returned function to clear the cache manually, e.g. between
147
+ unrelated parse calls if you're reusing one validator instance across a long-running
148
+ process:
149
+
150
+ ```js
151
+ isQName.reset();
152
+ ```
153
+
154
+ The cache is private to each `createValidator()` instance — there's no shared/global
155
+ cache, so unrelated callers never interfere with each other.
156
+
157
+ ---
158
+
74
159
  ### Diagnostic validation
75
160
 
76
161
  ```js
@@ -169,21 +254,34 @@ sanitize('my element', 'name', { replacement: '-' }) // 'my-element'
169
254
 
170
255
  `opts`:
171
256
  - `xmlVersion`: `'1.0'` (default) | `'1.1'`
257
+ - `asciiOnly`: boolean (default `false`) — ASCII-only fast path, see above
258
+
259
+ ### `createValidator(production, opts?)` → memoized `(str) => boolean`, with `.reset()`
260
+
261
+ `opts`:
262
+ - `xmlVersion`: `'1.0'` (default) | `'1.1'`
263
+ - `asciiOnly`: boolean (default `false`)
264
+ - `maxCacheSize`: number (default `2048`) — cache stops accepting new entries once reached; existing entries keep serving hits
172
265
 
173
266
  ### `validate(str, production, opts?)` → `ValidationResult`
174
267
 
175
268
  `production`: `'name'` | `'ncName'` | `'qName'` | `'nmToken'` | `'nmTokens'`
176
269
 
270
+ `opts`: same as boolean validators (`xmlVersion`, `asciiOnly`)
271
+
177
272
  ### `validateAll(strings[], production, opts?)` → `ValidationResult[]`
178
273
 
274
+ `opts`: same as `validate`
275
+
179
276
  ### `sanitize(str, production?, opts?)` → `string`
180
277
 
181
278
  `opts`:
182
279
  - `xmlVersion`: `'1.0'` | `'1.1'`
183
280
  - `replacement`: string (default `'_'`)
281
+ - `asciiOnly`: boolean (default `false`) — also replaces non-ASCII characters, not just XML-illegal ones
184
282
 
185
283
  ---
186
284
 
187
285
  ## License
188
286
 
189
- MIT
287
+ MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xml-naming",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Validates XML name productions — Name, NCName, QName, NMToken, NMTokens — for XML 1.0 and 1.1",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -16,9 +16,11 @@
16
16
  "README.md"
17
17
  ],
18
18
  "scripts": {
19
- "test": "jasmine specs/*spec.js"
19
+ "test": "jasmine specs/*spec.js",
20
+ "perf": "node specs/perf.js"
20
21
  },
21
22
  "keywords": [
23
+ "fast",
22
24
  "xml",
23
25
  "xml-name",
24
26
  "ncname",
@@ -1,6 +1,15 @@
1
1
  export interface ValidationOptions {
2
2
  /** XML specification version to validate against. Defaults to '1.0'. */
3
3
  xmlVersion?: '1.0' | '1.1';
4
+ /**
5
+ * Restrict matching to the ASCII subset of the NameStartChar/NameChar
6
+ * productions and skip unicode-aware regex matching entirely. Faster,
7
+ * especially for XML 1.1 (which otherwise requires the `/u` regex flag),
8
+ * but rejects legitimate non-ASCII XML names. Off by default for
9
+ * backward compatibility — opt in only when inputs are known to be ASCII.
10
+ * Defaults to false.
11
+ */
12
+ asciiOnly?: boolean;
4
13
  }
5
14
 
6
15
  export interface SanitizeOptions extends ValidationOptions {
@@ -55,6 +64,31 @@ export function nmToken(str: string, opts?: ValidationOptions): boolean;
55
64
  */
56
65
  export function nmTokens(str: string, opts?: ValidationOptions): boolean;
57
66
 
67
+ export interface CreateValidatorOptions extends ValidationOptions {
68
+ /**
69
+ * Max number of distinct strings to cache. Once reached, new strings are
70
+ * still validated correctly but are no longer cached; existing cached
71
+ * entries keep being served. Defaults to 2048.
72
+ */
73
+ maxCacheSize?: number;
74
+ }
75
+
76
+ export interface MemoizedValidator {
77
+ (str: string): boolean;
78
+ /** Clears the internal cache. */
79
+ reset(): void;
80
+ }
81
+
82
+ /**
83
+ * Returns a memoized boolean validator function for a single production.
84
+ * `opts` (xmlVersion, asciiOnly) are fixed at creation time. Repeated calls
85
+ * with the same string after the first are served from an internal cache
86
+ * instead of re-running the regex — useful when validating a small, reused
87
+ * vocabulary of names (e.g. tag/attribute names across many siblings in a
88
+ * document) many times.
89
+ */
90
+ export function createValidator(production: Production, opts?: CreateValidatorOptions): MemoizedValidator;
91
+
58
92
  /**
59
93
  * Validates a string against a named XML production and returns a detailed result.
60
94
  */