Как в скрипте реализовать поддержку наборов настроек

Зачем нужны наборы настроек

В первую очередь для того, чтобы сделать скрипт более универсальным и гибким, приспособить его для обработки различных типов публикаций. Для обеспечения этой гибкости нужно постараться максимально отделить настройки от логики. Такое отделение особенно важно для крупных и сложных скриптов. Однако, и простые на первый взгяд скрипты желательно сразу разрабатывать с учетом поддержки наборов настроек — это значительно упростит последующую доработку и модификацию. По опыту могу сказать, что внедрение поддержки наборов настроек в уже готовый скрипт намного труднее, чем разработка скрипта с изначальной поддержкой наборов настроек.

Немного теории. Как нам реорганизовать Рабкрин

Есть несколько типичных методов реализации наборов настроек. Сначала разберем их теоретически, а уже затем приступим к практике.

Метод №1. Сам себе настройщик

Первый и самый простой метод — хранить настройки в самом файле скрипта, желательно в верхней части текста, чтобы в случае необходимости можно было быстро найти и отредактировать набор настроек.

Для упрощения разработки может возникнуть соблазн сделать несколько копий одного скрипта с разными наборами настроек на все случаи. Этот путь изначально порочен, и ведет в пучину страданий, ошибок и отчаяния. Синхронизация исправлений в скрипте без специальных инструментов типа diff становится мучительной уже при трех различных копиях скрипта, а при пяти и более — синхронизация уже практически невозможна. Поэтому от неконтролируемого размножения копий скрипта нужно отказаться сразу — количество головной боли и возможных ошибок при развитии и поддержке скрипта напрочь перевешивают простоту разработки.

Вопрос о том, как реализовать наборы настроек именно как наборы, подробнее рассмотрен ниже в практической части. Ничего особенно сложного не предвидится, поскольку набор достаточно легко организовать при помощи простых и доступных средств, таких как массив или объект.

Метод №2а. Сепаратный мир

Логичным развитием метода №1 может стать дальнейшее отделение настроек от логики, а именно вынесение настроек в отдельный файл. Компания Adobe предусмотрела использование такого метода и внесла поддержку директивы include в спецификацию ExtendScript, за что мы можем выразить чистосердечное мерси.

При использовании директивы include препроцессор включает текст файла, переданного в директиве, в текст исполняемого скрипта так, как если бы этот текст присутствовал в самом тексте скрипта вместо директивы. Перенос настроек в отдельный файл (рекомендуемое расширение jsxinc) позволяет упростить редактирование самого скрипта, поскольку не придется отвлекаться на текст определения настроек — а он бывает довольно крупным.

С практической точки зрения в области реализации этот метод ничем от метода №1 не отличается.

Метод №2б. Сепаратный мир на выборной основе

Как дальнейшее развитие метода №2а можно рассматривать случай, когда набор настроек реализован в виде двух и более файлов настроек, по необходимости подключаемых к скрипту.

Это довольно спорный по ряду причин метод. Во-первых, при изменении структуры настоек, например, при добавлении какого-нибудь поля, придется редактировать более одного файла. Во-вторых, для подключения выбранного файла настроек с помощью директивы include придется использовать довольно страшненький метод типа

switch (set) {
	case 1:
		#include '01.jsxinc'
	break;
	case 2:
		#include '02.jsxinc'
	break;
},

работоспособность которого в последующих реализациях ExtendScript остается пока под вопросом.

Как вариант, можно написать некое подобие парсера файлов настроек с лото и библиотекаршами eval’ом и чтением из выбранного файла, но это те же костыли, только с рюшечками в виде дополнительного кода.

Метод №3. Будь как все

Развивая тему парсера файлов настроек можно прийти к реализации в скрипте парсера некоего стандарта настроек, например, ini-файлов. В целом, этот метод заслуживает внимания в том случае, если есть цель написать кроме «пользователей» набора настроек еще и редактор. Процесс написания сам по себе достаточно трудоемкий, и требует довольно значительных усилий. С одной стороны, однажды написанный парсер настроек может использоваться во многих проектах, с другой стороны этого метода фактически нет никаких преимуществ по сравнению с хранением настроек в файлах, подключаемых с помощью директивы include. Зато есть один очень значительный недостаток, а именно необходимость преобразования типов переменных. Причем, как примитивов JavaScript — строк в числа или массивы, — так и более сложных, например получение активного документа документа через вполне допустимую при использовании директивы include конструкцию типа var doc = app.activeDocument.

Метод №4. Используй силу

Наиболее мощным — и потому, как правило, применяемым в сложных проектах — методом реализации наборов настроек является использование xml-файлов. Несмотря на определенную сложность разработки скриптов, использующих такой метод хранения настроек, применение XML дает ряд преимуществ, переоценить которые очень сложно. Это и возможность типизации настроек при помощи DTD, и поиск при помощи XPATH, и многое-многое другое, например, возможность использования определенных настроек не только в скриптах, но и в сторонних приложениях.

