How to replace text with substitute that contains JSX code? - javascript

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.

Related

Skip or remove newline within pre and code tags in string

So currently I'm going for the effect of trying to display formatted code or in this case a graphql operation in my react app. Triggered by state, I want to display or remove certain variables.
const testing = `query {
getBooks {
${value.bookId?"id" : ""}
${value.bookEntry?"entry" : ""}
${value.bookTitle?"title" : ""}
}
}`
...
<div className="output">
<pre>
<code>{testing}</code>
</pre>
</div>
I'm stuck rendering something that looks like this!
There's probably a better way to go around this, but it's worth asking!
Add a filter to remove whitespaces before rendering.
Check my solution below:
// Solution:
function clearQueryString(queryString){
const result = []
const splitted = queryString.split('\n')
console.log({splitted})
for (let index = 0; index < splitted.length; index++) {
const element = splitted[index];
// regex credit: https://stackoverflow.com/questions/10261986/how-to-detect-string-which-contains-only-spaces/50971250
if (!element.replace(/\s/g, '').length) {
console.log(`#${index} string only contains whitespace!`);
} else {
result.push(element)
}
}
return result.join('\n')
}
// Usage:
const value = {}
const testing = `query {
getBooks {
${value.bookId?"id" : ""}
${value.bookEntry?"entry" : ""}
${value.bookTitle?"title" : ""}
author
otherfield
}
}`
document.getElementById('codeOutput').innerHTML = clearQueryString(testing);
<div className="output">
<pre>
<code id="codeOutput"></code>
</pre>
</div>

What are reliable approaches for finding words/text within an HTML-markup's text-content and replacing the matches with highlighting markup?

