How to match a string if it ends differently? - javascript

I have a list of navlinks. When I'm on a certain page, that navlink should be highlighted. I also want the page up (only) one level to have its navlink highlighted as well, so:
All pages: /blogs, blogs/careers, blogs/authors
Page: /blogs/author
Highlight: /blogs/author, /blogs
Page: /blogs/author/Lauren-Stephenson
Highlight: /blogs/author/Lauren-Stephenson, blogs/authors
Here's how I'm doing it:
import React from 'react';
const navlinks = ["/blogs", "blogs/careers", "blogs/authors"]
const currentPath = "/blogs/authors/Lauren-Stephenson"
export function App(props) {
return (
<div className='App'>
{navlinks.map((links) =>
<div style={{color: currentPath.includes(links) ? 'green' : 'white'}}>{links}</div>
)}
</div>
);
}
But my code not only highlights /blogs/Authors/, it also highlights /blogs, which is incorrect, because I only want the page up one level to be highlighted.
How can I do this?
currentPage: /blogs/Authors/Lauren-Steph
/blogs (HIGHLIGHTED) INCORRECTLY HIGHLIGHTED!
/blogs/careers
/blogs/authors (HIGHLIGHTED) Correct
currentPage: /blogs/Authors
/blogs
/blogs/careers
/blogs/authors (Correct)
currentPage: /blogs
/blogs (Correct)
/blogs/careers
/blogs/authors

So your highlighting logic seems to be: highlight:
current page
page one level above
Provided that link formats are consistent using absolute path, you can explicitly check against the 2 paths that should match instead of using string .includes()
const navLinks = ['/blog', '/blog/parent', '/blog/parent/child', '/blog/parent/child/grandchild', '/blog/parent/sibling', '/blog/other', '/blog/other/subpage']
const currentPath = '/blog/parent/child/grandchild'
const getHighlightedPaths = (currentPath) => {
return [
currentPath,
currentPath.substring(0, currentPath.lastIndexOf('/'))
]
}
const highlightedPaths = getHighlightedPaths(currentPath)
console.log('1. ', highlightedPaths)
console.log('2. Should highlight paths?', navLinks.map(link => link + ' - ' + (highlightedPaths.includes(link) ? 'yes' : 'no')))
Console output:
1. [
"/blog/parent/child/grandchild",
"/blog/parent/child"
]
2. Should highlight paths? [
"/blog - no",
"/blog/parent - no",
"/blog/parent/child - yes",
"/blog/parent/child/grandchild - yes",
"/blog/parent/sibling - no",
"/blog/other - no",
"/blog/other/subpage - no"
]

You got it the other way around. It's called: Array.protoype.includes() - Notice the "Array". Therefore use an Array:
links.includes(currentPath)
But... given your currentPath is somewhat a longer path string, you might want additionally check for if some of the navlinks startsWith a specific String:
const navlinks = ["/blogs", "/blogs/careers", "/blogs/authors"];
const checkNavPath = (path) => navlinks.some(nav => path.startsWith(nav));
console.log(checkNavPath("/blogs"));
console.log(checkNavPath("/blogs/authors"));
console.log(checkNavPath("/blogs/authors/Lauren-Stephenson"));
console.log(checkNavPath("/recipes/food/mashed-potato"));
Also, make sure your paths start all with /

The easiest way is to check if the path starts with the link and the link only has one extra "/".
Since you don't have proper data (not all of the links/paths start with "/") I had to write a function to normalize them.
const navlinks = ["/blogs", "blogs/careers", "blogs/authors"];
const normalizePath = (path) => path.replace(/^(?!\/)/, "/"); // add the starting "/" if it isn't there
const shouldHighlightLink = (currentPath, link) => {
currentPath = normalizePath(currentPath);
link = normalizePath(link);
return currentPath === link ||
(currentPath.startsWith(link + "/") &&
currentPath.split("/").length - 1 === link.split("/").length);
}
console.log(
"/blogs/authors/Lauren-Stephenson",
navlinks.reduce((acc, cur) => (
acc[normalizePath(cur)] = shouldHighlightLink("/blogs/authors/Lauren-Stephenson",
cur
), acc), {})
);
console.log(
"/blogs/authors",
navlinks.reduce((acc, cur) => (
acc[normalizePath(cur)] = shouldHighlightLink("/blogs/authors",
cur
), acc), {})
);

Related

Check if string start with an Array of strings

