You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23โ€“26.RSVP โ†’
Socket
Book a DemoSign in
Socket

path-expression-matcher

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

path-expression-matcher - npm Package Compare versions

Comparing version
1.0.0
to
1.1.0
+2
-2
package.json
{
"name": "path-expression-matcher",
"version": "1.0.0",
"version": "1.1.0",
"description": "Efficient path tracking and pattern matching for XML/JSON parsers",

@@ -13,3 +13,3 @@ "main": "src/index.js",

"scripts": {
"test": "node test/test.js"
"test": "node test/namespace_test.js && node test/test.js"
},

@@ -16,0 +16,0 @@ "keywords": [

@@ -40,2 +40,9 @@ # path-expression-matcher

}
// Namespace support
const nsExpr = new Expression("soap::Envelope.soap::Body..ns::UserId");
matcher.push("Envelope", null, "soap");
matcher.push("Body", null, "soap");
matcher.push("UserId", null, "ns");
console.log(matcher.toString()); // "soap:Envelope.soap:Body.ns:UserId"
```

@@ -82,7 +89,33 @@

### Namespaces
```javascript
"ns::user" // user with namespace "ns"
"soap::Envelope" // Envelope with namespace "soap"
"ns::user[id]" // user with namespace "ns" and "id" attribute
"ns::user:first" // First user with namespace "ns"
"*::user" // user with any namespace
"..ns::item" // item with namespace "ns" anywhere in tree
"soap::Envelope.soap::Body" // Nested namespaced elements
"ns::first" // Tag named "first" with namespace "ns" (NO ambiguity!)
```
**Namespace syntax:**
- Use **double colon (::)** for namespace: `ns::tag`
- Use **single colon (:)** for position: `tag:first`
- Combined: `ns::tag:first` (namespace + tag + position)
**Namespace matching rules:**
- Pattern `ns::user` matches only nodes with namespace "ns" and tag "user"
- Pattern `user` (no namespace) matches nodes with tag "user" regardless of namespace
- Pattern `*::user` matches tag "user" with any namespace (wildcard namespace)
- Namespaces are tracked separately for counter/position (e.g., `ns1::item` and `ns2::item` have independent counters)
### Combined Patterns
```javascript
"..user[id]:first" // First user with id, anywhere
"root..user[type=admin]" // Admin user under root
"..user[id]:first" // First user with id, anywhere
"root..user[type=admin]" // Admin user under root
"ns::user[id]:first" // First namespaced user with id
"soap::Envelope..ns::UserId" // UserId with namespace ns under SOAP envelope
```

@@ -130,3 +163,3 @@

##### `push(tagName, attrValues)`
##### `push(tagName, attrValues, namespace)`

@@ -138,2 +171,3 @@ Add a tag to the current path. Position and counter are automatically calculated.

- `attrValues` (object, optional): Attribute key-value pairs (current node only)
- `namespace` (string, optional): Namespace for the tag

@@ -144,2 +178,4 @@ **Example:**

matcher.push("item"); // No attributes
matcher.push("Envelope", null, "soap"); // With namespace
matcher.push("Body", { version: "1.1" }, "soap"); // With both
```

@@ -207,2 +243,10 @@

##### `getCurrentNamespace()`
Get current namespace.
```javascript
const ns = matcher.getCurrentNamespace(); // "soap" or undefined
```
##### `getAttrValue(attrName)`

@@ -258,9 +302,14 @@

##### `toString(separator?)`
##### `toString(separator?, includeNamespace?)`
Get path as string.
**Parameters:**
- `separator` (string, optional): Path separator (uses default if not provided)
- `includeNamespace` (boolean, optional): Whether to include namespaces (default: true)
```javascript
const path = matcher.toString(); // "root.users.user"
const path2 = matcher.toString('/'); // "root/users/user"
const path = matcher.toString(); // "root.ns:user.item"
const path2 = matcher.toString('/'); // "root/ns:user/item"
const path3 = matcher.toString('.', false); // "root.user.item" (no namespaces)
```

@@ -429,2 +478,44 @@

### Example 7: Namespace Support (XML/SOAP)
```javascript
const matcher = new Matcher();
const soapExpr = new Expression("soap::Envelope.soap::Body..ns::UserId");
// Parse SOAP document
matcher.push("Envelope", { xmlns: "..." }, "soap");
matcher.push("Body", null, "soap");
matcher.push("GetUserRequest", null, "ns");
matcher.push("UserId", null, "ns");
// Match namespaced pattern
if (matcher.matches(soapExpr)) {
console.log("Found UserId in SOAP body");
console.log(matcher.toString()); // "soap:Envelope.soap:Body.ns:GetUserRequest.ns:UserId"
}
// Namespace-specific counters
matcher.reset();
matcher.push("root");
matcher.push("item", null, "ns1"); // ns1::item counter=0
matcher.pop();
matcher.push("item", null, "ns2"); // ns2::item counter=0 (different namespace)
matcher.pop();
matcher.push("item", null, "ns1"); // ns1::item counter=1
const firstNs1Item = new Expression("root.ns1::item:first");
console.log(matcher.matches(firstNs1Item)); // false (counter=1)
const secondNs1Item = new Expression("root.ns1::item:nth(1)");
console.log(matcher.matches(secondNs1Item)); // true
// NO AMBIGUITY: Tags named after position keywords
matcher.reset();
matcher.push("root");
matcher.push("first", null, "ns"); // Tag named "first" with namespace
const expr = new Expression("root.ns::first");
console.log(matcher.matches(expr)); // true - matches namespace "ns", tag "first"
```
## ๐Ÿ—๏ธ Architecture

@@ -431,0 +522,0 @@

@@ -79,3 +79,3 @@ /**

* @private
* @param {string} part - Segment string (e.g., "user", "user[id]", "user:first")
* @param {string} part - Segment string (e.g., "user", "ns::user", "user[id]", "ns::user:first")
* @returns {Object} Segment object

@@ -86,41 +86,104 @@ */

// Match pattern: tagname[attr] or tagname[attr=value] or tagname:position
// Examples: user, user[id], user[type=admin], user:first, user[id]:first, user:nth(2)
const match = part.match(/^([^[\]:]+)(?:\[([^\]]+)\])?(?::(\w+(?:\(\d+\))?))?$/);
// NEW NAMESPACE SYNTAX (v2.0):
// ============================
// Namespace uses DOUBLE colon (::)
// Position uses SINGLE colon (:)
//
// Examples:
// "user" โ†’ tag
// "user:first" โ†’ tag + position
// "user[id]" โ†’ tag + attribute
// "user[id]:first" โ†’ tag + attribute + position
// "ns::user" โ†’ namespace + tag
// "ns::user:first" โ†’ namespace + tag + position
// "ns::user[id]" โ†’ namespace + tag + attribute
// "ns::user[id]:first" โ†’ namespace + tag + attribute + position
// "ns::first" โ†’ namespace + tag named "first" (NO ambiguity!)
//
// This eliminates all ambiguity:
// :: = namespace separator
// : = position selector
// [] = attributes
if (!match) {
throw new Error(`Invalid segment pattern: ${part}`);
// Step 1: Extract brackets [attr] or [attr=value]
let bracketContent = null;
let withoutBrackets = part;
const bracketMatch = part.match(/^([^\[]+)(\[[^\]]*\])(.*)$/);
if (bracketMatch) {
withoutBrackets = bracketMatch[1] + bracketMatch[3];
if (bracketMatch[2]) {
const content = bracketMatch[2].slice(1, -1);
if (content) {
bracketContent = content;
}
}
}
segment.tag = match[1].trim();
// Step 2: Check for namespace (double colon ::)
let namespace = undefined;
let tagAndPosition = withoutBrackets;
// Parse attribute condition [attr] or [attr=value]
if (match[2]) {
const attrExpr = match[2];
if (withoutBrackets.includes('::')) {
const nsIndex = withoutBrackets.indexOf('::');
namespace = withoutBrackets.substring(0, nsIndex).trim();
tagAndPosition = withoutBrackets.substring(nsIndex + 2).trim(); // Skip ::
if (attrExpr.includes('=')) {
const eqIndex = attrExpr.indexOf('=');
const attrName = attrExpr.substring(0, eqIndex).trim();
const attrValue = attrExpr.substring(eqIndex + 1).trim();
if (!namespace) {
throw new Error(`Invalid namespace in pattern: ${part}`);
}
}
segment.attrName = attrName;
segment.attrValue = attrValue;
// Step 3: Parse tag and position (single colon :)
let tag = undefined;
let positionMatch = null;
if (tagAndPosition.includes(':')) {
const colonIndex = tagAndPosition.lastIndexOf(':'); // Use last colon for position
const tagPart = tagAndPosition.substring(0, colonIndex).trim();
const posPart = tagAndPosition.substring(colonIndex + 1).trim();
// Verify position is a valid keyword
const isPositionKeyword = ['first', 'last', 'odd', 'even'].includes(posPart) ||
/^nth\(\d+\)$/.test(posPart);
if (isPositionKeyword) {
tag = tagPart;
positionMatch = posPart;
} else {
segment.attrName = attrExpr.trim();
// Not a valid position keyword, treat whole thing as tag
tag = tagAndPosition;
}
} else {
tag = tagAndPosition;
}
// Parse position selector :first, :nth(n), :odd, :even
if (match[3]) {
const posExpr = match[3];
if (!tag) {
throw new Error(`Invalid segment pattern: ${part}`);
}
// Check for :nth(n) pattern
const nthMatch = posExpr.match(/^nth\((\d+)\)$/);
segment.tag = tag;
if (namespace) {
segment.namespace = namespace;
}
// Step 4: Parse attributes
if (bracketContent) {
if (bracketContent.includes('=')) {
const eqIndex = bracketContent.indexOf('=');
segment.attrName = bracketContent.substring(0, eqIndex).trim();
segment.attrValue = bracketContent.substring(eqIndex + 1).trim();
} else {
segment.attrName = bracketContent.trim();
}
}
// Step 5: Parse position selector
if (positionMatch) {
const nthMatch = positionMatch.match(/^nth\((\d+)\)$/);
if (nthMatch) {
segment.position = 'nth';
segment.positionValue = parseInt(nthMatch[1], 10);
} else if (['first', 'odd', 'even'].includes(posExpr)) {
segment.position = posExpr;
} else {
throw new Error(`Invalid position selector: :${posExpr}`);
segment.position = positionMatch;
}

@@ -127,0 +190,0 @@ }

@@ -36,4 +36,5 @@ /**

* @param {Object} attrValues - Attribute key-value pairs for current node (optional)
* @param {string} namespace - Namespace for the tag (optional)
*/
push(tagName, attrValues = null) {
push(tagName, attrValues = null, namespace = null) {
// Remove values from previous current node (now becoming ancestor)

@@ -53,4 +54,7 @@ if (this.path.length > 0) {

// Create a unique key for sibling tracking that includes namespace
const siblingKey = namespace ? `${namespace}:${tagName}` : tagName;
// Calculate counter (how many times this tag appeared at this level)
const counter = siblings.get(tagName) || 0;
const counter = siblings.get(siblingKey) || 0;

@@ -64,3 +68,3 @@ // Calculate position (total children at this level so far)

// Update sibling count for this tag
siblings.set(tagName, counter + 1);
siblings.set(siblingKey, counter + 1);

@@ -74,2 +78,7 @@ // Create new node

// Store namespace if provided
if (namespace !== null && namespace !== undefined) {
node.namespace = namespace;
}
// Store values only for current node

@@ -94,5 +103,7 @@ if (attrValues !== null && attrValues !== undefined) {

// Clean up sibling tracking for this level
if (this.siblingStacks[this.path.length]) {
delete this.siblingStacks[this.path.length];
// Clean up sibling tracking for levels deeper than current
// After pop, path.length is the new depth
// We need to clean up siblingStacks[path.length + 1] and beyond
if (this.siblingStacks.length > this.path.length + 1) {
this.siblingStacks.length = this.path.length + 1;
}

@@ -126,2 +137,10 @@

/**
* Get current namespace
* @returns {string|undefined}
*/
getCurrentNamespace() {
return this.path.length > 0 ? this.path[this.path.length - 1].namespace : undefined;
}
/**
* Get current node's attribute value

@@ -186,7 +205,13 @@ * @param {string} attrName - Attribute name

* @param {string} separator - Optional separator (uses default if not provided)
* @param {boolean} includeNamespace - Whether to include namespace in output (default: true)
* @returns {string}
*/
toString(separator) {
toString(separator, includeNamespace = true) {
const sep = separator || this.separator;
return this.path.map(n => n.tag).join(sep);
return this.path.map(n => {
if (includeNamespace && n.namespace) {
return `${n.namespace}:${n.tag}`;
}
return n.tag;
}).join(sep);
}

@@ -321,2 +346,11 @@

// Match namespace if specified in segment
if (segment.namespace !== undefined) {
// Segment has namespace - node must match it
if (segment.namespace !== '*' && segment.namespace !== node.namespace) {
return false;
}
}
// If segment has no namespace, it matches nodes with or without namespace
// Match attribute name (check if node has this attribute)

@@ -323,0 +357,0 @@ // Can only check for current node since ancestors don't have values