I have some text. And I have a function that receives a word or phrase and I have to return the same text but with a span with a class around this keyword or phrase.
Example:
If I have this
text = https://www.website.com
I want
text = https://www.<span class="bold">website</span>.com
but what I'm getting is
text = website </span>.com&context=post" target="_blank" rel="noopener noreferrer">https://www.<span class="bold"> website </span>.com
What I'm doing is
...
const escapedPhrases = ["\\bwebsite\\b"]
const regex = new RegExp(`(${escapedPhrases.join('|')})`, 'gi');
text = text.replace(
regex,
'<span class="bold"> $1 </span>'
);
How can I improve my regex?
Also I have tried to "clean" the text after the replacement of <span class="bold"> $1 </span> to try of remove it if it's inside the href but with no success.
UPDATE for clarification:
I have this text:
text = `Follow me on
https://www.twitter.com
Thanks!`
Example 1:
I want to highlight the word twitter:
For this I want to add a span with class bold for example around twitter:
text = `Follow me on
https://www.<span class="bold">twitter</span>.com
Thanks!`
Example 2:
I want to highlight the word twitter.com:
For this I want to add a span with class bold for example around twitter.com:
text = `Follow me on
https://www.<span class="bold">twitter.com</span>
Thanks!`
Example 3:
I want to highlight the word https://twitter.com/:
For this I want to add a span with class bold for example around https://twitter.com/:
text = `Follow me on
<span class="bold">https://www.twitter.com</span>
Thanks!`
Example 4:
I have this text and want to highlight twitter:
text = `Follow me on
https://www.twitter.com
Thanks for follow my twitter!`
Then I have to return
text = `Follow me on
https://www.<span class="bold">twitter</span>.com
Thanks for follow my <span class="bold">twitter</span>!`
Regex is not a solution to everything, in that case, to only modifying the textContent and not the attribute maybe this following code will fit your needs:
let text = `Follow me on
https://www.twitter.com
Thanks for follow my twitter!`;
const replaceKeyword = (keyword, text) => {
let template = document.createElement('template');
template.innerHTML = text;
let children = template.content.childNodes;
let str = '';
let substitute = `<span style='color:red;font-weight:bold;'>${keyword}</span>`;
for (let child of children){
if (child.nodeType === 3){
// #text
str += child.textContent.replace(keyword, substitute);
} else if (child.nodeType === 1) {
// element
let nodeStr = child.textContent.replace(keyword, substitute);
child.innerHTML = nodeStr;
str += child.outerHTML;
}
}
return str;
}
let result = replaceKeyword('twitter', text);
console.log(result);
document.body.innerHTML = result;
With the latest features which got added to the requirements, the OP entirely changed the game. One now is talking about a full-text-search within the text-contents of html-markup.
Something similar to ...
How to highlight the search-result of a text-query within an html document ignoring the html tags?
Markdown-like functionality for tooltips ... or ... How to query text-nodes from DOM, find markdown-patterns, replace matches with HTML-markup and replace the original text-node with the new content?
What is a good enough approach for writing real-time text search and highlight functionality which does not break the order of text- and element-nodes
... with the last two one providing different but generic DOM-node/text-node based approaches.
As for the OP's problem. With requirements like finding a text-query within the text-content of html-code, one can not stick to a simple solution. One now has to assume nested markup.
Providing/adding a special markup around each search result has to start with firstly collecting every single text-node from the very DOM-fragment which had to be parsed before from the passed html-code.
Having such a base, one can not anymore just fire around with a regex based String.replace. One now has to replace/reassamble each text-node that partially matches the search-query with the text-contents which did not match and the part that now changes into an element-node due to the additional markup which gets wrapped around the matching text.
Thus just from the OP's last requirement change, one has to provide a generic full text search and highlight approach which of cause in addition has to take into account and to sanitize/handle white-space sequences and regex-specific characters within the provided search query ...
// node detection helpers.
function isElementNode(node) {
return (node && (node.nodeType === 1));
}
function isNonEmptyTextNode(node) {
return (
node
&& (node.nodeType === 3)
&& (node.nodeValue.trim() !== '')
&& (node.parentNode.tagName.toLowerCase() !== 'script')
);
}
// dom node render helper.
function insertNodeAfter(node, referenceNode) {
const { parentNode, nextSibling } = referenceNode;
if (nextSibling !== null) {
node = parentNode.insertBefore(node, nextSibling);
} else {
node = parentNode.appendChild(node);
}
return node;
}
// text node reducer functionality.
function collectNonEmptyTextNode(list, node) {
if (isNonEmptyTextNode(node)) {
list.push(node);
}
return list;
}
function collectTextNodeList(list, elmNode) {
return Array.from(
elmNode.childNodes
).reduce(
collectNonEmptyTextNode,
list
);
}
function getTextNodeList(rootNode) {
rootNode = (isElementNode(rootNode) && rootNode) || document.body;
const elementNodeList = Array.from(
rootNode.getElementsByTagName('*')
);
elementNodeList.unshift(rootNode);
return elementNodeList.reduce(collectTextNodeList, []);
}
// search result emphasizing functinality.
function createSearchMatch(text) {
const elmMatch = document.createElement('strong');
// elmMatch.classList.add("bold");
elmMatch.textContent = text;
return elmMatch;
}
function aggregateSearchResult(collector, text, idx) {
const { previousNode, regXSearch } = collector;
const currentNode = regXSearch.test(text)
? createSearchMatch(text)
: document.createTextNode(text);
if (idx === 0) {
previousNode.parentNode.replaceChild(currentNode, previousNode);
} else {
insertNodeAfter(currentNode, previousNode);
}
collector.previousNode = currentNode;
return collector;
}
function emphasizeTextContentMatch(textNode, regXSearch) {
// console.log(regXSearch);
textNode.textContent
.split(regXSearch)
.filter(text => text !== '')
.reduce(aggregateSearchResult, {
previousNode: textNode,
regXSearch,
})
}
function emphasizeEveryTextContentMatch(htmlCode, searchValue, isIgnoreCase) {
searchValue = searchValue.trim();
if (searchValue !== '') {
const replacementNode = document.createElement('div');
replacementNode.innerHTML = htmlCode;
const regXSearchString = searchValue
// escaping of regex specific characters.
.replace((/[.*+?^${}()|[\]\\]/g), '\\$&')
// additional escaping of whitespace (sequences).
.replace((/\s+/g), '\\s+');
const regXFlags = `g${ !!isIgnoreCase ? 'i' : '' }`;
const regXSearch = RegExp(`(${ regXSearchString })`, regXFlags);
getTextNodeList(replacementNode).forEach(textNode =>
emphasizeTextContentMatch(textNode, regXSearch)
);
htmlCode = replacementNode.innerHTML
}
return htmlCode;
}
const htmlLinkList = [
emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks!',
'twitter'
),
emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks!',
'twitter.com'
),
emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks!',
'https://www.twitter.com/'
),
emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks for follow my Twitter!',
'TWITTER',
true
),
emphasizeEveryTextContentMatch(
`Follow me on https://www.twitter.com/
Thanks
for follow
my Twitter!`,
'follow my twitter',
true
),
];
document.body.innerHTML = htmlLinkList.join('<br/>');
const container = document.createElement('code');
container.textContent = emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks for follow my Twitter!',
'TWITTER',
true
);
document.body.appendChild(container.cloneNode(true));
container.textContent = emphasizeEveryTextContentMatch(
`Follow me on https://www.twitter.com/
Thanks
for follow
my Twitter!`,
'follow my twitter',
true
);
document.body.appendChild(container.cloneNode(true));
code {
display: block;
margin: 10px 0;
padding: 0
}
a strong {
font-weight: bold;
}
.as-console-wrapper { min-height: 100%!important; top: 0; }

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>