I have an array of strings :
listOfTypes = ['blogPost', 'work']
And I have a routeName
routeName = blogPost-slug or routeName = blogPost;
When routeName contains = blogPost I want to return string "page".
If routeName start with string- (example: blogPost-), I want to return the type (from listOfTypes)
I made a forEach function that works. Only, I get several results (it's normal) when I only want one (if it is present in the array, then I return either the type or page)
listOfTypes = ['blogPost', 'work'];
// routeName is retrieved dynamically. This is for example, "blogPost" or "blogPost-slug" or "work-slug"
listOfTypes.forEach(type => {
// Check if route name starts with type + "-" because it's separator with subroute
// (/blogPost is a page, /blogPost-slug is a blogPost so check for blogPost-)
if(routeName.startsWith(type + '-')) {
return console.log(type);
} else {
return console.log('page');
}
});
// result: 'blogPost' and 'page'
// expected result: 'blogPost'
How can I do this and only get either the type or "page" if it doesn't match?
Thank you
Using Array#find:
const listOfTypes = ['blogPost', 'work'];
const getType = route =>
listOfTypes.find(type => route.startsWith(`${type}-`)) ?? 'page';
console.log( getType('blogPost') );
console.log( getType('blogPost-slug') );
console.log( getType('work-slug') );

How to replace text with substitute that contains JSX code?

interface ICard {
content: string,
blanks: Array<{word: string, hidden: boolean}>
}
function processCards():Array<any>{
if (cards !==null ){
const text = cards.map((card,cardIndex)=>{
var content = card.content
card.blanks.map((blank,blankIndex)=>{
// replace content
const visibility = (blank.hidden)?'hidden':'visible'
const click_blank = <span className={visibility} onClick={()=>toggleBlank(cardIndex,blankIndex)}>{blank.word}</span>
content = content.replace(blank.word,click_blank)
})
return content
})
return text
} else {
return []
}
}
I have an array of objects of type ICard.
Whenever card.blanks.word appears in card.content, I want to wrap that word in tags that contain a className style AND an onClick parameter.
It seems like I can't just replace the string using content.replace like I've tried, as replace() does not like the fact I have JSX in the code.
Is there another way to approach this problem?
You need to construct a new ReactElement from the parts of string preceding and following each blank.word, with the new span stuck in the middle. You can do this by iteratively building an array and then returning it wrapped in <> (<React.Fragment>). Here's a (javascript) example:
export default function App() {
const toggleBlankPlaceholder = (cardIndex, blankIndex) => {};
const cardIndexPlaceholder = 0;
const blanks = [
{ word: "foo", hidden: true },
{ word: "bar", hidden: false },
];
const content = "hello foo from bar!";
const res = [content];
for (const [blankIndex, { word, hidden }] of blanks.entries()) {
const re = new RegExp(`(.*?)${word}(.*)`);
const match = res[res.length - 1].match(re);
if (match) {
const [, prefix, suffix] = match;
res[res.length - 1] = prefix;
const visibility = hidden ? "hidden" : "visible";
res.push(
<span
className={visibility}
onClick={() =>
toggleBlankPlaceholder(cardIndexPlaceholder, blankIndex)
}
>
{word}
</span>
);
res.push(suffix);
}
}
return <>{res}</>;
}
The returned value will be hello <span class="hidden">foo</span> from <span class="visible">bar</span>!
A couple of things:
In your example, you used map over card.blanks without consuming the value. Please don't do that! If you don't intend to use the new array that map creates, use forEach instead.
In my example, I assumed for simplicity that each entry in blanks occurs 0 or 1 times in order in content. Your usage of replace in your example code would only have replaced the first occurrence of blank.word (see the docs), though I'm not sure that's what you intended. Your code did not make an ordering assumption, so you'll need to rework my example code a little depending on the desired behavior.

How can I give a key in JSX the value of a variable depending on conditions

I'm learning React by implementing a front-end interface for the note app API that I created. I have succeeded in having a list of all the note titles in my database appear. I want to be able to click on a title and have the note expand into the text of the note. The easiest way I've found for this is to give the "key" attribute of the 'li' as a variable and to also declare the same variable in the JSX { } object because they have the same name.
I've been looking for an answer for this for a few days and have been unable to find this exact problem. You can put a variable in a normal JSX expression but I need to do it on the 'li' which means technically in the HTML.
Here's some code to understand what I'm saying.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [open, setOpen] = useState(false);
useEffect(() => {
//AXIOS CALL
setTitles(response.data[0]);
});
}, []);
//^^^^^add the array there to stop the response.data from repeating WAY TOO MANY TIMES
let listTitles = titles.map(titles => (
<li className="noteTitles" key={titles.title}>
{titles.title}
</li>
));
let showText = titles.map(titles => (
<li className="openText" key= {titles.text_entry}>
{titles.text_entry}
</li>
))
let openNote = () => {
setOpen(open => !open);
if (open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{showText}
</ul>
</div>
);
}
if (!open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listTitles}
</ul>
</div>
);
}
};
return { openNote };
};
export default NoteData;
That is the code I currently have. Here's showing a more simplified version of the openNote function that maybe makes more sense and shows what I'm trying to do:
VariableHere = "";
let openNote = () => {
setOpen(open => !open);
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
};
let listNotes = titles.map(titles => (
<li className="noteTitles" key={VariableHere}>
{VariableHere}
</li>
));
return (
<div>
<ul onClick={openNote}>
{listNotes}
</ul>
</div>
);
On click of each element there should be a switch of the key elements so if the element is 'open' the key variable and given variable in the JSX object should be mapped to titles.text_entry and on '(!open)' the key and JSX should be mapped to titles.title.
first of all, you're using a ternary in a weird way:
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
Ternaries are meant to be expressions whose value is conditional, but you're using it like a shorthand if/else. Try something like
VariableHere = open ? titles.text_entry : titles.title;
which is both shorter and more readable.
Second of all, keys in an array of elements are meant to help React determine which elements to update, if an item represents the same object, its key shouldn't change. In this case, regardless of what you're displaying, an item in the array represents the same note. Always using the title as the key should be fine provided items can't have the same title. If they can, use some sort of unique ID instead. If the order of the items doesn't change throughout the life of the component, using the array index as the key is fine.
Lastly, what you seem to want to do is called "conditional rendering". There are many ways to achieve this in react, one such way is to use the pre-cited ternary operator. Here is a minimal working example:
const listNotes = titles.map(note => (
<li className="noteTitles" key={note.title}>
{open ? note.title : note.text_entry}
</li>
));
const openNote = () => {
setOpen(!open);
}
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listNotes}
</ul>
</div>
)
You could also use a ternary in the key expression, but as I talked about above, it's not a good idea to do so.
Given your data-structure, I think you can simplify your code a bit. There is no need to create separate arrays for titles and contents. It sounds like you just want to expand and collapse a note when it is selected.
Here is a really simplified version on how you an do this. I'll use a sample data-set since we don't have access to your API.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [currentNote, setCurrentNote] = useState({});
useEffect(() => {
//AXIOS CALL
// setTitles(response.data[0]);
let data = [
{ id: 1, title: "a", text_entry: "what" },
{ id: 2, title: "b", text_entry: "is" },
{ id: 3, title: "c", text_entry: "up?" }
];
setTitles(data);
}, []);
const handleClick = noteId => {
let selectedTitle = titles.find(title => title.id == noteId);
//"collapse" if already selected
if (noteId === currentNote.id) {
setCurrentNote({});
} else {
setCurrentNote(selectedTitle);
}
};
let listTitles = titles.map(title => (
<li
className="noteTitles"
key={title.title}
onClick={() => handleClick(title.id)}
>
{title.title}
{title.id === currentNote.id && <div>{title.text_entry}</div>}
</li>
));
return (
<div>
Click on link item
<ul>{listTitles}</ul>
</div>
);
};
See working sandbox: https://codesandbox.io/s/old-silence-366ne
The main updates:
You don't need to have an "open" state. To be more succinct and
accurate, you should have a currentNote state instead, which is
set when clicking on a list item.
Have your handleClick function accept a noteId as an argument.
Then use that noteId to find the corresponding note in your titles
state. Set that found note as the currentNote. If the selected
note was already the currentNote, simply set currentNote to an
empty object {}, thus creating our expanding/collapsing effect.
In the JSX, after the title, use a ternary operator to conditionally
display the currentNote. If the note being mapped matches the
currentNote, then you would display a div containing the
text_entry.

