Copy link to clipboard
Copied
I do a script to recognise tags <b> and <i> in paragraph text and replace font by choised font style and it works
But now I want to delete tags <b>, </b>, <i> and </i> but the diffrents font style disapare when I run a script to delete those tags
Illustrator keep the first style caracter it find in the paragraph
I have this :and I want to have this :
But it done that :
Here is a version that has a UI. If anybody uses it, please let me know if you find any bugs.
- Mark
/**
* Find/Change RegExp with UI.
* @author m1b
* @discussion https://community.adobe.com/t5/illustrator-discussions/illustrator-jsx-to-delete-lt-b-gt-lt-b-gt-tags-in-paragraph-text-an-keep-the-differents-font-style/m-p/14269543
*/
(function () {
if (app.documents.length === 0) {
alert('Please open a document and try again.');
return;
}
var settings = {
...
Copy link to clipboard
Copied
Hi @Jeffosaure, my guess is that your script is setting all the text in one go; something like:
var foundText.contents = textWithoutTags;
But when you set the `contents` of a text range, it (1) deletes the current text, eg "<i>italique</i>", then (2) inserts the specified text, eg. "italique" at the first insertion point of the text range (which now has no `contents`) and therefore (3) applies the text styles of that first insertion point to all the text. So that's why you may be losing the styling.
If my guess is correct, then here's what you can do to fix the problem: get text ranges that *only* include the tags themselves, not the middle contents, and remove them, starting at the last.
- Mark
Copy link to clipboard
Copied
There may be an easier way, but this is what came to me intuitively. Not extensively tested.
// select textFrame
var string1 = app.selection[0].textRange.contents;
var regex = /<b>|<\/b>|<i>|<\/i>/g;
// create array of attributes of characters
var textRanges = app.selection[0].textRanges;
var attributes = [];
for (var i = 0; i < textRanges.length; i++) {
var o = {};
o.textFont = textRanges[i].characterAttributes.textFont;
o.size = textRanges[i].characterAttributes.size;
attributes.push(o);
}
// create array of indices of to-be-removed substrings
var indices = [];
for (var i = regex.exec(string1); i != null; i = regex.exec(string1)) {
var o = {};
o.start = i.index;
if (i == "<b>" || i == "<i>") o.n = 3;
else o.n = 4;
indices.push(o);
}
// remove attributes of to-be-removed substrings
for (var i = indices.length - 1; i > -1; i--) {
attributes.splice(indices[i].start, indices[i].n)
}
// remove substrings
var string2 = string1.replace(regex, "");
app.selection[0].contents = string2;
// re-assign attributes to characters
for (var i = 0; i < textRanges.length; i++) {
textRanges[i].characterAttributes.textFont = attributes[i].textFont;
textRanges[i].characterAttributes.size = attributes[i].size;
}
Copy link to clipboard
Copied
EDIT 2023-12-01: The code for this post is the old version—I'll leave it for anyone who's interested because it's a bit simpler to understand, but if you just want to use it, please use the newer one I posted. The UI is totally separate to the function—you can use with or without the UI.
Hi @femkeblanco, I was pondering this too.
I realised that I had no general find/change function in my toolbox, so I've written one now. I take a slightly different approach to yours, by removing the found text except the first character, and then adding the replacement to just that first character. This means the non-found text will be untouched, such as the orange word BOLD in the screen grab below.
There is a lot of sanity checking and a bit for adjusting the text selection, but otherwise that's all it does. See what you think. It seems like a strange way to do it, but I don't know any way to create a TextRange by specifying the start and end chacacters. Let me know if you do!
- Mark
/**
* Demo of `findChangeRegExp` function.
* The function attempts to perform the find/change
* without modifying any character attributes (beyond
* those implicit in the change).
* @author m1b
* @discussion https://community.adobe.com/t5/illustrator-discussions/illustrator-jsx-to-delete-lt-b-gt-lt-b-gt-tags-in-paragraph-text-an-keep-the-differents-font-style/m-p/14269543
*/
(function () {
try {
/* demo example 1: find/change the whole document */
var counter = findChangeRegExp(app.activeDocument, /<\/?([ib])>/g, '');
/* demo example 2: find/change just the selection */
// var counter = findChange(app.activeDocument.selection, /<\/?([ib])>/g, '');
app.redraw();
alert('Performed ' + counter + ' find/change operations.');
}
catch (error) {
alert('Find Change function reported an error: ' + error.message);
}
})();
/**
* Performs a Find/Change operation on `text` and
* returns a count of how many changes were made.
* @author m1b
* @version 2023-12-01
* @param {TextRange|TextFrame} text - the text to search.
* @param {RegExp} findWhat - the RegExp to find with.
* @param {String} [changeTo] - the change-to string.
* @param {Boolean} [adjustSelection] - whether to adjust the selection (default: true when `text` has textSelection).
* @returns {Number}
*/
function findChangeRegExp(text, findWhat, changeTo, adjustSelection) {
var counter = 0;
if (text == undefined)
throw Error('findChange: bad `text` supplied. Expected [TextRange] or [TextFrame]');
if (
(
// to bypass bug in TextFrames object:
text.typename
&& text.typename == 'TextFrames'
)
|| text.constructor.name == 'Stories'
|| text.constructor.name == 'Array'
) {
// handle array of text frames
for (var i = text.length - 1; i >= 0; i--)
counter += findChangeRegExp(text[i], findWhat, changeTo, false);
return counter;
}
else if (text.constructor.name == 'Document')
return findChangeRegExp(text.stories, findWhat, changeTo, false);
else if (
text.constructor.name == 'Story'
|| text.constructor.name == 'TextFrame'
)
text = text.textRange;
else if (text.constructor.name !== 'TextRange')
throw TypeError('findChange: Bad `text` supplied.');
if (
findWhat !== undefined
&& findWhat.constructor.name === 'String'
)
findWhat = RegExp(findWhat, 'g');
else if (
findWhat == undefined
|| findWhat.constructor.name !== 'RegExp'
)
throw TypeError('findChange: Bad `findWhat` supplied.');
if (changeTo == undefined)
changeTo = '';
var contents = text.contents,
selectLength = 0,
selectStart,
selectEnd,
match,
found = [];
if (text.textSelection.length > 0) {
// record the selection for later
selectStart = text.textSelection[0].start;
selectEnd = text.textSelection[0].end;
selectLength = selectEnd - selectStart;
}
findWhat.lastIndex = 0;
while (match = findWhat.exec(contents))
found.push({
start: match.index,
end: findWhat.lastIndex,
str: contents.slice(match.index, findWhat.lastIndex),
});
counter = found.length;
// process each found, going backwards
var s, e;
while (match = found.pop()) {
// remove the found text, except first character
for (e = match.end - 1, s = match.start; s < e; e--) {
text.characters[e].remove();
selectEnd--;
}
var replacement = match.str.replace(findWhat, changeTo);
if (replacement) {
// set the contents of the first character
text.characters[e].contents = replacement;
selectEnd += replacement.length - match.str.length;
}
else {
// no contents to replace with
text.characters[e].remove();
selectEnd--;
}
}
if (
adjustSelection !== false
&& selectLength > 0
) {
// adjust the selection
app.selection = [];
for (var s = selectStart; s < selectEnd; s++)
text.story.characters[s].select(true);
}
return counter;
};
Copy link to clipboard
Copied
Here is a version that has a UI. If anybody uses it, please let me know if you find any bugs.
- Mark
/**
* Find/Change RegExp with UI.
* @author m1b
* @discussion https://community.adobe.com/t5/illustrator-discussions/illustrator-jsx-to-delete-lt-b-gt-lt-b-gt-tags-in-paragraph-text-an-keep-the-differents-font-style/m-p/14269543
*/
(function () {
if (app.documents.length === 0) {
alert('Please open a document and try again.');
return;
}
var settings = {
target: app.activeDocument,
findWhat: /<\/?[ib]>/,
changeTo: '',
ignoreLockedHidden: false,
showUI: true,
showResult: true,
};
// show UI
if (settings.showUI == true && ui(settings) == 2)
// user cancelled
return;
if (
settings.findWhat == undefined
|| settings.findWhat.constructor.name !== 'RegExp'
) {
alert('Sorry, "' + (settings.findText || 'findWhat') + '" was an invalid RegExp.');
return;
}
// do the find/change
var counter = findChangeRegExp(settings.target, settings.findWhat, settings.changeTo, settings.ignoreLockedHidden);
if (settings.showResult) {
app.redraw();
alert('Performed ' + counter + ' find/change operations.');
}
})();
/**
* Performs a Find/Change operation on `text` and
* returns a count of how many changes were made.
* @author m1b
* @version 2023-12-01
* @Param {TextRange|TextFrame} text - the text to search.
* @Param {RegExp} findWhat - the RegExp to find with.
* @Param {String} [changeTo] - the change-to string.
* @Param {Boolean} [ignoreLockedHidden] - whether to ignore locked or hidden text (default: true).
* @Param {Boolean} [adjustSelection] - whether to adjust the selection (default: true when `text` has textSelection).
* @Returns {Number}
*/
function findChangeRegExp(text, findWhat, changeTo, ignoreLockedHidden, adjustSelection) {
ignoreLockedHidden = ignoreLockedHidden !== false;
var counter = 0;
if (text == undefined)
throw Error('findChange: bad `text` supplied. Expected [TextRange] or [TextFrame]');
if (
(
// to bypass bug in TextFrames object:
text.typename
&& text.typename == 'TextFrames'
)
|| text.constructor.name == 'Stories'
|| text.constructor.name == 'Array'
) {
// handle array of text frames
for (var i = text.length - 1; i >= 0; i--)
counter += findChangeRegExp(text[i], findWhat, changeTo, ignoreLockedHidden, false) || 0;
return counter;
}
else if (text.constructor.name == 'GroupItem') {
// handle the group's items
for (var i = text.pageItems.length - 1; i >= 0; i--)
counter += findChangeRegExp(text.pageItems[i], findWhat, changeTo, ignoreLockedHidden, false) || 0;
return counter;
}
else if (text.constructor.name == 'Document')
return findChangeRegExp(text.stories, findWhat, changeTo, ignoreLockedHidden, false) || 0;
else if (
text.constructor.name == 'Story'
|| text.constructor.name == 'TextFrame'
)
text = text.textRange;
else if (text.constructor.name !== 'TextRange')
return;
if (
ignoreLockedHidden
&& !testItem(text.story.textFrames[0], isLockedOrHidden)
)
// ignore this text
return;
if (
findWhat !== undefined
&& findWhat.constructor.name === 'String'
)
findWhat = new RegExp(findWhat, 'g');
else if (
findWhat == undefined
|| findWhat.constructor.name !== 'RegExp'
)
throw TypeError('findChange: Bad `findWhat` supplied.');
// findWhat regex must be global!
if (!findWhat.global)
findWhat = new RegExp(findWhat.source, 'g' + (findWhat.ignoreCase ? 'i' : '') + (findWhat.multiline ? 'm' : ''));
// reset findWhat index
findWhat.lastIndex = 0;
if (changeTo == undefined)
changeTo = '';
var contents = text.contents,
selectLength = 0,
selectStart,
selectEnd,
match,
found = [];
if (text.textSelection.length > 0) {
// record the selection for later
selectStart = text.textSelection[0].start;
selectEnd = text.textSelection[0].end;
selectLength = selectEnd - selectStart;
}
while (match = findWhat.exec(contents))
found.push({
start: match.index,
end: findWhat.lastIndex,
str: contents.slice(match.index, findWhat.lastIndex),
});
counter = found.length;
// process each found, going backwards
var s, e;
while (match = found.pop()) {
// remove the found text, except first character
for (e = match.end - 1, s = match.start; s < e; e--) {
text.characters[e].remove();
selectEnd--;
}
var replacement = match.str.replace(findWhat, changeTo);
if (replacement) {
// set the contents of the first character
text.characters[e].contents = replacement;
selectEnd += replacement.length - match.str.length;
}
else {
// no contents to replace with
text.characters[e].remove();
selectEnd--;
}
}
if (
adjustSelection !== false
&& selectLength > 0
) {
// adjust the selection
app.selection = [];
for (var s = selectStart; s < selectEnd; s++)
text.story.characters[s].select(true);
}
return counter;
};
/**
* Performs `failFunction` on the item
* and its ancestors, returning false
* immediately that `failFunction`
* returns true.
* @Param {PageItem} item
* @Param {Function} failFunction
* @Returns {Boolean} - true, if item n
*/
function testItem(item, failFunction) {
while (
item.constructor.name !== 'Document'
&& item.hasOwnProperty('parent')
) {
if (failFunction(item))
return false;
item = item.parent;
}
return true;
};
/**
* Returns true if item
* is locked or hidden.
* @Param {PageItem} item - an Illustrator PageItem.
* @Returns {Boolean}
*/
function isLockedOrHidden(item) {
return (
(
item.locked != undefined
&& item.locked == true
)
|| (
item.hidden != undefined
&& item.hidden == true
)
|| (
item.layer != undefined
&& (
item.layer.locked == true
|| item.layer.visible == false
)
)
);
};
/**
* Shows UI suitable for Find/Change
* @Param {Object} settings
* @Returns {1|2} - ScriptUI result code.
*/
function ui(settings) {
var targets = [];
if (app.documents.length == 0)
return;
if (app.activeDocument.selection.length > 0)
targets.push(
{ label: 'Selection', get: function () { return app.activeDocument.selection } }
);
targets.push(
{ label: 'Document', get: function () { return app.activeDocument } },
);
var labelWidth = 100,
columnWidth = 200,
w = new Window("dialog { text:'Find/Change RegExp' }"),
fields = w.add("Group { orientation: 'column', alignment: ['fill','fill'] }"),
findWhatGroup = fields.add("Group { orientation: 'row', alignment: ['fill','fill'] }"),
findWhatLabel = findWhatGroup.add("StaticText { text: 'Find what:', justify: 'right' }"),
findWhatField = findWhatGroup.add("EditText { text:'', alignment:['fill','top'] }"),
changeToGroup = fields.add("Group { orientation: 'row', alignment: ['fill','fill'] }"),
changeToLabel = changeToGroup.add("StaticText { text: 'Change to:', justify: 'right' }"),
changeToField = changeToGroup.add("EditText { text:'', alignment:['fill','top'] }"),
targetMenuGroup = w.add("Group { orientation: 'row', alignment: ['fill','fill'] }"),
targetLabel = targetMenuGroup.add("StaticText { text: 'Target', justify: 'right' }"),
targetMenu = targetMenuGroup.add("Dropdownlist {alignment:['right','center'] }"),
checkBoxes = w.add("group {orientation:'row', alignment:['fill','bottom'], alignChildren:'left', margins:[5,0,5,0] }"),
ignoreCaseCheckBox = checkBoxes.add("Checkbox { text:'Ignore case', alignment:'left', value:false }"),
ignoreLockedHiddenCheckBox = checkBoxes.add("Checkbox { text:'Ignore locked/hidden text', alignment:'left', value:false }"),
bottomUI = w.add("group {orientation:'row', alignment:['fill','fill'], margins:[0,20,0,0] }"),
buttons = bottomUI.add("group {orientation:'row', alignment:['right','center'], alignChildren:'right' }"),
cancelButton = buttons.add("Button { text: 'Cancel', properties: {name:'cancel'} }"),
okayButton = buttons.add("Button { text:'Change', enabled: true, properties: {name:'ok'} }");
targetLabel.preferredSize.width = labelWidth;
targetMenu.preferredSize.width = columnWidth;
findWhatLabel.preferredSize.width = labelWidth;
findWhatField.preferredSize.width = columnWidth;
findWhatField.text = settings.findWhat.source;
changeToLabel.preferredSize.width = labelWidth;
changeToField.preferredSize.width = columnWidth;
changeToField.text = settings.changeTo;
ignoreCaseCheckBox.value = settings.findWhat.ignoreCase;
ignoreLockedHiddenCheckBox.value = settings.ignoreLockedHidden;
var targetTypes = [];
for (var i = 0; i < targets.length; i++)
targetTypes.push(targets[i].label);
buildMenu(targetMenu, targetTypes, settings.targetType);
okayButton.onClick = function () { return update() && w.close(1) };
cancelButton.onClick = function () { w.close(2) };
w.center();
return w.show();
/**
* Builds dropdown menu items.
*/
function buildMenu(menu, arr, index) {
for (var i = 0; i < arr.length; i++)
menu.add('item', arr[i]);
menu.selection = [index || 0];
};
/**
* Updates settings.
* @Returns {Boolean} - valid RegExp?
*/
function update() {
settings.target = targets[targetMenu.selection.index].get();
settings.changeTo = changeToField.text;
settings.findText = findWhatField.text;
settings.ignoreCase = !!ignoreCaseCheckBox.value;
settings.ignoreLockedHidden = !!ignoreLockedHiddenCheckBox.value;
try {
settings.findWhat = RegExp(findWhatField.text, 'g' + (ignoreCaseCheckBox.value ? 'i' : ''));
} catch (error) {
settings.findWhat = undefined;
}
return settings.findWhat !== undefined;
};
};
Copy link to clipboard
Copied
Oh Yeah
Amazing it works !!!
Yesterday I did a more simple script
// Mots à supprimer caractère par caractère
var motsASupprimer = ['<b>', '</b>', '<i>', '</i>'];
var doc = app.activeDocument;
function supprimerMots(textRange) {
for (var i = 0; i < motsASupprimer.length; i++) {
var mot = motsASupprimer[i];
// Supprimer le mot entier
var index = textRange.contents.indexOf(mot);
while (index !== -1) {
for (var j = 0; j < mot.length; j++) {
textRange.characters[index].remove();
}
index = textRange.contents.indexOf(mot);
}
}
}
for (var i = 0; i < doc.textFrames.length; i++) {
var texteCadre = doc.textFrames[i];
supprimerMots(texteCadre.textRange);
}
But with your edit window it's so cool
But can I make other choice in the field "Find What" : <\/?[ib]>
If I have some <strong> <em> tags ?
For <p> <a> etc... it's ok when I had it <\/?[ibpa]>
But if I whant to delete <strong> <\/?[strong]> it doesn't work
Copy link to clipboard
Copied
Should <\/?[strong]> have the escape character? If so, it will need an escape character for the escape character.
Copy link to clipboard
Copied
I'm french and I'm not sure I weel understand your answer
What do you meen by " it will need an escape character for the escape character." ?
Copy link to clipboard
Copied
I do that and it works
var settings = {
target: app.activeDocument,
findWhat: /<\/?[ibap]>|<br>|<\/br>|<strong>|<\/strong>|<em>|<\/em>/,
changeTo: '',
ignoreLockedHidden: false,
showUI: true,
showResult: true,
};
Copy link to clipboard
Copied
Hi @Jeffosaure, well done on your scripting, you will be able to use that skill to adapt parts of my script for your specific needs.
As for your question about <\strong>, you just aren't making a suitable regular expression. [strong] will match s,t,r,o,n, and g, but only one letter. Try this one:
/<\/?[^>\b]+>/
Eek! I know it looks hideous! Here's a breakdown of the parts:
The first matched character is a simple match: < matches <
Next might be a forward slash /, but the question mark means that it doesn't matter if it doesn't exist, and the forward slash must be escaped with a backslash because forward slashes are the RegExp literal* delimiters (think of it like putting a quote in a javascript string: myString = "this quote -> \" will break my string if I don't escape it!").
After that we have [^>\b]+ this means [ ... ] a character in a set, but [^ ... ] means any character *not* in this set and + means to match one of more of. So it will keep matching characters which are not > and are not at a word boundary \b.
Then finally we have the closing > which simply matches >
There are many websites for learning and testing regular expressions.
- Mark
* Extra note: a RegExp literal is when you form a RegExp in code like this /I'm a RegExp literal/ delimited by forward slashes. But If you use a forward slash in the UI text field, you don't need to escape it, because it isn't a literal—the RegExp is formed by using its constructor (see line 349).
Copy link to clipboard
Copied
Yeah @Jeffosaure, your Regex is definitely working! Excellent!
By the way, you can also write it like this:
/<\/?(i|b|a|p|br|strong|em)>
I put the options i, b, a, p, br, strong and em into parenthesis (called a capture group) which, in this case just serves to contain the options (without the parentheses, it would match <\/?i as the first option and b as the second, etc.).
- Mark
Copy link to clipboard
Copied
Should <\/?[strong]> have the escape character? If so, it will need an escape character for the escape character.
Hi @femkeblanco, see my extra note at the end of one of my posts here. You are right that you do have to escape the backslash if you want a backslash in a regex (or string) literal, but not if you make the RegExp with it's constructor which is how the UI version makes it. - Mark
Copy link to clipboard
Copied
@m1b Very nice. Makes more sense than my way.