Find all occurrences (letters) in array (word)

I want to create a hangman game in React.js, with this code when the user click on a letter, it will search the letter in the word and display her. It work correctly when the word contain only one same letter.
I would like this code work with word like 'PROGRAMMING' with 2 'M'.
handleChooseLetter = (index) => {
const usedWord = [...this.state.usedWord];
const chosenLetter = this.state.letters[index].letter;
var letterPosition = usedWord.indexOf(chosenLetter);
if (letterPosition >= 0) {
hiddenWord.splice(letterPosition, 1, chosenLetter);
this.setState({hiddenWord: hiddenWord});
}
}
I already try a while loop but it not work in my case:
var indices = [];
while(letterPosition >= 0) {
const hiddenWord = [...this.state.hiddenWord];
indices.push(letterPosition);
letterPosition = usedWord.indexOf(chosenLetter, letterPosition + 1);
hiddenWord.splice(letterPosition, 1, chosenLetter);
this.setState({hiddenWord: hiddenWord});
}
For me, the result is that find the letter and display them always for the last letter of the word.
I think my problem is with the splice method who splice the wrong letterPosition
Here my chooseWord function:
state = {
wordList: [
{ id: 1, word: 'PROGRAMMING'},
],
usedWord: [],
hiddenWord: [],
}
chooseWord() {
const wordList = [...this.state.wordList];
const listLength = wordList.length;
const randomWord = this.state.wordList[Math.floor(Math.random() * listLength)].word;
const splittedWord = randomWord.split("");
const arr = new Array(randomWord.length + 1).join("_").split("");
this.setState({
usedWord: splittedWord,
hiddenWord: arr
});
}
The simplest way is replace, not using an array:
const usedWord = "programming";
const chosenLetter = "m";
const hiddenWord = usedWord.replace(new RegExp("[^" + chosenLetter + "]", "g"), "_");
console.log(hiddenWord);
As the user adds more letters, you can add them to the character class:
const usedWord = "programming";
const chosenLetters = "mp";
const hiddenWord = usedWord.replace(new RegExp("[^" + chosenLetters + "]", "g"), "_");
console.log(hiddenWord);
React Example:
class Hangman extends React.Component {
constructor(...args) {
super(...args);
this.state = {
availableLetters: "abcdefghijklmnopqrstuvwxyz",
chosenLetters: "",
word: this.props.word
};
this.chooseLetter = this.chooseLetter.bind(this);
}
chooseLetter({target: {tagName, type, value}}) {
if (tagName === "INPUT" && type === "button") {
this.setState(prevState => ({chosenLetters: prevState.chosenLetters + value}));
}
}
render() {
const {word, chosenLetters} = this.state;
const hiddenWord = word.replace(new RegExp("[^" + chosenLetters + "]", "g"), "_");
return <div>
<div>Word: <span className="hiddenWord">{hiddenWord}</span></div>
<div onClick={this.chooseLetter} style={{marginTop: "8px"}}>
{[...this.state.availableLetters].map(
letter => <input type="button" value={letter} disabled={chosenLetters.includes(letter)} />
)}
</div>
</div>;
}
}
ReactDOM.render(
<Hangman word="programming" />,
document.getElementById("root")
);
.hiddenWord {
font-family: monospace;
letter-spacing: 1em;
font-size: 18px;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
For single English alphabet letters, you don't have to worry about using new RegExp(chosenLetter, "g") because none of the English alphabetic letters has special meaning in a regular expression. If you did have characters with special meaning (., $, etc.), you'd escape the character before passing it to the RegExp constructor; see this question's answers for ways to do that.
I've added letter input <input onChange={this.handleChooseLetter} value={letter} /> and changed your handleChooseLetter function to iterate through letters of used word if at least 1 letter is found (because your usedWord.indexOf(chosenLetter) always returns 1 index only), so I decided to iterate entire word and check for chosen letter, if letter on that index exists, I just insert that letter to the hidden word on the same index - because hidden and used words have the same length:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
hiddenWord: "___________",
usedWord: "PROGRAMMING",
letter: ""
};
}
handleChooseLetter = e => {
const usedWord = [...this.state.usedWord];
const chosenLetter = e.target.value.toLocaleUpperCase();
let letterPosition = usedWord.indexOf(chosenLetter);
if (letterPosition > -1) {
this.setState(prevState => {
const hiddenWord = [...prevState.hiddenWord];
for (let i = 0; i < usedWord.length; i++) {
if (usedWord[i] === chosenLetter) {
hiddenWord[i] = chosenLetter;
}
}
return { hiddenWord, letter: "" };
});
return;
}
this.setState({ letter: "" });
};
render() {
const { hiddenWord, letter } = this.state;
return (
<div className="App">
{[...hiddenWord].map((letter, index) => (
<span key={index}>{letter} </span>
))}
<input onChange={this.handleChooseLetter} value={letter} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Aha! I had to do a similar project for my class once. Heres the github repo for your reference:
https://github.com/LordKriegan/WraithFood
Essentially, what I did was I created an object with several strings and a few functions. When the game loads, it picks a random word from my list and sets 2 of the strings. One is the full word (lets call this property fullWord), the other is basically a string of the same length with all letters converted to underscores (let's call this one guessedWord, also see setWord() in the game.js file).
But you are interested in checking the letters! Thats easy enough. Javascript strings have a built-in method called .includes(), which takes a string as a parameter. Since I had my word saved in the object, I simply ran a .includes() on that word with the letter I wanted to check, then if it passed all my validation (letter already guessed, whether it is or isnt in the word, etc), I ran a for loop on guessedWord with another String method called .charAt(i). This method simply returns the character at position i in the string its called on. Since I have to strings in my object, which I KNOW to be the same length, I can execute a for loop on fullWord, check every letter at position i, and then reset the guessedWord property with .substr(). This method returns partial strings based on what parameters you pass it. Essentially I set guessedWord to + + . This was the checkLet() function in the game object.
I would reccomend reading up on string methods at https://www.w3schools.com/js/js_string_methods.asp

Check string for array elements and systematically replace them with HTML tags - (JS, ReactJS)

I need to search a string, and if it has any values that match my array, I need to add <span></span> tags to them to add custom CSS. I am using reactJS.
How do I search the string for objects from my array?
Example:
let string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}'
let array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'},...]
findAllOccurrances = () => {???}
Then systematically replace them '{{inputX}}' with <span className='bizarre-highlight'>{{inputX}}</span>
My intent is to add custom CSS to any text in the div which matches my array, so if you got any ideas please shoot! Again, using reactJS if that helps.
I created a component that will replace the elements that need to be highlighted with a span you can test it here
The component is:
import React from 'react';
export default ({ terms, children }) => {
const result = []
const regex = terms.map(escapeRegExp).join('|');
const re = new RegExp(regex);
let text = (' ' + children).slice(1); // copy
let match = re.exec(text);
while (match != null) {
const str = match.toString();
result.push(text.slice(0, match.index));
result.push(<span className="highlighted">{str}</span>);
text = text.slice(match.index + str.length);
match = re.exec(text);
}
result.push(text);
return result;
}
function escapeRegExp (str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
And you should use it like this:
import React from 'react';
import Highlighter from './Highlighter';
const terms = [ '{{input1}}', '{{input2}}' ]
const App = () => (
<div>
<Highlighter terms={terms}>
{'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}'}
</Highlighter>
</div>
);
Use String#replace with a RegExp to find all instances of '{{inputX}}', and wrap the matches with the span:
const string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input3}}'
const array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'}]
const pattern = new RegExp(array.map(({ parameter }) => parameter).join('|'), 'g');
const result = string.replace(pattern, (match) =>
`<span className='bizarre-highlight'>${match}</span>`
)
console.log(result)
use Array#map to extract values for wrapping in <span> and then cycle on them for replacement:
let string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}';
let array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'}];
array.map(el => { return el.parameter }).forEach(str => {
string = string.split(str).join("<span className=\'bizarre-highlight\'>" + str + "</span>");
});
console.log(string);

Categories