Target fragment within react const

I am building my first react site, using gatsby with prismic.io as the CMS for my news section.
Within prismic I am using slices for quotes and featured images in each of the news stories and am looking to try and pull this data into my page, however I am unsure how to target the specific fragment names that I have created within the relevant const that has been set up for each.
GraphQL Query
export const query = graphql`
query ($slug:String){
prismicNewsStory (uid:{eq: $slug}) {
data {
body {
__typename
... on PrismicNewsStoryBodyQuote {
primary {
quote {
text
}
}
}
... on PrismicNewsStoryBodyFeaturedImage {
primary {
featured_image {
url
}
}
}
}
}
}
}
`
Targetting consts
const quote = props.data.prismicNewsStory.data.body[0].primary.quote.text
const featured_image = props.data.prismicNewsStory.data.body[1].primary.featured_image.url
As the slices are optional within prismic, I am encountering issues on some of the news stories when a featured_image is added before a quote, making them swap order within the body.
Question
Is there a way within each const to target a particular fragment or is there a better way for me to do this?
//get the array
const body = props.data.prismicNewsStory.data.body;
const {feature_image : fi0, quote: q0} = body[0].primary;
// above line is equivalent to:
// const fi0 = body[0].primary.feature_image;
// const q0 = body[0].primary.quote;
// when order is reversed q0 will be undefined
const {feature_image : fi = fi0, quote : q = q0} = body[1].primary;
// above line is equivalent to:
// const fi = body[1].primary.feature_image || fi0;
// const q = body[1].primary.quote || q0;
// when order is reversed fi0 will be assigned to fi
const feature_image = fi.url;
const quote = q.text
or use a reduce
const reduceStory = (acc, item) => ({
feature_image: acc.feature_image|| item.primary.feature_image,
quote: acc.quote || item.primary.quote
})
const story = props.data.prismicNewsStory.data.body.reduce(reduceStory, {});
const feature_image = story.feature_image.url;
const quote = story.quote.text
>
After looking and learning a bit more learning with #paul-mcbride we came up with the following solution to target any __typename.
const body = props.data.prismicNewsStory.data.body.reduce((object, item) => ({
...object,
[item.__typename]: item.primary
}), {});
You can now use the targeted slice name.
<FeaturedImage src={body.PrismicNewsStoryBodyFeaturedImage.featured_image.url} />
or
<QuoteText>{body.PrismicNewsStoryBodyQuote.quote.text}</QuoteText>

