Реальная задача

В предыдущих статьях цикла Вы познакомились с языком AppleScript, научились пользоваться редактором скриптов, находить описания объектов, их свойств и методов в словарях программ. Рассмотрели мы и некоторые простые примеры. Однако AppleScript способен на гораздо большее. Скажем, объединить для решения поставленной задачи несколько различных программ. Этим мы сегодня и займемся.

Примечание 1: в данной статье использован BBEdit 5. В шестой версии словарь программы был изменен, поэтому описанный скрипт без существенных изменений не сможет работать с ней и последующими.
Примечание 2: Эта часть цикла наиболее далека от реалий сегодняшнего использования Mac OS.

Ставим задачу

Вполне реальная ситуация: Вы ведете подборку ответов на часто задаваемые вопросы (FAQ) и размещаете ее на web-сайте. Вначале их немного, и вполне можно обойтись обычным текстовым редактором. Но время идет, вручную сортировать вопросы, размещать их в нужном месте файла, строить содержание становится все труднее. Конечно, идеальным способом было бы организовать базу данных и строить страницы «на лету». Да вот только сайт размещен на одном из «халявных» хостингов, так что приходится обходиться статическими html-документами. Как быть?

Для сбора информации используем базу данных, сделанную с помощью программы FileMaker Pro{сноска: к сожалению, мне не известен способ полноценного управления базами данных в AppleWorks из AppleScript}. Структура у нее предельно проста: единственная таблица с тремя полями типа «text»: Category, Question и Answer. Причем, для категории должен быть задан список допустимых значений.

Определение полей базы данных

Список значений поля «Категория»

Для публикации накопленная информация будет переноситься в html-файлы. Их будет столько же, сколько категорий — ведь вопросов много, а большой документ и загружается долго, и ориентироваться в нем трудно. Строить страницы, вообще говоря, можно почти в любом скриптуемом текстовом редакторе. Мы же воспользуемся одним из наиболее популярных — BBEdit.

Определим требования к генерируемым web-страницам:

  1. Страница должна включать заголовок (название категории), содержание (список вопросов со ссылками) и основную часть.
  2. Название категории должно также быть включено в титульную строку.
  3. В основном тексте необходимо обеспечить выделение каким-либо способом начала каждого вопроса и ответа.
  4. У пользователя должна быть возможность возврата к содержанию.
  5. Переводы строк заменяются тегом <br>.
  6. Если в ответе упоминается тот или иной internet-ресурс, его URL преобразуется в ссылку.
  7. Ну, и, естественно, получившийся файл должен полностью соответствовать спецификациям HTML 4.

Свойства. Свойства? Свойства!

При формировании html-файлов мы будем вставлять в текст большое число различных элементов разметки. Именно они определяют вид получающихся страниц. Вряд ли Вы хотите, чтоб дизайн сайта был раз и навсегда заданным. Но при каждом изменении внимательно просматривать весь скрипт, стараясь не пропустить ничего существенного, крайне неудобно. Воспользуемся хорошо знакомым любому программисту приемом — вынесем определения этих строк в самое начало программы, определив «свойства» нашего будущего скрипта. Помните, есть в AppleScript такие переменные (property), сразу получающие заданные значения?

Итак…

property DBname : "FAQ.fmp" -- имя файла базы данных

Конечно, можно было бы сделать выбор файла с помощью диалогового окна или оформить скрипт как droplet (см. Часть 1). Но в нашем случае это вряд ли оправдано: база данных используется всегда одна и та же, стало быть незачем человеку при каждом запуске программы делать лишние операции.

Теперь укажем все необходимые фрагменты html-кода. Элементы формирующие заголовочную часть:

property headerText1 : ¬
"<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional¬
//EN"r"http://www.w3.org/TR/REC-html40/loose.dtd">r¬
<html><head><title>FAQ: "
property headerText2 : "</title></head>r¬
<body bgcolor="#ffffff">r<h1>"
property headerText3 : "</h1>"

Обратите внимание: некоторые теги должны включать кавычки. Для AppleScript это — спецсимвол, поэтому в строке они указываются в виде «»». Также здесь встречается символ перевода строки — «r». Вообще-то без него вполне можно обойтись. Это даже на несколько байт уменьшит размер файла. Но если Вы предполагаете выполнять какое-либо последующее «ручное» редактирование, экономить на нем не стоит.

Заключительные теги документа:

property footerText : "</body></html>"

Строки, оформляющие список-содержание (на начальной устанавливаем «якорь» — метку для возврата):

property beginTOC : "<a name="toc"></a><ul>r"
property endTOC : "</ul>r"

Оформление основного текста. Я использовал просто слова «вопрос» и «ответ», но, конечно же, тут могут быть и ссылки на графические элементы.

property beginQ : "<p><b>Вопрос: </b>"
property endQ : "</p>"
property beginA : "<div><b>Ответ: </b>"

После каждого ответа вставляем гиперссылку на начало содержания:

