четверг, 7 апреля 2016 г.

Об XML-фильтрах OpenOffice. Часть 2-я.


  1. Стили текста.
  2. Просматриваем родительские стили.
  3. Обходим контейнеры.

Стили текста.



Стили описывают способы отображения всего, что есть в документе. Характеристик там очень много и за подробностями, естественно, надо лезть в спецификацию. Но для текста мы будем рассматривать самые основные. Возьмем за основу разметку Cyberforum и постараемся сделать так, чтобы документ, созданный в OpenOffice можно было конвертировать в BB-коды, необязательно реализовывать все, но это будет достаточно показательным примером.

Итак смотрим документ. Возьмем то место, где текст выделен красным и синим цветами.
Код xml Выделить
<text:span text:style-name="T21">
  Текст красного цвета
</text:span>
и
Код xml Выделить
<text:span text:style-name="T22">
  Текст синего цвета
</text:span>
Как можно убедиться, никаких упоминаний цвета здесь нет, зато мы видим атрибут text:style-name со значениями T21 и T22 соответсвтвенно. Таким образом информацию о цвете и других характеристиках отображения этих элементов надо поискать среди стилей с такими именами. Ищем стили.
Код xml Выделить
<style:style style:name="T21" style:family="text">
  <style:text-properties fo:color="#ff0000" fo:font-size="12pt" fo:font-style="normal" fo:font-weight="normal" style:font-size-asian="12pt" style:font-style-asian="normal" style:font-weight-asian="normal" style:font-size-complex="12pt"/>
</style:style>
<style:style style:name="T22" style:family="text">
  <style:text-properties fo:color="#00b0f0" fo:font-size="12pt" fo:font-style="normal" fo:font-weight="normal" style:font-size-asian="12pt" style:font-style-asian="normal" style:font-weight-asian="normal" style:font-size-complex="12pt"/>
</style:style>
Они оказались рядышком и вполне себе информативны. Внутри каждого элемента style:style мы видим элемент style:text-properties, каждый из которых имеет атрибут fo:color, его значение, как нетрудно догадаться, как раз и есть тот самый цвет, которым и будет окрашен текст. Значение представлено в HTML-формате, так что понять его смысл совсем нетрудно. Здесь следует заметить, что все это было найдено интуитивно и смысл многих элементов и атрибутов понятен безо всяких спецификаций. Кроме того можно заметить и другие атрибуты, смысл которых не нуждается в пояснении. Например несложно понять, что fo-font-size - это размер шрифта, fo:font-style - стиль шрифта (в данном случае он нормальный, но если бы был наклонным, то значение атрибута было бы italic), fo:font-weight - жирность(здесь она опять-таки нормальная, но несложно догадаться, что у жирного шрифта она будет иметь значение bold, что мы дальше и увидим). Есть так же менее понятные фрагменты, хотя тоже догадаться что они означают - можно:
style:font-style-asian="normal" style:font-weight-asian="normal". Но нам и не нужно понимать абсолютно все. Мы пытаемся разобраться в основных характеристиках, а они выглядят вполне понятно. Конечно, может возникнуть вопрос: а какой из этих атрибутов важнее при определении нужной характеристики, например, fo:font-size или style:font-size-complex? Ведь какого-то из них может не оказаться там где мы будем его искать. Или он будет иметь другое значение. В таких ситуациях надо смотреть определения для других элементов, сравнивать, в худшем случае - смотреть спецификацию. Но в основном ничего непосильно сложного делать не придется.

Итак, займемся поиском цвета. Допустим у нас есть какая-то переменная или параметр под именем text-node, которая ссылается на некий текстовый узел. Наша задача, используя только что обретенные знания определить для этого узла цвет. Мы выяснили, что текстовый узел содержится в элементе, имеющем атрибут text:style-name, значение этого атрибута совпадает со значением атрибута style:name нужного нам стиля. А у этого стиля есть дочерний элемент style:text-properties и вот значение его атрибута fo:color нас как раз и интересует.
Для начала следует сказать, что LibreOffice (а скорей всего и OpenOffice тоже) не поддерживают фильтры, которые выдают форматы, отличные от XML. Таким образом аутпут-метод у нас должен быть XML и документ должен получаться well-formed. Поскольку мы собираемся генерировать текст, то придется заключить его в какой-то XML-элемент и в блок CDATA. Последнее не предусмотрено XSLT, так что его придется создать как текст.
Код xml Выделить
<xsl:template match="/">
  <root>
    <xsl:text disable-output-escaping="yes">
      &lt;![CDATA[
    </xsl:text>
    Здесь будет содержимое.
    <xsl:text disable-output-escaping="yes">
      ]]&gt;
    </xsl:text>
  </root>
</xsl:template>

В том месте, где написано "Здесь будет содержимое" мы будем размещать результаты опытов. Для начала напишем функцию, которая ищет цвет текста.
Код xml Выделить
<xsl:function name="my:find-color">
  <xsl:param name="text-node"/>
  <xsl:variable name="style-name" select="$text-node/../@text:style-name"/>
  <xsl:variable name="style" select="$text-node/ancestor::*[last()]//style:style[@style:name = $style-name]"/>
  <xsl:value-of select="$style/style:text-properties/@fo:color"/>
</xsl:function>

Поясню, что тут происходит. Сначала для переменной style-name мы вычисляем имя стиля. Для этого находим родительский элемент и получаем значение соответствующего атрибута. Здесь все просто. Дальше мы ищем сам стиль. Поскольку функции в XSLT 2 не имеют контекста, то выражение //style:style не найдет ничего и даже может привести к ошибке. Поэтому сначала мы вычисляем корневой элемент того документа, узел которого был передан как аргумент, а корневой элемент - это последний элемент на оси ancestor (по этой оси индексация производится снизу-вверх). Дальше все вроде просто: ищем в документе стиль с ранее найденным именем и у этого стиля находим дочерний элемент с соответствующим атрибутом.
Далее в том месте где будет формироваться содержимое выходного документа мы вызовем эту функцию для "красного" текста следующим образом.
Код xml Выделить
<xsl:value-of select="my:find-color(//text()[. = 'Текст красного цвета'])"/>
Результат получается вот такой
Код xml Выделить
<?xml version="1.0" encoding="utf-8"?>
<root>
  <![CDATA[
          #ff0000
          ]]>
</root>
Можно проделать то же самое с текстом синего цвета и сомневаться в результате не приходится, но... Если мы попробуем проделать следующее
Код xml Выделить
<xsl:value-of select="my:find-color(//text()[. = 'Заголовок 1'])"/>
То результат вместо цвета будет содержать пустую строку. Это было бы нормально, если бы мы запросили узел с текстом "Обычный текст", не потому, что у него нет цвета, просто цвет по умолчанию видимо нигде не описывается. Но "Заголовок 1" у нас вроде как синенький. Может быть этот узел вообще не найден? Так легко проверить, что найден. Откуда тогда программа знает, что надо его синеньким отображать? Смотрим код.
Код xml Выделить
<text:h text:style-name="P7" text:outline-level="1">
  Заголовок 1
</text:h> 
Находим стиль по имени "P7"
Код xml Выделить
<style:style style:name="P7" style:family="paragraph" style:parent-style-name="Heading_20_1" style:master-page-name="MP0">
  <style:paragraph-properties style:page-number="auto" fo:break-before="page"/>
</style:style>
Действительно никакого упоминания цвета там нет. Мало того, там элемент описывающий свойства текста вообще отсутствует, а вместо него свойства абзаца. Так откуда же берется информация о цвете заголовка под номером один? Если внимательнее присмотреться к представленному коду, то можно заметить атрибут style:parent-style-name со значением "Heading_20_1". Несложно догадаться, что это ссылка на родительский стиль, от него унаследован данный, поэтому вероятно недостающую информацию мы могли бы почерпнуть именно из него. Смотрим
Код xml Выделить
<style:style style:name="Heading_20_1" style:display-name="Heading 1" style:family="paragraph" style:parent-style-name="Обычный" style:next-style-name="Обычный" style:default-outline-level="1" style:class="text">
  <style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0cm" loext:contextual-spacing="false" fo:keep-together="always" fo:hyphenation-ladder-count="no-limit" fo:keep-with-next="always"/>
  <style:text-properties fo:color="#2e74b5" style:font-name="Calibri Light" fo:font-family="&apos;Calibri Light&apos;" style:font-family-generic="swiss" style:font-pitch="variable" fo:font-size="16pt" style:font-name-asian="Times New Roman" style:font-family-asian="&apos;Times New Roman&apos;" style:font-family-generic-asian="roman" style:font-pitch-asian="variable" style:font-size-asian="16pt" style:font-name-complex="Times New Roman" style:font-family-complex="&apos;Times New Roman&apos;" style:font-family-generic-complex="roman" style:font-pitch-complex="variable" style:font-size-complex="16pt" fo:hyphenate="false" fo:hyphenation-remain-char-count="2" fo:hyphenation-push-char-count="2"/>
</style:style>
Действительно все нашлось и свойства текста наряду со свойствами абзаца и искомый атрибут fo:color. Кроме того здесь видно, что данный стиль так же имеет атрибут style:parent-style-name. Таким образом при поисках того или иного свойства, нам возможно придется подниматься по линии наследования неопределенное количество уровней, пока не найдем то, что ищем. А стало быть написанная ранее функция никуда не годится или точнее - годится только для самых простых случаев.

Просматриваем родительские стили.



Теперь нам нужно написать новую функцию, которая будет двигаться по стилям, переходя от потомков к предкам, если это необходимо, до тех пор, пока не будет найден ответ. Но тут есть один нюанс: ведь такое может произойти с любым свойством, а не только с цветом. Мы видели, что непосредственный стиль заголовка вообще не содержал текстовых свойств, а какие-то правила к нему применялись и, как выяснилось правил этих оказалось довольно много. Поэтому писать отдельную функцию, которая запрашивает только цвет, при том, что информацию о любом другом свойстве стиля придется искать точно так же - как-то неразумно. Лучше напишем функцию, которая будет принимать стиль и имя атрибута, значение которого мы ищем в стилях. В этом случае мы не будем привязаны конкретно к цвету, а вместо этого получим возможность запрашивать любые свойства стиля.
Код xml Выделить
<xsl:function name="my:seek-parent-styles">
  <xsl:param name="style"/>
  <xsl:param name="property-name"/>
  <xsl:variable name="property-value" select="$style/style:*[contains(name(), '-properties')]/@*[name() = $property-name]"/>
  <xsl:choose>
    <xsl:when test="$property-value != ''">
      <xsl:value-of select="$property-value"/>
    </xsl:when>
    <xsl:when test="$style[@style:parent-style-name]">
      <xsl:variable name="parent-style" select="$style/ancestor::*[last()]//style:style[@style:name = $style/@style:parent-style-name]"/>
      <xsl:value-of select="my:seek-parent-styles($parent-style, $property-name)"/>
    </xsl:when>
    <xsl:otherwise/>
  </xsl:choose>
</xsl:function>
Итак, что мы тут делаем.
Сначала у стиля, полученного с аргументом style мы пытаемся запросить значение атрибута непосредственно. Поскольку в примере со стилем заголовка мы видели, что стиль может иметь несколько дочерних элементов со свойствами, мы не ищем конкретно элемент style:text-properties, а вместо этого берем все элементы с префиксом style и содержащие "-properties" в имени, или можно было искать заканчивающиеся этим текстом (fn:ends-with). Если такой атрибут найден - возвращаем его значение, если нет - ищем у стиля атрибут style:parent-style-name. Найдено имя родительского стиля - находим сам стиль и вызываем эту же функцию рекурсивно, передавая ей при этом родительский стиль и имя атрибута, полученное из параметра. Не выполняется ни одно из условий - ничего не возвращаем.

Далее тестируем функцию
Код xml Выделить
<xsl:variable name="style-name" select="//text()[. = 'Заголовок 1']/../@text:style-name"/>
<xsl:value-of select="my:seek-parent-styles(//style:style[@style:name = $style-name], 'fo:color')"/>
На выходе получаем #2e74b5. Можно попробовать применить функцию для поиска другого свойства, например размера шрифта заголовка
Код xml Выделить
<xsl:variable name="style-name" select="//text()[. = 'Заголовок 1']/../@text:style-name"/>
<xsl:value-of select="my:seek-parent-styles(//style:style[@style:name = $style-name], 'fo:font-size')"/>
Получаем 16pt. То есть пока вроде все работает, но расслабляться рано.

Обходим контейнеры.

Теперь попробуем применить эту же функцию к тексту, написанному курсивом(в моем документе-примере он записан как "Курисив", ошибку я заметил еще до того, как выложил, но исправлять не стал).
Код xml Выделить
<xsl:variable name="style-name" select="//text()[. = 'Курисив']/../@text:style-name"/>
<xsl:value-of select="my:seek-parent-styles(//style:style[@style:name = $style-name], 'fo:font-style')"/>
К сожалению этот код выводит пустую строку. То есть выявить то самое место, из которого можно определить, что данный текст следует выводить курсивом нам не удалось. Пока не удалось.

Если просмотреть визуально все стили данного узла по иерархии вверх, то действительно там нигде нет информации о том, что текст должен отображаться курсивом. Для того, чтобы понять, откуда берется эта информация мы просмотрим код данного узла в немного более широком контексте чем раньше.
Код xml Выделить
<text:p text:style-name="Обычный">
  <text:span text:style-name="Название_20_книги">
    <text:span text:style-name="T4">
      Курисив
    </text:span>
  </text:span>
</text:p>
Несложно заметить, что текст "упакован" не в один, а аж в три элемента, имеющих свойство text:style-name. Поскольку внешние элементы вообще не имеют непосредственных текстовых узлов, то разумно предположить, что правила форматирования текста, заданные в стилях, примененных к этим элементов, так же распространяются и на текст, расположенный во вложенных элементах. В частности, если мы просмотрим стиль "Название_20_книги", то обнаружим, что искомое правило fo:font-style="italic" определено именно в нем.
Код xml Выделить
<style:style style:name="Название_20_книги" style:display-name="Название книги" style:family="text" style:parent-style-name="Основной_20_шрифт_20_абзаца">
  <style:text-properties fo:letter-spacing="0.009cm" fo:font-style="italic" fo:font-weight="bold" style:font-style-asian="italic" style:font-weight-asian="bold" style:font-style-complex="italic" style:font-weight-complex="bold"/>
</style:style>
Есть еще один момент, с которым надо определиться до того, как начнем писать функцию. Если мы просмотрим код текста, написанного жирным, так же как мы делали это с курсивом, то обнаружим, что среди его контейнеров также имеется элемент со стилем "Название_20_книги"
Код xml Выделить
<text:p text:style-name="Обычный">
  <text:span text:style-name="Название_20_книги">
    <text:span text:style-name="T1">
      Жирный
    </text:span>
  </text:span>
</text:p>
Так что по идее он тоже должен бы быть написан курсивом. Но это не так. Понять, почему "жирный" текст не наследуют стиль шрифта от этого элемента можно, если просмотреть описание стиля "T1", определенного в непосредственном контейнере текстового узла.
Код xml Выделить
<style:style style:name="T1" style:family="text">
  <style:text-properties fo:font-style="normal" style:font-style-asian="normal"/>
</style:style>
То есть иными словами: стиль наследуемый от контейнера может быть переопределен в более близком контейнере. Стало быть при поиске по контейнеру нам надо искать значение свойства, описанное в ближайшем контейнере, в котором его описание присутствует. В принципе точно так же мы действовали и при поиске стилей-предков. Кроме того, не будем забывать о том, что найдя стиль, его свойства следует запрашивать с помощью той самой функции, которая ищет по всей иерархии предков.
Код xml Выделить
<xsl:function name="my:style-property-value">
  <xsl:param name="node"/>
  <xsl:param name="property-name"/>
  <xsl:variable name="root" select="$node/ancestor::*[last()]"/>
  <xsl:variable name="ancestor-style-name" select="$node/ancestor::*[@text:style-name][1]/@text:style-name"/>
  <xsl:variable name="ancestor-style" select="$root//style:style[@style:name = $ancestor-style-name]"/>
  <xsl:variable name="property-value" select="my:seek-parent-styles($ancestor-style, $property-name)"/>
  <xsl:if test="$ancestor-style-name != ''">
    <xsl:value-of select="
              if ($property-value != '') then
                  $property-value
              else
                  (my:style-property-value($node/ancestor::*[@text:style-name][1], $property-name))
              "
            />
  </xsl:if>
</xsl:function>
Поясню, что происходит.

С параметром "property-name", как я полагаю, проблем не должно возникнуть. Это имя атрибута, значение которого мы ищем.

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

Переменная "root" возвращает корневой элемент документа. Я уже говорил, что функции не имеют контекста, так что его надо найти по узлу документа.

Переменная "ancestor-style-name". В ней мы вычисляем имя стиля ближайшего стилизированного контейнера. Я не стал тут делать как это делалось раньше, когда для поиска имени стиля использовался просто непосредственный контейнер узла по двум причинам: во-первых, нет гарантии, что любой текстовый узел будет расположен в контейнере, имеющем ссылку на стиль; во-вторых, функция вызывается рекурсивно и тут тоже нет гарантии, что непосредственный родитель элемента также будет содержать ссылкку на стиль. В примерах, которые мы рассматривали это не актуально, но гарантировать ничего нельзя, так что лучше написать более надежный код.

Переменная "ancestor-style" находит стиль по имени.

Переменная "property-value" ищет значение свойства в стиле "ancestor-style" с помощью ранее написанной функции "my:seek-parent-styles", то есть с просмотром родительских стилей.

Далее, если существует и найдено значение интересующего нас атрибута, то это значение и возвращается. Если стиль существует и значение свойства не найдено - вызываем функцию рекурсивно и в качестве узла, с которого будет начат отсчет передаем уже родительский контейнер, то есть перемещаемся по иерархии стилизованных контейнеров на один уровень выше(например в рассматриваемом случае от элемента со стилем "T1" переходим к элементу со стилем "Название_20_книги"). Если контейнер со стилем не найден - ничего не возвращаем.

Тестиуем функцию
Код xml Выделить
<xsl:value-of select="my:style-property-value(//text()[. = 'Курисив'], 'fo:font-style')"/>
Получаем "italic", что, собственно нам и нужно. Можно поэкспериментировать с другими узламим и атрибутами. Если функция ничего не возвратила, значит свойство не задано. По крайней мере не задано рассмотренными здесь способами, а поручиться за то, что других не существует - невозможно. Например что означает style:master-page-name="MP0" у стиля заголовка? Возможно в мастер-страницах что-то тоже задается. Или опять-таки у стиля заголовка было еще такое определение style:next-style-name="Обычный". Что это? Возможно оно влияет на отображение следующего абзаца. Пока это неясно. Но те параметры, со значением которых мы разобрались, эти две функции находят(хотя потестировать их еще было бы совсем не лишним).

Комментариев нет :

Отправить комментарий