Abnormal programming
Python
JavaScript
August 2018 6

thindf — новый текстовый формат данных (альтернатива JSON)

Файл AppData\Local\Dropbox\info.json:
{
    "personal": {
        "host": 5060852864,
        "is_team": false,
        "subscription_type": "Basic",
        "path": "C:\\Users\\DNS\\Dropbox"
    }
}
В новом формате выглядит так:
 
personal
    host = 5060852864
    is_team = 0B
    subscription_type = Basic
    path = C:\Users\DNS\Dropbox


Конфигурационный файл редактора Sublime Text:
{
    "added_words":
    [
        "plugin",
        "habrahabr",
        "закомментированным"
    ],
    "default_line_ending": "unix",
    //"font_face": "Fira Code",
    "font_size": 11,
    "ignored_packages":
    [
        "Sublimerge Pro",
        "Vintage"
    ],
    "ignored_words":
    [
        "utf"
    ],
    "line_numbers": false,
    "show_encoding": true,
    "show_line_endings": true,
    "tab_size": 4,
    "theme": "Default.sublime-theme"
}
В новом формате выглядит так:
 
added_words = [
    plugin
    habrahabr
    закомментированным
]

default_line_ending = unix
//font_face = Fira Code
font_size = 11
ignored_packages = [
    Sublimerge Pro
    Vintage
]

ignored_words = [
    utf
]

line_numbers = 0B
show_encoding = 1B
show_line_endings = 1B
tab_size = 4
theme = Default.sublime-theme


Немного истории


Данный формат обязан своим появлением другому формату со странным названием blockpar.
Blockpar разработал Алексей Дубовой (один из основателей Elemental Games) в процессе работы над игрой Космические рейнджеры. Впоследствии Александр Зеберг (бывший ведущий программист Katauri Interactive [после "распада" компании на Katauri Interactive и Elemental Games он ушёл в Katauri]) решил использовать данный формат для игры King's Bounty: Легенда о рыцаре.

Определение каждого игрового объекта хранилось в формате blockpar в отдельном файле с расширением .atom, например вот вырезка из файла data/data.kfs/spider.atom:
main {
  class=chesspiece
  model=spider.bma
  cullcat=0
}
arena_params {
  race=neutral
  cost=24
  level=1
  leadership=14
  attack=4
  defense=4
  ...
  resistances {
    physical=20
    poison=0
    magic=0
    fire=-10
  }
  ...
}
...

Впоследствии [во время работы над проектом Royal Quest] я немного расширил этот формат, чтобы можно было вместо:
button {
    name=close
    pos=400,600
    size=200,40
    image=button_close.png
    anchors=0,0,100,0
}
писать так:
button=close,400,600,200,40 {
    image=button_close.png
    anchors=0,0,100,0
}

