Поступил вопрос с мест по поводу автоматической сортировки абзацев в тексте. Примерный смысл вопроса следующий:
Большинство скриптов сортировки, в том числе и скрипт paraTrooper.js, написанный мной в далеком 2004 году, умеют сортировать абзацы поштучно. А что делать, если нужно отсортировать по неким признакам группы абзацев, сохранив при этом взаимное расположение абзацев внутри группы?
Задача интересная не только с точки зрения разработки, но и как пример, на котором можно наглядно продемонстрировать некоторые приемы работы с текстом в скриптах для InDesign.
Постановка задачи
Формально задача поставлена следующим образом. Имеется текст, отформатированный тремя стилями: header (заголовок), subheader (подзаголовок) и text (обычный текст). Текст, начинающийся от абзаца со стилем header до следующего абзаца с таким же стилем (не включая этот абзац) считается группой. Нужно отсортировать такие группы по определенному признаку, в данном случае по номеру в тексте абзаца-заголовка.
В виде допущения будем считать, что текст состоит только из групп, которые надо отсортировать.
В тексте, соответственно, девять произвольно расположенных групп абзацев.
Что должен сделать скрипт:
- найти и сформировать группы,
- сортировать группы,
- переместить абзацы согласно сортировке.
И все это быстро и надежно.
Текст скрипта
#target indesign
var langStrings = {
nodoc: localize( { en: 'No documents are open!', ru: 'Нет открытых документов!' } ),
notext: localize( { en: 'No text are selected!', ru: 'Не выбран текст!' } ),
nostyle: localize( { en: 'Can\'t find predefined paragraph style!', ru: 'Не найден предопределенный стиль абзаца!' } ),
}
var iniStart = 'header';
function numsort(a, b) {
var anum = parseInt(a[0].contents.toString().replace(/^[^\d]+/gim, ''));
var bnum = parseInt(b[0].contents.toString().replace(/^[^\d]+/gim, ''));
return anum - bnum;
}
function main() {
try {
var doc = app.activeDocument;
} catch (error) {
alert(langStrings.nodoc);
return false;
}
try {
var story = app.selection[0].parentStory;
} catch (error) {
alert(langStrings.notext);
return false;
}
var style = doc.paragraphStyles.itemByName(iniStart);
if (!style.isValid) {
alert(langStrings.nostyle);
return false;
}
var regions = [];
var current = null;
for (var i = 0; i < story.paragraphs.length; i++) {
var para = story.paragraphs[i];
if (para.appliedParagraphStyle == style) {
if (current != null) {
regions.push(current);
}
var current = [];
current.push(para);
} else {
if (current != null) {
current.push(para);
}
}
}
if (current != null) {
regions.push(current);
}
var beg = regions[0];
var end = regions[regions.length - 1];
regions.sort(numsort);
var temptf = doc.pages[0].textFrames.add();
var ts = temptf.parentStory;
for (var i = 0; i < regions.length; i++) {
var reg = story.paragraphs.itemByRange(regions[i][0], regions[i][regions[i].length - 1]);
reg.duplicate(LocationOptions.AT_END, ts);
}
var prevtext = story.paragraphs.itemByRange(beg[0], end[end.length - 1]).getElements()[0];
if (prevtext.characters.lastItem().contents != '\u000D') {
ts.insertionPoints.firstItem().contents = '\u000D';
}
var newtext = ts.paragraphs.itemByRange(ts.paragraphs.firstItem(), ts.paragraphs.lastItem());
newtext.move(LocationOptions.AFTER, prevtext);
prevtext.paragraphs.everyItem().remove();
temptf.remove();
}
main();
Разбор кода
В первых строках кода объявляются директива препроцессора и переменная для локализации сообщений. Следом объявляется переменная, в которой указывается имя стиля заголовка, по которому и будет происходить группировка абзацев.
var iniStart = 'header';
Затем объявляются две функции: numsort — для сортировки групп (подробно рассмотрим позже) и main — основная функция, где и будет происходить основная работа.
В функции main сначала проверяются на наличие объекты и объявляются переменные doc (рабочий документ), story (текст для обработки) и style (стиль абзаца-заголовка, отмечающего начало групп). Если нужный объект не обнаружен, то выводится сообщение и функция возвращает false. Все довольно стандартно, разве что получение и проверка стиля абзаца имеют некоторую специфику, на которую стоит обратить внимание. При желании можно написать диалоговое окно, в котором пользователь может выбрать стиль для абзаца-заголовка. Это намного удобнее для пользователя, но мы такой диалог писать, конечно, не будем.
Следующий кусок кода отвечает непосредственно за группировку абзацев.
var regions = [];
var current = null;
for (var i = 0; i < story.paragraphs.length; i++) {
var para = story.paragraphs[i];
if (para.appliedParagraphStyle == style) {
if (current != null) {
regions.push(current);
}
var current = [];
current.push(para);
} else {
if (current != null) {
current.push(para);
}
}
}
if (current != null) {
regions.push(current);
}
Что в нем происходит.
Сначала объявляются две переменные: regions (массив для хранения групп абзацев) и current (переменная для формирования отдельной группы). Поскольку ни одна группа еще не сформирована, переменной current нужно присвоить значение null.
Затем в цикле перебираются все абзацы выбранного текста story. Если стиль, примененный к очередному абзацу будет стилем заголовка, то предыдущая группа (переменная current) будет добавлена к массиву групп (если, конечно, current не равна null), а переменная current будет заново инициализирована как новый массив, куда будет добавлен абзац заголовка. Если же стиль абзаца не является стилем заголовка, то очередной абзац будет добавлен к текущей группе current (опять же, если current не равна null).
После завершения цикла в в массив regions добавляется последняя группа, если она была сформирована (потому, что в цикле добавление группы в переменную regions происходит только тогда, когда объявляется новая группа).
Необходимое замечание. Приведенный метод группировки абзацев — топорный, ничуть не интеллектуальный и достаточно расточительный по ресурсам в случае действительно большого текста. Но зато он прост, нагляден и вполне надежен. При необходимости (а она обязательно возникнет в реальной работе) можно изменить алгоритм группировки абзацев до неузнаваемости, используя поиск по стилю или прочие способы.
Следующие две строки
var beg = regions[0];
var end = regions[regions.length - 1];
объявляют две переменных, которые нам пригодятся в дальнейшем. По сути это первая и последняя группа абзацев в массиве regions.
Следующая строка
regions.sort(numsort);
сортирует массив групп.
Если кто не знает: JavaScript позволяет использовать для сортировки массивов произвольные функции, передав имя функции сортировки в качестве параметра функции sort().
Рассмотрим, как происходит сортировка в функции numsort.
function numsort(a, b) {
var anum = parseInt(a[0].contents.toString().replace(/^[^\d]+/gim, ''));
var bnum = parseInt(b[0].contents.toString().replace(/^[^\d]+/gim, ''));
return anum - bnum;
}
Любая функция сортировки имеет два параметра, в которые при вызове передаются два сравниваемых элемента массива. В нашем случае элементы массива — это группы абзацев, причем в этих группах первый элемент является абзацем-заголовком, у которого есть номер, по каковому номеру эти группы и нужно сортировать. В общем, как всегда: игла в яйце, яйцо в утке, утка в зайце и так далее. Отличие от народной сказки только в том, что вытащить нужное значение довольно просто: используем встроенную функцию parseInt(), куда передадим текст заголовка, предварительно очистив от лишних знаков в начале текста. «Заголовок 1» преобразуется в «1»; строковое значение преобразуется в числовое. Функция возвращает разницу между номером из первого параметра и номером из второго, а массив волшебным образом сортируется по возрастанию.
Следующие две строки
var temptf = doc.pages[0].textFrames.add();
var ts = temptf.parentStory;
определяют небольшой финт, который поможет существенно упростить именно сортировку. Дело в том, что передвигать группы абзацев внутри исходного текста крайне неудобно, поскольку после каждого перемещения абзацы меняют индекс. А поскольку InDesign абзацы воспринимает исключительно по индексу, то при перемещении абзацев внутри текста получится неудобоваримая каша. Поэтому в документе создается временный текстовый фрейм, в который скрипт будет дублировать группы абзацев в правильном порядке.
Что, собственно, и происходит в следующем цикле.
for (var i = 0; i < regions.length; i++) {
var reg = story.paragraphs.itemByRange(regions[i][0], regions[i][regions[i].length - 1]);
reg.duplicate(LocationOptions.AT_END, ts);
}
Из каждой группы абзацев в отсортированном массиве regions формируется последовательность, которая дублируется в конец созданной story. Текст отсортирован.
Следующая задача — заменить несортированный текст готовым отсортированным. Слабые духом могут использовать app.copy() и app.paste(), но этот путь не для нас. Использования стандартного буфера обмена следует избегать до последнего, поскольку это потенциальная дыра для крайне тяжело отслеживаемых ошибок. Например, если в процессе работы скрипта содержимое буфера заменит любая другая программа.
Для замены текста мы вставим после него новый отсортированный текст и удалим предыдущий. Почему после, а не до — да все по той же причине индексов у абзацев. Зачем рассчитывать, на сколько они сместились, если можно не рассчитывать вообще.
Вот здесь нам и пригодятся переменные beg и end. Поскольку мы их взяли из еще не сортированного массива групп абзацев, то через них можно получить начало и конец несортированного текста, который нужно заменить. Получаем последовательность:
var prevtext = story.paragraphs.itemByRange(beg[0], end[end.length - 1]).getElements()[0];
Еще один маленький фокус. Если последний абзац заменяемого текста не заканчивается знаком конца абзаца (например, если это последний не пустой абзац в тексте), то в начало отсортированного текста добавляем знак конца абзаца.
if (prevtext.characters.lastItem().contents != '\u000D') {
ts.insertionPoints.firstItem().contents = '\u000D';
}
Если этого не сделать, то первый абзац нового текста объединится с последним абзацем старого текста в один абзац, после чего этот самый составной абзац успешно будет удален через один шаг с криком: «Доктор, мы его теряем!».
Получаем новый текст в виде последовательности абзацев:
var newtext = ts.paragraphs.itemByRange(ts.paragraphs.firstItem(), ts.paragraphs.lastItem());
Переносим новый текст на позицию сразу после исходного несортированного текста:
newtext.move(LocationOptions.AFTER, prevtext);
Удаляем исходный несортированный текст:
prevtext.paragraphs.everyItem().remove();
Обратите внимание, удаляем «поабзацно», а не целиком:
prevtext.remove();
иначе может получиться очень неприятный сюрприз, когда при удалении вроде бы только старого текста будет удален и новый. Это опять-таки фокусы индексации абзацев.
Последним действием удаляем временный текстовый фрейм.
temptf.remove();
Вот и все, что нужно для такой интересной задачи, как сортировка групп абзацев.
Файлы
Скрипт: ParaRegSort.zip
Текст, использованный для тестирования: pgs.zip