property endA : "</div>r¬
<p align="right"><a href="#toc">К началу</a></p>"

Еще одно свойство я ввел просто для удобства чтения текста скрипта:

property CR : "r"

Тут же объявим несколько глобальных переменных:

global HTMLname -- имя очередного html-файла
global headerText -- текст, предшествующий содержанию
global Question -- текст очередного вопроса
global Answer -- текст очередного ответа
global qCounter -- счетчик вопросов
global sCounter -- счетчик разделов (категорий)

Теперь приступим к наиболее сложной части — построению алгоритма.

Сыграем в кубики

На этот раз задача перед нами не столь проста, как раньше: предстоит работать с несколькими документами в двух различных программах, выполняя по ходу дела не одно преобразование текста. Как же составить скрипт и не запутаться? Ответ очевиден: нужно разбить большую задачу на несколько подзадач, найти и описать алгоритм решения каждой из них, а затем из получившихся «кубиков» построить необходимую программу. Благо, в AppleScript имеется специальная конструкция — обработчик (см. Часть 2).

Чаще всего программисты используют так называемый метод проектирования «сверху-вниз». При этом сперва составляется «головная» программа, описывающая последовательность выполнения процедур-«кубиков». В этот момент важно только, ЧТО делает каждый модуль. А вот КАК он это делает, решится на следующем этапе — при «детализации».

Используют и обратный метод — сборочный или «проектирования снизу-вверх». Он удобен, когда у Вас уже есть «библиотека» готовых процедур.

Воспользуемся таким методом и мы. Итак, каждая страница будет состоять из содержания и основной части. Их удобно формировать одновременно в двух отдельных текстовых файлах, а затем соединить. Значит, вначале нужно будет подготовить эти два файла. Затем перебрать все записи базы данных с одним и тем же значением поля Category, выписывая из них вопросы и ответы. Наконец, внести правку, «слить» тексты и сохранить результат на диске. Эти действия должны быть выполнены для каждой категории, содержащейся в базе данных. В результате головная программа будет выглядеть примерно вот так:

tell DBaccess
set listSection to listSec(DBname)

Получим список всех разделов FAQ. Как — будет описано в процедуре listSec.

set sCounter to 1

Это — счетчик разделов. Он нам пригодится для формирования имен html-файлов. Сперва мы устанавливаем его в единицу (можно и в ноль — кому как больше нравится), а затем после записи каждой страницы будем увеличивать.

Все остальные действия будем повторять с каждой категорией из списка:

repeat with section in listSection

Получаем (опять же, с помощью нашей собственной специальной процедуры) список всех записей заданной категории. Очевидно, что нет смысла строить «пустую» страницу, поэтому проверим, есть ли в списке хоть одна запись:

set listFAQ to listRec(DBname, section)
if listFAQ is not {} then

Подготавливаем имя html-файла и полный текст, предшествующий содержанию. Заметьте: чтобы имена были уникальными, мы, не мудрствуя лукаво, используем в качестве их составной части порядковый номер. Естественно, не забыв преобразовать в строку.

set HTMLname to "faq" & (sCounter as string) & ".html"
set headerText to headerText1 & section & headerText2 & section & headerText3

Создаем основной и вспомогательный текстовые файлы, заносим в них «стандартные» элементы:

MakeDest()
MakeScrap()

Обнуляем счетчик вопросов (номер вопроса используем, чтобы генерировать уникальные метки для гиперссылок,— абсолютно так же как поступили с именами файлов):

set qCounter to 0

Из каждой записи списка берем вопрос и ответ, записываем их (с необходимыми тегами разметки) в содержание и основной текст:

repeat with FAQ in listFAQ
set Question to item 2 of FAQ
set Answer to item 3 of FAQ
writeTOC(Question)
writeFAQ(Question, Answer)
set qCounter to qCounter + 1
end repeat

Идет запись в «основную» часть документа

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

FinalTask()
set sCounter to sCounter + 1
end if
end repeat
end tell

Ну, вот. Головная программа готова. Правда, просто? Но, чтобы скрипт заработал, придется еще потрудиться. Заметьте, в операторе Tell в качестве объекта был указан «DBaccess». Это имя не прикладной программы, как было в предыдущих примерах, а скриптовой библиотеки. Она должна включать все используемые процедуры. А оформляется такая структура с помощью специального оператора:

script DBaccess
процедуры
end script

Описанием процедур мы сейчас и займемся.

База данных, ау!

Перво-наперво, как Вы помните, предстоит узнать, вопросы по каким темам могут быть в нашей базе. Это сделать очень просто — ведь допустимые значения поля Category у нас заданы списком, остается только его прочесть. Открываем базу данных:

on listSec(DB)
tell application "FileMaker Pro"
open DB
go to database DB

Эта строка необходима, даже если открыта только одна БД.

Делаем все записи видимыми, так как «скрытые» не будут учтены при обработке.

show records
set Sect to choices of field 1 of document DB
end tell
return Sect
end listSec

