npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 - Где документ перестал разбираться, счет с 1
  • found - Найденный символ или 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.js
  • src/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)