Skip to main content
Participant
November 9, 2023
Answered

GREP replace quotation marks within quotation marks

  • November 9, 2023
  • 3 replies
  • 1899 views

Hi, I am looking to find and replace quotation marks within quotation marks, as example:

He cited: «As someone said «I need to better understand grep» and he was right».

 

Sorry for the silly example, but I want to find and select the second « (in order to replace with "), so i guess the line should said something like: '«', after another '«' followed by whatever except '»'. But i really cannot do it.

 

After that, I should select the first » for same reason.

 

Thank you

This topic has been closed for replies.
Correct answer Joel Cherney

It would be absolutely possible to do this one with a lookbehind, but that's not how I started figuring this one out. Can you go into more detail on what you're trying to do? It sounds like you simply want to replace all double chevron quotes with double quote marks. I'm not sure why you'd want to do it in multiple passes; it looks to me like it's best done in one pass. 

 

The "followed by whatever except" bit is easy enough to do in GREP. If there are multiple nested quotes in a single paragraph, we'd need to have an expression that wasn't greedy; we want it to stop at the first double chevron close quote, right? The expression for that is

.+?

So my "Find" expression would be

(«)(.+?)(«)(.+?)(»)(.+?)(»)

 and my "Change to" expression would be

"$2"$4"$6"

because enclosing an expression in parentheses allows you to handle each parenthetical expression separately in the "Change to" field by addressing each group with a dollar sign. So this one replaces all the double chevron quotes with typographer's quotes. I would assume that you'd want the nested quotes to be single quotes, not double quotes, so I'd suggest this "Change to" expression instead:

"$2'$4'$6"

which leads to this behavior:

 

If that's not what you're looking for, please do post again and fill us in.  If you absolutely had to do one pass to replace the inner quotes, followed by another pass to replace the outer quotes, the first "Change to" expression would be more like 

$1$2"$4"$6$7

(Also I'm sure that my method is a bit of a brute force method, and there are far more elegant ways to pull it off.)

 

3 replies

m1b
Community Expert
Community Expert
November 10, 2023

Hi @giognis4789300, you already have a good answer, but I was intrigued by this problem and decided to write a script to solve it. Here is what I came up with:

/**
 * Example of "findChangeNestedBrackets" function:
 * Find and process bracketing in selected text
 * so that inner brackets (level 1 and above)
 * are changed from chevrons to quote marks.
 * Easily customizable via the `changer` function.
 * @author m1b
 * @discussion https://community.adobe.com/t5/indesign-discussions/grep-replace-quotation-marks-within-quotation-marks/m-p/14223446
 */
function main() {

    /**
     * This is the function that decides what
     * to change, based on the char and level.
     * Customize this to suit your needs.
     * Important note: the closing level is 1
     * higher than the opening level, so that
     * level 1 opening matches with level 2 closing.
     * @param {String} ch - a single character.
     * @param {Number} level - the hierachical level of bracketing.
     * @param {String} changeTo - private variable, only here for convenience.
     */
    function changeNestedChevronsToQuotes(ch, level, changeTo) {

        // opening bracket
        if (ch == '«') {
            if (level == 1) changeTo = '“';
            else if (level == 2) changeTo = '‘';
            return [changeTo, true];
        }

        // closing bracket
        else if (ch == '»') {
            if (level == 2) changeTo = '”';
            else if (level == 3) changeTo = '’';
            return [changeTo, false];
        }

        // this is just for our peace-of-mind:
        if (level == 4) throw Error('We did not plan for level 4 nested brackets!');

    }

    findChangeNestedBrackets({
        text: app.activeDocument.selection[0],
        changer: changeNestedChevronsToQuotes,
        showResults: true,
        warnIfUnbalanced: true,
    });

};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Find/Change Brackets');


/**
 * Perform find/change operation on characters used
 * as opening/closing bracketing, with reference
 * to their hierachical level.
 *
 * Example `changer` function:
 *
 *    function (ch, level, changeTo) {
 *       // opening bracket
 *       if (ch == '«') {
 *           if (level == 1) changeTo = '“';
 *           else if (level == 2) changeTo = '‘';
 *           return [changeTo, true];
 *       }
 *       // closing bracket
 *       else if (ch == '»') {
 *           if (level - 1 == 1) changeTo = '”';
 *           else if (level - 1 == 2) changeTo = '’';
 *           return [changeTo, false];
 *       }
 *       // this is just for our peace-of-mind:
 *       if (level == 3) throw Error('We did not plan for level 3 nested brackets!');
 *    };
 *
 * The `changer` function returns:
 * [changeToCharacter, risenToNewLevel],
 * which is used to
 *   (a) know if the character needs changing
 *       (undefined means no change), and
 *   (b) know when the level needs to change.
 *
 * @author m1b
 * @version 2023-11-10
 * @param {Object} options
 * @param {Text|TextFrame|Story} options.text - the text to find in.
 * @param {Function} options.changer - a function that returns a replacement char, if any, given `ch` {char} and `level` {Number}.
 * @param {Boolean} options.showResults - whether to show count of home many changes.
 */
function findChangeNestedBrackets(options) {

    options = options || {};

    var changer = options.changer,
        text = options.text;

    if (
        changer == undefined
        || typeof changer !== 'function'
    )
        throw Error('findNestedBrackets: bad `changer` function supplied.');

    if (
        text == undefined
        || !text.hasOwnProperty('characters')
    )
        return [];

    var contents = text.contents,
        change,
        counter = 0,
        level = 0;

    for (var i = 0; i < contents.length; i++) {

        if (change = changer(contents[i], level)) {

            if (change[0]) {
                // make the change
                text.characters[i].contents = change[0];
                counter++;
            }

            // adjust level depending on whether
            // change was opening or closing
            level += change[1] === true ? 1 : -1;

        }

    }

    // some messages for user:

    if (
        options.warnIfUnbalanced
        && level !== 0
    )
        alert(
            'Warning\nBackets found in target text were unbalanced by ' + level
            + (level == 1
                ? ' level. This usually means a bracket was opened but never closed.'
                : ' levels. This usually means ' + level + ' brackets were opened but never closed.'
            )
        );

    if (options.showResults)
        alert('Changed ' + counter + ' brackets.');

    return counter;

};

 

As I wrote in the script documentation, you can customize the changes by editing the "changeNestedChevronsToQuotes" function.

 

Currently it is configured so that a first level of chevrons does nothing (where level is 0), and the next level of nested chevrons are changed to double-quotes, and then next level of nested chevrons are changed to single-quotes.

- Mark

 

 

P.S. @Joel Cherney, my middle name is literally HappyToTakeAnHourToScriptATwoMinuteJob. 🙂 What's wrong with me!

Joel Cherney
Community Expert
Community Expert
November 12, 2023

That's great, Mark! I mean, not only does it address the OP's question, but I think it also proves the intro-CS-course point I was trying to make with the wrong terminology a few posts upthread. If you need to parse nested tags, you need to be able to address the innermost tag set. And if you can't...

 

        // this is just for our peace-of-mind:
        if (level == 4) throw Error('We did not plan for level 4 nested brackets!');

 

Well, I don't know about you, Giognis, but my mind is at peace, now. 

 

 

m1b
Community Expert
Community Expert
November 13, 2023

Thanks @Joel Cherney! As you rightly mentioned, the parsing of brackets is a case where regex on it's own won't cover all cases.

 

Also, @giognis4789300, I put the if (level == 4) check just for a demonstration. In practice, you may prefer something like

if (level > 1) changeTo = '”';

which means it would change every level of nesting after the first to the same character.

- Mark 

Joel Cherney
Community Expert
Joel CherneyCommunity ExpertCorrect answer
Community Expert
November 9, 2023

It would be absolutely possible to do this one with a lookbehind, but that's not how I started figuring this one out. Can you go into more detail on what you're trying to do? It sounds like you simply want to replace all double chevron quotes with double quote marks. I'm not sure why you'd want to do it in multiple passes; it looks to me like it's best done in one pass. 

 

The "followed by whatever except" bit is easy enough to do in GREP. If there are multiple nested quotes in a single paragraph, we'd need to have an expression that wasn't greedy; we want it to stop at the first double chevron close quote, right? The expression for that is

.+?

So my "Find" expression would be

(«)(.+?)(«)(.+?)(»)(.+?)(»)

 and my "Change to" expression would be

"$2"$4"$6"

because enclosing an expression in parentheses allows you to handle each parenthetical expression separately in the "Change to" field by addressing each group with a dollar sign. So this one replaces all the double chevron quotes with typographer's quotes. I would assume that you'd want the nested quotes to be single quotes, not double quotes, so I'd suggest this "Change to" expression instead:

"$2'$4'$6"

which leads to this behavior:

 

If that's not what you're looking for, please do post again and fill us in.  If you absolutely had to do one pass to replace the inner quotes, followed by another pass to replace the outer quotes, the first "Change to" expression would be more like 

$1$2"$4"$6$7

(Also I'm sure that my method is a bit of a brute force method, and there are far more elegant ways to pull it off.)

 

Participant
November 9, 2023

I'm sorry but this solution leads to a problem, that it finds and selects also three subsequent quotations

 

 

Robert at ID-Tasker
Legend
November 9, 2023

This should help you a bit:

 

https://carijansen.com/positive-lookbehind-grep-for-designers/

 

But I'm not GREP guru and I'm on my phone.

 

@FRIdNGE or @Joel Cherney should be able to give you the correct expression on the spot.