JSON, который можно комментировать

    Не все JSON нельзя комментировать (например, Хром[иум] вполне переносит комментарии в manifest.json), но в стандарте не предусмотрены комментарии к нему. Поэтому ряд функций в NodeJS не обрабатывают комментарии в формате JS и считают их ошибкой. Точно так же, AJAX с форматом JSON принимает их за ошибку. Поэтому для конфигурационных файлов в формате JSON имеется масса неудобств при попытках их использовать как человеко-читаемые файлы. Может быть, это иногда хорошо. Если хотим прокомментировать, то будем вынуждены оформить комментарий под или над строкой как «ключ-значение».
    ...{...
        "some-key_comment":"my comment for key and value",
        "some-key":"some-value",
    ...}...
    
    Но если комментарии не пишем, следуя суровости протоколов, ошибки возникают уже из-за другого фактора — забывания смысла параметров настроек при редактировании человеком.
    ...{...
        "some-key":"some-value", //какой-какой key?? Ай, комментарии - нельзя!
    ...}...
    

    Придумаем JSON-подобный формат с комментариями в стиле JS, чтобы их можно было выполнять как JS, а, очистив от комментариев — читать как JSON. ("TL:DR: покажите мне код.")

    Сыр-бор и источник


    Кстати, Дуглас Крокфорд, который это всё устроил, в 2012 году объяснил: )

    Я убрал комментарии из JSON, потому что видел людей, использующих их для хранения директив разбора — практика, которая разрушила бы совместимость (формата). Я знаю, что отсутствие комментариев некоторых печалит, но их (комментариев) не должно быть.

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

    Сделал он это на G+, где можно ставить только «плюсы», а комментарии закрыл. Так что, какова бы ни была реакция общества под объяснением, мы увидим только «плюсы» (или смотреть у тех, кто расшаривал этот пост).

    И цитата Крокфорда из другого места:
    Основная причина, отчего я удалил комментарии — были люди, которые пытались парсить данные на основе комментариев, что полностью ломало совместимость. Я никак не мог контролировать их, поэтому лучшим выходом было комментарии удалить.


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

    Всё уже сделано до нас


    В том и дело, что требуется ещё один парсер, а так — большой проблемы нет. И проблема этим не ограничивается — иногда надо файл слегка изменить, оставив комментарии. Первую часть (парсер) решили, например, через JSON.minify(). Вторую и ряд других проблем (концевые запятые или вообще без них, многострочные комменты, ключи и строки-значения без кавычек) — не поленились решить в Hjson, потратив 750 строк на код JS (с комментариями на 10-15%).

    Стоп, а нужно ли это вообще?


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

    Есть ещё хакеры, которым подойдёт совершенно хакерский, валидный способ записи JSON с последовательным повторением одинаковых ключей (в JS+»use strict" даст ошибку). Значение первого ключа в большинстве парсеров не сохранится, поэтому его (и все такие, кроме последнего) можно использовать как комментарии. Способ тоже страдает «машинностью».

    ...{...
        "some-key":"comments_comments",
        "some-key":"some-value",
    ...}...
    

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

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

    Мост между роботами и людьми


    Можно придумать плагин к Grunt/Gulp (например, grunt-strip-json-comments, Gulp...) для очистки файлов от комментриев. Но суть действия сводится к небольшому (до 1 К) регулярному выражению, которое проще написать в Gruntfile.js, чем в него же вписывать ещё один плагин. Более того, такое же выражение нужно и для JS на клиенте, чтобы читать тот же JSON, поэтому от его явного вида мы всё равно не убежим.

    Методы для преобразований форматов собраны в объект jsonComm, который работает в среде Javascript. Для решения частных задач не нужен весь объект — не всегда имеет смысл брать в проект все методы. Например с задачей простого удаления комментариев (как в gulp-strip-json-comments) справляется метод, состоящий из одного регулярного выражения (jsonComm.unComment(), до 1 КБ несжатого кода; пример далее; в тестовом разделе проекта jsonComm есть тесты и бенчмарки для оценки корректности и быстродействия), которое даже компилировать не надо, если нет цели применять разные настройки правил.

    Настройки могут быть, к примеру, такие. Каким символом отмечать начало комментария? Если в среде чистого JS есть уверенный ответ — "//", то сторонники Пайтона или YAML скажут — "#". Попытки объединить непримиримых приводят к настройкам правил и к конверторам — тем самым, с которых, в том числе, начали. В среде адептов JS нет надобности в настройках, и они выжгут из проекта упоминание о "#". Потому что нельзя тратить 36 микросекуд (микро-) на генерацию регекспа ради лояльности к такой ереси. Лоялисты — тоже выжгут, но удлинят регексп и станут тратить 0.1-0.5 (условно) микросекунд (микро-) уже не на генерацию, а на каждый цикл перекодировки. За это их ненавидят пуритане. Ведь роботы мыслят гораздо быстрее, и им медлительность видится в другом масштабе.

    Какие задачи можно решать при комментировании JSON?
    • просто читать в JS или NodeJS формат jsonComm (с комментариями), удалять из них комментарии и далее верифицировать как обычный JSON в JSON.parse(); то же самое, что делает большинство проектов по добавлению комментариев в JSON. Работает быстро (десятки-сотни мкс).
    • читать не JSON, а файлы JS (с кодом) чтобы из оставшейся части взять некоторые константы как настройки (например, в NodeJS), когда файл JS будет их тоже использовать при своём исполнении в другом месте (на клиенте) — своеобразный шаблон с упрощением структуры конфигурации;
    • как в предыдущем пункте, но уже хочется изменить некоторые настройки после прочтения (например, в Ноде обновить номер сборки или внести настройки конфига), чтобы далее JS на клиенте, ничего не подозревая, использовал их. Это — аналог шаблона на чтение-запись.

    Здесь — не все мыслимые задачи, но группа, достаточная для простого конфигурирования проекта. Вместо Yaml с комментариями или костыльных комментариев в чистом JSON — пишем jsonComm с расширением *.js, который может или читаться как JSON (на сервере, при сборке проекта или на клиенте), или выполняться как JS, имея комментарии в JS- или YAML-стиле.

    Задачи разделяются на 2 практических случая — когда нам не нужно редактировать свой jsonComm, и когда редактировать нужно, при этом оставляя все комментарии. Когда происходит только чтение (это же — случай клиентского AJAX), ограничиваемся единственным методом jsonComm.unComment() c одним регекспом, и далее — JSON.parse().

    Случай записи изменённых значений или ключей потребует небольшой процедуры парсинга текстового файла JsonComm (с комментариями, без их удаления) или JS, чтобы точечно изменить требуемое. Манипуляция возможна для файлов "*.js", если коды языка в них не будем трогать скриптами — требуется лишь не ошибаться в записи значений ключей. К необходимым методам добавляется второй: jsonComm.change().

    Какие ещё задачи можно решать при комментировании JSON?
    Задачи академического типа:
    • получить «валидный» доступ к комментариям jsonComm, переведя их сначала в пары «ключ#»-«комментарий», выбрав основу ключа из той строки, возле которой он найден, а затем, после парсинга из правильного JSON — обрабатывать их далее (например, переводя в другой формат);
    • работать с Yaml напрямую (но теряем признанную браузером/средой JS основу для валидации)
    • взаимное преобразование в Yaml и обратно через выше сделанный валидный JSON;
    • то же для XML; тогда получится кластер из четвёрки языков описания данных, 2 из которых признаны в браузерах и многочисленных вычислительных средах.

    Особенность этих задач — практической необходимости в них нет, но видя нишу, место под них зарезервировано (функции toYaml, toXml, fromYaml, fromXml, to; последняя — это «в jsonComm»). Без комментариев — такой кластер и без того уже есть в работах других библиотек. Чтобы влиться в него с комментариями, нужно начать хотя бы с функции перевода комментариев jsonComm в один из валидных и признанных форматов. Очевидно, первый кандидат — JSON.

    Первое же знакомство со способами комментирования создаёт много вопросов — к каким ключам привязвать комментарии до найденной пары, а каким — после? Например, комментарии после разделителя-запятой, но стоящие на той же строке, обычно относятся к предыдущей паре, поэтому на разделитель будет влиять и окончание строки. Второе: многострочные комментарии логически могут относиться к разным смежным парам. Третье: а к чему относятся комментарии в массивах? Их ключи выражены неявно, и логично бы создать рядом лежащий массив. А если он многомерный и с редким заполнением? Четвёртое: комментариев на строке может быть несколько; пара может быть растянута на 3 и более строк.

    Весь этот круг не менее надуманных, но реальных проблем потребует достаточно продуманного подхода, чтобы автоматическая конвертация комментариев не приводила к ухудшению читаемости форматов. Поэтому не будем спешить с их изготовлением. Займёмся лучше формальным описанием грамматики jsonComm.

    Грамматика jsonComm


    Чаще всего встречаются пары в файлах JSON, зписанные на отдельных строчках:
    <набор пробельных отступов>"<ключ>": <значение>
    

    Значение — строка в кавычках или другие термы по всем правилам JSON. Ключ — любая строка, лишь с особым экранированием кавычек внутри себя. Между элементами могут быть пробельные символы, а разделяются пары запятыми или скобками, которые могут стоять где угодно до или после пары, в том числе, на соседних строках.

    У нас будет очень похожий формат (jsonComm), с той разницей, что на месте пробельных символов могут быть комментарии 2 типов.

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

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

    С учётом сказанного, основная конструкция грамматики jsonComm выглядит так:
    (("{" | "[" | ",")<пробелы>)
        <пробелы-комменты>
      (<пробелы>"<ключ>"<пробелы>)
        <пробелы-комменты>
      (<пробелы>(":")<пробелы>)
        <пробелы-комменты>
      (<пробелы><значение><пробелы>)
        <пробелы-комменты>
    |
        <пробелы-комменты>
      (<пробелы><значение><пробелы>)
        <пробелы-комменты>
      (<пробелы>("}" | "]" | ","))
    

    После фильтрации выбрасывается всё, что не в скобках, и остаётся всё, что изображено в круглых скобках. С некоторыми особенностями, конечно, которые на этой упрощённой схеме не отображены (пустое значение означает значение-структуру). Схема может пропустить неправильный JSON, может вообще пропустить любой текст, кроме комментариев, например, программу или текст книги. И это хорошо тем, что при валидации, если её делают, JSON всё равно проверяется, а если валидации нет, а текст парсится JS-компилятором, то надобность в удалении комментариев отсутствует, схема в этом режиме не работает.

    Похожая, более сложная схема нужна будет для вставления комментариев-значений (функция jsonComm.comm2json()). В ней из jsonComm вида
    ...
        ,"some-key":"some-value" //comments_comments
    ...}...
    

    создаём
    ...
        "some-key#":"comments_comments",
        "some-key":"some-value",
    ...}...
    

    или без строчки с ключом для комментария. Если в области текста, относящейся к паре, встретилось несколько комментариев, всех их копируют в значение «some-key#». Но если комментарий встретился не в районе пары (в массиве, до или после всех скобок), он игнорируется. Все символы комментария приходится преобразовывать в валидные для JSON. Например, табы — в "\t", "\" — в "\\",….

    Как на практике содержать JsonComm?


    До сих пор мы могли записать без проблем и плагинов только JSON со всеми оговорками отсутствия комментариев или с присутствием, но в виде значений (или править JS как текстовые файлы, или хранить в БД). Сейчас будем пользоваться изменяемыми (для NodeJS) файлами jsonComm, имеющими расширение *.js.

    Выявлены 2 практические ниши применения JsonComm-файлов: на чтение конфигураций, оформленных с комментариями, на обновление конфигураций, и одна академическая — конвертор форматов.

    Если файлы нужно только читать (клиентский JS и прочее), читаем их как xhr.responseText в AJAX или как *.js и преобразуем JsonComm в объекты-структуры с валидацией через JSON.parse().

    Если файлы нужно модифицировать, то для быстроты работы используем алгоритм поиска и замены по уникальным ключам, не повторяющимся в файле (jsonComm.change()). Тогда не нужно делать дерево разбора, одновременно обходя комментарии (впрочем, это тоже не должно быть медленно, но отдельный сложный однопроходный алгоритм).

    Нет проблем добавить пайтоновский стиль однострочных комментариев (#comments_comments). Но тогда не будет работать способ чтения как файла *.js. В коде проекта заложена возможность отключить синтаксис "#" у комментариев (на начальном этапе компиляции регекспа).

    Простые случаи, когда это нужно:

    * В сборщике проекта на Grunt/Gulp/… вычисляем новый номер версии и запоминаем его в тот же файл конфигураци.

    * там же, в сборщике, создаём константы проекта на основе других параметров сборки и пишем их как параметры для JS.

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

    Реализация


    Чтобы долго не крутить циклы, преобразование выполняется «одним махом» на регулярных выражениях.

    Выкусывание комментариев


    Преобразователь фактически работает как цикл по строкам, методично выкусывая комментарии и пропуская допустимые фрагменты JSON. На его базе несложно построить и распознаватели текста комментариев, чтобы их сохранять в особые ключи-значения. Таким способом, мы допускаем комментарии для дальнейших операций, но не для того, чтобы «нарушить совместимость» (при желании — всегда можно), а чтобы код с комментарием был более удобной записью хуже читаемого выражения из 2 пар «ключ-комментарий» и «ключ-значение».

    Не
       "ключ_": "комментарий",
       "ключ": "значение",
    

    , а
      "ключ": "значение" //комментарий
    


    Решение выполняет также задачу по распределению ответственности за валидность кода. Всё, что относится к комментариям, контролируется визуально и с подсветкой синтаксиса в IDE разработчиком. Правильность остального JSON разбирает стандартный парсер JSON.parse().

    Начнём с простого. Как приблизительно работает парсинг на регекспах? Попробуем удалять концевые комментарии. (Код не используется далее, он — только для примера.)

    ('\n'+строки_файла).replace(/(^|\r?\n)(([^"\r\n:]*"(\\"|[^"\r\n])*"|[^"\r\n:]*):?([^"\r\n]*"[^"\r\n]*"|[^"\r\n]*)[^\/"\r\n]*?|[^\/"\r\n]*?)(\/\*\*\/|\/\*([\s\S]?(?!\*\/))+.{3}|\/\/[^\r\n]*)/g,'$1$2')

    Для понимания, как оно устроено, обратим внимание на функциональные части:

    (^|\r?\n) — захватывающие скобки для отображения предыдущего переноса строки.
    Следующая за ним скобка и её пара — ...[^\/"\r\n]*?) — вторые используемые для копирования захватывающие скобки.
    "(\\"|[^"\r\n])*" — ключ или строка в кавычках; если кавычек нет — далее ищется альтернатива из просто

    \s*\/\*\*\/|\s*\/\*([\s\S]?(?!\*\/))+.{3} — парсер многострочного комментария.
    \/\/[^\r\n]* — парсер однострочного комментария до конца строки.

    С выкусыванием комментариев в конце строки у этого несложного выражения — всё отлично. Хуже дело — с выкусыванием комментариев со звёздочкой между ключами и значениями. Можно пренебречь и не писать таких комментариев. Тем более, что у «конкурента» YAML имеются только концевые. Но, имея функциональные части, уже можно построить более сложное выражение, чтобы не накладывать таких ограничений. При этом придётся не просто оставлять «всё до комментария в строке», но и между ними — усложняются оставляемые фрагменты. Фактически, это — вся jsonComm.unComment(jsonCommString). Именно эту строчку можно копировать в Gruntfile.js вместо подключения модуля, чтобы очистить строку JSON от комментариев.

    jsonCommString.replace(/(?:(?:((?:\{|,)\s*)(?:(?:\s*(?:\/\/|#)[^\r\n]*(\r?\n|$))*(?:\s*\/\*\*\/|\s*\/\*(?:[\s\S]?(?!\*\/))+.{3})*)*(\s*"(?:\\"|[^\r\n"])*"\s*)(?:(?:\s*(?:\/\/|#)[^\r\n]*(\r?\n|$))*(?:\s*\/\*\*\/|\s*\/\*(?:[\s\S]?(?!\*\/))+.{3})*)*(\s*:\s*)(?:(?:\s*(?:\/\/|#)[^\r\n]*(\r?\n|$))*(?:\s*\/\*\*\/|\s*\/\*(?:[\s\S]?(?!\*\/))+.{3})*)*(\s*(?:[0-9.eE+-]+|true|false|null|"(?:\\"|[^\r\n"])*"|(?!:\{|:\[))\s*)(?:(?:\s*(?:\/\/|#)[^\r\n]*(\r?\n|$))*(?:\s*\/\*\*\/|\s*\/\*(?:[\s\S]?(?!\*\/))+.{3})*)*(\s*(?:\}|(?!,))\s*)?)+?|(?:((?:\[|,)\s*)(?:(?:\s*(?:\/\/|#)[^\r\n]*(\r?\n|$))*(?:\s*\/\*\*\/|\s*\/\*(?:[\s\S]?(?!\*\/))+.{3})*)*(\s*(?:[0-9.eE+-]+|true|false|null|"(?:\\"|[^\r\n"])*"|(?!:\{|:\[))\s*)(?:(?:\s*(?:\/\/|#)[^\r\n]*(\r?\n|$))*(?:\s*\/\*\*\/|\s*\/\*(?:[\s\S]?(?!\*\/))+.{3})*)*(\s*(?:\]|(?!,))\s*)?)+?|(?:(?:\s*(?:\/\/|#)[^\r\n]*(\r?\n|$))*(?:\s*\/\*\*\/|\s*\/\*(?:[\s\S]?(?!\*\/))+.{3})*)*\s*)/g,'$1$2$3$4$5$6$7$8$9$10$11$12$13$14')

    Тут широко используются незахватывающие скобки, чтобы оставить только захватывающие, для дальнейшей простоты второго аргумента в .replace(). (Подсказка-лайфхак: такие строки лучше всего читать в редакторе, имеющем выделение с подсветкой и выделение парных скобок, напр. от jetbrains.)

    Для преобразования строки jsonComm в JSON достаточно этого выражения. Как показывают бенчмарки, это преобразование достаточно хорошо летает — время выполнения — десятки-сотни микросекунд на страницу (сильно зависит от сложности разбора). Хуже будет дело с академическим скриптом для вывода комментариев в JSON, когда в replace() понадобится функция.

    Так, мы получили валидный JSON, решив первую часть задачи — прочитать jsonComm.

    Затем, парсинг валидности оставшегося кода, как задумано, возлагается на стандартную JSON.parse(), после чего получаем структуру данных в JS. Следующая часть задачи — кое-что автоматически подредактировать в исходном тексте, оставив комментарии на местах.

    Вставка некоторых обновлённых значений


    Созданное отделение данных от комментариев прекрасно используется для поиска по части текста, не включающей комментарии. Соответственно, правка значений тоже ведётся по шаблону пары «ключ-значение» и не попадает в области комментариев, сколь бы похожим на них текст там не был.

    Напомним ограничения, которые накладываем на правки.
    • в тексте jsonComm ищутся уникальные ключи (в кавычках). Копии ключей в комментариях игнорируются. Для одинаковых имён ключей из разных веток структуры — изменится первое и создастся некритическая ошибка в отчёте.
    • цепочки ключей не анализируются (для простоты и скорости поиска)
    • править пары, записанные не с начала строки — без проблем, потому что распознаватель ориентирован на скобки и запятые как маркеры пар. Но для удобства чтения и контроля лучше записывать изменяемые пары с начала строки.
    • удалить пару нельзя; самое большее — заменить на null или "". Как следствие, редактируемые ключи продолжают работать при любых автоматических изменениях, исчезнуть могут только при ручных.
    • изменяются только примитивы; массивы и структуры остаются на месте. Попытка изменить структуру приводит к нефатальной ошибке (пишется в лог ошибок).
    • изменение (переименование) ключей возможно, хотя противоречит человеко-ориентированному подходу и может привести к нарушению цепочки автоматических изменений, которое будет сложно отлаживать. Этим механизмом, возможно, удобно менять значения местами (перестановкой не значений, а ключей).


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

    Тогда всё очень просто и быстро работает: в исходном файле отыскиваются единственные образцы вида «ключ» (в двойных кавычках), после чего скриптом имеем доступ к значению — строке, числу или логическому.

    Изменение значений выполняется функцией jsonComm.change(h), где h — одноранговый набор пар «ключ»-«новое значение». (В крайнем случае — «ключ»- {«новый ключ»:«новое значение»}.)

    Что интересно, для .change() файл (строка) не обязан быть приводимым к JSON и к нему не обязательно пытаться применять .unComment(). Это может быть JS-файл, который сначала выполнится (например, только для того, чтобы прочитать из него текущие значения настроек вместо чтения JSON), а затем к нему применится модификация значений. Т.е. .change() — это тоже достаточно автономная функция в сборке.

    Академические задачи: смена формата файла


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

    получить комментарии в JSON (функция преобразования — jsonComm.comm2json),
    работа с YAML,
    двусторонние преобразования «jsonComm — YAML».
    то же для «jsonComm — XML».

    В силу их невостребованности, для последних поставлены функции-заглушки, и только первая (comm2json) сделана ради академического интереса, для ответа на вопрос — насколько медленнее это будет. Приходится делать replace, в котором захватить параметры комментариев через функцию, а затем символ за символом проверять комментарии и преобразовывать имеющиеся в допустимые для JSON строковые символы.

    Краткий ответ — становится медленее в 30 раз и тоже сильно зависит от сложности разбора, количества комментариев. Тестовый пример уложился примерно в 1 миллисекунду, но реальность легко сделает жизнь сложнее. Зато мы получаем первый инструмент для «полностью валидного» последующего преобразования в другие форматы данных (Yaml, XML).

    Результаты теста


    Посмотрим, как на субноуте средней руки эти 3 функции справляются с небольшим контрольным jsonComm, имеющим всевозможные (конечно, не все) сложности для парсинга. На скриншотах — исходные данные, но в проекте можно найти код этих данных и провести тесты на своём компьютере и браузере. На Firefox 34 (jsonComm.unComment):

    На Хроме в этом тесте — вдвое лучшие результаты.

    Как выполняется парсинг комментариев (jsonComm.comm2json)? Здесь замена работает через replace(,function).

    У Хрома здесь и далее — сопоставимые результаты. Это значит, что его специально оптимизировали на замены строк (.replace()) без функций. В любом случае, первый тест — очень быстро, этот — умеренно.

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

    Как изменяются значения ключей (jsonComm.change)? Здесь форма и красивость результата — уже на первом месте, потому что предназначено для чтения конфигов людьми. Правила замены показаны в объекте jCommChanges.

    Тут можно обратить внимание, что массивы и структуры не меняются по определению наших правил. Даже если написано «multiline1: {newKey:'newValue'}», изменяется только ключ, а запрос на изменение значения игнорируется.

    Чем больше изменений требуется сделать на том же участке jsonComm, тем медленнее работает скрипт (что логично). Исходя из приведённых объёмов, можно оценить, какой будет скорость на больших JSON. В общем, скорость — достаточно хорошая, если даже для правок идёт речь о единицах миллисекунд.

    Как упоминалось, первая функция с компилированным регекспом в несжатом виде занимает менее 1 КБ. Минифицированные первые 3 функции с выбрасыванием нереализованных остальных заглушек — 2.1 КБ (src/jsonComm.min.js).

    Новые вклады в проект


    Что хотелось бы увидеть в проекте от новых контрибьюторов?

    1) Кроме академических разделов, есть элементы парсинга, которые не помешало бы оттачивать в коде, чтобы выделять комментарии точнее (как показывает тестовый вывод в jsonCommTest.htm под заголовком «jsonWithComm», выходная строка .comm2json() не очень совершенна). Впрочем, в JSON.stringify уже есть способы вывести строку красивее, как показывает следующая строка лога под заголовком «jsObjWithComm».

    2) Интересно было бы сравнение скорости со скриптовым парсингом.

    3) Не отмечаются ошибки неуникального парсинга. Не обрабатываются JSON в виде одного примитива.

    4) Плагины для Grunt, Gulp,….

    Приветствуется тестирование для сложных случаев исходных файлов и сообщение об ошибках в issues, распространение ссылок для другой потенциальной аудитории (Китай и некоторые другие развитые страны, в которых Гитхаб не заблокирован).

    ● Всё, что описано — работает здесь: jsonComm — проект (Github), описание проекта (англ.).
    Json — стандарт, rfc-4627 о нём же, rfc-7159 (март 2014): обновление стандарта.
    ● Вопрос "Как мне прокомментировать JSON?" на SO.
    JSON.minify() (блог) и Github.
    grunt-, gulp, broccoli-, strip-json-comments (Github)
    JSON Comments (другой автор)
    Предложения по совершенствованию JSON (англ., 2011)
    JSON5 (перекликается с прежним)
    Hjson, the Human JSON (Hjson keep my comments when updating a config file.)

    Only registered users can participate in poll. Log in, please.

    [Для разработчиков] Было ли у вас желание использовать в разработках обычные комментарии в JsON?

    Прочитал ли я статью? Какая часть статьи мне оказалась полезной?

    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 49

      +23
      Для конфигурационных файлов сложно придумать что-либо лучше YAML'а:
      • Минимум лишних символов (никаких фигурных скобок, запятых и даже кавычек, если они явно не нужны)
      • Неограниченная вложенность дерева (у JSON тоже, а вот седой INI таким похвастать не может)
      • Комментарии как в конце строки, так и занимающие всю строку
      • Многострочные значения
      • Массивы в качестве значений
      • Возможность переиспользовать поддеревья (пример)

      Даже официальный сайт отформатирован как YAML: www.yaml.org/

      А JSON хорош для передачи данных, или для их хранения, куда человеку лезть не нужно.
        0
        Серьезно, это IMHO, но лично мне Yaml кажется настолько человекочитаемым, что его становится сложно читать. Вот уже лет пять, как знаю об этом формате, но до сих пор не понимаю его. А вот JSON, тем более, с комментариями, не вызывает никаких трудностей.

        Один мой знакомый достаточно точно выразился. Yaml — для любителей python, а JSON — для тех, кто любит C и жить не может без скобочек.
          +4
          По-моему, если называть переменные своими именами и класть их в логически правильные контейнеры, то проблем не возникает и без комментариев. На худой конец, если очень хочется, то создайте обычный JS файл и изливайтесь мыслью по древу — это будет намного понятнее читать, чем предложенный автором транслятор (меня, лично, напрягает бесконечное количество разнообразных слэшей).
            0
            Порой даже названные своими именами значения, уложенные в правильные контейнеры требуют комментариев.

            А что касается js файла, далеко не везде есть возможность исполнять js. Если проект, например, на C++, то гораздо логичнее использовать JSON парсер, поддерживающий комментарии.
              0
              Ну, если речь идёт о C++ (в коем я, увы, несведущ), то 3-х минутный поиск в гугле выдал мне вот это решение. Или есть причины, почему оно не подходит? (т.к. на мой взгляд оно выглядит намного лаконичней и читабельней). Или решение автора производительней?
                0
                Мой изначальный посыл в том, что Yaml — на любителя, и JSON с комментариями может быть гораздо удобнее.

                Собственно на js и так есть транслятор из коробки:
                JSON.parse(JSON.minify(text))

                Для других языков существуют специальные парсеры (как тот же jsoncpp).

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

                Ну а само решение проблемы — красивое, потому что короткое и всего одной регуляркой, но не поддерживаемое. Хотя, если вынести части регулярки в define секцию и дать им символьные имена, можно добиться гораздо большей читаемости и поддерживаемости, но труЪ кулл-хацкеры так не делают, да.
          +4
          проблема то в том что de-facto куча либ уже использует json для конфигов. И что в нем нет комментариев — неимоверно бесит.
          Например бывает в bower — нужно временно отключить какую-то библиотеку. Если ее просто удалить — то уже через пару дней просто забудешь что она вообще когда-то была. А вот если закомментировать — то вряд-ли
            +11
            > И что в нем нет комментариев — неимоверно бесит.

            Гораздо больше бесит что надо удалять последнюю запятую :(
              +1
              А про запятую, кстати, много пожеланий по этой теме встречалось. И в «Hjson, the Human JSON» реализовано и в «JSON5».
              +3
              Система контроля версий Вам возможно поможет
              0
              Все правильно. Для разных целей — разные инструменты.
                –1
                  0
                  Всё хорошо, пока табы в отступах не побьются…
                    0
                    Сами по себе?
                      0
                      Как вы догадались, нет. При редактировании руками в каком-нибудь редакторе, раз уж формат позиционируется и как human-writeable. Мало ли где включен retab…

                      Хотя с точки зрения парсинга он очень приятен.
                        +1
                        Ну, любой более-менее вменяемый редактор умеет автоматически определять способ табуляции или позволяет указать его для определённых типов файлов. В конце концов, даже если какой-то редактор и накосячит с табами, то парсер должен выдать соответствующее сообщение об ошибке.
                +4
                Редактор Sublime Text, например, использует формат JSON с комментариями в качестве конфигурационных файлов. Мне это видится вполне читабельным и удобным решением.

                В одном из своих проектов я тоже использовал JSON с комментариями для человекочитаемых конфигов. «Выкусывал» комментарии с помощью регулярных выражений перед тем как отдавать JSON-парсеру.

                Обоснование автором JSON отсутствия комментариев мне не понятно. Отсутствие комментариев из коробки просто раздражает.
                  0
                  Про комментарии в JSON: fadefade.com/json-comments.html Очень остроумный способ.
                    0
                    В статье упомянут (как «очень хакерский способ»). Минус лишь в том, что доступа из JSON к ним нет. Легко обойти, добавляя в конец ключей "_". И приходим к тому, как jsonComm трансформируют в JSON. :)
                      +9
                      Зачем к комментариям нужен доступ из JSON?
                        –5
                        Ради академического интереса: у нас есть информация, но нет к ней доступа. Из-за этого не можем конвертировать файлы между форматами (JSON-JS-YAML-XML). Получив доступ — сможем.
                          0
                          У нас есть информация для человека, он может её прочитать, что ещё нужно? Хочется сконвертировать, можно другой парсер написать.
                            –2
                            Это и есть подход Крокфорда и хакеров, их устраивает этот формат. Других не устраивает избыточность символов, поэтому хочется сконвертировать. Конвертируется, действительно, другим парсером (.comm2json), но суть не этом, а в том, что «хакерам» парсер вообще не нужен, они считают этот формат полностью достаточным.
                          +1
                          Генерировать документацию?
                            0
                            Не всех устраивает моё первое объяснение?

                            OK, поясню с другой стороны. Почему Крокфорд удалил комментарии из стандарта? Потому что их парсили своим языком и использовали как данные, из-за этого процедуры становились проектно-зависимыми. Стали вынуждены писать комментарии как ключи и значения JSON, а значит, появился к ним доступ из JSON. Появилась возможность работать с ними без парсинга (и, в частности, конвертировать в другие форматы). Предложение хакерского комментирования снова выводит комментарии из поля доступности средствами JSON. Поэтому требуется инструмент возвращения их в это поле.

                            Но вместо того, чтобы работать с хакерскими комментариями, гораздо удобнее за стандарт принять обычные. (Удобнее не для «суровых программистов», а для всех.) Через jsonComm.comm2json() вводится процедура перевода в доступность из JSON, которую остаётся написать под нужный язык проекта.
                              +1
                              Есть такая вещь, как схема json, дополнительным плюсом она дает возможность не только комментировать, но и валидировать и указывать тип данных. Если установить за правило иметь для каждой отдельной структуры json свою схему с заполненным description, комментарии становятся уже не так критичны. Более того для десятков json с одной и той же структурой, нужна лишь одна схема с описанием, а не дублирование комментариев в каждом json. ИМХО, но это более надежно и просто, чем лишний костыль с регулярным выражением (которое может в какой-то момент оказаться не верным).
                                0
                                Схема, как можно посмотреть аналогию на xml.com — другой инструмент, язык описания грамматики. Если нам нужно строить для JSON грамматику, а затем изъясняться на JSON — это одно. Но если нам нужно описывать всегда разные конфиги или всегда разные форматы данных для хранения в БД, а в них пояснять цели конкретных значений, то нам нужны всего лишь комментарии. Если пойдём по пути описания схем, то, во-первых, это хуже хакерского комментария: если в нём мы пишем коммент рядом со строчкой и дублируем ключ, то там пишем коммент в другом файле (и тоже дублируем ключ). В чём выигрыш?

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

                                А вообще, если увлечься JSON-грамматиками, то они быстро закончат, как XSLT :).
                                  +1
                                  А так часто нужны комментарии именно к данным? Если вам нужно комментировать данные, то на мой взгляд стоит завести отдельное поле в JSON, где и комментировать данные.

                                  Что проще понять:

                                  {«question»:«42» //The Ultimate Question of Life, the Universe, and Everything}

                                  или

                                  {«question»:{«descripton»:«The Ultimate Question of Life, the Universe, and Everything», «value»:«42»}

                                  Не вижу особого смысла в комментариях именно к данным, другое дело если нужно комментировать структуру.

                                  Плюс такие данные в Json можно обрабатывать штатными средствами.
                                    0
                                    В конфигах нужны комментарии именно к значениям данных. По Вашему примеру — именно об этом идёт речь, когда я описывал художественное сравнение «суровых программистов» и обычных. Первым достаточно {«question»:{«descripton:», «value»:} И даже {«question»: [value, comment]}. Но все, в том числе и первые, привыкли к {question: value //comment}.
                                      0
                                      Почему вы считаете, что все привыкли к {question: value //comment}? Мне например более удобно читать правильный JSON c несколькими полями, тем более что IDE хорошо умеет форматировать именно валидный JSON, а руками делать переносы и отступы мучительно долго. А вот такой формат JSON с комментариями наоборот несколько неуютен для привыкших работать с правильным JSON'ом.

                                        +1
                                        Ради интереса попробуем оба формата:

                                        {
                                           "order1":"N 21", // Накладная на принтер
                                           "order2":"M 23", // Накладная на мышку
                                           "order3":"S 231", // Чек за пицу и пиво
                                        }
                                        


                                        {
                                           "order1"  : {
                                               "number": "N 21", 
                                               "name": "Накладная на принтер"
                                           },
                                           "order2": {
                                               "number": "M 23", 
                                               "name": "Накладная на мышку"
                                           },
                                           "order3": {
                                               "number": "S 231", 
                                               "name": "Чек за пицу и пиво"
                                           }
                                        }
                                        


                                        Не знаю, для меня второй вариант хотя и более длинный, но куда более человекочитаемый и понятный.
                                          0
                                          Если речь — о данных, то да. Если о комментариях к ним, то первый вариант. Разница в том, что комментарии не планируем использовать (а доступ к ним нужен для конверсии форматов).
                                            0
                                            Можете привести пример? В случае, который я описал выше поле name для системы тоже не нужно, но его можно ввести для удобства отладки и читаемости json'a. Не вижу проблемы в добавлении поля. которого не планируем использовать в самой системе для повышения читаемости.

                                            Я вижу следующие проблемы у json'a с комментариями:
                                            1) Лишние преобразования, лишний код, лишние возможности для ошибок,
                                            2) Невозможно отправить json c комментариями другим системам и организациям,
                                            3) Скорее всего при отправке с веба на бек, все комментарии исчезнут,
                                            4) Отсутствие комментариев при отладке приложения, так как они удаляются намного раньше,
                                            5) Сложность валидации и форматирование на уровне IDE,
                                            6) Нестандартная форма json'a, требующая привыкания,
                                            7) Требование танцев с бубном при преобразовании форматов,

                                            То есть json с комментариями можно использовать только для локальных конфигов, которые никуда не передаются и используются в рамках одной системы, но при этом нет проблемы комментарии добавлять как отдельное поля (в формате выше).
                                              0
                                              Ваш пример полностью подходит. Если name для системы не нужно, то слово «name» — излишне, вынос его в значение — тоже. Но главное, что этот спор я предвидел и описал в начале статьи как неизбежный и непримиримый.
                                              1) возможны, но для того отлаживать и тестировать надо;
                                              2) отправить можно, вместе с процедурой очистки, если роботам, без неё, если людям;
                                              3) если напишут на бек-языках чистильщики, то можно пользоваться; Нода уже умеет;
                                              4) как раз для неудаления комментариев задумывались и делались 2 другие функции (comm2json, change);
                                              5) в IDE они распознаются как JS;
                                              6) комментарии — полностью стандартны для JS и Пайтона;
                                              7) это всегда;
                                              > конфигов, которые никуда не передаются
                                              --для этого и задумывалось. Но чтобы не добавлять в отдельные поля — чтобы комментарии для людей, JSON — для скриптов. comm2json — это так, задел для будущего, вклад в конверторы, показ того, что это в рамках подхода — просто и коротко.
                          0
                          Есть в посте, автору не нравится.
                            +2
                            Что у этого сайта с цветами O_O

                            image
                              +7
                              Остроумный, пока не наткнёшься на парсер, который будет брать первое из повторяющихся значений.
                              В случае поиска одного ключа в конфиге обычно нет смысла сканировать конфиг дальше, если уже нашёл значение.
                              В вашем случае это будет комментарий.
                                +1
                                А вы попробуйте в уме оба парсера «написать», сразу поймёте, что это поведение надо программировать специально. Случайно оно получиться не может. Тогда как поведение «берём последнее» получается естественным образом.
                                  0
                                  Может, если лексер будет обрабатывать значения в словаре справа на лево, что в случае с json — вполне допустимое поведение:

                                  yacc/bison:
                                  dictionary: LBRACE dictionary_list RBRACE
                                      {
                                        $$ = $2;
                                      }
                                      | LBRACE RBRACE
                                      { return new Dictionary(); }
                                  
                                  dictionary_list: dictionary_element
                                      {
                                        $$ = new Dictionary();
                                        $$->Set($1->key, $1->value);
                                        delete $1;
                                      }
                                      | dictionary_element COMMA dictionary_list
                                      {
                                        $$ = $3;
                                        $$->Set($1->key; $1->value);
                                        delete $1;
                                      }
                                      ;
                                  
                                  dictionary_element: string COLON value
                                      {
                                         $$ = new DictionaryElement($1, $3);
                                      }
                                      ;
                                  

                                    0
                                    То есть сначала сделать разбор в лексемы, а потом зачем-то обработать их с конца?
                                      +1
                                      То есть сначала сделать разбор в лексемы, а синтаксический анализатор использовать такой, при генерации которого использовалась запись грамматики, в соответствии с которой порядок элементов в словаре будет обратный.

                                      Сравните разницу в фрагменте:
                                      Обратный порядок:
                                          | dictionary_element COMMA dictionary_list
                                          {
                                            $$ = $3;
                                            $$->Set($1->key, $1->value);
                                            delete $1;
                                          }
                                          ;
                                      


                                      Прямой порядок:
                                          | dictionary_list COMMA dictionary_element
                                          {
                                            $$ = $1;
                                            $$->Set($3->key, $3->value);
                                            delete $3;
                                          }
                                          ;
                                      


                                      Если не брать во внимание возможность того, что ключи в словаре могут повторяться (а с чего бы собственно это предусматривать?), то как именно писать — без разницы и скорее дело привычки того, кто пишет грамматику для лексического анализатора.
                              +5
                              Ещё есть такой human json, как HOCON от typesafe. Там возможностей всяких вообще очень много.
                              Из основных:
                              — подстановки (причем более продвинутые, чем в yaml);
                              — автоматические слияния объектов и файлов;
                              — чтение переменных окружения, если нужно;
                              — ну и комментарии конечно.
                              Правда не знаю, как там с реализацией для языков, отличных от java.
                              github.com/typesafehub/config/
                                0
                                Идея хорошая. Я даже был написал порт его под ноду. Но при использовании оказалось, что некоторые вещи, как необязательные кавычки часто приводят к тому, что очепятка находится пожже чем хотелось бы. Перешел на надстройку над uglify.js (чтобы получать строку и позицию ошибки).
                                +4
                                Пока русские делали свой парсер для комментариев в JSON… американцы использовали второй файлик, куда и писали комментарии к данным из JSON.
                                +2
                                Мы вот в своих nodejs проектах используем yamlify — он, в частности, позволяет рекваирить напрямую yaml-файлы вместо json:

                                var mod = require('mod'); // реквайрим mod.yaml
                                

                                Причем это работает даже c browserify.

                                Так же нодовцам советую взгянуть на yapm — отличный поддерживаемый форк npm, позволяющий вместо package.json использовать package.yaml
                                  +1
                                  Мы в Badoo для конфигурационных файлов демонов используем как раз json. Похачили сишную json либу и теперь можем делать комментарии и оставлять последнюю запятую в списках.
                                    0
                                    Комментарии нужно вводить в стандарт JSON, а не заниматься велосипедостроением:) Если либа с открытыми исходниками — в ее можно добавить поддержку комментариев. Если достаточное количество библиотек поддержит комментарии — появится фактически новый стандарт, расширяющий json. Рано или поздно его стандартизируют официально. И со временем старый — без комментариев — уйдет в небытие
                                      +1

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


                                      • ("(?:\\[\s\S]|[^"])*") что означает- внутри кавычек есть: что-то экранированное, или что угодно, кроме кавычки
                                      • ((?:\/\/|#)[^\n]*) что означает — однострочный комментарий
                                      • (\/\*[\s\S]*?\*\/) что означает многострочный комментарий

                                      Результирующее выражение:


                                      ("(?:\\[\s\S]|[^"])*")|((?:\/\/|#)[^\n]*)|(\/\*[\s\S]*?\*\/)

                                      и оставить от него только $1 в строке замены.
                                      https://regex101.com/r/wD0gQ6/1
                                      Почему это будет работать вообще и работать корректно? Движок регексов будет последовательно применять эти альтернативы к каждой позиции текста, встретив кавычку применит первую альтернативу (если JSON валиден), встретив начало комментариев применит другие альтернативы и при этом захватит всю эту составную часть целиком, то есть внутри комментария или кавычек может быть что угодно- ложного срабатывания не будет.


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


                                      1. Помимо чего-то внутри кавычек в тексте могут быть числа, true, false, null (массивы и объекты не рассматриваю, потому что в топике они тоже не учитываются)
                                      2. Именованный ключ нам может встретиться после символов { и , — необходимо установить флаг, что когда встретится что-то в кавычках — это будет ключ.

                                      Результирующее регулярное выражение:


                                      ([,{])|("(?:\\[\s\S]|[^"])*"|true|false|null|[0-9.eE+-]+)|((?:\/\/|#)[^\n]*)|(\/\*[\s\S]*?\*\/)

                                      Весь код помещается в несколько строк:


                                      var regex = /([,{])|("(?:\\[\s\S]|[^"])*"|true|false|null|[0-9.eE+-]+)|((?:\/\/|#)[^\n]*)|(\/\*[\s\S]*?\*\/)/g;
                                      var nextIsKey = false;
                                      var nextIsReplaced = false;
                                      var replaceKey = "aaa";
                                      var replaceValue = '"Its works!"'; // заменяем строковым значением
                                      var replacer = function( $0, $1, $2, $3, $4 ) {
                                          if ( typeof $1 !== 'undefined' ) {
                                              nextIsKey = true;
                                          } else if ( typeof $2 !== 'undefined' ){
                                              if ( nextIsReplaced ) {
                                                  nextIsKey = false;
                                                  nextIsReplaced = false;
                                                  return replaceValue;
                                              };
                                              if ( nextIsKey && $2 == '"'+replaceKey+'"' ) nextIsReplaced = true;
                                              nextIsKey = false;
                                          }
                                          return $0; // не меняем ничего, если это не указано отдельно
                                      };
                                      console.log( jComm.replace( regex, replacer ) );

                                      https://jsfiddle.net/hb5LzuL7/


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


                                      P.S. Не сочтите меня снобом, пожалуйста. Просто начал писать комментарий про неоправданно сложное регулярное выражение там где все решается значительно проще, но заодно накидал код замены, поэтому такой большой комментарий вышел.

                                      Only users with full accounts can post comments. Log in, please.