Skip to main content
Participating Frequently
March 11, 2022
Answered

Search and replace a text, only in visible layers

  • March 11, 2022
  • 2 replies
  • 1872 views

Hello,

 

Please I need a script to search for a textA in visible layers only and replace it by textB. Could you help.

 

Thanks in advance.

This topic has been closed for replies.
Correct answer Inventsable

Here's my attempt. Recursive lookup of ancestors and sublayers, supports any amount of find/replace options, also has options for enabling whether to affect locked or hidden layers:

 

var config = {
  // Support for any amount of find/replace parameters here
  targets: [
    {
      find: "foo",
      replace: "bar",
    },
    {
      find: /(locked|hidden)/i, // including support for Regex patterns
      replace: "Success",
    },
  ],
  affects: {
    // names: false, // Whether it changes names, not implemented but possible
    content: true, // Must be true at this basic state of the script
  },
  searchIn: {
    hidden: false, // Whether it changes hidden layer contents
    locked: false, // Whether it changes locked layer contents
  },
};
//
// Don't alter the below unless you know what you're doing.
//
// Very simple ES6 polyfills for Array methods to make code more concise:
Array.prototype.filter = function (callback) {
  var filtered = [];
  for (var i = 0; i < this.length; i++)
    if (callback(this[i], i, this)) filtered.push(this[i]);
  return filtered;
};
Array.prototype.forEach = function (callback) {
  for (var i = 0; i < this.length; i++) callback(this[i], i, this);
};
// This is a bit too verbose for what we need here, but as-is can recurse to find sublayers no matter the depth if we use:
// var list = get("layers"); <-- This is *all* layers and sublayers, not just the root-level / depth 0 ones from doc.layers.
function get(type, parent, deep) {
  if (arguments.length == 1 || !parent) {
    parent = app.activeDocument;
    deep = true;
  }
  var result = [];
  if (!parent[type]) return [];
  for (var i = 0; i < parent[type].length; i++) {
    result.push(parent[type][i]);
    if (parent[type][i][type] && deep)
      result = [].concat(result, get(type, parent[type][i], deep));
  }
  return result;
}

// The main obstacle is getting a recursive parent chain, then determining if any part of this chain doesn't meet requirements:
function checkAncestry(frame) {
  // So we'll split the logic into just constructing a recursive Array containing data about parents when applicable:
  function getAncestry(frame) {
    // And have a dedicated recursive function to look into the parent then recurse and concat an Array when needed:
    function getParentChain(item, data) {
      if (item.parent && /layer/i.test(item.parent.typename))
        data = getParentChain(
          item.parent,
          [].concat(
            { hidden: !item.parent.visible, locked: item.parent.locked },
            data
          )
        );
      return data;
    }
    // For ease, we start the chain with the textFrame's hidden/locked data:
    return getParentChain(frame, [
      { hidden: frame.hidden, locked: frame.locked },
    ]);
  }
  // Now that we have a list like [ { locked: true, hidden: false }, {locked: false, hidden: false }, ... ]
  // We can filter it to see if there are any hidden/lockeds present:
  var chain = getAncestry(frame),
    hiddenStatus =
      chain.filter(function (i) {
        return i.hidden;
      }).length > 0,
    lockedStatus =
      chain.filter(function (i) {
        return i.locked;
      }).length > 0;
  // AI doesn't handle short circuiting well, so if no locked/hiddens exist, we'll explicitly return:
  if (!lockedStatus && !hiddenStatus) return true;
  // Otherwise we need to ensure the chain contents either match the queries or returned empty:
  else
    return (
      (hiddenStatus == config.searchIn.hidden || !hiddenStatus) &&
      (lockedStatus == config.searchIn.locked || !lockedStatus)
    );
}

// Our main function can now be very concise, we get all text frames:
get("textFrames")
  // But remove any that don't match our search queries for hidden/locked
  .filter(function (i) {
    return checkAncestry(i);
    // Then for every remaining textFrame:
  })
  .forEach(function (frame) {
    // And for every find/replace option:
    config.targets.forEach(function (target) {
      // Replace the text content of the frame:
      frame.contents = frame.contents.replace(target.find, target.replace);
    });
  });

2 replies

Inventsable
InventsableCorrect answer
Legend
March 12, 2022

Here's my attempt. Recursive lookup of ancestors and sublayers, supports any amount of find/replace options, also has options for enabling whether to affect locked or hidden layers:

 