Вывод из теоретической части: наиболее перспективными и удобными методами хранения наборов настроек являются методы №2а и №4. Про хранение настроек в xml нужно писать подробно и с многочисленными примерами, причем не одну статью, а целый цикл, поэтому для практики остановимся на методе №2а. Тем более, что он подойдет для любых скриптов, даже довольно сложных.

Практика. Как оно работает

Для примера предлагается очень простой и бесполезный скрипт, где наборы настроек используются для определения свойств простых диалогов.

Скрипт:

#target "InDesign"
#includepath '../', '../options', '../settings'
#include 'settings.jsxinc'

// Oleg Butrin 
// adobescripts.wordpress.com
// Example of options sets in InDesign CS6

// localisation
$.localize = true;
var a = {};
a.DefaultSettingsNotFound= localize({en: 'Default settings not found!'});
a.UserSettingsNotDefined = localize({en: 'User settings not defined. Use default settings'});
a.UserSettingsNotValid = localize({en: 'User settings not valid. Use default settings'});
var ui = {};
ui.SelectOptionsSet = localize({en: 'Select options set:'});
ui.SelectUserOptions = localize({en: 'Select user options'});
ui.FrameWidth = localize({en: 'Frame width:'});
ui.FrameHeight = localize({en: 'Frame height:'});
ui.FontSize = localize({en: 'Font size:'});
ui.FrameColor = localize({en: 'Frame color:'});
ui.TextColor = localize({en: 'Text color:'});

// default options
var def_options = {
	name: 'default',
	framewidth: 100,
	frameheight: 80,
	fontmin: 16,
	fontmax: 22,
	fontdef: 18,
	framecolor: ['Cyan', 'Magenta', 'Yellow'],
	textcolor: ['Black', 'Paper']
};
// collect names from array, contains named elements
function collectNames (namedarray) {
	result = [];
	for (var i = 0; i < namedarray.length; i++) {
		if (namedarray[i].hasOwnProperty('name')) {
			result.push(namedarray[i].name);
		}
	}
	return result;
}
// check user defined settings
function checkSettings (settingarray, setdef) {
	for (var i = 0; i < settingarray.length; i++) {
		var set = settingarray[i];
		for (var field in setdef) {
			if (!set.hasOwnProperty(field) || typeof(set[field])!=typeof(setdef[field])) {
				return false;
			}
		}
	}
	return true;
}
// main function
function main () {
	// set short variable for options
	var s;
	// check default options defined
	if (typeof(def_options)==undefined) {
		alert(a.DefaultSettingsNotFound);
		return false;
	}
	// check user options defined
	if (typeof(set_options)==undefined) {
		alert(a.UserSettingsNotDefined);
		s = def_options;
	}
	// check settings in user options
	if (!checkSettings(set_options, def_options)) {
		alert(a.UserSettingsNotValid);
		s = def_options;
	}
	// user select active options set
	if (s!=def_options) {
		// dialog
		var setDialog = app.dialogs.add({name: ui.SelectOptionsSet});
		with (setDialog.dialogColumns.add()) {
			dialogRows.add().staticTexts.add({staticLabel: ui.SelectOptionsSet});
			var user_set = dialogRows.add().dropdowns.add({stringList: collectNames(set_options), selectedIndex: 0});
		}
		if (!setDialog.show()) {
			return false;
		}
		// set selected
		s = set_options[user_set.selectedIndex];
	}
	// main dialog
	var mainDialog = app.dialogs.add({name: ui.SelectUserOptions});
	with (mainDialog.dialogColumns.add().borderPanels.add().dialogColumns.add()) {
		dialogRows.add().staticTexts.add({staticLabel: ui.SelectUserOptions});
		with (dialogRows.add()) {
			// labels
			with (dialogColumns.add()) {
				dialogRows.add().staticTexts.add({staticLabel: ui.FrameWidth});
				dialogRows.add().staticTexts.add({staticLabel: ui.FrameHeight});
				dialogRows.add().staticTexts.add({staticLabel: ui.FontSize});
				dialogRows.add().staticTexts.add({staticLabel: ui.FrameColor});
				dialogRows.add().staticTexts.add({staticLabel: ui.TextColor});
			}
			// dialog items
			with (dialogColumns.add()) {
				dialogRows.add().staticTexts.add({staticLabel: s.framewidth.toString()});
				dialogRows.add().staticTexts.add({staticLabel: s.frameheight.toString()});	
				var user_pointsize = dialogRows.add().realEditboxes.add({minimumValue: s.fontmin, maximumValue: s.fontmax, editValue: s.fontdef, smallNudge: 0.5, largeNudge: 1});
				var user_framecolor = dialogRows.add().dropdowns.add({stringList: s.framecolor, selectedIndex:0});
				var user_textcolor = dialogRows.add().dropdowns.add({stringList: s.textcolor, selectedIndex:0});
			}
		}
	}
	if (!mainDialog.show()) {
		return false;
	}
	// demo processing
	try {
		var doc = app.documents.add();
		var frameColor = doc.colors.itemByName(s.framecolor[user_framecolor.selectedIndex]);
		var textColor = doc.colors.itemByName(s.textcolor[user_textcolor.selectedIndex]);
		var pointSize = user_pointsize.editValue;
		var frame = doc.pages[0].textFrames.add(undefined, undefined, undefined, {geometricBounds: [0,0, s.frameheight, s.framewidth]});
		frame.fillColor = frameColor;
		frame.parentStory.contents = 'TEXT\u000D';
		var para = frame.parentStory.paragraphs[0];
		para.fillColor = textColor;
		para.pointSize = pointSize;
	} catch (error) {
		alert(error);
		return false;
	}
	return true;
}

