path-expression-matcher
Advanced tools
+2
-2
| { | ||
| "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": [ |
+97
-6
@@ -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 @@ |
+88
-25
@@ -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 @@ } |
+42
-8
@@ -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 |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
37316
22.95%589
17.33%622
17.14%