Determine if a path is subdirectory of another in Node.js

I am working on a MQTT handler for which I want to emit an event for each parent directory where there is a event listener. For example:
If there are the following MQTT paths available, where there are subscriptors –there are event listeners for these paths–
test
replyer/request
test/replyer/request
And someone publishes on topic test/replyer/request/#issuer, there should be 2 events emmited: test, test/replyer/request.
Given than any path is possible and there is no list of available valid events, we must check only if a path is a parent of another. Can we do this with regex? If so, how would it look like? Is there a simpler/more efficient solution?
Let Node itself do the work.
const path = require('path');
const relative = path.relative(parent, dir);
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
It does normalisation for you as well.
const path = require('path');
const tests = [
['/foo', '/foo'],
['/foo', '/bar'],
['/foo', '/foobar'],
['/foo', '/foo/bar'],
['/foo', '/foo/../bar'],
['/foo', '/foo/./bar'],
['/bar/../foo', '/foo/bar'],
['/foo', './bar'],
['C:\\Foo', 'C:\\Foo\\Bar'],
['C:\\Foo', 'C:\\Bar'],
['C:\\Foo', 'D:\\Foo\\Bar'],
];
tests.forEach(([parent, dir]) => {
const relative = path.relative(parent, dir);
const isSubdir = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
console.log(`[${parent}, ${dir}] => ${isSubdir} (${relative})`);
});
Works on Windows across drives too.
[/foo, /foo] => false ()
[/foo, /bar] => false (..\bar)
[/foo, /foobar] => false (..\foobar)
[/foo, /foo/bar] => true (bar)
[/foo, /foo/../bar] => false (..\bar)
[/foo, /foo/./bar] => true (bar)
[/bar/../foo, /foo/bar] => true (bar)
[/foo, ./bar] => false (..\Users\kozhevnikov\Desktop\bar)
[C:\Foo, C:\Foo\Bar] => true (Bar)
[C:\Foo, C:\Bar] => false (..\Bar)
[C:\Foo, D:\Foo\Bar] => false (D:\Foo\Bar)
2021 Answer
Use #Ilya's solution.
2017 Answer
In ES6.
const isChildOf = (child, parent) => {
if (child === parent) return false
let parentTokens = parent.split('/').filter(i => i.length)
let childTokens = child.split('/').filter(i => i.length)
return parentTokens.every((t, i) => childTokens[i] === t)
}
If you're working in node.js and you want to make it cross-platform, include the path module and replace split('/') with split(path.sep).
How it works:
So, you want to find out if a directory (like home/etc/subdirectory) is a subdirectory of another directory (like home/etc).
It takes both the hypothesised child and parent paths and convert them into arrays using split:
['home', 'etc', 'subdirectory'], ['home', 'etc']
It then iterates through all of the tokens in the parent array and checks them one-by-one against their relative position in the child array using ES6's .every().
If everything in parent matches up to everything in child, knowing that we've ruled out they are exactly the same directory (using child !== parent), we will have our answer.
For those who care about performances, which seem to pass unnoticed to people who have already answered, checking whether the sub path starts with its parent path should be enough.
const path = require('path');
function isSubPathOf(subPath, parentPath) {
parentPath = normalize(parentPath);
if (subPath.length <= parentPath.length)
return false;
function normalize(p) {
p = path.normalize(p);
if (!p.endsWith(path.sep))
p += path.sep;
return p;
}
subPath = normalize(subPath);
return subPath.startsWith(parentPath);
}
console.log(isSubPathOf('/a/b/c/d/e', '/a/b/c'));
console.log(isSubPathOf('/a/b/c/de', '/a/b/c'));
console.log(isSubPathOf('/a/b/c', '/a/y/c'));
console.log(isSubPathOf('/a/y/c/k', '/a/y/c'));
This is a really old question, but I came up with a dead simple solution for this using Node's built–in path.relative: if the child is inside the parent, the relative path from the former to the latter will always start with '..'.
import { relative } from 'path';
function isSubDirectory(parent, child) {
return relative(child, parent).startsWith('..');
}
There are a couple of things going on here that are needed to prevent failure:
Should we attempt to resolve filesystem paths? (I think so)
Checking if one directory contains another should work with symlinks
I came up with a solution that attempts to resolve filesystem paths as much as possible while allowing paths which may or may not exist:
Split the path on the OS's path separator
Resolve as many of those path components in the filesystem as possible
Append remaining components that couldn't be resolved
If the relative path between parent and child doesn't start with .. + path.sep and isn't .. then the parent path contains the child path
This all works, assuming that any nonexistent path components would be created using only directories and files (no symlinks). For example say your script needs to write only to whitelisted paths and you're accepting untrusted (user-supplied) filenames. You could create subdirectories using something like PHP's mkdir with $recursive = true to create the directory structure in one step, similar to this example.
Here is the code (not runnable until Stack Overflow supports Node.js), the important functions are resolveFileSystemPath() and pathContains():
const kWin32 = false;
const fs = require('fs');
const path = kWin32 ? require('path').win32 : require('path');
////////// functions //////////
// resolves (possibly nonexistent) path in filesystem, assuming that any missing components would be files or directories (not symlinks)
function resolveFileSystemPath(thePath) {
let remainders = [];
for (
let parts = path.normalize(thePath).split(path.sep); // handle any combination of "/" or "\" path separators
parts.length > 0;
remainders.unshift(parts.pop())
) {
try {
thePath =
fs.realpathSync(parts.join('/')) + // fs expects "/" for cross-platform compatibility
(remainders.length ? path.sep + remainders.join(path.sep) : ''); // if all attempts fail, then path remains unchanged
break;
} catch (e) {}
}
return path.normalize(thePath);
}
// returns true if parentPath contains childPath, assuming that any missing components would be files or directories (not symlinks)
function pathContains(parentPath, childPath, resolveFileSystemPaths = true) {
if (resolveFileSystemPaths) {
parentPath = resolveFileSystemPath(parentPath);
childPath = resolveFileSystemPath(childPath);
}
const relativePath = path.relative(parentPath, childPath);
return !relativePath.startsWith('..' + path.sep) && relativePath != '..';
}
////////// file/directory/symlink creation //////////
console.log('directory contents:');
console.log();
try {
fs.mkdirSync('parent');
} catch (e) {} // suppress error if already exists
fs.writeFileSync('parent/child.txt', 'Hello, world!');
try {
fs.mkdirSync('outside');
} catch (e) {} // suppress error if already exists
try {
fs.symlinkSync(path.relative('parent', 'outside'), 'parent/child-symlink');
} catch (e) {} // suppress error if already exists
fs.readdirSync('.').forEach(file => {
const stat = fs.lstatSync(file);
console.log(
stat.isFile()
? 'file'
: stat.isDirectory() ? 'dir ' : stat.isSymbolicLink() ? 'link' : ' ',
file
);
});
fs.readdirSync('parent').forEach(file => {
file = 'parent/' + file;
const stat = fs.lstatSync(file);
console.log(
stat.isFile()
? 'file'
: stat.isDirectory() ? 'dir ' : stat.isSymbolicLink() ? 'link' : ' ',
file
);
});
////////// tests //////////
console.log();
console.log(
"path.resolve('parent/child.txt'): ",
path.resolve('parent/child.txt')
);
console.log(
"fs.realpathSync('parent/child.txt'): ",
fs.realpathSync('parent/child.txt')
);
console.log(
"path.resolve('parent/child-symlink'): ",
path.resolve('parent/child-symlink')
);
console.log(
"fs.realpathSync('parent/child-symlink'):",
fs.realpathSync('parent/child-symlink')
);
console.log();
console.log(
'parent contains .: ',
pathContains('parent', '.', true)
);
console.log(
'parent contains ..: ',
pathContains('parent', '..', true)
);
console.log(
'parent contains parent: ',
pathContains('parent', 'parent', true)
);
console.log(
'parent contains parent/.: ',
pathContains('parent', 'parent/.', true)
);
console.log(
'parent contains parent/..: ',
pathContains('parent', 'parent/..', true)
);
console.log(
'parent contains parent/child.txt (unresolved): ',
pathContains('parent', 'parent/child.txt', false)
);
console.log(
'parent contains parent/child.txt (resolved): ',
pathContains('parent', 'parent/child.txt', true)
);
console.log(
'parent contains parent/child-symlink (unresolved):',
pathContains('parent', 'parent/child-symlink', false)
);
console.log(
'parent contains parent/child-symlink (resolved): ',
pathContains('parent', 'parent/child-symlink', true)
);
Output:
directory contents:
file .bash_logout
file .bashrc
file .profile
file config.json
dir node_modules
dir outside
dir parent
link parent/child-symlink
file parent/child.txt
path.resolve('parent/child.txt'): /home/runner/parent/child.txt
fs.realpathSync('parent/child.txt'): /home/runner/parent/child.txt
path.resolve('parent/child-symlink'): /home/runner/parent/child-symlink
fs.realpathSync('parent/child-symlink'): /home/runner/outside
parent contains .: false
parent contains ..: false
parent contains parent: true
parent contains parent/.: true
parent contains parent/..: false
parent contains parent/child.txt (unresolved): true
parent contains parent/child.txt (resolved): true
parent contains parent/child-symlink (unresolved): true
parent contains parent/child-symlink (resolved): false
Live example: https://repl.it/repls/LawngreenWorriedGreyware
The last line of the output is the important one, showing how resolved filesystem paths lead to the correct result (unlike the unresolved result above it).
Limiting filesystem reads/writes to certain directories is so important for security that I hope Node.js incorporates this functionality into their builtins. I haven't tested this on a native Windows box so please let me know if the kWin32 flag is working. I'll try to curate this answer as time allows.
Based on & improved Dom Vinyard's code:
const path = require('path');
function isAncestorDir(papa, child) {
const papaDirs = papa.split(path.sep).filter(dir => dir!=='');
const childDirs = child.split(path.sep).filter(dir => dir!=='');
return papaDirs.every((dir, i) => childDirs[i] === dir);
}
Result in:
assert(isAncestorDir('/path/to/parent', '/path/to/parent/and/child')===true);
assert(isAncestorDir('/path/to/parent', '/path/to')===false);
assert(isAncestorDir('/path/to/parent', '/path/to/parent')===true);
Doing it with regex is one way to go about it (for every path that has an event listener, check whether the published topic starts with that path), but since it's more likely you'll have many different paths than you having absurdly long URLs, breaking down the published topic might be more efficient.
Something like this is probably easier to read, too:
Edit: #huaoguo is definitely right, indexOf === 0 is all we really need!
let paths = [
'test',
'replyer/request',
'test/replyer/request'
]
let topic = 'test/replyer/request/#issuer'
let respondingPaths = (paths, topic) => paths.filter(path => topic.indexOf(path) === 0)
console.log(respondingPaths(paths, topic)) // ['test', 'test/replyer/request']
I would also like to point to the npm package path-is-inside which does exactly what the TO has been asking for:
Usage (quoted from the readme):
Pretty simple. First the path being tested; then the potential parent. Like so:
var pathIsInside = require("path-is-inside");
pathIsInside("/x/y/z", "/x/y") // true
pathIsInside("/x/y", "/x/y/z") // false
Paths are considered to be inside themselves:
pathIsInside("/x/y", "/x/y"); // true
For me, it does the job, and it is for sure a better to maintain such non-trivial logic in an extra package instead of a StackOverflow answer. :-)
#dom-vinyard's idea is good but code is not working correctly, for instance with this input:
isChildOf('/x/y', '/x') //false
I wrote my own version here:
function isParentOf(child, parent) {
const childTokens = child.split('/').filter(i => i.length);
const parentTokens = parent.split('/').filter(i => i.length);
if (parentTokens.length > childTokens.length || childTokens.length === parentTokens.length) {
return false;
}
return childTokens
.slice(0, parentTokens.length)
.every((childToken, index) => parentTokens[index] === childToken);
}
Here another solution that use indexOf (or that work by comparing strings).
In the function bellow i didn't use indexOf in order to support multiple path separators. You can check, but if you are sure you have just one separator, you can use indexOf with no problem.
The trick is to check if the path end with a separator, if not you just add such a separator to it. In that, there will be no problem of having a substring that is not a complete path in the child path. [/this/isme_man and /this/isme] (the first is child of the second, if we simply use indexOf (which of course if false), but if you do using the trick like this [/this/isme/ and /this/isme_man/] and you compare using same indexOf there will be no problem, and it work nikel)].
Notice too that there is an option, to allow a check with orEqual (a child or equal), it's the third optional parameter.
Check the code bellow.
const PATH_SEPA = ['\\', '/'];
function isPathChildOf(path, parentPath, orEqual) {
path = path.trim();
parentPath = parentPath.trim();
// trick: making sure the paths end with a separator
let lastChar_path = path[path.length - 1];
let lastChar_parentPath = path[parentPath.length - 1];
if (lastChar_parentPath !== '\\' && lastChar_parentPath !== '/') parentPath += '/';
if (lastChar_path !== '\\' && lastChar_path !== '/') path += '/';
if (!orEqual && parentPath.length >= path.length) return false; // parent path should be smaller in characters then the child path (and they should be all the same from the start , if they differ in one char then they are not related)
for (let i = 0; i < parentPath.length; i++) {
// if both are not separators, then we compare (if one is separator, the other is not, the are different, then it return false, if they are both no separators, then it come down to comparaison, if they are same nothing happen, if they are different it return false)
if (!(isPathSeparator(parentPath[i]) && isPathSeparator(path[i])) && parentPath[i] !== path[i]) {
return false;
}
}
return true;
}
function isPathSeparator(chr) {
for (let i = 0; i < PATH_SEPA.length; i++) {
if (chr === PATH_SEPA[i]) return true;
}
return false;
}
Here a test example:
let path = '/ok/this/is/the/path';
let parentPath = '/ok/this/is';
let parentPath2 = '/ok/this/is/';
let parentPath3 = '/notok/this/is/different';
console.log("/ok/this/is/the/path' is child of /ok/this/is => " + isPathChildOf(path, parentPath));
console.log("/ok/this/is/the/path' is child of /ok/this/is/=> " + isPathChildOf(path, parentPath2));
console.log("/ok/this/is/' is child of /ok/this/is/ => " + isPathChildOf(parentPath2, parentPath2));
console.log("/ok/this/is/the/path' is child of /notok/this/is/different => " + isPathChildOf(path, parentPath3));
// test number 2:
console.log('test number 2 : ');
console.log("=============================");
let pthParent = '/look/at/this/path';
let pth = '/look/at/this/patholabi/hola'; // in normal use of indexof it will return true (know too we didn't use indexof just to support the different path separators, otherwise we would have used indexof in our function)
//expected result is false
console.log(`${pth} is a child of ${pthParent} ===> ${isPathChildOf(pth, pthParent)}`);
let pthParent2 = '/look/at/this/path';
let pth2 = '/look/at/this/path/hola';
//expected result is true
console.log(`${pth2} is a child of ${pthParent2} ===> ${isPathChildOf(pth2, pthParent2)}`);
let pthParent3 = '/look/at/this/path';
let pth3 = '/look/at/this/pathholabi';
//expected result is false
console.log(`${pth3} is a child of ${pthParent3} ===> ${isPathChildOf(pth3, pthParent3)}`);
// test 3: equality
console.log('\ntest 3 : equality');
console.log("==========================");
let pParent = "/this/is/same/Path";
let p = "/this\\is/same/Path/";
console.log(`${p} is child of ${pParent} ====> ${isPathChildOf(p, pParent, true)}`);
You can see in the last example how we used the function to check for both being a child or equal (which can be really handful).
Also know that, You can check my two related github repos, that include too another implementation for the split method (a spliting method with multiple separator without using the regex engine)), also this method too, and some good explanation (check the comments within the codes):
https://github.com/MohamedLamineAllal/isPathChildOfJS
https://github.com/MohamedLamineAllal/splitStrJS
In my understanding, the issue is a little more complex than what my colleagues understand.
Paths could be tested with fs.existsSync(), but in this case we would create a dependency on the fs library and limit it to testing only absolute directories, both should be tested for their respective existence unless that the user would be interested in not using Windows as the Operating System so as not to need to inform the root directory, which is formed by default by "letter:\" treating the partitions as if they were sisters to each other, no none being nested to a larger directory.
The operation is totally different on Unix systems. The root directory is by default / and all mounted disks are nested to it, making it unique for the entire OS. It can be seen, therefore, that this is not ideal.
It is not possible to solve only with regular expression, considering that, if "pathFoldersLength" is greater than "path2CheckIfIsSupposedParentFoldersLength", a false negative could be obtained if, for example, "path" was equal to "laden /subfolder-1" and "path2CheckIfIsParent" to "bin/laden/subfolder-1/subfolder-1.1" and if the search was done with $ at the end of "path"; leaving without the $ would give a false positive if, for example, "path" was equal to "bin/laden/subfolder-1/subfolder-1.1" and "path2CheckIfIsParent" to "laden/subfolder-1";
If "pathFoldersLength" is less than "path2CheckIfIsSupposedParentFoldersLength, you could get a false negative if, for example, "path" is equal to "laden/subpath-1" and "path2CheckIfIsParent" to "bin/laden/ subpath-1" if the search was done with ^ at the beginning of "path2CheckIfIsParent", or it could give a false positive if, for example, "path" was equal to "bin/laden" and "path2CheckIfIsParent" to " bin/laden/subpath-1".
In this way we eliminate dependencies, leaving the method as little language dependent as possible.
import Path from 'path';
const isASubpathFromAnyOfSubpathSet = (path, path2CheckIfIsParent, isAbsolute = true) => {
const pathSeparatorPattern = new RegExp(/\\+/, 'g');
const pathSeparatorAtStartOrAtEndPattern = new RegExp(/^\/+|\/+$/, 'g');
path = path.replace(pathSeparatorPattern, `/`);
path2CheckIfIsParent = path2CheckIfIsParent.replace(pathSeparatorPattern, `/`);
path = path.replace(pathSeparatorAtStartOrAtEndPattern , ``);
path2CheckIfIsParent = path2CheckIfIsParent.replace(pathSeparatorAtStartOrAtEndPattern , ``)
if (path === path2CheckIfIsParent) return false;
const pathFolders = path.split(`/`);
const path2CheckIfIsSupposedParentFolders = path2CheckIfIsParent.split(`/`);
const pathFoldersLength = pathFolders.length;
const path2CheckIfIsSupposedParentFoldersLength = path2CheckIfIsSupposedParentFolders.length;
const indexesOf = [];
let pathFolderIndex = 0;
let supposedParentFolderIndex = 0;
let stopCriterian;
for (let i = 0; i < path2CheckIfIsSupposedParentFoldersLength; i++) {
if (pathFolders[0] === path2CheckIfIsSupposedParentFolders[i]) indexesOf.push(i);
}
if (indexesOf.length) {
if (isAbsolute) {
if (pathFoldersLength > path2CheckIfIsSupposedParentFoldersLength) {
path2CheckIfIsParent = path2CheckIfIsParent.replace(/\./g, `\\.`);
return (new RegExp(`^${path2CheckIfIsParent}`)).test(path);
}
} else {
if (indexesOf[0] === 0) indexesOf.shift();
if (pathFoldersLength < path2CheckIfIsSupposedParentFoldersLength) {
stopCriterian = () => pathFolderIndex < pathFoldersLength - 1;
} else {
stopCriterian = () => supposedParentFolderIndex < path2CheckIfIsSupposedParentFoldersLength - 1;
}
for (let indexOf of indexesOf) {
pathFolderIndex = 0;
for (supposedParentFolderIndex = indexOf; stopCriterian();) {
if (path2CheckIfIsSupposedParentFolders[supposedParentFolderIndex] !== pathFolders[pathFolderIndex]) break;
supposedParentFolderIndex++;
pathFolderIndex++;
}
}
if (pathFoldersLength < path2CheckIfIsSupposedParentFoldersLength) {
return pathFolderIndex === pathFoldersLength - 1;
} else {
return supposedParentFolderIndex === path2CheckIfIsSupposedParentFoldersLength - 1;
}
}
}
return false;
}
/*
// >
console.log(isASubpathFromAnyOfSubpathSet (`bin/laden/subfolder-1`, `bin/laden`)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1/subfolder-1.1.1`, `bin/laden/subfolder-1`)) // => false
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1`, `bin/laden`)) // => false
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1/subfolder-1.1.1`, `bin/laden/subfolder-1`, false)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1`, `bin/laden`, false)) // => true
// <
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1`, `bin/laden/subfolder-1/subfolder-1.1`, false)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1`, `bin/laden/subfolder-1`, false)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`subfolder-1/subfolder-1.1`, `bin/laden/subfolder-1`, false)) // => true
// ===
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1`, `bin/laden/subfolder-1`, false)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1`, `bin/laden`, false)) // => true
/**/
Use indexOf to check that the path to the child directory starts with the path to the parent directory is enough:
function isParentOf(parent, dir) {
return dir.indexOf(parent) === 0;
}
isParentOf('test/replyer/request/#issuer', 'test') // true
isParentOf('test/replyer/request/#issuer', 'replyer/request') // false
isParentOf('test/replyer/request/#issuer', 'test/replyer/request') // true

Categories