Copy link to clipboard
Copied
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
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 paragrap
...Copy link to clipboard
Copied
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.
Copy link to clipboard
Copied
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.)
Copy link to clipboard
Copied
Copy link to clipboard
Copied
Copy link to clipboard
Copied
But Joel Cherney made it "not greedy" - by using "?".
It's because my second capture group doesn't explicity exclude a close double chevron. Once you have identical nesting markers, then regex aren't really the best tool for sorting 'em out. It's why we're told Never Parse Tag Formats Using Regex, right? Because it makes more sense to parse a phrase with lots of nested tags by walking down to the innermost tag's content and working up from there; trying to parse the tag structure inline with regex just leads to false positives or false negatives.
Copy link to clipboard
Copied
That's why I started with "can you go into more detail"; because ny expression works just fine when there's only nested quoted phrases inside of another. in your document. You apparently have phrases with no nested quotes, and you don't want to match those, correct? You'd think that this would be obvious, but actually the only way to figure out the correct expression is to have all of that kind of information.
For example, it makes a lot more sense to take two passes through, now. Are you going to leave all of the phrases with «no nested quoted phrases» with their double chevron quotes? Or are they too going to have their quotes changed? Because it'd be comparatively easy to do all of the no-nested-quotes phrases first, so that your second pass would only need to catch more complicated phrases. Are any of those «phrases that «we» want to capture enclosing «more than one» nested quoted phrase»?
Also, I'm curious about the point at which it makes more sense to say: "Okay, these queries find all of these phrases, but we only want to perform the Change To statement on half of the phrases. We have maybe eight hundred such phrases in roughly three hundred pages, so hey Joel, why don't you sit down and spend the next four to six minutes stepping through the whole document, applying the Change To expression only to the roughly 50% of the phrases that need changes to be made?"
to which I respond:
"But Boss, I would far rather spend ninety minutes cooking up The Perfect GREP than I would like to spend six minutes hitting Find Next or Change/Find as appropriate."
and Boss says:
"Well okay then Joel, feel free to spend as much time as you need cooking up The Perfect GREP. It's not like the number of minutes spent here is going to affect our bottom line, or your invoice to us, or anything that actually matters. The only thing that matters is a perfect regular expression."
Copy link to clipboard
Copied
Still making me laugh, Joel.
~Barb
Copy link to clipboard
Copied
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!
Copy link to clipboard
Copied
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.
Copy link to clipboard
Copied
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