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.
* @7111211 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.
* @7111211 m1b
* @version 2023-12-01
* @9397041 {TextRange|TextFrame} text - the text to search.
* @9397041 {RegExp} findWhat - the RegExp to find with.
* @9397041 {String} [changeTo] - the change-to string.
* @9397041 {Boolean} [ignoreLockedHidden] - whether to ignore locked or hidden text (default: true).
* @9397041 {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.
* @9397041 {PageItem} item
* @9397041 {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.
* @9397041 {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
* @9397041 {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;
};
};