main();

Файл с набором настроек (settings.jsxinc):

var set_options = [
	{
		name: 'small',
		framewidth: 40,
		frameheight: 30,
		fontmin: 6,
		fontmax: 12,
		fontdef: 8,
		framecolor: ['Magenta', 'Yellow'],
		textcolor: ['Black']
	},
	{
		name: 'middle',
		framewidth: 70,
		frameheight: 50,
		fontmin: 9,
		fontmax: 16,
		fontdef: 12,
		framecolor: ['Cyan', 'Yellow'],
		textcolor: ['Black', 'Paper']
	},
	{
		name: 'big',
		framewidth: 100,
		frameheight: 80,
		fontmin: 16,
		fontmax: 22,
		fontdef: 18,
		framecolor: ['Cyan', 'Magenta', 'Yellow'],
		textcolor: ['Black', 'Paper']
	},
];

Скрипт создает новый документ, помещает в него текстовый фрейм с заранее определенным текстом и выбираемым размером шрифта, красит фрейм и текст в выбранный цвет. Размеры текстового фрейма и доступные размеры шрифта определяются в настройках.

Разберем внутреннее устройство скрипта и назначение функций.

#includepath '../', '../options', '../settings'
#include 'settings.jsxinc'

При помощи этих директив препроцессора подключается файл settings.jsxinc, который может находиться в той же папке, что и и файл скрипта или в подпапках options и settings. Если файл не будет найден по указанным путям, скрипт завершится  ошибкой так и не успев начать выполнение.

var a = {};
var ui = {};

Определяются переменные с короткими именами для хранения строк локализации для предупреждений (a — alerts) и диалоговых окон(ui — user interface).

var def_options = {
	name: 'default',
	framewidth: 100,
	frameheight: 80,
	fontmin: 16,
	fontmax: 22,
	fontdef: 18,
	framecolor: ['Cyan', 'Magenta', 'Yellow'],
	textcolor: ['Black', 'Paper']
};

Определяются дефолтные настройки. При наличии отсутствия пользовательских настороек будут использованы именно эти настойки из текста скрипта. Создавать пользовательские наборы достаточно просто — скопировать, вставить, отредактировать.

Набор настроек реализован в виде объекта и имеет некоторое количество полей:

  • name — имя набора
  • framewidth — ширина фрейма
  • frameheight — высота фрейма
  • fontmin — минимальный размер шрифта
  • fontmax — максимальный размер шрифта
  • fontdef — размер шрифта по-умолчанию
  • framecolor — цвет фрейма
  • textcolor — цвет текста

Все поля объекта строго обязательны к заполнению. Кроме того, тип полей всех пользовательских наборов должен совпадать с типом соответствующего поля набора по-умолчанию: строковой, числовой, массив.

Размеры фрейма предполагают использование миллиметров, размеры шрифта — пунктов. Цвета указаны по именам — решение не само лучшее, но для примера вполне подойдет.

function collectNames (namedarray)

Функция для получения списка имен любого массива (или коллекции), элементы которого имеют имена. Наборы настроек имена имеют.

function checkSettings (settingarray, setdef)

Функция для проверки соответствия пользовательских наборов настроек дефолтному набору. Для каждого пользовательского набора в массиве проверяется наличие полей и их тип. Это довольно слабая проверка, но и она позволяет определить заведомо неподходящий набор. Опять же, что немаловажно, проверяется наличие имени набора. Отсутствие поля имени в наборе обязательно приведет к тому, что в скрипте будет использован не тот набор, который пользователь выбрал в диалоговом окне.

function main()

Основная функция скрипта. Начинается с проверки наличия дефолтного набора настроек, наличия массива пользовательских наборов и соответствия пользовательских наборов формату дефолтного массива.

var s;

Определение переменной для выбранного набора. Если пользовательских наборов нет, будет использован дефолтный.

var setDialog

Диалог  выбора пользовательского набора. Для получения списка наборов используется ранее определенная функция collectNames().

var mainDialog

Основной диалог, в котором используются значения полей набора настроек.

Наибольшее количество полей — целых три штуки — используется в переменной user_pointsize для определения максимального, минимального значений, а также значения по-умолчанию элемента типа realEditbox.

Для элементов типа dropdown используются поля типа массив. Поскольку в наборе это поле определено именно как массив, никакие преобразования не нужны.

Наборы настроек в файле settings.jsxinc являются элементами массива set_options. Никаких проблем при ручном редактировании возникнуть не должно — при соблюдении предложенного формата набора настроек.

Итого

Предложенный метод использования наборов настроек достаточно прост, гибок, эффективен и умеренно безопасен.  К использованию рекомендую.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s