var config = {
  // Support for any amount of find/replace parameters here
  targets: [
    {
      find: "foo",
      replace: "bar",
    },
    {
      find: /(locked|hidden)/i, // including support for Regex patterns
      replace: "Success",
    },
  ],
  affects: {
    // names: false, // Whether it changes names, not implemented but possible
    content: true, // Must be true at this basic state of the script
  },
  searchIn: {
    hidden: false, // Whether it changes hidden layer contents
    locked: false, // Whether it changes locked layer contents
  },
};
//
// Don't alter the below unless you know what you're doing.
//
// Very simple ES6 polyfills for Array methods to make code more concise:
Array.prototype.filter = function (callback) {
  var filtered = [];
  for (var i = 0; i < this.length; i++)
    if (callback(this[i], i, this)) filtered.push(this[i]);
  return filtered;
};
Array.prototype.forEach = function (callback) {
  for (var i = 0; i < this.length; i++) callback(this[i], i, this);
};
// This is a bit too verbose for what we need here, but as-is can recurse to find sublayers no matter the depth if we use:
// var list = get("layers"); <-- This is *all* layers and sublayers, not just the root-level / depth 0 ones from doc.layers.
function get(type, parent, deep) {
  if (arguments.length == 1 || !parent) {
    parent = app.activeDocument;
    deep = true;
  }
  var result = [];
  if (!parent[type]) return [];
  for (var i = 0; i < parent[type].length; i++) {
    result.push(parent[type][i]);
    if (parent[type][i][type] && deep)
      result = [].concat(result, get(type, parent[type][i], deep));
  }
  return result;
}

// The main obstacle is getting a recursive parent chain, then determining if any part of this chain doesn't meet requirements:
function checkAncestry(frame) {
  // So we'll split the logic into just constructing a recursive Array containing data about parents when applicable:
  function getAncestry(frame) {
    // And have a dedicated recursive function to look into the parent then recurse and concat an Array when needed:
    function getParentChain(item, data) {
      if (item.parent && /layer/i.test(item.parent.typename))
        data = getParentChain(
          item.parent,
          [].concat(
            { hidden: !item.parent.visible, locked: item.parent.locked },
            data
          )
        );
      return data;
    }
    // For ease, we start the chain with the textFrame's hidden/locked data:
    return getParentChain(frame, [
      { hidden: frame.hidden, locked: frame.locked },
    ]);
  }
  // Now that we have a list like [ { locked: true, hidden: false }, {locked: false, hidden: false }, ... ]
  // We can filter it to see if there are any hidden/lockeds present:
  var chain = getAncestry(frame),
    hiddenStatus =
      chain.filter(function (i) {
        return i.hidden;
      }).length > 0,
    lockedStatus =
      chain.filter(function (i) {
        return i.locked;
      }).length > 0;
  // AI doesn't handle short circuiting well, so if no locked/hiddens exist, we'll explicitly return:
  if (!lockedStatus && !hiddenStatus) return true;
  // Otherwise we need to ensure the chain contents either match the queries or returned empty:
  else
    return (
      (hiddenStatus == config.searchIn.hidden || !hiddenStatus) &&
      (lockedStatus == config.searchIn.locked || !lockedStatus)
    );
}

// Our main function can now be very concise, we get all text frames:
get("textFrames")
  // But remove any that don't match our search queries for hidden/locked
  .filter(function (i) {
    return checkAncestry(i);
    // Then for every remaining textFrame:
  })
  .forEach(function (frame) {
    // And for every find/replace option:
    config.targets.forEach(function (target) {
      // Replace the text content of the frame:
      frame.contents = frame.contents.replace(target.find, target.replace);
    });
  });
Participating Frequently
March 12, 2022

Flawless!

 

Thank you very much, it does the task.

Parts4Arts
Inspiring
March 11, 2022

Should the text in a written text (TextFrame) be searched and replaced or the names of objects in the current layer?

Both would be programmable without much effort.

- j.

Participating Frequently
March 11, 2022

Hello,

 

I want to change only the content of the textframe

 

For example, replace text "Sky" with "mountain"..without any other names change (if not necessary).

Parts4Arts
Inspiring
March 11, 2022

I am sorry I can't send it because of confidentiality restriction. It pertains to a customer.

 

I think I found the issue: The script does not apply to sublayers containing "textA".

 


ok. sublayers. 🙂 I guessed so.

i can expand the script accordingly, but not until tomorrow. it is now 10:50 pm here in germany and time to sleep.

till tomorrow,

– j.