Представление многострочных данных в javascript и юзерскриптах

в 11:47, , рубрики: javascript, userscript, метки: ,

Речь пойдёт о проблеме кроссбраузерного представления многострочных данных в javascript. Это могло не быть проблемой, если бы Firefox умел работать с функциями так же, как другие браузеры. Единый кроссбраузерный способ представления так и не найден, несмотря на 2-дневные усилия. Если бы он был, его, наверное, уже стали применять на сайте userscripts.org. Пока что имеется раздельный способ представления: один — для юзерскриптов Firefox, другой — для всех остальных. Также, для Scriptish существует возможность чтения формата метаданных (директив), что не охватывает произвольного формата, но решает задачу, чаще всего встречающуюся в юзерскриптах. Не рассматриваем решение с 2 запросами на сервер, с обращением к внешним HTML, XML и с дублированием данных, потому что хотим данные и скрипт получить в одном запросе, в одном файле, и файл этот — типа «text/javascript».

Многострочное представление данных во всех браузерах, кроме Firefox

Решение для любых скриптов, юзер- и обычных, но не для Firefox.

s = function(){/*

	произвольные многострочные данные, кроме завершения комментария

*/}.toString()

Далее, очистив текст от функциональных скобок регулярным выражением, получим чистые многострочные данные. Пренебрегаем тем, что в данных не должны встречаться символы конца комментария '*/'. С учётом этого, входной текст требует минимальной проверки и замены на безопасные строки, которые затем требуется перекодировать обратно после получения строки s.

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

Отсутствие подходящей функции создало потребность придумать создателям GreaseMonkey заменитель в юзерскриптах, потому что в них присутствуют строки-комментарии (директивы-метаданные), к которым иногда полезно иметь доступ из юзерскрипта и не дублировать их. Синтаксис метаданных ("@ключ значение") не позволяет использовать оборачивание кавычками или бекслеш в конце строки. В метаданных содержится много полезной информации — версия скрипта, описание и данные дополнительных директив. Но и вообще, для произвольных многострочных данных их представление было бы очень полезно. Ведь значительно удобнее собирать скрипты включением нескольких строк нескриптовой информации, чем преобразовывать каждую строчку к специальному виду.

Если бы специально для Firefox имелась некоторая функция .toStringWithComments(), мы бы имели выход из положения, программно совместив оба метода. Но и этого (насколько можно судить) нет. Для решения вопроса в GreaseMonkey создали специальный тип и синтаксис данных: XML-формат многострочных данных. Вместо выражения в коде скрипта пишут литерал, начинающийся с "<". Для простого Javascript, как полагается, этот синтаксис вызывает такую же ошибку, как и для остальных браузеров. Именно несовместимость синтаксисов служит причиной таких проблем, которые обсуждаются в этой статье — не отыскивается способов совмещения того и другого синтаксисов (через eval() есть, но только для однострочных данных, для которых и без того есть кавычки).

Возможный путь решения на уровне аддона

Кстати, на уровне аддона GreaseMonkey создать функцию .toStringWithComments() — такое же простое дело, как создание других специальных функций. Может быть, авторам GM/Scriptish подобное уже предлагали. Самое большее, что можно достичь с имеющимся механизмом выражения типа XML — свести различия до одного-двух символов, но от этого столь же мало пользы, как от исходной несовместимости. Нужен внешний генератор кода (скрипт PHP, nodeJS или руки и голова пользователя), который выбирал бы синтаксис в зависимости от браузера.

s = <![CDATA[

	произвольные многострочные данные, кроме завершения CDATA

]]>.toString();

— для Firefox + GreaseMonkey (Fx+GM) или Scriptish — или вышеупомянутый код для остальных браузеров. Принципиальность здесь в том, что GreaseMonkey допускает многострочный произвольный текст (включая комментарии) в коде скрипта, а все остальные браузеры — нет, за исключением упомянутого трюка с функцией. Совместить то и другое не получается.

Для Fx+GM допустим и такой синтаксис:

s = <>
	произвольные многострочные данные,
	кроме завершения тега
</>.toString();

или

s = <anyName>
	произвольные многострочные данные,
	кроме завершения тега
</anyName>.toString();

Текст внутри может содержать CDATA. Этот формат XML (если посмотреть (typeof s), получим 'xml') тоже не помогает решению.

Заметим, что для чистого Firefox решения по-прежнему нет. И это довольно странно — годы разработки, мощнейшая поддержка разработчиков у себя на сайте, а функции типа .toStringWithComments() — нет. Возможно, решение спрятано в недрах MDN (Mozilla Development Network) и кто-то об этом знает?

Решения с порчей (преобразованием) символов каждой строки

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

