Harry
Harry

Reputation: 54959

Regex replacement only if it's in braces and not in quotes

I'm looking to do replacements in unknown third-party inputs in strings that sometimes have quotes among them. I want to replace a wholeword whereever it occurs if it's in braces {} or marked with {replaceinside}{/replaceinside} unless it's in double or single-quotes, and unless the quote is escaped.

Regex so far largely comes from this question: Regex replace word in string that are not in quotes Thanks Wiktor Stribiżew and anubhava who helped me out there.

I thought after asking that question it would be easy to integrate the final braces requirement but guess not.

Here's my test string and the output:

`name: FOO
{favoriteQuote: "I am my own FOO."}
children: 'FOO\'s children'
{cars: ownersList[FOO]}
statement = {FOO} + " is the owner of the house."
{replaceinside}FOO "FOO" {'FOO' "    1 + FOO + 2 " ABCFOOXYZ} "  str1\"FOO\"str3'FOO'\'\'" '  str1\'FOOstr3"FOO"\"\"' \"FOO\"{/replaceinside}`

I would like to replace FOO with BAR. Expected Output:

`name: FOO
{favoriteQuote: "I am my own FOO."}
children: 'FOO\'s children'
{cars: ownersList[BAR]}
statement = {BAR} + " is the owner of the house."
{replaceinside}BAR "FOO" {'FOO' "    1 + FOO + 2 " ABCFOOXYZ} "  str1\"FOO\"str3'FOO'\'\'" '  str1\'FOOstr3"FOO"\"\"' \"BAR\"{/replaceinside}`

Ultimately I would like to use the text as a template and create an array of strings with mapped replacements. Like so:

var v = "BAR", arr = ["BAR", "BAZ"], finalArr = [], swappedFromValue = "FOO"

const text = String.raw`
name: FOO
{favoriteQuote: "I am my own FOO."}
children: 'FOO\'s children'
{cars: ownersList[FOO]}
statement = {FOO} + " is the owner of the house."
{replaceinside}FOO "FOO" {'FOO' "    1 + FOO + 2 " ABCFOOXYZ} "  str1\"FOO\"str3'FOO'\'\'" '  str1\'FOOstr3"FOO"\"\"' \"FOO\"{/replaceinside}
`

var replacementRegex = new RegExp('{(.*?)((?:[^\\\\]|^)(?:\\\\{2})*(?:"[^"\\\\]*(?:\\\\[^][^"\\\\]*)*"|\'[^\'\\\\]*(?:\\\\[^][^\'\\\\]*)*\'))|\\b' + swappedFromValue + '\\b(.*?)}', 'g') // single line
var replacementInsideRegex = new RegExp('{replaceinside}(.*?)((?:[^\\\\]|^)(?:\\\\{2})*(?:"[^"\\\\]*(?:\\\\[^][^"\\\\]*)*"|\'[^\'\\\\]*(?:\\\\[^][^\'\\\\]*)*\'))|\\b' + swappedFromValue + '\\b(.*?){/replaceinside}', 'gs') // multi line

function replaceFunc (v, k) {
  var replacedText = text.replace(replacementRegex, function (match, group) {
    return group || v;
  })
  replacedText = replacedText.replace(replacementInsideRegex, function (match, group) {
    return group || v;
  })
  finalArr.push(replacedText)
}

arr.forEach(replaceFunc)
document.body.innerHTML = finalArr.join("<br><br>")

Expected output:

`name: FOO
{favoriteQuote: "I am my own FOO."}
children: 'FOO\'s children'
{cars: ownersList[BAR]}
statement = {BAR} + " is the owner of the house."
{replaceinside}BAR "FOO" {'FOO' "    1 + FOO + 2 " ABCFOOXYZ} "  str1\"FOO\"str3'FOO'\'\'" '  str1\'FOOstr3"FOO"\"\"' \"BAR\"{/replaceinside}

name: FOO
{favoriteQuote: "I am my own FOO."}
children: 'FOO\'s children'
{cars: ownersList[BAZ]}
statement = {BAZ} + " is the owner of the house."
{replaceinside}BAZ "FOO" {'FOO' "    1 + FOO + 2 " ABCFOOXYZ} "  str1\"FOO\"str3'FOO'\'\'" '  str1\'FOOstr3"FOO"\"\"' \"BAZ\"{/replaceinside}`

fiddle: https://jsfiddle.net/x7016sv4/1/

Upvotes: 1

Views: 52

Answers (1)

trincot
trincot

Reputation: 350365

As the solution should also deal well with nested braces, and JavaScript regular expression have no recursion capability, I would suggest to move some of the logic out of the regular expression into JavaScript code. A regular expression can still be used to identify the individual parts, but some code will be needed to:

  • Maintain a stack of nested braces and nested tags
  • Pair balanced braces/tags
  • Make the replacement conditional on a non-empty stack
  • Decide how nesting of braces and tags should be dealt with when they overlap, like { aa {replaceinside} bb } cc {/replaceinside} dd. For instance, braces could be thought of as ineffective inside tags.

function solution(text, source, target) {
    const regex = RegExp(String.raw`(?:\\["'])+|\{\/?replaceinside\}|[{}]|(['"])(?:\\.|.)*?\1|\b${source}\b|.`, "gs");
    let stack = [];
    return input.replace(regex, function (token) {
        if (token === "{replaceinside}") {
            stack.push("{/replaceinside}");
        } else if (token === "{" && stack.at(-1) !== "{/replaceinside}") { // Braces inside tags have no meaning
            stack.push("}");
        } else if (token === stack.at(-1)) { // Pair balanced braces/tags
            stack.pop();
        } else if (token === "\n") { // Auto-close all braces
            stack = stack.filter(item => item !== "}");
        } else if (token === source && stack.length) { // Only replace within braces/tags
            return target;
        }
        return token;
    });
}

let input = String.raw`name: FOO
{favoriteQuote: "I am my own FOO."}
children: 'FOO\'s children'
{cars: ownersList[FOO]}
statement = {FOO} + " is the owner of the house."
{replaceinside}FOO "FOO" {'FOO' "    1 + FOO + 2 " ABCFOOXYZ} "  str1\"FOO\"str3'FOO'\'\'" '  str1\'FOOstr3"FOO"\"\"' \"FOO\"{/replaceinside}`;

console.log(solution(input, "FOO", "BAR"));

Upvotes: 1

Related Questions