links-notation
v0.31.3
Published
Links Notation parser for JavaScript
Readme
Парсер протокола Lino для JavaScript
Реализация парсера протокола Lino для JavaScript с использованием Bun и генератора парсеров Peggy.js.
Документы с отступами
parseIndentedDocument() и formatIndentedDocument() преобразуют текстовые
родительские строки и их дочерние строки в Map<string, string | string[]>:
import { parseIndentedDocument, formatIndentedDocument } from 'links-notation';
const answers = parseIndentedDocument(`Ваш опыт?
JavaScript
Навыки
JavaScript
Python
`);
console.log(answers.get('Ваш опыт?')); // 'JavaScript'
console.log(answers.get('Навыки')); // ['JavaScript', 'Python']
console.log(formatIndentedDocument(answers));Без дочерних строк значение равно [], с одной строкой — строке, с несколькими
— массиву. Повторяющиеся родительские строки объединяют дочерние значения в
порядке документа. Опция { multipleValues: 'join' } объединяет несколько
значений через перевод строки. Поддерживаются опции Parser: comments,
maxInputSize и maxDepth. Вложенность глубже одного дочернего уровня и
именованные ссылки со значениями отклоняются.
Форматирование использует два пробела для отступа и завершающий перевод строки. Обычный текст с одиночными пробелами остаётся без кавычек; специальные символы, значимые пробелы и зарезервированные префиксы литералов заключаются в кавычки. Кавычки экранируются удвоением. Пустые строки и управляющие символы используют версионированные литералы, введённые в 0.24.0. Массив из одного элемента при чтении становится строкой. Многострочная строка сохраняется одной ссылкой с точными переводами строк, а элементы массива записываются отдельными дочерними строками.
Parser.parseGroups() возвращает дерево существующих объектов LinksGroup:
element содержит ссылку текущей строки, а children — группы непосредственно
вложенных строк. Поведение Parser.parse() сохраняется.
Функции escapeReference() и unescapeReference() экспортируются из пакета и
доступны как статические методы Link. Опция { minimal: true } разрешает
обычные пробелы при экранировании целой текстовой строки. По умолчанию пробелы
по-прежнему заключаются в кавычки. unescapeReference() принимает целую
сериализованную ссылку, включая версионированные литералы; значения Link.id
уже декодированы.
См. пример базы вопросов и ответов.
Установка
Использование Bun (рекомендуется)
cd js
bun installИспользование npm
cd js
npm installСборка
Компиляция грамматики Peggy.js:
bun run build:grammarСборка проекта:
bun run buildТестирование
Запуск тестов:
bun testРежим наблюдения:
bun test --watchИспользование
Базовый парсинг
import { Parser, Link } from 'links-notation';
// Создание парсера
const parser = new Parser();
// Парсинг строки в формате Lino
const input = `папа (любитМаму: любит маму)
сын любитМаму
дочь любитМаму
все (любят маму)`;
const result = parser.parse(input);
console.log(result);
// Обращение к распарсенной структуре
result.forEach((link) => {
console.log(link.toString());
});Работа со связями
import { Link } from 'links-notation';
// Создание связей программно
const link = new Link('родитель', [new Link('ребенок1'), new Link('ребенок2')]);
console.log(link.toString()); // (родитель: ребенок1 ребенок2)
// Доступ к свойствам связи
console.log('ID:', link.id);
console.log('Значения:', link.values);Расширенное использование
// Обработка вложенных структур
const input = `родитель
ребенок1
ребенок2
внук1
внук2`;
const parsed = await parser.parse(input);
// Работа с группами
import { LinksGroup } from 'links-notation';
const group = new LinksGroup(parsed);
console.log(group.format());Как документ был записан (CST)
Парсер сохраняет, как была записана каждая ссылка. Кавычки не меняют ссылку:
1 и '1' - одна и та же ссылка, поэтому сравнение, форматирование и JSON
остаются прежними. Каждая разобранная связь хранит rawId - ID в точности
так, как он записан, и quote - кавычку, в которую он заключён:
import { Parser } from 'links-notation';
const [link] = new Parser().parse("(t 1 '1')");
const [, bare, quoted] = link.values;
bare.equals(quoted); // true: это одна ссылка
[bare.rawId, bare.quote]; // ['1', null]
[quoted.rawId, quoted.quote]; // ["'1'", "'"]
link.format(); // '(t 1 1)'rawId равен null у связей без ID, у связей, созданных в коде, и у связей,
построенных объединением путей по отступам. new Parser({ syntax: false })
(режим AST) не сохраняет его.
parseCst(input) возвращает конкретное синтаксическое дерево без потерь:
каждый символ, включая комментарии, пустые строки, отступы, кавычки и
переводы строк, принадлежит ровно одному токену, поэтому исходный текст
всегда восстанавливается: new Parser().parseCst(text).toString() === text.
Узел document содержит узлы line; строка содержит свои токены вместе с
переводом строки; узел group содержит (, строки внутри и ). Токены
ссылок хранят value (что означает ссылка) и quote. parseCst отклоняет
ровно то, что отклоняет parse. Подробности - в
разборе issue 364.
Потоковый парсер
StreamParser принимает произвольные порции данных и отправляет события только
для завершённых связей верхнего уровня. Отключите накопление для обработки с
ограниченной памятью.
import { StreamParser } from 'links-notation';
const stream = new StreamParser({ collect: false, maxBufferSize: 1024 * 1024 });
stream.on('link', (link) => console.log(link.toString()));
stream.on('error', (error) => console.error(error));
stream.write('профиль:\n имя Ада\n');
stream.end('следующая связь');StreamParser.parse(chunks) и StreamParser.parseAsync(chunks) предоставляют
нативные синхронные и асинхронные итераторы. Также доступны position(),
drain(), reset() и ограничение размера незавершённой записи. См.
исполняемый пример.
Пакет не импортирует встроенные модули Node.js, поэтому сборщикам для браузера
не нужны полифилы: например, esbuild entry.js --bundle --platform=browser.
Встроенный в StreamParser эмиттер поддерживает методы подписки
EventEmitter из Node.js (on, once, off, removeListener, emit,
listenerCount и другие). До версии 0.27.1 StreamParser импортировал модуль
events, и для браузерной сборки требовался пакет-полифил events.
Группы отступных ID
В JavaScript, C++, Swift и Kotlin доступен parseGroups.
Для отступного ID, например x:\n a\n b, элемент x: имеет пустой
список значений, а его дети — ссылки a и b. Свёртка детей в значения
элемента даёт (x: a b), как канонический parse. Явное (a) сохраняет
связь с одним значением; обычный заголовок x: kind сохраняет прежние
дочерние связи и развёртку путей. Начиная с 0.30.2, ссылка в листе доступна
как child.element, а не child.element.values[0].
Примеры синтаксиса
Дуплеты (2-кортежи)
папа (любитМаму: любит маму)
сын любитМаму
дочь любитМаму
все (любят маму)Триплеты (3-кортежи)
папа имеет машину
мама имеет дом
(папа и мама) счастливыN-кортежи со ссылками
(нотацияСвязей: нотация связей)
(Это тоже нотацияСвязей)
(нотацияСвязей поддерживает (неограниченное количество (ссылок) в каждой связи))Структура с отступами
родитель
ребенок1
ребенок2
внук1
внук2Многострочные группы
Скобочная группа открывает вложенный контекст: её тело начинается заново с нулевого уровня отступа и подчиняется тем же правилам, что и корень документа, поэтому перенос строки внутри скобок — это структура, а не оформление.
value (
id "1"
label "one"
)Документ выше разбирается в (value ((id 1) (label one))) — два потомка,
каждый из которых сам является связью, — а не в один плоский список, в котором
граница между id и label была бы потеряна. Тело, умещающееся в одну строку,
по-прежнему сворачивается в одну связь, так что (a b c) не меняется.
import { Parser, formatLinks } from 'links-notation';
const links = new Parser().parse(`value (
id "1"
label "one"
)`);
console.log(formatLinks(links)); // (value ((id 1) (label one)))Комментарии
# скрывает остаток строки, на которой стоит, поэтому документ может нести
пояснения о самом себе:
# машины, на которые идёт выкладка
deploy: staging # пока только stagingК моменту чтения документа обоих комментариев уже нет, остаётся одна связь
(deploy: staging). # открывает комментарий только там, где могла бы
начаться ссылка, поэтому # внутри токена (issue#1047) и # внутри ссылки
в кавычках ("#") остаются обычными символами.
Форматтер соблюдает то же правило с другой стороны: ссылка, начинающаяся с #,
записывается в кавычках ('#tag'), поэтому написанный им документ читается
обратно как он сам.
Комментарии включены по умолчанию, а парсеру можно велеть снова читать # как
обычный символ - для документов, написанных до появления комментариев:
import { Parser, formatLinks } from 'links-notation';
const document =
'# машины, на которые идёт выкладка\ndeploy: staging # пока только staging\n';
console.log(formatLinks(new Parser().parse(document))); // (deploy: staging)
const plain = new Parser({ comments: false });
console.log(formatLinks(plain.parse('# a b\n'))); // (# a b)Справочник API
Классы
Parser
Основной класс парсера для преобразования строк в связи.
constructor(options)- Создать парсер;options.commentsсо значениемfalseчитает#как обычный символ, а не как начало комментарияoptions.maxInputSize- Максимальный размер входных данных в байтах (по умолчанию: 10MB)options.maxDepth- Насколько глубоко могут быть вложены связи (по умолчанию:DEFAULT_MAX_DEPTH, 64). Каждая группа в скобках и каждый уровень отступа - это один уровень; документ с более глубокой вложенностью отклоняется сParseError, а не разбирается рекурсивно, пока не кончится стек
initialize()- Инициализация парсера (асинхронно)parse(input)- Парсинг строки Lino и возвращение связейparseCst(input)- Конкретное синтаксическое дерево без потерь (CstNode)options.syntax- Со значениемfalse(режим AST) связи не хранятrawId(по умолчанию:true)
Link
Представляет одну связь с ID и значениями.
constructor(id, values = [])- Создание новой связиtoString()- Преобразование связи в строковый форматid- Идентификатор связиvalues- Массив дочерних значений/связейrawId- ID в точности так, как он записан в документе, илиnull;equals,formatи JSON его не учитываютquote-',"или`, еслиrawIdв кавычках, иначеnull
LinksGroup
Контейнер для группировки связанных связей.
constructor(links)- Создание новой группыformat()- Форматирование группы в строку
ParseError
Выбрасывается методом parse(input), когда документ не разбирается. Сообщение
говорит, где документ перестал быть понятным, и цитирует сломанную строку:
import { Parser, ParseError } from 'links-notation';
try {
new Parser().parse('ci_gate x\nstage: rust: nextest\n');
} catch (error) {
console.error(error.message);
if (error instanceof ParseError) {
console.error(`${error.line}:${error.column} (смещение ${error.offset})`);
}
}Syntax error at line 2, column 12: Expected "(", [ \t], [\r\n], or [^ \t\n\r(:)] but ":" found.
2 | stage: rust: nextest
| ^offset- Смещение сломанной позиции от начала документаline,column- Где документ перестал разбираться, счет с 1found- Найденный символ илиnullв конце документаlineText- Сломанная строка, как она написанаsnippet- Сломанная строка с указателем под нужным столбцомlocation- Позиция в том виде, в каком ее сообщает сгенерированный парсерcause- Ошибка, которую выбросил сгенерированный парсерmaxDepth- Наибольшая допустимая вложенность, если документ вложен глубже;nullдля любой другой ошибки
Документ, вложенный глубже maxDepth, отклоняется на той группе или строке,
которая на один уровень глубже допустимого:
Nesting too deep at line 1, column 4: nesting depth exceeds the maximum of 3
1 | ((((a))))
| ^Структура проекта
src/grammar.pegjs- Определение грамматики Peggy.jssrc/Link.js- Структура данных связиsrc/LinksGroup.js- Контейнер групп связейsrc/ParseError.js- Ошибка разбора с позицией дефектаsrc/Parser.js- Обертка парсераsrc/index.js- Главная точка входаtests/- Файлы тестов
Зависимости
- Peggy.js (5.0.6) - Генератор парсеров
- Среда выполнения Bun (разработка)
Информация о пакете
- Пакет:
links-notation - Лицензия: Unlicense (см. LICENSE)