Решение с различием на 2 символа (только для юзерскриптов)

Оно пригодится для обычного скрипта (в котором Fx-версия не будет выдавать результат, поэтому для неё придётся формировать данные по-другому) или ручной/серверной правки 2 версий юзерскриптов, чтобы внешнему скрипту требовалось минимальное вмешательство для совместимости. Основано оно на «хитроумной» подстройке символов так, чтобы они стали похожи на правильный синтаксис для всех браузеров.

Чем проще подстройка, тем лучше, потому что затем лишние символы придётся выкусывать.

//получение многострочных данных в s для Fx+GM;
var isFxGM = typeof GM_getResourceText !='undefined';
//если добавить "=" в конец строки, многострочные данные станут читаться в Fx+GM
  var f = function(CDATA, s){s
//var f = function(CDATA, s){s=  //пример кода только для Fx+GM или Scriptish
<![CDATA[0]]/*
	произвольные многострочные данные,
	кроме завершения комментария и CDATA
*///]]>
return s};
var currMeta = isFxGM ? f([]).toString() : f.toString();
var win = (typeof unsafeWindow !='undefined')? unsafeWindow: window;
win.console.log(currMeta); //будет показывать сырые данные, с обвязкой
win.console.log('--------------');
win.console.log(currMeta.replace(/(^[sS]*?/*r?n?|*/[sS]*$)/gm,'')); //чистые данные с обрезкой мусора по краям

Здесь фокус в том, что выражение до данных — это простое незначащее выражение s < ![CDATA[0]], которому необязательно задавать параметры, лишь бы CDATA была переменной массива. Поэтому в остальных браузерах конструкция игнорируется, а для Fx+GM выражение s= <![CDATA[0]]...]]>; — это многострочный XML-документ.

Решение это — взаимоисключающее. Один код работает только в Fx+GM, а в других даёт ошибку, другой — в остальных работает, в Fx+GM даёт ошибку. Это плохо для публикаций скриптов на userscripts.org, для которых желателен скрипт, работающий везде, для тех, кто не читает примечания. Для нечитающих примечания подойдёт вариант второго скрипта, с необходимостью удаления 2 символов комментария, чтобы скрипт полноценно заработал в Fx+GM. Вот этот код:

