How to replace double quotes nested in another double quotes with single quotes

Viewed 886

I'm currently using a replace script to auto fix both single quotes and double quotes.

However, I cannot find a solution to change a double quote nested anywhere inside another double quotes

So that:

“Here’s some extra text at the beginning “abc” and at the end”

should be

“Here’s some extra text at the beginning ‘abc’ and at the end”

Currently, I can only auto-fix this type if it's beside the other quotes (e.g. ““abc””) using a simple replace script

.replaceAll("““", "“‘") and .replaceAll("””", "’”")

Is it possible to use regex to target any double quotes nested inside another?

Note: It's important that the quotes are curly (“” and ‘’) and not the straight ones ("" and '').

function fixTextarea(textarea) {
  textarea.value = textarea.value.replace(" ,", ",")
    .replaceAll(" ;", ";")
    .replaceAll(" .", ".")
    .replaceAll("  ", " ")
    .replaceAll("   ", " ")
    .replaceAll("“ ", "“")
    .replaceAll(" ”", "”")
    .replaceAll("““", "“‘")
    .replaceAll("””", "’”")
    .replaceAll(/(^|[-\u2014\s(\["])'/g, "$1\u2018")
    .replaceAll(/'/g, "\u2019")
    .replaceAll(/(^|[-\u2014/\[(\u2018\s])"/g, "$1\u201c")
    .replaceAll(/"/g, "\u201d")
};

function fixtext() {
  let textarea = document.getElementById("textarea1");
  textarea.select();
  fixTextarea(textarea);
}

window.addEventListener('DOMContentLoaded', function(e) {
  var area = document.getElementById("textarea1");

  var getCount = function(str, search) {
    return str.split(search).length - 1;
  };

  var replace = function(search, replaceWith) {
    if (typeof(search) == "object") {
      area.value = area.value.replace(search, replaceWith);
      return;
    }
    if (area.value.indexOf(search) >= 0) {
      var start = area.selectionStart;
      var end = area.selectionEnd;
      var textBefore = area.value.substr(0, end);
      var lengthDiff = (replaceWith.length - search.length) * getCount(textBefore, search);
      area.value = area.value.replace(search, replaceWith);
      area.selectionStart = start + lengthDiff;
      area.selectionEnd = end + lengthDiff;
    }
  };

});
<textarea class="lined" id="textarea1" name="textarea1" spellcheck="true" placeholder="" onpaste="console.log('onpastefromhtml')"></textarea>
<br><br>
<button onclick="fixtext()"> Fixit</button>

3 Answers

You could keep track of the depth of the nesting of quotes (whether or not double), and replace them in such a way that on even depths they are double quotes and on odd depths they are single quotes. One precaution is to exclude the apostrophe in ’s (There might be a few other exceptional cases where the apostrophe should not be taken as the close of a quote):

let s = "“Here’s some extra text at the beginning “abc” and at the end”";

let depth = 0;
let result = s.replace(/[“”‘]|’(?!s)/g, m => 
    "“‘".includes(m) ? "“‘"[depth++] : "”’"[--depth]);

console.log(result);

NB: This will even fix some cases where quotes are wrongly paired.

It could be integrated into your current function like this:

function fixTextarea(textarea) {
    let depth = 0; // <-- added
    textarea.value = textarea.value.replace(" ,", ",")
        .replaceAll(" ;", ";")
        .replaceAll(" .", ".")
        .replaceAll("  ", " ")
        .replaceAll("   ", " ")
        .replaceAll("“ ", "“")
        .replaceAll(" ”", "”")
        // (removed two lines here)
        .replaceAll(/(^|[-\u2014\s(\["])'/g, "$1\u2018")
        .replaceAll(/'/g, "\u2019")
        .replaceAll(/(^|[-\u2014/\[(\u2018\s])"/g, "$1\u201c")
        .replaceAll(/"/g, "\u201d")
        // added:
        .replace(/[“”‘]|’(?!s)/g, m => 
             "“‘".includes(m) ? "“‘"[depth++] : "”’"[--depth])
};

I didn't check the other replacements you are doing in detail, but I have some doubts about these two:

.replaceAll(/'/g, "\u2019") 
.replaceAll(/"/g, "\u201d")

These calls replace straight quotes by curly closing quotes, which would obviously create results where you don't have matching opening quotes...

I have a similar concern about the two other replacements that are made near those two.

Ok, my previous answer sucked.

Now just for fun: a nested quote parsing utitlity for straigth and curly quotes without any RegExp.

const {
  nestedDoubleQuotes2Single,
  nestedSingleQuotes2Double
} = nestedQuotesParser();
const singleCurly2Double = `‘Here’s some extra ‘text’ at the beginning ‘abc’ and at the end ’`;
const doubleCurly2Single = "“Here’s some extra “text” at the beginning “abc” and at the end ”";
const doubleStraight2Single = '"Here\'s some extra "text" at the beginning "abc" and at the end. "';
const singleStraight2Double = "'Here's some extra 'text' at the beginning 'abc' and at the end'";

console.log(nestedDoubleQuotes2Single(doubleCurly2Single, true));
console.log(nestedDoubleQuotes2Single(doubleStraight2Single));
console.log(nestedSingleQuotes2Double(singleStraight2Double));
console.log(nestedSingleQuotes2Double(singleCurly2Double, true));

function nestedQuotesParser() {
  let isCurly = false;
  const quoting = {
    get all() {
      return isCurly ? `“”‘’` : `""'`;
    },
    get single() {
      return isCurly ? `‘’` : `''`;
    },
    get double() {
      return isCurly ? `“”` : `""`;
    },
  };
  const reQuotDouble = m => quoting.single[quoting.all.indexOf(m)] || m;
  const reQuotSingle = m => quoting.double[quoting.all.indexOf(m) - 2] || m;
  const checkNestedSingle = (s, chr, i) =>
    ~quoting.all.indexOf(chr) && (i < s.length - 2 && (s[i - 1] === " " || s[i + 1] === " "));

  return {
    nestedDoubleQuotes2Single: (s, curly = false) => {
      isCurly = curly;
      return s.split("")
        .reduce((acc, chr, i) =>
          acc + (i > 0 && i < s.length - 1 &&
            ~quoting.all.indexOf(chr) ? reQuotDouble(chr) : chr), '');
    },
    nestedSingleQuotes2Double: (s, curly = false) => {
      isCurly = curly;
      return s.split("").reduce((acc, chr, i) =>
        acc + (s.length - i < s.length - 1 && checkNestedSingle(s, chr, i) ?
          curly && reQuotSingle(chr) || '"' :
          chr), "");
    },
  };
}

Your code, using this utility:

function fixTextarea(textarea) {
    let depth = 0; // <-- added
    const value = textarea.value.replace(" ,", ",")
        .replaceAll(" ;", ";")
        .replaceAll(" .", ".")
        .replaceAll("  ", " ")
        .replaceAll("   ", " ")
        .replaceAll("“ ", "“")
        .replaceAll(" ”", "”")
        .replaceAll(/(^|[-\u2014\s(\["])'/g, "$1\u2018")
        .replaceAll(/'/g, "\u2019")
        .replaceAll(/(^|[-\u2014/\[(\u2018\s])"/g, "$1\u201c")
        .replaceAll(/"/g, "\u201d");
    textarea.value = nestedDoubleQuotes2Single(value, true);
};

Try this:

console.log("“Here’s some extra text at the beginning “abc” and at the end”"
  .replace(/(“.*?)“(.*?)”(.*”)/, "$1‘$2’$3"));

Example at regex101

Related