@playcanvas/react
Version:
A React renderer for PlayCanvas – build interactive 3D applications using React's declarative paradigm.
167 lines • 5.9 kB
JavaScript
export class PathMatcher {
caseSensitive;
constructor(options = {}) {
this.caseSensitive = options.caseSensitive ?? true;
}
/**
* Normalizes a path for comparison
*/
normalizePath(path) {
return this.caseSensitive ? path : path.toLowerCase();
}
/**
* Checks if a path matches a pattern
*/
match(pattern, entityPath, entity) {
// Check for component filter [componentType] anywhere in the pattern
const componentMatch = pattern.match(/\[([^\]]+)\]/);
let pathPattern = pattern;
let componentType = null;
if (componentMatch) {
componentType = componentMatch[1];
// Replace the filter from any position and clean up resulting dots
pathPattern = pattern
.replace(componentMatch[0], '')
.replace(/^\.+/, '') // Remove leading dots
.replace(/\.+$/, '') // Remove trailing dots
.replace(/\.\.+/g, '.'); // Collapse multiple dots to single dot
}
// If there's a component filter, check it first
if (componentType) {
if (!entity.c?.[componentType]) {
return false;
}
}
// If only component filter (no path), return true
if (!pathPattern) {
return !!componentType;
}
// Match path pattern
const pathMatch = this.matchPath(pathPattern, entityPath);
return pathMatch;
}
/**
* Matches a path against a pattern with wildcards
* Handles exact paths, single-level wildcards (*), and multi-level wildcards (**)
*/
matchPath(pattern, path) {
const normalizedPattern = this.normalizePath(pattern);
const normalizedPath = this.normalizePath(path);
// Special case: pattern is just "**" which matches any path
if (normalizedPattern === '**') {
return true;
}
// Handle empty pattern - should not match anything
if (normalizedPattern === '') {
return false;
}
// Split pattern into parts and filter out empty segments
const patternParts = normalizedPattern.split('.').filter(p => p !== '');
// If all parts were empty (e.g., ".." or "."), return false
if (patternParts.length === 0) {
return false;
}
// Build regex string with special handling for **
let regexStr = '^';
for (let i = 0; i < patternParts.length; i++) {
const part = patternParts[i];
if (part === '**') {
// ** matches zero or more complete segments
// Need to handle: A.**.B should match A.B, A.X.B, A.X.Y.B
if (i === 0) {
// At start: match zero or more segments (each ending with a dot)
regexStr += '(?:[^.]+\\.)*';
}
else if (i === patternParts.length - 1) {
// At end: match zero or more segments with required leading dot
regexStr += '(?:\\.(?:[^.]+))*';
}
else {
// In middle: match zero or more segments, handling dots carefully
regexStr += '(?:(?:\\.(?:[^.]+))*\\.)?';
}
}
else if (part === '*') {
// * matches exactly one segment
if (i > 0)
regexStr += '\\.';
regexStr += '[^.]+';
}
else {
// Literal part
if (i > 0 && patternParts[i - 1] !== '**') {
regexStr += '\\.';
}
// Escape special regex characters
regexStr += part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
regexStr += '$';
const regex = new RegExp(regexStr);
return regex.test(normalizedPath);
}
/**
* Calculates specificity score for a pattern (used for conflict resolution)
*/
getSpecificity(pattern) {
if (typeof pattern === 'function') {
return 0; // Predicates have lowest specificity
}
let score = 0;
// Remove component filter from anywhere in the pattern
const pathPattern = pattern.replace(/\[([^\]]+)\]/, '');
// Count exact parts (not wildcards)
const parts = pathPattern.split('.').filter(p => p !== '');
for (const part of parts) {
if (part === '**') {
score += 1;
}
else if (part === '*') {
score += 10;
}
else if (part) {
score += 100;
}
}
// Component filter adds specificity
if (pattern.includes('[')) {
score += 50;
}
return score;
}
/**
* Finds all entities matching a pattern in a hierarchy
*/
findMatching(pattern, hierarchyCache) {
const matches = [];
for (const metadata of hierarchyCache.values()) {
if (this.matches(pattern, metadata)) {
matches.push(metadata);
}
}
return matches;
}
/**
* Checks if a pattern matches an entity
*/
matches(pattern, metadata) {
if (typeof pattern === 'function') {
return pattern(metadata.entity, metadata);
}
return this.match(pattern, metadata.path, metadata.entity);
}
/**
* Resolves a path relative to a parent path
*/
resolvePath(parentPath, relativePath) {
if (!parentPath) {
return relativePath;
}
return `${parentPath}.${relativePath}`;
}
}
/**
* Default path matcher instance
*/
export const defaultPathMatcher = new PathMatcher();
//# sourceMappingURL=path-matcher.js.map