var win = (typeof unsafeWindow !='undefined')? unsafeWindow: window;
var isFxGM = typeof GM_getResourceText !='undefined';
//если удалить "/*" перед "<!", многострочные данные станут читаться в Fx+GM
var f = function(s){return(s=
/*<![CDATA[*//*
	произвольные многострочные данные,
	кроме завершения комментария и CDATA
*/s//]]>
)};
var currMeta = isFxGM ? f('удалите "/*" перед "<!" в коде скрипта').toString() : f.toString();
win.console.log(currMeta); //будет показывать сырые данные, с обвязкой
win.console.log('--------------');
win.console.log(currMeta.replace(/(^[sS]*?//*r?n?|*/s[sS]*$)/gm,'')); //чистые данные с обрезкой мусора по краям

Здесь специальная строка напоминает доступным пользователю способом о том, что надо сделать со скриптом, чтобы он стал правильно работать в Firefox. В зависимости от приложения, строка 'удалите "/*" перед "<!" в коде скрипта' может иметь другое содержание, а задача скрипта — определить, что он неполноценно работает и сообщить пользователю о том, как это исправить (когда он доберётся до нужной функциональности).

Противоположная задача — сделать работающий скрипт в Fx+GM и давать сообщение для остальных браузеров — к сожалению, не решена, потому что формат XML на месте выражения JS создаёт неразрешимую задачу для корректной работы обычного Javascript. Её можно решить с использованием eval(), но только для однострочного произвольного текста (что вообще не проблема и для других способов решения — например, ставить данные в строку JS). Можно попытаться создать аддон-плагин для GreaseMonkey и Scriptish с добавлением новой функциональности (GM_toStringWithComments()), об этом — в конце статьи. Но тут надо знать GM изнутри, поэтому проблему стоит попытаться переадресовать разработчикам (GreaseMonkey или Scriptish). Это решило бы и задачу полноценной совместимости скриптов.

Пример использования разработки

В юзерскриптах используются команды-директивы в формате комментариев перед скриптом. Они остаются работоспособны (во всех браузерах), если перед ними напишут некоторые необходимые скрипты. Обычно это используется для того, чтобы в Fx+GM обернуть комментарии тегами XML, используя специальный, внедрённый в GreaseMonkey синтаксис. Вся проблема в том, что тогда скрипт перестаёт быть совместимым с остальными браузерами. Только оболочка над оболочкой — аддон TamperMonkey для Chrome мог справляться с этим. И сейчас справляется, но усложняет работу со скриптами, перенося их в отдельный собственный список с точки зрения пользователя. Кроме того, это — не кроссбраузерное решение.

Например, в новой версии скрипта HabrAjax в заголовок файла вставлена картинка (логотип), которая принимается оболочкой Firefox и показывает логотип перед расширением на странице about:addons, а в остальных браузерах игнорируется. Чтобы не писать более 500 байт кода дважды, читаем заголовок с директивами описанным выше способом и берём данные оттуда. Это нужно не только для частных задач, но и для более качественного сервиса автообновления скрипта. Из заголовка читаем версию скрипта, описание, историю изменений (если написана в специальной самодельной директиве "@update"), сравниваем с изменениями на сервере через *.meta.js и в удобном виде, в удобное время предлагаем обновиться. Можем ограничить минорные обновления версии в правах показа, т.е. создать удобства, до которых далеко встроенным в браузер автообновлениям скриптов, которые поддерживаются Scriptish, начиная с 4-й версии Firefox и GreaseMonkey, начиная c 0.9.13 (3 ноября 2011, 8-я версия Firefox).

Пример пути решения для юзерскрипта

Допустим, в GM/Scriptish сделали бы функцию GM_toStringWithComments. Тогда кроссбраузерное получение многострочных данных в юзерскрипте было бы без лишних проблем:

var f = function(){/*
	произвольные многострочные данные, кроме завершения комментария
*/};
if(typeof GM_toStringWithComments =='undefined')
	GM_toStringWithComments = function(f){return f.toString();};
var s = GM_toStringWithComments(f).replace(/(^function(){/*|*/$)/gm,'');

Сейчас этот код тоже работает, но для Fx+GM (и для просто Fx) выдаётся пустая строка.

И тут, специально для юзерскриптов, у Scriptish — хорошая новость...

Недавно у этого аддона появилась функция, позволяющая читать метаданные — то самое, для чего планировалась toStringWithComments(), но в более ограниченном применении. Поскольку у GreaseMonkey её нет, для пользователей, не читающих инструкции, придётся определить эту функцию с деградацией — с тем самым признаком для Fx+GM, который позволит сообщить пользователю, что делать дальше.

Обратим внимание, что директива не заменит более низкоуровневую желаемую функцию toStringWithComments(), потому что она собирает правильно оформленные описания в виде пар «ключ-значение».

Встречаем функцию API Scriptish GM_getMetadata()!

Она выдаёт метаданные — директивы, оформленные как комментарии, чтобы прочитать данные, которые передаёт юзерскрипт в Scriptish о себе. Кроме необходимых для работы данных (области видимости), пользователю через скрипт становится доступна информация о названии, версии, лицензии скрипта и обо всех остальных директивах, в которые он мог оформить данные, полезные для скрипта, например, то же описание изменений в скрипте по сравнению с предыдущей версией и номер скрипта на сервере.

Составляем долгожданный кроссбраузерный юзерскрипт для Firefox+Scriptish (с GreaseMonkey он будет сообщать пользователю о необходимости ручной подстройки). Реальный работающий пример можно увидеть в скрипте HabrAjax версии 0.81+. В начале файла скрипта — обёртка метаданных. Для Fx+GM появляется напоминание о неудобстве только при задействовании функций, в которых требуется извлечение метаданных. Пользователя просят изменить 2 байта в скрипте (это придётся ему делать после каждого обновления!) или просто сменить аддон на Scriptish.

Результат

Как видим, общего кроссбраузерного решения не найдено. Возможно, оно отсутствует, но по сумме неудобств, оно должно было где-то решаться или хотя бы быть отмечена проблема. Для обычных скриптов вообще нет решения в Firefox. Для юзерскиптов имеется некроссбраузерное решение в виде XML-синтаксиса выражения в Fx+GM и специально обёрнутых комментариев для остальных браузеров. Имеется кроссбраузерное решение для Firefox+Scriptish и остальных браузеров только для получения метаданных.

Если кто-то знает по опыту специальную функцию типа toStringWithComments() в Greasemonkey, Scriptish или Javascript — просьба сообщить. Если кто-то может сформулировать проблему для разработчиков GM или Scriptish или знает, где она уже описана — просьба тоже сообщить об этом им и нам.

Сейчас имеем необходимость поддерживать отдельную версию для Firefox + GM или напоминание об этом для пользователя (но уже не для Scriptish!), если есть желание читать директивы (не более) юзерскрипта. Как решение, выходящее за пределы поставленной задачи — нет проблем хранить произвольные данные в другом файле и читать по AJAX.

Автор: spmbt


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js