• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
1

Illustrator jsx to delete <b>, </b> tags in paragraph text an keep the differents font style

Community Beginner ,
Nov 30, 2023 Nov 30, 2023

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 :Capture d'écran 2023-11-30 142536.pngand I want to have this :Capture d'écran 2023-11-30 142941.png

But it done that :Capture d'écran 2023-11-30 143000.png

TOPICS
Scripting

Views

261

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines

correct answers 1 Correct answer

Community Expert , Nov 30, 2023 Nov 30, 2023

Here is a version that has a UI. If anybody uses it, please let me know if you find any bugs.

- Mark

Screenshot 2023-12-01 at 15.23.22.png

 

 

/**
 * 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 = {
    
...

Votes

Translate

Translate
Adobe
Community Expert ,
Nov 30, 2023 Nov 30, 2023

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

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Nov 30, 2023 Nov 30, 2023

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;
}

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Nov 30, 2023 Nov 30, 2023

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.gif

 

/**
 * 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;

};

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Nov 30, 2023 Nov 30, 2023

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

Screenshot 2023-12-01 at 15.23.22.png

 

 

/**
 * 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;

    };

};

 

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Beginner ,
Dec 01, 2023 Dec 01, 2023

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

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Dec 01, 2023 Dec 01, 2023

Copy link to clipboard

Copied

Should <\/?[strong]> have the escape character?  If so, it will need an escape character for the escape character. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Beginner ,
Dec 01, 2023 Dec 01, 2023

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."  ?

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Beginner ,
Dec 01, 2023 Dec 01, 2023

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,
};

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Dec 01, 2023 Dec 01, 2023

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).

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Dec 01, 2023 Dec 01, 2023

Copy link to clipboard

Copied

LATEST

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 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Dec 01, 2023 Dec 01, 2023

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

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Dec 01, 2023 Dec 01, 2023

Copy link to clipboard

Copied

@m1b  Very nice.  Makes more sense than my way. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines