понедельник, 11 апреля 2016 г.

CustomButtons часть 6-я. Генерация текста.

  1. Генерация текста из шаблонов
  2. Преобразование страницы с помощью XSLT
То, что будет описано в этой статье не относится исключительно к CustomButtons и платформе Mozilla, тем не менее при работе с этим расширением может оказаться полезным.

Генерация текста из шаблонов



Создание HTML-кода для отображения выходных данных и создания диалоговых окон может оказаться процессом достаточно трудоемким, особенно если приходится генерировать большие блоки кода. Если это статический текст, то внедрить его в код скрипта совсем нетрудно, в предыдущей статье я уже приводил код, с помощью которого это можно легко сделать.
Код javascript Выделить
gClipboard.write(JSON.stringify(gClipboard.read()));
. Если же требуется более-менее сложная генерация кода, где результат зависит от переданных аргументов, этот способ уже не подходит. Обычно для таких целей используются текстовые шаблоны. Примеров можно вспомнить много, поскольку с помощью шаблонов создается большинство веб-сайтов: ASP, JSP, Razor и т. д. Естественно, было бы неплохо, обзавестись чем-то подобным и для наших целей, к тому же это может понадобиться не только для создания HTML, но и для любых других текстовых данных.

За основу я взял формат шаблона T4, точнее его директивы <# #> и <#= #>, другие нам не понадобятся. Идея такая: на основе синтаксиса T4 создается фрагмент кода, в котором текст смешан с кодом JavaScript и далее из этого шаблона создается генерирующая функция, которую можно будет использовать в коде кнопок для создания текста. Для преобразования шаблона в код мы создадим кнопку, которая код шаблона в буфере обмена будет заменять кодом функции-генератора. Код такой кнопки достаточно прост
Кликните здесь для просмотра всего текста
Код javascript Выделить

String.prototype.trim = function () { return this.replace(/(^\s+)|(\s$)/g, ""); }

 

function transformTemplate(template)

{

    var blocks = template.replace(/(\<#[\s\S]*?#\>|\<#=[\s\S]*?#\>)/gm, "\u001f$1\u001f").split("\u001f");

    var rv = "function(args)\n{\n\tvar returnValue = \"\";\n";

    var stmre = /\<#([\s\S]*?)#\>/, expre = /\<#=([\s\S]*?)#\>/;

    for (var i = 0; i < blocks.length; i++)

    {

        if (expre.test(blocks[i]))

        {

            var b = blocks[i].replace(expre, "$1").trim()

            rv += b.length > 0 ? "\treturnValue += " + blocks[i].replace(expre, "$1").trim() + ";\n" : "";

        }

        else if (stmre.test(blocks[i]))

        {

            var b = blocks[i].replace(stmre, "$1").trim();

            rv += b.length > 0 ? "\t" + b + ";\n" : "";

        }

 

        else

        { rv += "\treturnValue += " + JSON.stringify(blocks[i]) + ";\n" }

    }

    rv += "\treturn returnValue;\n}\n";

    return rv;

}

gClipboard.write(transformTemplate(gClipboard.read()));

 

Преобразовать текстовый шаблон в буфере обмена
Теперь возьмем простой пример шаблона, который будет генерировать код HTML-элемента select. Шаблон будет принимать массив строк, из каждой строки будет создан элемент option, а значение атрибута value будет для каждой опции иметь числовое значение, увеличивающееся на 1 и начинающееся с 1. Шаблон будет выглядеть так
Цитата:
<select>
<# for(var i = 0; i < args.length; i++){ #>
<option value="<#= i + 1 #>"><#= args[i] #></option>
<# } #>
</select>
После преобразования с помощью нашей кнопки текст приобретет следующий вид
Код javascript Выделить

function(args)

{

    var returnValue = "";

    returnValue += "<select>\n";

    for(var i = 0; i < args.length; i++){;

        returnValue += "\n<option value=\"";

        returnValue += i + 1;

        returnValue += "\">";

        returnValue += args[i];

        returnValue += "</option>\n";

    };

    returnValue += "\n</select>";

    return returnValue;

}

 

Кратко поясню. При помощи конструкции <#= expression #> в код вставляются выражения, а в результирующем тексте они будут заменены результатами вычислений, то есть вместо <#= 2 + 2 #> будет вставлено 4. При помощи конструкции <# codeblock #> вставляются любые блоки кода. Остальной текст вставляется как есть. В шаблоне можно использовать аргументы, если ничего не менять, то у функции есть параметр args, который можно использовать в коде. В результате генерируется код анонимной функции. При вставке ее в код кнопки нужно дать ей имя. Если в коде шаблона использовались несколько параметров, то надо вместо args написать список параметров, которые были реально использованы. Тип параметров тоже может быть любым, но при вызове функций надо учитывать какой тип был использован. Например в данном случае args использовался как строковый массив, стало быть вызывая функцию надо передавать ей именно его.
Кликните здесь для просмотра всего текста
Код html5 Выделить

<!DOCTYPE html>

 

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">

<head>

    <meta charset="utf-8" />

    <title>Тестирование сгенерированной функции</title>

</head>

<body>

    <div id="qqq"></div>

    <script>

       var createSelect = function(args)

        {

            var returnValue = "";

            returnValue += "<select>\n";

            for(var i = 0; i < args.length; i++){;

                returnValue += "\n<option value=\"";

                returnValue += i + 1;

                returnValue += "\">";

                returnValue += args[i];

                returnValue += "</option>\n";

            };

            returnValue += "\n</select>";

            return returnValue;

        }

 

       var qqq = document.getElementById("qqq");

       qqq.innerHTML = createSelect("Понедельник,Вторник,Среда,Четверг,Пятница,Суббота,Воскресенье".split(","));

 

    </script>

</body>

</html>

 


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

Преобразование страницы с помощью XSLT



Зачастую (чтобы не сказать "как правило") формировать HTML приходится на основе страницы, загруженной в активной вкладке браузера. В качестве примера можно взять описание загрузки аудио из вконтактика, о котором я писал здесь
Качаем видео и музыку из "вконтактика".
Там собирались данные со страницы вконтактика с аудиотреками и из них формировалась страница со ссылками, которые потом можно было загрузить с помощью расширения DownThemAll. В противовес чистому JavaScript-решению можно решить эту же задачу с помощью XSLT. Это декларативный язык и на нем формировать HTML намного удобнее, особенно, если речь идет о более-менее сложном коде. При этом JavaScript-код, который будет все это обрабатывать, отличаться будет только значением переменной, содержащей XSLT-код. Для решения задачи создания ссылок на аудиофайлы вконтактика создадим XSLT
Кликните здесь для просмотра всего текста
Код xml Выделить

<?xml version="1.0" encoding="utf-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml"

   exclude-result-prefixes="html">

  <xsl:output method="html" indent="yes"/>

   

  <xsl:template match="/">

    <html>

      <head>

        <title>Ссылки на аудиофайлы</title>

        <meta charset="utf-8"/>

      </head>

      <body>

        <xsl:for-each select="//html:div[contains(@class, 'audio') and contains(@class, 'fl_l')]">

          <div>

            <a>

              <xsl:attribute name="href">

                <xsl:value-of select="substring-before(.//html:input[starts-with(@id, 'audio_info')][1]/@value, ',')"/>

              </xsl:attribute>

              <xsl:value-of select=".//html:div[contains(@class, 'title_wrap')]"/>

            </a>

          </div>

        </xsl:for-each>

      </body>

    </html>

  </xsl:template>

   

</xsl:stylesheet>

 


Теперь преобразуем его JavaScript-переменную и на ее основе создадим код кнопки
Кликните здесь для просмотра всего текста
Код javascript Выделить

function opentTransformedPage(xsltCode)

{

    var xsltProc = new XSLTProcessor();

    var parser = new DOMParser();

    var xsltDoc = parser.parseFromString(xsltCode, "application/xml");

    var serializer = new XMLSerializer();

    var pageCode = serializer.serializeToString(content.document.documentElement);

    var pageXml = parser.parseFromString(pageCode, "application/xml");

    xsltProc.importStylesheet(xsltDoc);

    var resultDoc = xsltProc.transformToDocument(pageXml);

    var resultCode = serializer.serializeToString(resultDoc);

    var newwin = content.open("about:blank");

    newwin.onload = function (evt)

    {

        newwin.document.documentElement.innerHTML = resultCode;

    };

 

}

 

var xsltCode = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" xmlns:html=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/1999/xhtml\"\n   exclude-result-prefixes=\"html\">\n  <xsl:output method=\"html\" indent=\"yes\"/>\n\n  <xsl:template match=\"/\">\n    <html>\n      <head>\n        <title>Ссылки на аудиофайлы</title>\n        <meta charset=\"utf-8\"/>\n      </head>\n      <body>\n        <xsl:for-each select=\"//html:div[contains(@class, 'audio') and contains(@class, 'fl_l')]\">\n          <div>\n            <a>\n              <xsl:attribute name=\"href\">\n                <xsl:value-of select=\"substring-before(.//html:input[starts-with(@id, 'audio_info')][1]/@value, ',')\"/>\n              </xsl:attribute>\n              <xsl:value-of select=\".//html:div[contains(@class, 'title_wrap')]\"/>\n            </a>\n          </div>\n        </xsl:for-each>\n      </body>\n    </html>\n  </xsl:template>\n\n</xsl:stylesheet>\n";

opentTransformedPage(xsltCode);

 

Преобразовать vk-аудио в ссылки
Собственно теперь, открыв страничку с аудио вконтактика и нажав эту кнопку мы получим страницу со ссылками. Основная логика JavaScript кода заключена в функции opentTransformedPage, передавая ей разный код XSLT можно выполнять разные преобразования.

Другой пример, когда нам может понадобиться XSLT - копирование страницы или ее части. Например, если мы хотим скопировать статьи из своего блога на киберфоруме в другое место, скажем в blogger или еще куда. Можно, конечно, скопировать и вставить или скопировать нужную часть HTML-кода, но в этом случае будет иметь место ряд неприятных моментов. Во-первых, там перестанут работать спойлеры, "Выделить код", да и мало ли что еще. Все это неплохо бы убрать или переформатировать так, чтобы оно там работало. Во-вторых, исчезнет форматирование текста, поскольку правила стилей применяются к элементам в соответствии с селекторами, да и описаны они где-то за пределами того фрагмента, который копируется.

В данном случае можно решить проблему тем, что преобразованию будет подвергнута не сама страница, а клон, либо всей страницы, либо какого-то ее фрагмента. Создав клон с помощью cloneNode, его можно как следует обработать прежде чем подвергать сериализации. Например обойти рекурсивно все элементы и назначить им явно вычисленные правила стиля. Кроме того, иногда XMLSerializer сериализует некорректный документ, после этого XSLTProcessor не может его обработать. То есть бывает полезно перед выполнением преобразования сначала как-то обработать документ в JavaScript-коде. Вот пример такого решения
Кликните здесь для просмотра всего текста
Код javascript Выделить

var rules = "color,font-size,font-family,font-weight,font-style".split(",")

 

function getCssText(element)

{

    var css = ""

    var compStyle = content.getComputedStyle(element);

    for (var i = 0; i < rules.length; i++)

    {

        css += rules[i] + ": " + compStyle[rules[i]] + ";";

    }

    return css;

}

 

 

function processPageClone(node)

{

    var pclone = node.cloneNode(true);

    var elements = pclone.querySelectorAll("*");

    for (var i = 0; i < elements.length; i++)

    {

        elements[i].setAttribute("style", getCssText(elements[i]));

    }

 

    return pclone;

}

 

function openNewPage(pageCode)

{

    var newwin = content.open("about:blank");

    newwin.onload = function (evt)

    {

        newwin.document.body.innerHTML = pageCode;

    };

}

 

function getTransformedPage(xsltCode, fragmentSelector)

{

    var xsltProc = new XSLTProcessor();

    var parser = new DOMParser();

    var xsltDoc = parser.parseFromString(xsltCode, "application/xml");

    var serializer = new XMLSerializer();

    var pageClone = processPageClone(content.document.querySelector(fragmentSelector));

    var pageCode = serializer.serializeToString(pageClone);

    gClipboard.write(pageCode);

    var pageXml = parser.parseFromString(pageCode, "application/xml");

    xsltProc.importStylesheet(xsltDoc);

    var resultDoc = xsltProc.transformToDocument(pageXml);

    var resultCode = serializer.serializeToString(resultDoc);

    return resultCode;

}

 

 

var xsltCode = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n  xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:html=\"http://www.w3.org/1999/xhtml\"\n  exclude-result-prefixes=\"html\">\n  <xsl:output method=\"html\" indent=\"yes\"/>\n\n  <xsl:template match=\"@* | node()\">\n    <xsl:copy>\n      <xsl:apply-templates select=\"@* | node()\"/>\n    </xsl:copy>\n  </xsl:template>\n\n  </xsl:stylesheet>\n";

 

openNewPage(getTransformedPage(xsltCode, "#blog_message"));

 

В данном случае выполняется тождественное преобразование, но перед этим код фрагмента страницы обрабатывается функцией processPageClone. Функция getTransformedPage принмает не только XSLT-код, но и селектор фрагмента, который будет обработан. Этот код предназначен для копирования записи блога киберфорума, с сохранением форматирования текста. Функция getCssText получает элемент и создает для него CSS-код. Но делает это на основе списка правил, которые надо копировать. Список содержится в массиве rules, который я разместил в начале кода, чтобы его было удобнее редактировать, если потребуется что-то добавить или удалить. Открытие окна тоже целесообразно вынести в отдельную функцию, чтобы код преобразования можно было использовать не только для открытия преобразованной страницы, но и для сохранения или размещения в буфере обмена.

Другой вариант - обрабатывать всю страницу, но в processPageClone вносить коррективы в документ. Например в данном случае неправильно обрабатываются фреймы, но они и не нужны, так что можно их просто удалить. А нужный блок страницы будет выбираться уже в преобразовании как в следующем примере
Кликните здесь для просмотра всего текста
Код xml Выделить

<?xml version="1.0" encoding="utf-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

  xmlns="http://www.w3.org/1999/xhtml" xmlns:html="http://www.w3.org/1999/xhtml"

  exclude-result-prefixes="html">

  <xsl:output method="html" indent="yes"/>

   

  <xsl:template match="@* | node()">

    <xsl:copy>

      <xsl:apply-templates select="@* | node()"/>

    </xsl:copy>

  </xsl:template>

   

  <xsl:template match="/">

    <div>

      <xsl:apply-templates select="//*[@id='blog_message']"/>

    </div>

  </xsl:template>

   

</xsl:stylesheet>

 


Сам код кнопки
Кликните здесь для просмотра всего текста
Код javascript Выделить

var rules = "color,font-size,font-family,font-weight,font-style".split(",")

 

function getCssText(element)

{

    var css = ""

    var compStyle = content.getComputedStyle(element);

    for (var i = 0; i < rules.length; i++)

    {

        css += rules[i] + ": " + compStyle[rules[i]] + ";";

    }

    return css;

}

 

 

function processPageClone(node)

{

    var pclone = node.cloneNode(true);

    var frames = pclone.querySelectorAll("iframe");

    for (var i = 0; i < frames.length; i++)

    {

        var f = frames[i];

        f.parentElement.removeChild(f);

    }

    var elements = pclone.querySelectorAll("*");

    for (var i = 0; i < elements.length; i++)

    {

        elements[i].setAttribute("style", getCssText(elements[i]));

    }

    return pclone;

}

 

function openNewPage(pageCode)

{

    var newwin = content.open("about:blank");

    newwin.onload = function (evt)

    {

        newwin.document.body.innerHTML = pageCode;

    };

}

 

function getTransformedPage(xsltCode)

{

    var xsltProc = new XSLTProcessor();

    var parser = new DOMParser();

    var xsltDoc = parser.parseFromString(xsltCode, "application/xml");

    var serializer = new XMLSerializer();

    var pageClone = processPageClone(content.document);

    var pageCode = serializer.serializeToString(pageClone.documentElement);

    var pageXml = parser.parseFromString(pageCode, "application/xml");

    xsltProc.importStylesheet(xsltDoc);

    var resultDoc = xsltProc.transformToDocument(pageXml);

    var resultCode = serializer.serializeToString(resultDoc);

    return resultCode;

}

 

 

var xsltCode = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n  xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:html=\"http://www.w3.org/1999/xhtml\"\n  exclude-result-prefixes=\"html\">\n  <xsl:output method=\"html\" indent=\"yes\"/>\n\n  <xsl:template match=\"@* | node()\">\n    <xsl:copy>\n      <xsl:apply-templates select=\"@* | node()\"/>\n    </xsl:copy>\n  </xsl:template>\n\n  <xsl:template match=\"/\">\n    <div>\n      <xsl:apply-templates select=\"//*[@id='blog_message']\"/>\n    </div>\n  </xsl:template>\n\n</xsl:stylesheet>\n";

 

openNewPage(getTransformedPage(xsltCode));

 

Результат будет аналогичным.
Также может понадобиться исправить ссылки. Например, локальные ссылки (внутри документа) могут быть абсолютными, что при переносе не есть хорошо. То же касается ссылок на другие документы, которые также могут быть перенесены, их лучше сделать относительными и заменить новыми адресами. Приведу пример, где локальные ссылки исправляются, в XSLT удалены спойлеры и ссылки "Выделить код", которые не будут работать на другом ресурсе без соответствующих скриптов.
Кликните здесь для просмотра всего текста
Код javascript Выделить

var rules = "color,font-size,font-family,font-weight,font-style".split(",")

 

function getCssText(element)

{

    var css = ""

    var compStyle = content.getComputedStyle(element);

    for (var i = 0; i < rules.length; i++)

    {

        css += rules[i] + ": " + compStyle[rules[i]] + ";";

    }

    return css;

}

 

function correctLink(a)

{

    var href = content.document.location.href;

    var ahref = a.getAttribute("href");

    if (ahref && ahref.startsWith(href + "#"))

    {

        var splHref = ahref.split("#");

        a.setAttribute("href", (splHref.length > 1 ? "#" + splHref[1] : ""));

    }

}

 

function processPageClone(node)

{

    var pclone = node.cloneNode(true);

    var frames = pclone.querySelectorAll("iframe");

    for (var i = 0; i < frames.length; i++)

    {

        var f = frames[i];

        f.parentElement.removeChild(f);

    }

    var elements = pclone.querySelectorAll("*");

    for (var i = 0; i < elements.length; i++)

    {

        var el = elements[i];

        el.setAttribute("style", getCssText(el));

        if (el.tagName == "A")

        {

            correctLink(el);

        }

    }

    return pclone;

}

 

function openNewPage(pageCode)

{

    var newwin = content.open("about:blank");

    newwin.onload = function (evt)

    {

        newwin.document.body.innerHTML = pageCode;

    };

}

 

function getTransformedPage(xsltCode)

{

    var xsltProc = new XSLTProcessor();

    var parser = new DOMParser();

    var xsltDoc = parser.parseFromString(xsltCode, "application/xml");

    var serializer = new XMLSerializer();

    var pageClone = processPageClone(content.document);

    var pageCode = serializer.serializeToString(pageClone.documentElement);

    var pageXml = parser.parseFromString(pageCode, "application/xml");

    xsltProc.importStylesheet(xsltDoc);

    var resultDoc = xsltProc.transformToDocument(pageXml);

    var resultCode = serializer.serializeToString(resultDoc);

    return resultCode;

}

 

 

var xsltCode = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n  xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:html=\"http://www.w3.org/1999/xhtml\"\n  exclude-result-prefixes=\"html\">\n  <xsl:output method=\"html\" indent=\"yes\"/>\n\n  <xsl:template match=\"@* | node()\">\n    <xsl:copy>\n      <xsl:apply-templates select=\"@* | node()\"/>\n    </xsl:copy>\n  </xsl:template>\n\n  <xsl:template match=\"/\">\n    <div>\n      <xsl:apply-templates select=\"//*[@id='blog_message']\"/>\n    </div>\n  </xsl:template>\n\n  <xsl:template match=\"html:div[@class='spoiler-wrap']\">\n    <div style=\"width:90%;overflow:auto;\">\n        <xsl:apply-templates select=\".//html:div[@class='spoiler-body']\"/>\n    </div>\n  </xsl:template>\n  \n<xsl:template match=\"html:a[text()='Выделить код']\"/>\n\n</xsl:stylesheet>\n";

 

openNewPage(getTransformedPage(xsltCode));

 

Если код надо сохранить в буфер обмена вместо открытия в новой вкладке, то вместо последней строчки надо использовать
Код javascript Выделить
gClipboard.write(getTransformedPage(xsltCode));
Для сохранения в файл уже раньше приводил код.


Во всех приведенных примерах сам код XSLT сохранялся как статическая переменная, однако можно воспользоваться идеей изложенной в предыдущем параграфе и сохранять XSLT, созданный из динамического шаблона в виде функции. XSLTProcessor не дает нам возможности делать многое, что могут другие процессоры, например - передавать параметры в преобразование или расширять его скриптами и дополнительными функциями. Отчасти эту задачу можно решить путем программной генерации скрипта. Например, если преобразование создается из шаблона, то передать параметры можно так
Цитата:
<xsl:param name="param1"><#= args.param1 #></xsl:param>
Вряд ли на практике будут возникать такие сложные ситуации, когда в этом действительно будет необходимость, но тем не менее...

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

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