Также я добавил поддержку многострочных строковых значений посредством обратного апострофа (backtick — `):
button=... {
    onmouseover=`
    ...
    `
}
Почему я отказался от `строк, заключённых в обратные апострофы`
В значении параметров допустимо непосредственно вставлять код скриптов, а в комментариях в коде я использую обратные апострофы в том же значении, как они применяются в Markdown и пк-разметке. Например:
if len(indentation_levels) and indentation_levels[-1][0] == -1: # сразу после символа `{` идёт новый произвольный отступ, который действует вплоть до парного символа `}`

И наконец в результате моего знакомства с Python-ом идея отказа от фигурных скобок настолько захватила меня, что я решил, что формат blockpar можно ещё больше упростить [отказавшись от обязательных фигурных скобок].

Также влияние на меня оказали:
  • Текстовый формат хранения в Yet Another Serialization Library, использующейся в клиенте Royal Quest.
  • Формат файла конфигурации nginx (впрочем, я отбросил идею отказаться от разделителя (символа = или :) между именем параметра и его значением (почему)).
  • YAML (а именно идея использовать . перед именем элемента массива [в YAML используется -]).

Почему 0В и 1В?
  • Зачастую true/false используются в значении yes/no (YES/NO используется в Objective-C), on/off или enable/disable (например: you can enable show line endings; turn logging on/off; is digit? yes), а в булевой алгебре используются 0 и 1, так что использование ключевых слов true и false в большинстве случае довольно спорно.
  • Мне не нравится истина/ложь в русской версии формата, а 0В и 1В (здесь В — русская заглавная в) можно связать с 0Выключено и 1Включено. [Вопрос о целесообразности русской версии прошу не поднимать.]
  • 0В и 1В используются в языке программирования 11l по причинам обозначенным в документации.

Строки в одиночных парных кавычках


Ещё один [помимо 0В и 1В] спорный/непривычный элемент данного формата — это использование парных кавычек ‘’ для сырых строк [без управляющих последовательностей \ escape sequences].
Но мой выбор оправдывает тот факт, что Консорциум Юникода утвердил этот год — кодом открывающей одиночной парной кавычки.

Как такие кавычки набирать на клавиатуре — смотрите здесь.

Если в строке присутствуют непарные кавычки, тогда нужно выполнить "балансировку строки" аналогично тому, как это делается в пк-разметке для вставки HTML-кода.
Например есть строка don’t.
Так как в ней присутствует несбалансированная закрывающая кавычка, добавим балансирующую открывающую кавычку в самое начало строки: don’t.
Сбалансированную строку заключаем в парные кавычки: ‘don’t.
Теперь необходимо как-то показать парсеру, что добавленную слева кавычку не следует включать в строку, так как она нужна только для восстановления баланса. Для этого используется символ машинописного апострофа ', который нужно поставить по одной штуке на каждую балансирующую кавычку [таким образом, один машинописный апостроф "съедает" одну парную кавычку], в данном случае его необходимо поставить в начало строки: '‘‘don’t’.
Сбалансированную строку можно как есть вставлять в другие строки в парных кавычках:
‘text = '‘‘don’t’’.

Использование


В данный момент есть реализация на Python и на JavaScript (можно попробовать cконвертировать JSON в новый формат прямо в браузере на веб-странице проекта).

Для Python — устанавливаем как обычно:
pip install thindf

Для JavaScript:
npm install thindf
node
const thindf = require('thindf');

И используем:
  • thindf.to_thindf(object, indent = 4) для получения строки в формате thindf соответствующей переданному объекту (аналог json.dumps и JSON.stringify).
  • thindf.parse(str) для получения объекта из строки в формате thindf (аналог json.loads и JSON.parse).

В заключение приведу ещё несколько примеров:
Несколько строчек из моего Default (Windows).sublime-keymap:
[
{ "keys": ["f4"], "command": "f4" },
{ "keys": ["shift+f4"], "command": "f4", "args": {"shift_key_pressed": true} },
{ "keys": ["alt+shift+`"], "command": "insert", "args": {"characters": "`"} }, // (
{ "keys": [":",      ")"], "command": "insert_snippet", "args": {"contents": ":)(:"} },

{ "keys": ["alt+9"], "context": [{"key": "selector", "operator": "equal", "operand": "text.pq"}], "command": "insert_pq" }, // ‘ (for balance)
{ "keys": ["alt+0"], "context": [{"key": "selector", "operator": "equal", "operand": "text.pq"}], "command": "insert", "args": {"characters": "’"} },
]
С использованием нового формата я бы записал так:
f4 = on_f4()
shift+f4 = on_f4(shift_key_pressed' 1B)
alt+shift+` = insert(characters' ‘`’) // (
:,) = insert_snippet(contents' ‘:)(:’)

alt+9 = if selector == ‘text.pq’ {insert_pq()} else 0B // ‘ (for balance)
alt+0 = if selector == ‘text.pq’ {insert(characters' "’")} else 0B

Кусочек из файла d.json [из репозитория менеджера плагинов для Sublime Text]:
{
    "schema_version": "3.0.0",
    "packages": [
        {
            "name": "Django Lookup Snippets",
            "details": "https://github.com/icycandle/sublime-django-lookup",
            "releases": [
                {
                    "sublime_text": "*",
                    "tags": true
                }
            ]
        },
        {
            "name": "Django Manage Commands",
            "details": "https://github.com/vladimirnani/DjangoCommands",
            "labels": ["Django", "python", "web", "management"],
            "releases": [
                {
                    "sublime_text": "<3000",
                    "tags": "st2-"
                },
                {
                    "sublime_text": ">=3000",
                    "tags": "st3-"
                }
            ]
        }
    ]
}
В новом формате выглядит так:
schema_version = ‘3.0.0’
packages = [
.   name = Django Lookup Snippets
    details = https://github.com/icycandle/sublime-django-lookup
    releases = [
    .   sublime_text = *
        tags = 1B
    ]

.   name = Django Manage Commands
    details = https://github.com/vladimirnani/DjangoCommands
    labels = [
        Django
        python
        web
        management
    ]
    releases = [
    .   sublime_text = <3000
        tags = st2-
    .   sublime_text = >=3000
        tags = st3-
    ]
]

Some corner cases:
{
    "a": "‘...’",
    "b": "string which ends with a space ",
    "c d": "\n",
    "e ": "3",
    "dirs": [
        ["Doc,Scans", ".t’xt"]
    ],
    "node": null,
    "n" : "N",
    "files": [],
    "f": "[]",
    "ff": [
        []
    ],
    "products": {}
}
 
a = ‘‘...’’
b = ‘string which ends with a space ’
c d = "\n"
‘e ’ = ‘3’
dirs = [
    [‘Doc,Scans’, '‘‘.t’xt’]
]
node = N
n = ‘N’
files = []
f = ‘[]’
ff = [
    []
]
products = {}


-16
5.5k 15
Comments 76