Исходная база данных

Немногим сложнее получить список записей заданной категории. Поскольку every record выдает только «видимые» записи, достаточно предварительно выполнить поиск по полю Category. Дополнительно здесь можно отсортировать вопросы по алфавиту:

on listRec(DB, section)
tell application "FileMaker Pro"
go to database DB
try
show (every record of database DB whose first cell = section)
sort first layout by field "Question"
set Rec to every record of document DB
on error
set Rec to {}
end try
end tell
return Rec
end listRec

Заметьте: в случае отсутствия записей указанной категории значением процедуры станет пустой список. Это делается с помощью обработчика ошибок (см Часть 2).

Заготовки для странички

Теперь займемся построением собственно web-страницы. В основной файл нужно сразу вписать весь текст, предшествующий содержанию, а также теги, открывающие список:

on MakeDest()
tell application "BBEdit 5.1"
activate
make new window with properties {name:HTMLname, ¬
contents:headerText & CR & beginTOC & CR}
end tell
end MakeDest

Во вспомогательный файл (имя для него можно задать совершенно произвольно) записываем пока только теги, завершающие список:

on MakeScrap()
tell application "BBEdit 5.1"
make new window with properties {name:"Scrap", contents:endTOC & CR}
end tell
end MakeScrap

Формируем строку содержания и записываем ее в файл. Строка, кроме текста вопроса, содержит гиперссылку и помещается в теги <li>&</li>. Имя метки включает номер вопроса. Обратите внимание: здесь вновь используется преобразование типа — «as string».

on writeTOC(qText)
set itemText to "<li><a href="#a" & (qCounter as string) & "">" & qText ¬
& "</a></li>"
tell application "BBEdit 5.1"
tell window HTMLname
set index to 1
insert text itemText & CR
end tell
end tell
end writeTOC

Почти также поступаем и с основным текстом. Отличия здесь, во-первых, в том, что кроме строки-вопроса есть еще и ответ. А во-вторых, само построение строк несколько более сложное. Ведь мы вынесли теги оформления этой части в «свойства». {сноска: вставка текста всегда происходит в активное — «верхнее» — окно, активизация его в BBEdit делается командой «set index to 1»}

on writeFAQ(qText, aText)
set itemText1 to beginQ & "<a name="a" & (qCounter as string) & ""></a>" & ¬
qText & endQ
set itemText2 to beginA & aText & endA
tell application "BBEdit 5.1"
tell window "Scrap"
set index to 1
insert text itemText1 & CR & itemText2 & CR
end tell
end tell
end writeFAQ

Завершающие штрихи

Последний этап генерации страницы — объединение двух ее частей. Но перед этим необходимо завершить разметку основной части:

on FinalTask()
tell application "BBEdit 5.1"
tell window "Scrap"

Перед каждым переводом строки вставляем тег <br>:

replace Every Occurrence searching for CR using "<br>" & CR with start at top

В некоторых случаях этот тег может оказаться лишним — уберем его. В моем варианте предусмотрены четыре ситуации. При заданных сейчас значениях «свойств» из них возможны только первые две, остальные — «на всякий случай».

replace Every Occurrence searching for "</p><br>" using "</p>" ¬
with start at top
replace Every Occurrence searching for "</div><br>" using "</div>" ¬
with start at top
replace Every Occurrence searching for "</li><br>" using "</li>" ¬
with start at top
replace Every Occurrence searching for "l><br>" using "l>" with start ¬
at top

Теперь превратим в гиперссылки все URL web-страниц, встречающиеся в тексте:

replace Every Occurrence searching for "http://[-a-z0-9./]*" using ¬
"<a href="&">&</a>" with start at top and grep

Обработку URL удалось свести к единственной команде благодаря использованию малоизвестного среди пользователей Mac OS мощного механизма, реализованного в BBEdit — grep. Он подобен по действию аналогичной команде Unix, предназначенной для работы с «регулярными выражениями». В данном случае программа ищет текст, состоящий из «http://» и любого количества буквенно-цифровых символов, дефисов, точек и наклонных линий. Причем найденный фрагмент используется как часть нового текста.

Теперь, подготовив основной текст, сохраняем ссылку на него в переменной mainText, переходим в основной документ и окончательно «собираем» страницу:

set mainText to its text
end tell
tell window HTMLname
set index to 1
insert text mainText & CR & footerText
end tell

HTML-страница построена

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

save window 1 to HTMLname
close window 1
close window "Scrap" saving no
end tell
end FinalTask

А вот так готовая страница выглядит в браузере

Вот и все. С готовым скриптом можно поступить по-разному. Например, поместить в папку «BBEdit Scripts» и вызывать из меню программы. Или сохранить в виде приложения — тогда он сам будет запускать и FileMaker, и BBEdit, чтобы выполнить необходимую обработку. Как дополнить скрипт, чтобы после выполнения задания эти программы автоматически закрывались — пусть будет маленьким «домашним заданием».

Часть 4 →