m1b
Community Expert
m1b
Community Expert
Activity
an hour ago
Hi @Phil5C41 you already have @Peter Kahrel's perfect answer so you're probably done, but I wanted to learn a bit about scripting style tag mapping myself, so I wrote a little script that does what your script is trying to do. I figured I may as well share it.
I changed your mapping system so that the tag names are the object keys and the values are RegExps. This is because I wanted to be able to have multiple paragraph styles that would be mapped to the same tag. For example, when headings appear in different colors to match a chapter color theme, we could name them "H1-Chapter 1", "H1-Chapter 2", etc, and the script would tag them all "H1". (This would have worked with your approach—using .indexOf—but it isn't as configurable as RegExp .test and it may give false positives, such as tagging a style named "MONTH1" as "H1".)
- Mark
/**
* @file Assign Paragraph Style Export Tags.js
*
* Will assign a pdf export tag to any matched
* paragraph style in active document.
*
* I have set the `tagMappings` to match a paragraph style name, either:
* - being the tag name by itself eg. "P", or
* - starting with tag name followed by a hyphen, eg. "P-Small" and "P-Medium".
*
* @author m1b
* @version 2025-02-27
* @discussion https://community.adobe.com/t5/indesign-discussions/script-to-automate-the-tagging-of-paragraph-styles-based-on-keyword/m-p/15179157
*/
function main() {
var doc = app.activeDocument;
if (!doc) {
alert("No active document found.");
return;
}
// tag name mapped to style name test RegExp
var tagMappings = {
"H1": /^H1($|-)/,
"H2": /^H2($|-)/,
"H3": /^H3($|-)/,
"H4": /^H4($|-)/,
"H5": /^H5($|-)/,
"H6": /^H6($|-)/,
"P": /^P($|-)/,
};
var paragraphStyles = doc.allParagraphStyles;
var counter = 0;
for (var i = 0; i < paragraphStyles.length; i++) {
var style = paragraphStyles[i];
tagMappingLoop:
for (var tagName in tagMappings) {
if (
tagMappings.hasOwnProperty(tagName)
&& tagMappings[tagName].test(style.name)
) {
var tagMap = style.styleExportTagMaps[0];
if (tagMap.isValid)
// update the first export tag map
tagMap.properties = {
exportTag: tagName,
exportType: 'PDF',
exportClass: '',
exportAttributes: '',
};
else
// add a new export tag map
tagMap = style.styleExportTagMaps.add('PDF', tagName, '', '');
counter++;
break tagMappingLoop; // Stop checking once a match is found
}
}
}
alert('Updated ' + counter + " paragraph styles with export tags.");
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Assign Paragraph Style Export Tags');
... View more
‎Feb 25, 2025
04:03 PM
Robert is right, this is something that should be fixed in the code. Something like this:
var files = myFolder.getFiles();
files.sort(function(a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
... View more
‎Feb 25, 2025
03:26 PM
1 Upvote
Great. I have written a script that will process your svg files. Instructions are in the script header. You can look through and get an idea of what it is doing. It just removes some unwanted attributes from each path and adds a class name "line" and adds a CSS style definition. You should edit the style definition to suit your needs. Let me know how it goes.
- Mark
Before:
After:
/**
* @File Style SVG Files.js
*
* Set up:
* Run this script once, to create "SVG IMPORT" and "SVG EXPORT"
* folders in the same location as this script file.
*
* Usage:
* 1. Put your svg files into "SVG IMPORT"
* 2. Run script
* 3. Collect the resulting svg files from "SVG EXPORT"
*
* @author m1b
* @version 2025-02-26
* @discussion https://community.adobe.com/t5/illustrator-discussions/illustrator-script-to-edit-multiply-files/m-p/15177289
*/
(function () {
// edit this to match your needs
// must be valid CSS
var style = [
'fill: none;',
'stroke: #1d1d1b;',
'stroke-width: .2px;',
'stroke-miterlimit: 10;',
];
var svgImportFolder = Folder(File($.fileName).parent + '/SVG IMPORT/'),
svgExportFolder = Folder(File($.fileName).parent + '/SVG EXPORT/');
if (!svgImportFolder.exists)
svgImportFolder.create();
if (!svgExportFolder.exists)
svgExportFolder.create();
// collect the svg files
var files = svgImportFolder.getFiles(),
fileCounter = 0;
// this is the style definition element
var styleElement = new XML(
'<defs><style>.line {##STYLE##}</style></defs>'
.replace('##STYLE##', style.join(' '))
);
fileLoop:
for (var i = 0; i < files.length; i++) {
var f = files[i];
f.open('r');
var svg = f.read();
if (!svg)
continue fileLoop;
try {
var xml = new XML(svg),
elements = xml.descendants(),
len = elements.length(),
elementCounter = 0;
} catch (error) {
// failed to parse XML
continue fileLoop;
}
elementLoop:
for (var j = 0; j < len; j++) {
var el = elements[j];
if ('path' !== el.localName())
continue elementLoop;
// remove unwanted attributes
delete el['@stroke-width'];
delete el['@stroke'];
delete el['@fill'];
// add our class
el['@class'] = 'line';
elementCounter++;
}
if (0 === elementCounter)
continue fileLoop;
// Append <defs> to the root <svg>
xml.insertChildBefore(elements[0], styleElement);
// write to file
var exportFile = File(svgExportFolder + '/' + decodeURI(f.name));
exportFile.open('w');
var success = exportFile.write(xml.toString());
if (success)
fileCounter++
}
alert('Processed ' + fileCounter + ' svg files.');
})();
Edit 2025-02-26: minor typo.
... View more
‎Feb 25, 2025
12:42 PM
Hi @Roci It might be possible with a script. Could you share one or two of the svg files?
- Mark
... View more
‎Feb 24, 2025
07:20 PM
Hi @Mariano– can you post the script that generates the CSV file? I suspect it would be easy to fix it.
- Mark
... View more
‎Feb 24, 2025
03:15 PM
Hi @Utson_Avila2888 I see. Yes I hadn't expected anyone to fill or stroke a clipping mask, but I've updated my code above with a check for it. Should work correctly now.
- Mark
P.S. A couple of notes on posting code: (1) please use the </> button to format your code, and (2) if you are posting someone else's code publicly, it is impolite to remove their authorship notice.
... View more
‎Feb 23, 2025
10:02 PM
2 Upvotes
Hi @optimisticperson, as others have rightly said, there is no other way than adjusting by hand because Illustrator has no capability to judge properties such as the "visual weight" of an object. No doubt one day it will! Making disparate logos sit comfortably together is a task involving significant skill.
However, perhaps it would still save some time to give a rough approximation? I have written a script that scales the selected page items such that the area of each item's bounds matches the average area of all the items.
It won't do a great job, because the bounds of a logo isn't a good metric for the visual weight, but it might provide a starting point. Let me know if it is helpful.
- Mark
/**
* @file Scale To Average Area.js
*
* Scales the selection such that the area
* of each item's geometric bounds match
* the average area of the selected items.
*
* @author m1b
* @version 2025-02-24
* @discussion https://community.adobe.com/t5/illustrator-discussions/how-to-automatically-resize-a-bunch-of-logos-so-they-re-visually-proportional/m-p/15168811
*/
(function () {
var doc = app.activeDocument,
items = doc.selection;
if (0 === items.length)
return alert('Please select some items and try again.');
scaleItemsToAverageArea(items);
})();
/**
* Scales each page item in `items` such that the area
* of each item's geometric bounds match the average area
* of the selected items.
* @date 2025-02-24
* @param {Array<PageItem>} items - the items to scale.
*/
function scaleItemsToAverageArea(items) {
var areas = [],
areaSum = 0;
for (var i = 0, bounds, area; i < items.length; i++) {
bounds = getItemBoundsIllustrator(items[i], false);
area = (bounds[2] - bounds[0]) * -(bounds[3] - bounds[1]);
areas[i] = area;
areaSum += area;
}
var averageArea = Math.sqrt(areaSum) / items.length;
for (var i = 0, scaleFactor; i < areas.length; i++) {
scaleFactor = averageArea / Math.sqrt(areas[i]);
items[i].resize(scaleFactor * 100, scaleFactor * 100);
}
};
/**
* Returns bounds of item(s).
* @author m1b
* @version 2024-03-10
* @param {PageItem|Array<PageItem>} item - an Illustrator PageItem or array of PageItems.
* @param {Boolean} [geometric] - if false, returns visible bounds.
* @param {Array} [bounds] - private parameter, used when recursing.
* @returns {Array} - the calculated bounds.
*/
function getItemBoundsIllustrator(item, geometric, bounds) {
var newBounds = [],
boundsKey = geometric ? 'geometricBounds' : 'visibleBounds';
if (undefined == item)
return;
if (
item.typename == 'GroupItem'
|| item.constructor.name == 'Array'
) {
var children = item.typename == 'GroupItem' ? item.pageItems : item,
contentBounds = [],
isClippingGroup = (item.hasOwnProperty('clipped') && item.clipped == true),
clipBounds;
for (var i = 0, child; i < children.length; i++) {
child = children[i];
if (
child.hasOwnProperty('clipping')
&& true === child.clipping
)
// the clipping item
clipBounds = child.geometricBounds;
else
contentBounds.push(getItemBoundsIllustrator(child, geometric, bounds));
}
newBounds = combineBounds(contentBounds);
if (isClippingGroup)
newBounds = intersectionOfBounds([clipBounds, newBounds]);
}
else if (
'TextFrame' === item.constructor.name
&& TextType.AREATEXT !== item.kind
) {
// get bounds of outlined text
var dup = item.duplicate().createOutline();
newBounds = dup[boundsKey];
dup.remove();
}
else if (item.hasOwnProperty(boundsKey)) {
newBounds = item[boundsKey];
}
// `bounds` will exist if this is a recursive execution
bounds = (undefined == bounds)
? bounds = newBounds
: bounds = combineBounds([newBounds, bounds]);
return bounds;
};
/**
* Returns the combined bounds of all bounds supplied.
* Works with Illustrator or Indesign bounds.
* @author m1b
* @version 2024-03-09
* @param {Array<bounds>} boundsArray - an array of bounds [L, T, R, B] or [T, L , B, R].
* @returns {bounds?} - the combined bounds.
*/
function combineBounds(boundsArray) {
var combinedBounds = boundsArray[0],
comparator;
if (/indesign/i.test(app.name))
comparator = [Math.min, Math.min, Math.max, Math.max];
else
comparator = [Math.min, Math.max, Math.max, Math.min];
// iterate through the rest of the bounds
for (var i = 1; i < boundsArray.length; i++) {
var bounds = boundsArray[i];
combinedBounds = [
comparator[0](combinedBounds[0], bounds[0]),
comparator[1](combinedBounds[1], bounds[1]),
comparator[2](combinedBounds[2], bounds[2]),
comparator[3](combinedBounds[3], bounds[3]),
];
}
return combinedBounds;
};
/**
* Returns the overlapping rectangle
* of two or more rectangles.
* NOTE: Returns undefined if ANY
* rectangles do not intersect.
* @author m1b
* @version 2024-09-05
* @param {Array<bounds>} arrayOfBounds - an array of bounds [L, T, R, B] or [T, L , B, R].
* @returns {bounds?} - intersecting bounds.
*/
function intersectionOfBounds(arrayOfBounds) {
var comparator;
if (/indesign/i.test(app.name))
comparator = [Math.max, Math.max, Math.min, Math.min];
else
comparator = [Math.max, Math.min, Math.min, Math.max];
// sort a copy of array
var bounds = arrayOfBounds
.slice(0)
.sort(function (a, b) { return b[0] - a[0] || a[1] - b[1] });
// start with first bounds
var intersection = bounds.shift(),
b;
// compare each bounds, getting smaller
while (b = bounds.shift()) {
// if doesn't intersect, bail out
if (!boundsDoIntersect(intersection, b))
return;
intersection = [
comparator[0](intersection[0], b[0]),
comparator[1](intersection[1], b[1]),
comparator[2](intersection[2], b[2]),
comparator[3](intersection[3], b[3]),
];
}
return intersection;
};
/**
* Returns true if the two bounds intersect.
* @author m1b
* @version 2024-03-10
* @param {Array} bounds1 - bounds array.
* @param {Array} bounds2 - bounds array.
* @param {Boolean} [TLBR] - whether bounds arrays are interpreted as [t, l, b, r] or [l, t, r, b] (default: based on app).
* @returns {Boolean}
*/
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (undefined == TLBR)
TLBR = (/indesign/i.test(app.name));
return !(
TLBR
// TLBR
? (
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
)
// LTRB
: (
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
)
);
};
... View more
‎Feb 23, 2025
08:51 PM
Hi @Utson_Avila2888 for your learning, I've made two changes to your code:
1. I've written a function—getItemBoundsIllustrator—that does a bit more heavy lifting when trying to get the actual bounds, and I've called it from your distributeObjects function, and
2. I've changed the way the items are positioned: rather than settings "position" property, I use the translate method—you can do it either way, but I find translate more understandable.
I tested with your sample file and it seemed to work correctly for me. Let me know if that gets you moving forward.
- Mark
(function () {
var doc = app.activeDocument;
var selx = doc.selection;
if (selx.length < 2) {
alert("Por favor, selecciona al menos 2 objetos.");
} else {
var userValues = showDialog();
if (userValues) {
distributeObjects(selx, userValues);
}
}
})();
function showDialog() {
var dialog = new Window('dialog', 'Distribuir Objetos Apilados');
dialog.add('statictext', undefined, 'Separación:');
var separationInput = dialog.add('edittext', undefined, '1');
separationInput.characters = 5;
var unitsGroup = dialog.add('group');
var cmButton = unitsGroup.add('radiobutton', undefined, 'cm');
var mmButton = unitsGroup.add('radiobutton', undefined, 'mm');
var pxButton = unitsGroup.add('radiobutton', undefined, 'px');
cmButton.value = true;
dialog.add('statictext', undefined, 'Modo de distribución:');
var modeGroup = dialog.add('group');
var gridButton = modeGroup.add('radiobutton', undefined, 'Grid');
var horizontalButton = modeGroup.add('radiobutton', undefined, 'Horizontal');
var verticalButton = modeGroup.add('radiobutton', undefined, 'Vertical');
verticalButton.value = true;
dialog.add('statictext', undefined, 'Columnas (solo para Grid):');
var columnsInput = dialog.add('edittext', undefined, '3');
columnsInput.characters = 5;
var buttons = dialog.add('group');
buttons.alignment = 'center';
var cancelButton = buttons.add('button', undefined, 'Cancelar', { name: 'cancel' });
var okButton = buttons.add('button', undefined, 'Aceptar', { name: 'ok' });
okButton.onClick = function () {
dialog.close(1);
};
cancelButton.onClick = function () {
dialog.close(0);
};
if (dialog.show() == 1) {
return {
separation: parseFloat(separationInput.text),
unit: cmButton.value ? 'cm' : (mmButton.value ? 'mm' : 'px'),
mode: gridButton.value ? 'grid' : (horizontalButton.value ? 'horizontal' : 'vertical'),
columns: parseInt(columnsInput.text)
};
}
return null;
}
function distributeObjects(sel, userValues) {
var separation = userValues.separation;
var unitMultiplier = (userValues.unit == 'cm') ? 28.3465 : (userValues.unit == 'mm' ? 2.83465 : 1);
separation *= unitMultiplier;
var mode = userValues.mode;
var gridCols = (mode == 'grid') ? userValues.columns : (mode == 'horizontal' ? sel.length : 1);
var currentX = 0, currentY = 0, maxRowH = 0;
if (mode == 'horizontal' || mode == 'grid') {
sel.sort(function (a, b) {
return getVisibleBounds(a)[0] - getVisibleBounds(b)[0];
});
} else if (mode == 'vertical') {
sel.sort(function (a, b) {
return getVisibleBounds(a)[3] - getVisibleBounds(b)[3];
});
}
for (var i = 0; i < sel.length; i++) {
var bounds = getItemBoundsIllustrator(sel[i]);
var objectWidth = bounds[2] - bounds[0];
var objectHeight = bounds[1] - bounds[3];
var dx = currentX - bounds[0];
var dy = currentY - bounds[1];
sel[i].translate(dx, dy, true, true, true, true);
if (mode == 'horizontal') {
currentX += (objectWidth + separation);
} else if (mode == 'vertical') {
currentY -= (objectHeight + separation);
} else if (mode == 'grid') {
currentX += (objectWidth + separation);
if ((i % gridCols) == (gridCols - 1)) {
currentX = 0;
currentY -= (maxRowH + separation);
maxRowH = 0;
}
maxRowH = Math.max(maxRowH, objectHeight);
}
}
}
function getVisibleBounds(item) {
if (item.typename === 'GroupItem' && item.clipped) {
for (var i = 0; i < item.pageItems.length; i++) {
if (item.pageItems[i].clipping) {
return item.pageItems[i].geometricBounds;
}
}
}
return item.geometricBounds;
}
/**
* Returns bounds of item(s).
* @author m1b
* @version 2025-02-25
* @param {PageItem|Array<PageItem>} item - an Illustrator PageItem or array of PageItems.
* @param {Boolean} [geometric] - if false, returns visible bounds.
* @param {Array} [bounds] - private parameter, used when recursing.
* @returns {Array} - the calculated bounds.
*/
function getItemBoundsIllustrator(item, geometric, bounds) {
var newBounds = [],
boundsKey = geometric ? 'geometricBounds' : 'visibleBounds';
if (undefined == item)
return;
if (
item.typename == 'GroupItem'
|| item.constructor.name == 'Array'
) {
var children = item.typename == 'GroupItem' ? item.pageItems : item,
contentBounds = [],
isClippingGroup = (item.hasOwnProperty('clipped') && item.clipped == true),
clipBounds;
for (var i = 0, child; i < children.length; i++) {
child = children[i];
if (
child.hasOwnProperty('clipping')
&& true === child.clipping
&& true !== child.stroked
&& true !== child.filled
)
// the clipping item
clipBounds = child.geometricBounds;
else
contentBounds.push(getItemBoundsIllustrator(child, geometric, bounds));
}
newBounds = combineBounds(contentBounds);
if (
isClippingGroup
&& clipBounds
)
newBounds = intersectionOfBounds([clipBounds, newBounds]);
}
else if (
'TextFrame' === item.constructor.name
&& TextType.AREATEXT !== item.kind
) {
// get bounds of outlined text
var dup = item.duplicate().createOutline();
newBounds = dup[boundsKey];
dup.remove();
}
else if (item.hasOwnProperty(boundsKey)) {
newBounds = item[boundsKey];
}
// `bounds` will exist if this is a recursive execution
bounds = (undefined == bounds)
? bounds = newBounds
: bounds = combineBounds([newBounds, bounds]);
return bounds;
};
/**
* Returns the combined bounds of all bounds supplied.
* Works with Illustrator or Indesign bounds.
* @author m1b
* @version 2024-03-09
* @param {Array<bounds>} boundsArray - an array of bounds [L, T, R, B] or [T, L , B, R].
* @returns {bounds?} - the combined bounds.
*/
function combineBounds(boundsArray) {
var combinedBounds = boundsArray[0],
comparator;
if (/indesign/i.test(app.name))
comparator = [Math.min, Math.min, Math.max, Math.max];
else
comparator = [Math.min, Math.max, Math.max, Math.min];
// iterate through the rest of the bounds
for (var i = 1; i < boundsArray.length; i++) {
var bounds = boundsArray[i];
combinedBounds = [
comparator[0](combinedBounds[0], bounds[0]),
comparator[1](combinedBounds[1], bounds[1]),
comparator[2](combinedBounds[2], bounds[2]),
comparator[3](combinedBounds[3], bounds[3]),
];
}
return combinedBounds;
};
/**
* Returns the overlapping rectangle
* of two or more rectangles.
* NOTE: Returns undefined if ANY
* rectangles do not intersect.
* @author m1b
* @version 2024-09-05
* @param {Array<bounds>} arrayOfBounds - an array of bounds [L, T, R, B] or [T, L , B, R].
* @returns {bounds?} - intersecting bounds.
*/
function intersectionOfBounds(arrayOfBounds) {
var comparator;
if (/indesign/i.test(app.name))
comparator = [Math.max, Math.max, Math.min, Math.min];
else
comparator = [Math.max, Math.min, Math.min, Math.max];
// sort a copy of array
var bounds = arrayOfBounds
.slice(0)
.sort(function (a, b) { return b[0] - a[0] || a[1] - b[1] });
// start with first bounds
var intersection = bounds.shift(),
b;
// compare each bounds, getting smaller
while (b = bounds.shift()) {
// if doesn't intersect, bail out
if (!boundsDoIntersect(intersection, b))
return;
intersection = [
comparator[0](intersection[0], b[0]),
comparator[1](intersection[1], b[1]),
comparator[2](intersection[2], b[2]),
comparator[3](intersection[3], b[3]),
];
}
return intersection;
};
/**
* Returns true if the two bounds intersect.
* @author m1b
* @version 2024-03-10
* @param {Array} bounds1 - bounds array.
* @param {Array} bounds2 - bounds array.
* @param {Boolean} [TLBR] - whether bounds arrays are interpreted as [t, l, b, r] or [l, t, r, b] (default: based on app).
* @returns {Boolean}
*/
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (undefined == TLBR)
TLBR = (/indesign/i.test(app.name));
return !(
TLBR
// TLBR
? (
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
)
// LTRB
: (
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
)
);
};
Edit 2025-02-25: added a check for a filled or stroked clipping mask.
... View more
‎Feb 22, 2025
08:34 PM
I have reported the bug. Please vote on it, if you can.
- Mark
... View more
‎Feb 22, 2025
08:27 PM
Hi all, I'm planning to post a bug report on uservoice, but I was hoping I could get a few people to test my demo document and see if they see the same bug.
To test:
1. open my attached .indd
2. place your insertion point cursor in the paragraph colored magenta.
3. type a character (what happens? for me the spanning breaks dramatically—see graphic below)
4. type another character (what happens? for me it goes back to normal
5. keep typing (for me it jumps back and forth)
I have noticed that if I change the magenta paragraph from using Adobe Paragraph Composer to Adobe Single-line Composer, the problem goes away. But it took me a while to troubleshoot it, so it would be nice to fix it.
Anyway, if you have time, please let me know your results, and which version of Indesign and OS you are running. I will link to this post in the bug report so Adobe can read your results too. Thanks.
- Mark
... View more
‎Feb 21, 2025
05:41 PM
Hi @Alexander Rott, here is an example of deriving the Lab values from RGB, CMYK, or Grayscale colors. You should be able to incorporate into your own script. This will be helpful: app.convertSampleColor(). Also here is another answer that uses this method.
- Mark
/**
* @file Show Lab Color Breakdown Demo.js
*
* Display the Lab color breakdown of the selected item's fillColor.
*
* @author m1b
* @version 2025-02-22
* @discussion https://community.adobe.com/t5/illustrator-discussions/using-javascript-to-get-lab-color-information/m-p/15169713
*/
(function () {
var doc = app.activeDocument,
item = doc.selection[0];
if (
undefined == item
|| !item.hasOwnProperty('fillColor')
|| 'NoColor' === item.fillColor.constructor.name
)
return alert('Please select an item with a fill color and try again.');
// we only what an array of the color channel values
var breakdown = getColorBreakdown(item.fillColor) || [];
// pick the colorspace based on the number of channels in the breakdown
var colorSpace = [
null,
ImageColorSpace.GrayScale,
null,
ImageColorSpace.RGB,
ImageColorSpace.CMYK,
][breakdown.length];
// this is what you are looking for!
var labValues = app.convertSampleColor(colorSpace, breakdown, ImageColorSpace.LAB, ColorConvertPurpose.defaultpurpose);
alert("L: " + labValues[0] + ", a: " + labValues[1] + ", b: " + labValues[2]);
})();
/**
* Returns an array of color channel values.
* @author m1b
* @version 2022-05-23
* @param {Swatch|SpotColor|Color} col - the source color.
* @param {Number} [tintFactor] - a number in range 0..1 (default: 1).
* @returns {Array<Number>}
*/
function getColorBreakdown(col, tintFactor) {
tintFactor = tintFactor || 1;
if (col.hasOwnProperty('color'))
col = col.color;
if ('SpotColor' === col.constructor.name)
col = col.spot.color;
if ('CMYKColor' === col.constructor.name)
return [col.cyan * tintFactor, col.magenta * tintFactor, col.yellow * tintFactor, col.black * tintFactor];
else if ('RGBColor' === col.constructor.name)
return [col.red * tintFactor, col.green * tintFactor, col.blue * tintFactor];
else if ('GrayColor' === col.constructor.name)
return [col.gray * tintFactor];
};
Edit 2025-02-22: added code to choose the correct source ImageColorSpace. Oops!
... View more
‎Feb 19, 2025
08:28 PM
Hi @Robert at ID-Tasker, yes it recurses. Here:
this.parent.create()
... View more
‎Feb 19, 2025
08:24 PM
Hi all, for what it's worth, I've created a bug report on uservoice. Please vote!
- Mark
... View more
‎Feb 19, 2025
03:32 PM
@Robert at ID-Tasker you are no village idiot!—you are an expert like each of us. I have made many more errors than you have on this one thread—some quite embarassing—so on that metric I would win the village idiot olympics here.
We could, as time permits, go back and look over our contributions on a thread such as this, and analyze the contribution we made. Did our comments push forward the discussion, or side-track it?
I find it uncomfortable work, as I see all my mistakes, but it can be edifying.
- Mark
... View more
‎Feb 19, 2025
06:10 AM
Hi @dublove, I am having trouble understanding this post. Would it be possible to post a BEFORE and AFTER document? Make it as simple as possible, but showing what you want.
- Mark
... View more
‎Feb 18, 2025
06:51 PM
Hi Robert, I try not to read between the lines, due to my often poor success rate.
> You're using JavaScript's RegEx on a text variable/string - not InDesign's GREP, that works on text objects.
Again, you are exactly correct!
But for your sake I will go into reading-between-the-lines territory and guess that you are concerned that because my example that uses a different flavour of grep (ExtendScript's RegExp vs Indesign's PCRE) my example will not be applicable. This is a legitimate concern, but rest assured, I knew what I was doing: the grep pattern I used in my example is 100% compatible with both flavours.
So why didn't I just use an actual Indesign grep example? I was lazy. Sorry.
But I will rectify my laziness now—
Here is the exact same example, converted into normal Indesign Find Change Grep context:
EDIT 2025-02-20: Do not bother reading this. Yes, this is a faithful conversion of my previous example, but it is a useless example—I could have used a normal capture group along with $1, $3 and $4, and it would have been fine. See my note on the earlier post. My apologies for wasting your time!
1. Using the non-capture group gives me what I want.
2. In this case using a normal capture group messes up the capture group indices and I get a mess.
Hope that helps.
- Mark
Edit: a couple of typos.
... View more
‎Feb 18, 2025
04:33 PM
Exactly @Robert at ID-Tasker! That line would be a poor choice to highlight.
- Mark
... View more
‎Feb 18, 2025
04:32 PM
Last comments/suggestions on your latest idml, @James Gifford—NitroPress: (apologies for wrong fonts in screen shots).
Paragraph Return and Carriage Return: I wondered it might be more readable to combine these, consistent with "End of Story".
1. If you did decide to remove the non-marking group, I would nominate the "negated character set" for its spot. They are really useful—let me know if you want examples.
2. the term "subexpression" in my opinion focuses on the wrong thing (I mean there are subexpressions elsewhere, eg. lookbehinds have subexpressions). I would suggest "Capture Group" which is a standard term.
3. Regarding "Reset Match": I would suggest the note "Discards any text already captured". Also it mustn't show a magenta box because there are no parameters for \K.
Your chart is a fantastic addition and I sincerely hope you aren't regretting sharing it here! 🙂
- Mark
... View more
‎Feb 18, 2025
03:38 PM
Edit 2025-02-20: Don't bother reading this example. Sorry to all, but something was nagging me and I realised that example I dredged up is a useless one. I mis-remembered the reason I had for using the non-capturing group: it was just for code-readability—I probably just liked the neatness of matching 1, 2, 3. I could have just used a normal capture group and accessed indexes 1 , 3, and 4 and it would have been fine I think. So please ignore this example—it is useless. Sorry!
@James Gifford—NitroPress
Funnily enough I had never considered that memory or speed were a reason to use non-marking groups (AKA non-capture groups), although both of those are good reasons.
I use them when I need to control the indexing of the captured text. My example is in a scripting context, and off the top of my head I don't remember ever using this in the Indesign UI but I don't see why it wouldn't apply when using group references in the changeTo field. [EDIT: yes it does apply perfectly—see my answer to Robert below for the same example converted into normal Indesign find/change grep.]
Here is a simple example:
function getJobNumber(doc) {
const matchJobNumber = /(^(?:([A-Z]{2})-?)?([-\d]+))_/;
var match = doc.name.match(matchJobNumber);
if (match && match.length > 1)
return {
fullCode: match[1],
countryCode: match[2],
numberCode: match[3],
};
};
The brief was that some document names started with job numbers in "UK-12345_" format, some had no hyphen "UK12345_" and some were just "12345_". I needed to collect (1) the full job number, (2) the countryCode by itself, and (3) the number.
(?:([A-Z]{2})-?)?
This part collects the country code. I needed to group it because this whole part is optional (the question mark at the end). But I never want to use its captured string because I only want the two letter country code without the possibly trailing hyphen.
Using a normal capture group (([A-Z]{2})-?)? would have misaligned the indices of the groups between the case where a country code existed and when it didn't. So using (?: ) keeps that optional grouping out of my results. The inner capture group ([A-Z]{2}) happily is still allocated its rightful index even if the outer non-capture group is not found (in which case the country code match[2] is an empty string).
Having said all that, I would judge that non-marking groups were well beyond the needs of the 99% users and if you needed space for something I would be tempted to leave it off your chart altogether.
- Mark
... View more
‎Feb 17, 2025
08:20 PM
Yes I thought so. I don't know how to do exactly what you want. There might be a clever way with Actions or graphic styles, but I don't know any. Sorry.
Good luck and let us know if you discover a good way!
- Mark
... View more
‎Feb 17, 2025
08:15 PM
@James Gifford—NitroPress
Peter wrote: > ... in a note you indicate that non-marking sub-expressions are obsolete. They aren't, they may not be used much, but they're certainly not obsolete.
Yes, definitely! Non-marking (AKA non-capturing) groups are very useful when needed.
James wrote: > Which, really, I think covers 99% of what InDesign GREP users would want to know in order to choose one over the other.
True, but it misses my point, which I can summarise in one sentence: Teaching \K as "Reset match" or "Discard capture" or "Clear capture" (or, for the mnemonic, "Klear capture"? or even—sigh—"Keep out" [Edit: or Peter's improvement: "keepbehind"?]) is no harder than teaching it as an "(Inclusive) Lookbehind*" and has the benefit that this description is real, not notional, and stands a chance to implant an accurate mental model in the student, that will withstand contact with the wider world.
Now I am imagining—for the sake of the exercise—a cohort of students leaving the academy and going out into the world to say things like "Oh, you'll need an Inclusive Lookbehind in that case." If that doesn't make the hair on your neck stand up, then, good for you—you are a normal healthly person! ‌🙂‌ - Mark
Edit: had to reformat because forum software was putting my reply into 5 columns! It's has clearly had enough of nitpicking!
... View more
‎Feb 17, 2025
06:41 PM
> I've come out of retirement for lesser things! @Peter Kahrel Haha, love it! I sometimes enjoy a bit of nitpicking when the mood takes me—and I am grateful for the indulgence here—but I promise to not make it a regular habit on other threads!
tldr;
I've realised that we simply don't agree on a point you made earlier, Peter, that
"Lookbehind is a functional notion, not a formal one."
I guess I would feel awkward referring to \K as a lookbehind in the same way a car mechanic might feel awkward in a discussion wherein "turbocharger" and "supercharger" where interchanged. These mechanisms might give very similar, high-level results, but are otherwise fundamentally different. And, importantly, those very slight high-level differences are 100% explained by the fundamental differences between them (eg, a turbocharger will have a small delay because it is driven by exhaust gases, while the supercharger is driven by the crankshaft and has no delay). The words "supercharger" and "turbocharger" are engineering terms, not day-to-day terms and in many contexts they could be used imprecisely with no problems. However, if I was teaching anybody in a related field—say, auto-electrician?—I would be more careful to use correct terminology.
This is why I, personally, would hesitate to describe \K as a lookbehind. But I can see why it is a perfectly reasonable thing to do in some circumstances. It would rarely be a big deal in the real world, and certainly isn't here on this thread. 🙂
And with that I will sign off, and thanks for indulging me. I had no idea my initial comment was going to provoke so much examination. It was fun. - Mark
___________________________ Further nitpicking and details, from your last reply. This is for the masochists only!
> because neither lookbehind captures what it's looking behind for
This claim is wrong. During the \K procedure, "apple" IS captured (or "matched", or "consumed"—let me know if the wording is the problem here) and when the \K activates, the captured contents is discarded—the bucket is emptied. During the positive lookbehind procedure "apple" is never captured at all. The grep engine is first looking to match the "1" of the regular expression. As it marches on it sees a "1" and then evaluates the "apple", probably backwards. Arrows show the difference. If you draw arrows showing the location of the "current character" under scrutiny by the state machine, in the \K example it marches left to right, with no backtracking, but in any lookbehind example it will see a match (but not yet capture) eg. the "1", then cast it's eye back to the left "e", "l", "p", "p", "a", evaluating the symbols in the lookbehind (?<=) and then actually capture the "1". It is a totally different process and here you can see why the latter is a lookbehind and the former is not.
> I don't think that 'discarded' is a useful term here
It was useful to me, writing the previous paragraph.
> When a search pattern is placed in parentheses the results are captured (and can be referred to using \1, \2, etc or $1, $2, etc).
You are forgetting the root capture group $0, which DID capture "apple" in the \K version (before it was discarded), but DIDN'T in the positive lookbehind version. But capture groups, per se, aren't relevent to my point.
> now do I begin to understand how you understand the difference: the \K lookbehind (in your example) looks for \D+ and when found, checks whether it's followed by \d+,
That is a strange way of describing the normal greap engine character-by-character matching behaviour which, yes, goes left-to-right matching as it goes. The \K just resets in the middle of this process. There is no lookbehind happening in the \K process—it is a simple forward-only process and will be very fast code. I'm sure that's why they implemented it.
A student in one school might ask: "I love this \K version of the positive lookbehind—it's so simple and fast. What is the negative lookbehind version?" At the same time a student in another school might never ponder what the opposite of "discard the captured content" is.
> Are you sure that that's how it works?
At the level of this discussion, yep!
> Coming back to your dialog, the teacher should have pointed out to the student that they could use ((?<=apple)|(?<=banana))\d+
Yes, that would be a great thing for the teacher to explain—no matter what anyone thinks about our nitpicking!
... View more
‎Feb 17, 2025
05:09 PM
@j.khakase Thanks for your sample files.
Unfortunately, the scripting API doesn't provide access to anything but the very most basic appearance, eg. a fill color and a stroke color. Because in your example file you use a more complex appearance (two fills and a live effect nested in a fill), a script won't really help here.
One solution would be to simplify the artwork—eg. split the artwork into a basic colored rectangle plus a point text frame. This would make a scripting approach feasible. See the attached demo file and try it with the script below. Note: this is just a demonstration—your exact needs will probably require something more specialized.
- Mark
Everytime you run script it will apply a random color from the same color group.
Can select multiple page items:
/**
* @file Change Fill Color By Color Group Random Swatch.js
*
* Experiment of applying swatch to the selected items,
* where the swatch is chosen at random from amongst the
* item's fill color's parent swatch group.
*
* @author m1b
* @discussion https://community.adobe.com/t5/illustrator-discussions/how-to-vary-hues-for-dynamic-shapes/m-p/15159237
*/
(function () {
var doc = app.activeDocument,
items = doc.selection;
if (0 === item.length)
return alert('Please select one or more page items and try again.');
for (var i = 0; i < items.length; i++)
applyRandomColorGroupColor(doc, items[i]);
})();
/**
* Apply a fill color to `item`, by choosing a random
* swatch color from `item`s current fill color's
* parent swatch group.
* @author m1b
* @version 2025-02-18
* @param {Document} doc - the item's Document.
* @param {PageItem} item - the item to color.
*/
function applyRandomColorGroupColor(doc, item) {
if (!item.hasOwnProperty('fillColor'))
return;
// get all the colors from the item's color's parent color group
var swatches = getColorGroupSwatchesForColor(doc, item.fillColor);
if (
!swatches
|| 0 === swatches.length
)
return alert('Could not find a color group for the selected item\'s color.')
// random swatch
item.fillColor = swatches[Math.floor(Math.random() * swatches.length)].color;
};
/**
* Returns all the swatches in the same color group as `targetColor`.
* @author m1b
* @version 2025-02-18
* @param {Document} doc - an Illustrator Document.
* @param {Color} targetColor - the target of the search.
* @returns {Swatches?}
*/
function getColorGroupSwatchesForColor(doc, targetColor) {
var swatchGroups = doc.swatchGroups;
for (var i = 0; i < swatchGroups.length; i++) {
var swatches = swatchGroups[i].getAllSwatches();
for (var j = 0; j < swatches.length; j++)
// compare stringified versions
if (stringify(swatches[j].color) === stringify(targetColor))
return swatches;
}
};
/**
* Simple stringify object, with
* special handling of Swatches.
* @author m1b
* @version 2023-04-26
* @param {Object} obj - the object to stringify.
* @returns {String}
*/
function stringify(obj) {
var str = obj.toString();
for (var key in obj) {
if (!obj.hasOwnProperty(key))
continue;
if (
key == 'spot'
|| key == 'color'
)
str += stringify(obj[key]);
else
str += obj[key];
}
return str;
};
... View more
‎Feb 17, 2025
04:03 PM
Oh sorry @vectora98504848 I somehow didn't copy half of the script! I have fixed it now. - Mark
... View more
‎Feb 17, 2025
06:08 AM
Okay @vectora98504848 here is the script again, now with René's improved function. Let us know how it goes!
- Mark
/**
* @file Unrotate Selected Items 2.js
*
* Makes an attempt to remove any recorded rotation
* that can be derived from the selected items.
* Finds rotation either in the item's BBAccumRotation
* tag or the item's matrix.
*
* @author m1b
* @version 2025-02-17
* @discussion https://community.adobe.com/t5/illustrator-discussions/script-to-return-or-restore-rotation-of-multiple-selected-objects/m-p/15146692
*/
(function () {
var doc = app.activeDocument;
var items = getItems({
from: doc.selection,
getPageItems: true,
getGroupItems: true,
});
if (0 === items.length)
return alert('Please select some items to rotate and try again.');
for (var i = 0; i < items.length; i++)
unrotate2(items[i]);
})();
/**
* Attempt to return a page item to unrotated state.
* Modified by renél80416020 to apply rotation
* to group items and compound path items correctly.
* Important: will only work if the item has a valid
* BBAccumRotation tag or a matrix that is ammenable
* to derivation of the rotation.
* @author m1b and renél80416020
* @version 2025-02-14
* @param {PageItem} item - the item to unrotate.
* @return {Number} - the amount of rotation, in degrees.
*/
function unrotate2(item) {
var grp = false, trs = false;
var accumRotation = undefined,
rotationTag;
if (item.hasOwnProperty('tags')) {
if (item.typename == "GroupItem" || item.typename == "CompoundPathItem") {
if (item.typename == "CompoundPathItem") {
rotationTag = getThing(item.pathItems[0].tags, 'name', 'BBAccumRotation');
trs = true;
}
else {
rotationTag = getThing(item.pageItems[0].tags, 'name', 'BBAccumRotation');
grp = true;
}
}
else rotationTag = getThing(item.tags, 'name', 'BBAccumRotation');
if (rotationTag)
accumRotation = rotationTag.value * 180 / Math.PI;
}
if (
undefined == accumRotation
&& item.hasOwnProperty('matrix')
)
// derive rotation from the matrix
accumRotation = getRotationFromMatrix(item.matrix);
if (undefined == accumRotation)
return;
// rotate the item
item.rotate(- accumRotation);
if (rotationTag) {
// update tag
var pi;
if (grp)
for (var i = 0; i < item.pageItems.length; i++) {
pi = item.pageItems[i];
rotationTag = getThing(pi.tags, 'name', 'BBAccumRotation');
if (rotationTag != undefined) { rotationTag.value = 0; }
}
if (trs) {
pi = item.pathItems[0];
rotationTag = getThing(pi.tags, 'name', 'BBAccumRotation');
if (rotationTag != undefined) { rotationTag.value = 0; }
rotationTag = getThing(item.tags, 'name', 'BBAccumRotation');
if (rotationTag != undefined) { rotationTag.value = 0; }
}
if (!trs && !grp)
rotationTag.value = 0;
}
};
/**
* Returns a thing with matching property.
* If `key` is undefined, evaluate the object itself.
* @author m1b
* @version 2024-04-21
* @param {Array|Collection} things - the things to look through.
* @param {String} [key] - the property name (default: undefined).
* @param {*} value - the value to match.
*/
function getThing(things, key, value) {
for (var i = 0, obj; i < things.length; i++)
if ((undefined == key ? things[i] : things[i][key]) == value)
return things[i];
};
/**
* Returns the rotation amount, in degrees,
* of the given (not skewed) matrix.
* @author m1b
* @version 2023-10-20
* @param {Matrix} matrix - an Illustrator Matrix.
* @returns {Number}
*/
function getRotationFromMatrix(matrix) {
if (!matrix.hasOwnProperty('mValueA'))
throw new Error('getRotationFromMatrix: bad `matrix` supplied.');
// scaling factors
var scaleX = Math.sqrt(matrix.mValueA * matrix.mValueA + matrix.mValueC * matrix.mValueC);
// scaleY = Math.sqrt(matrix.mValueB * matrix.mValueB + matrix.mValueD * matrix.mValueD);
// rotation angle
var radians = Math.acos(matrix.mValueA / scaleX),
degrees = radians * (180 / Math.PI);
return Math.round(degrees * 1000) / 1000;
};
/** ------------------------------------------------------------------- *
* GET ITEMS *
* -------------------------------------------------------------------- *
* @author m1b *
* @version 2024-03-01 *
* -------------------------------------------------------------------- *
* Collects page items from a `from` source, eg. a Document, Layer, *
* GroupItem, or Array. Will look inside group items up to `maxDepth`. *
* Search can be filtered using `filter` function. Note that the *
* filter function is evaluated last in the filtering process. *
* -------------------------------------------------------------------- *
* Example 1. Get all items in document: *
* *
* var myItems = getItems({ from: app.activeDocument }); *
* *
* -------------------------------------------------------------------- *
* Example 2. Get all selected items except groups: *
* *
* var myItems = getItems({ *
* from: app.activeDocument.selection, *
* getGroupItems: false, *
* }); *
* *
* -------------------------------------------------------------------- *
* Example 3. Using `filter` function to choose item type: *
* *
* var myItems = getItems({ *
* from: app.activeDocument, *
* filter: function (item) { *
* return ( *
* 'PathItem' === item.typename *
* || 'CompoundPathItem' === item.typename *
* ); *
* } *
* }); *
* *
* -------------------------------------------------------------------- *
* Example 4. Using `filter` function: *
* *
* var myItems = getItems({ *
* from: app.activeDocument, *
* filter: onlyPngLinks *
* }); *
* *
* function onlyPngLinks(item, depth) { *
* return ( *
* 'PlacedItem' === item.typename *
* && '.png' === item.file.name.slice(-4).toLowerCase() *
* ); *
* }; *
* *
* -------------------------------------------------------------------- *
* Example 4. Using the `filter` function for custom collecting: *
* *
* This example bypasses the normal returned array and instead *
* captures items in an "external" array `itemsByDepth`. *
* *
* var itemsByDepth = []; *
* *
* function getItemsByDepth(item, depth) { *
* if (undefined == itemsByDepth[depth]) *
* itemsByDepth[depth] = []; *
* itemsByDepth[depth].push(item); *
* }; *
* *
* getItems({ *
* from: app.activeDocument, *
* filter: getItemsByDepth *
* }); *
* *
* -------------------------------------------------------------------- *
* @param {Object} options - parameters
* @param {PageItem|Array<PageItem>|Document|Layer} options.from - the thing(s) to look in, eg. a selection.
* @param {Function} [options.filter] - function that, given a found item, must return true (default: no filtering).
* @param {Boolean} [options.getPageItems] - whether to include page items in returned items (default: true).
* @param {Boolean} [options.getGroupItems] - whether to include GroupItems in returned items (default: true).
* @param {Boolean} [options.getLayers] - whether to include Layers in returned items (default: false).
* @param {Boolean} [options.getHiddenItems] - whether to include hidden items in returned items (default: true).
* @param {Boolean} [options.getLockedItems] - whether to include locked items in returned items (default: true).
* @param {Boolean} [options.getGuideItems] - whether to include guide items in returned items (default: false).
* @param {Number} [options.maxDepth] - deepest folder level (recursion depth limit) (default: 99).
* @param {Boolean} [options.returnFirstMatch] - whether to return only the first found item (default: false).
* @param {Number} [depth] - the current depth (private).
* @returns {Array|PageItem} - all the found items in a flat array, or the first found item if `returnFirstMatch`.
*/
function getItems(options, depth) {
// defaults
options = options || {};
var found = [],
depth = depth || 0,
items = options.from;
if (!options.initialized)
// once-off initialization
if (!initialize())
return [];
itemsLoop:
for (var i = 0, item, len = items.length; i < len; i++) {
item = items[i];
if (
false === excludeFilter(item)
&& true === includeFilter(item)
) {
// item found!
found.push(item);
if (options.returnFirstMatch)
break itemsLoop;
}
if (
'GroupItem' !== item.constructor.name
&& 'Layer' !== item.typename
)
// only items with children from here
continue itemsLoop;
if (
excludeHidden(item)
|| excludeLocked(item)
)
// don't look into excluded containers
continue itemsLoop;
if (depth >= options.maxDepth)
// don't go deeper
continue itemsLoop;
// set up for the next depth
options.from = item.pageItems;
// look inside
found = found.concat(getItems(options, depth + 1));
}
// this level done
if (true == options.returnFirstMatch)
return found[0];
else
return found;
/**
* Returns true when the item should be not be found.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeFilter(item) {
return (
isAlreadyFound(item)
// is hidden
|| excludeHidden(item)
// is locked
|| excludeLocked(item)
// is guide
|| (
false === options.getGuideItems
&& true === item.guides
)
// is layer
|| (
false === options.getLayers
&& 'Layer' === item.typename
)
// is group item
|| (
false === options.getGroupItems
&& 'GroupItem' === item.typename
)
// is page item
|| (
false === options.getPageItems
&& 'GroupItem' !== item.typename
&& undefined != item.uuid
)
);
};
/**
* Returns true when the item should be included.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function includeFilter(item) {
return (
undefined == options.filter
|| options.filter(item, depth)
);
};
/**
* Returns true when the item should
* be excluded because it is hidden.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeHidden(item) {
return (
false === options.getHiddenItems
&& (
true === item.hidden
|| false === item.visible
)
);
};
/**
* Returns true when the item should
* be excluded because it is locked.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeLocked(item) {
return (
false === options.getLockedItems
&& true === item.locked
);
};
/**
* Returns true if item was already
* found, and marks item as found,
* to avoid finding same item twice.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function isAlreadyFound(item) {
var uuid = item.hasOwnProperty('uuid')
? item.uuid
: item.typename + item.zOrderPosition,
isFound = !!options.isFound[uuid];
options.isFound[uuid] = true;
return isFound;
}
/**
* Returns the initialised `options` object.
* @returns {Object}
*/
function initialize() {
// make a new object, so we don't pollute the original
options = {
initialized: true,
depth: 0,
isFound: {},
filter: options.filter,
getPageItems: false !== options.getPageItems,
getGroupItems: false !== options.getGroupItems,
getLayers: true === options.getLayers,
getHiddenItems: false !== options.getHiddenItems,
getLockedItems: false !== options.getLockedItems,
getGuideItems: true === options.getGuideItems,
maxDepth: options.maxDepth,
returnFirstMatch: options.returnFirstMatch,
};
if (
undefined == options.maxDepth
|| !options.maxDepth instanceof Number
)
options.maxDepth = 99;
// items is a single layer
if ('Layer' === items.typename)
items = [items];
// items is a document
else if ('Document' === items.constructor.name) {
var layers = items.layers;
items = [];
for (var i = 0; i < layers.length; i++)
items.push(layers[i]);
}
else if ('Array' !== items.constructor.name)
items = [items];
return items.length > 0;
};
};
... View more
‎Feb 17, 2025
02:35 AM
@Marc Autret, what fun! Beautiful!
- Mark
... View more
‎Feb 17, 2025
02:25 AM
Thanks for letting us know, Olivier. All the best.
... View more
‎Feb 16, 2025
06:09 AM
Hi @Peter Kahrel I am conscious that you have given this your last shot, but (sorry!) I do seem to want to comment on a couple of things. 😬
> ... and some people do, not only because it's more flexible, but also because it's less typing and you don't have to wonder whether the < comes before the = or the other way around.
Totally valid point, and gives a good reason to use \K over a lookbehind. It is not a valid reason to *call* it a lookbehind, however.
> As to your comparison chart, you can phrase thing any way to suit a purpose. ...
Yes! And that's my point: I prefer to use phrases—where pedagogically appropriate—to correspond to how it actually is. I mean, your hypothetical phrasing isn't remotely correct—on the right, the entire matched contents is *never* discarded and the "apple" in the lookbehind is never captured. On these two points the \K performs the exact opposite to a positive lookbehind!
Anyway, while I might enjoy talking about this kind of minutiae when it takes my fancy, I don't expect others to be similarly moved—except maybe Robert 😉 I see you there!—so I fully understand if you want to leave this where it is.
- Mark
... View more
‎Feb 16, 2025
05:52 AM
Oh boy, Robert, perhaps I am just not correcty parsing your conversational style here. It seems to me that you have asked several closed-ended questions to which I answered immediately. I don't remember telling you to get lost.
Perhaps it is the difference between your post
> Or it all works as intended - and I just misunderstood it?
and Peter's post
> I don't see why you wouldn't want to call \K a lookbehind. Like the classic lookbehind, it finds things if they're preceded by a certain pattern. Lookbehind is a functional notion, not a formal one.
Can you see the difference? I didn't even know how to answer your question, whereas Peter's reply—which wasn't even a question—not only clarified his thoughts on the topic somewhat, but also gave me something concrete to respond-to, and I responded at considerable length, for no other reasons than (a) I wanted Peter and other readers to understand where I was coming from, (b) I wanted to explore a subtle philosophical concept, and (c) I wanted to practice writing and presenting a topic in a helpful way (and yes, yes, I know I did a poor job—but I've done worse before so I am not totally unhappy). A long, detailed response does not mean the topic is "a big deal"—it might be just interesting, or whatever.
Also, Peter is not being weirdly combative, which I appreciate.
- Mark
... View more