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

@brandup/ui-messageeditor

v1.0.55

Published

Message editor styled as a chat bubble.

Readme

@brandup/ui-messageeditor

Build Status

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

Устроен по тому же принципу, что и @brandup/ui-textbox: исходный input/textarea остаётся носителем значения и участвует в форме (валидация, submit, FormData), а ввод ведёт @brandup/ui-richeditor в соседнем редактируемом элементе.

Установка

npm i @brandup/ui-messageeditor

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

import MessageEditor from "@brandup/ui-messageeditor";

const elem = document.querySelector("textarea") as HTMLTextAreaElement;
const editor = new MessageEditor(elem);

editor.onChange(({ value }) => console.log(value));

Конструктор вставляет плашку на место поля и переносит само поле внутрь — оно скрыто, но остаётся в форме и хранит значение.

Поле может объявить автофокус — нативным autofocus либо data-autofocus: плашка получает фокус сразу после сборки, а каретка встаёт в конец текста. Когда автофокус отменяется — см. @brandup/ui-input.

Свойства

| Свойство | Тип | Описание | | --- | --- | --- | | placeholder | string \| null | Текст-заглушка; берётся из атрибута placeholder поля | | personalization | boolean | Включена ли персонализация; по умолчанию нет | | variables | MessageVariable[] | Переменные персонализации; из опций либо из атрибута data-variables | | variablesEmpty | string \| null | Текст в окне персонализации при пустом списке; из опций либо из атрибута data-variables-empty | | variablesSetup | string \| (() => void \| boolean) | Настройка полей: адрес или действие для ссылки в окне персонализации; адрес — и из атрибута data-variables-setup | | variablesSetupText | string | Подпись ссылки на настройку; из опций либо из атрибута data-variables-setup-text (по умолчанию «Настроить поля») | | blocks | BlockType[] | Типы блоков сообщения: quote, code; из опций либо из атрибута data-blocks (по умолчанию все) | | tools | FormatTool[] | Инструменты форматирования; из опций либо из атрибута data-tools (по умолчанию все) | | keepFocus | boolean | Держать ли фокус, пока открыт попап смайликов (по умолчанию да, а на сенсорном устройстве нет) | | source | boolean | Показ выхода: переключатель над плашкой и панель с разметкой значения; из опций либо из атрибута data-source (по умолчанию выключен) | | variableLength | number | Сколько символов отводится переменной при подсчёте messageLength; из опций либо из атрибута data-variable-length (по умолчанию 30) |

Многострочность включена всегда: сообщение — это абзацы. Enter даёт один перенос (\n), два нажатия — пустую строку (\n\n), как в мессенджерах; форму он не отправляет.

Форматирование тоже включено всегда: панель с объявленными инструментами (по умолчанию — все) и кнопкой очистки формата всплывает над плашкой при фокусе. Кнопка очистки гаснет, когда снимать нечего. Вставка смайлика вынесена из панели в собственную кнопку внутри плашки — она доступна сразу, не дожидаясь фокуса в поле. Первой группой в попапе стоят недавно выбранные символы; помнит их @brandup/ui-richeditor — в localStorage, общем для всех редакторов источника. В disabled не строится ни панель, ни кнопка.

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

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

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

Состояния disabled, readonly и required берутся у поля-носителя, как у остальных контролов кита. Разметку значения плашка показывает и в них — переключать её там нечем, но показать читателю жирный жирным она обязана; звёздочки вместо него означали бы не то, что дойдёт до получателя.

Набор разметки

«Мессенджер» — это не один канал, и понимают они разное: подчёркивания и спойлера нет в WhatsApp, цитаты нет в VK, разметки нет в SMS вовсе. Кнопка, разметку которой канал не покажет, хуже отсутствующей — размеченный ею текст уйдёт получателю либо голым, либо сырыми маркерами. Поэтому и инструменты, и типы блоков объявляются набором: опцией или атрибутом поля-носителя (значения через пробел, пустое значение — пустой набор). По умолчанию доступно всё.

<textarea data-tools="bold italic strike" data-blocks="quote"></textarea>
<textarea data-tools="" data-blocks=""></textarea>

| | Опция | Атрибут | Значения | Пустой набор | | --- | --- | --- | --- | --- | | Инструменты | tools | data-tools | bold, italic, strike, underline, spoiler, code | Текст без разметки | | Типы блоков | blocks | data-blocks | quote, code | Только обычный текст |

Порядок значений в атрибуте не важен: кнопки идут в каноническом порядке набора, незнакомые значения отбрасываются. Переданный в опциях набор имеет приоритет над атрибутом — приложение знает канал точнее, чем разметка от сервера.

Отдельного набора для разбора значения нет: ограничение действует и на него. Разметка снятого инструмента остаётся в тексте как есть — на экране сырыми маркерами, в значении ровно такой, какой была, — и уезжает обратно без потерь. Так же её увидит и получатель в канале, который её не знает. Кнопка очистки формата исчезает вместе с последним инструментом: снимать становится нечего. Отмена, повтор и кнопки рандомизации с переменными от набора не зависят — это не разметка, и канала они не касаются.

Тем же набором разбирается и вставка. Значение хранится маркдауном, поэтому маркеры во вставляемом простом тексте значат то же, что в значении, и разбираются так же: **раз** из буфера становится жирным, а маркер снятого инструмента остаётся текстом. Это касается и текстовых файлов (.md, .txt), брошенных в плашку, — файл вставляется содержимым через тот же разбор (см. @brandup/ui-richeditor).

Моноширинный и блок кода делит одна кнопка — как в мессенджерах: выделена одна строка или её часть, значит моноширинный, а несколько строк (или ничего не выделено) — блок из этих строк, остальной текст остаётся как был. Внутри кода остальные инструменты выключены: значение берёт оттуда голый текст. Внутри цитаты и блока кода Enter заканчивает блок и возвращает к обычному тексту, а строку внутри блока переносит Shift+Enter. Выйти из блока можно и Backspace в его начале, и той же кнопкой панели. Подробности разбора и хранения — в @brandup/ui-richeditor.

Показ выхода

Значение хранится не HTML, а разметкой мессенджеров (markdown), и иногда её нужно видеть целиком — сверить перед отправкой, скопировать в шаблон, показать поддержке, что именно уйдёт в канал. Для этого над плашкой встаёт переключатель на два режима, а рядом с ней — панель, в которую рендерится выход: Текст показывает сообщение, Markdown — его разметку.

По умолчанию ничего этого нет: пишущему сообщение сырая разметка не нужна, а показанная без спроса требует объяснений. Включается опцией source или атрибутом data-source поля-носителя:

<textarea data-source></textarea>
new MessageEditor(elem, { source: true });

Название второй кнопки — это формат хранения (Markdown), в него и переключают. Панель показывает ровно то, что лежит в поле-носителе и уедет в форму, поэтому отложенное при печати изменение доставляется перед показом. Значение от переключения не меняется: панель только показывает, править по-прежнему можно в плашке — текст в ней выделяется и копируется, но не редактируется. Пока панель открыта, показанное следует за значением: его меняет и хост через setValue, и окно правки, открытое до переключения.

Два вида одного значения разом на экране не держатся: панель показывается вместо плашки, иначе непонятно, какой из них правится. Фокус при переключении уходит из плашки — панель форматирования иначе висела бы над скрытым текстом. Обратно в неё он встаёт вместе с прежней кареткой, когда режим переключают кнопкой: нажали в поле — значит, продолжают писать. В disabled и readonly переключатель работает (смотреть значение — не значит изменять его), но фокус там не возвращается: в readonly фокус выделяет всё сообщение целиком, и смена вида делала бы то же самое.

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

editor.toggleSource(true); // показать разметку
editor.toggleSource(); // обратно к сообщению, фокус остаётся где был
editor.toggleSource(false, true); // обратно к сообщению вместе с фокусом и кареткой

Переменные и рандомизация

Две доменные кнопки в панели, которых нет у обычного редактора. Обе открывают модальное окно и вставляют результат в каретку; выделение при этом не теряется — панель не забирает фокус.

Персонализация {ИМЯ} — выбор переменной из списка. По умолчанию выключена: без неё нет ни кнопки, ни подсветки, и {ИМЯ} остаётся обычным текстом. Включается опцией personalization, атрибутом data-personalization или самим объявлением списка — объявили, значит нужна. В значение уходит ключ ({ИМЯ}), а в тексте и в списке показывается название, если оно задано. Подставляет переменную приложение при отправке, компонент только вставляет разметку.

Рандомизация [раз|два] (спинтакс) — варианты набираются вручную, вариант выбирается при отправке. Выделенный текст попадает в окно первым вариантом — уже без символов самой конструкции ([, ], |) и в одну строку: внутри варианта они развалили бы спинтакс, поэтому их не вводят и не вставляют, и текст со стороны проходит ту же чистку. Без выделения первым вариантом становится слово под кареткой — рандомизируют чаще всего то слово, на котором стоят, — и собранный спинтакс встаёт на его место, а не разрывает слово; каретка вне слова (за точкой) открывает окно пустым. Готовый спинтакс, открытый на правку кликом, разбирается на варианты как есть.

Обе конструкции подсвечиваются прямо в тексте и атомарны: в поле их не отредактировать, клик по конструкции открывает то же окно для правки. Backspace и Delete у конструкции стирают её целиком — стереть в ней символ нельзя, а нативное удаление рядом с нередактируемым элементом браузеры делают по-разному; за конструкцией в конце строки к тому же стоит невидимая опора каретки, и нажатие уходило бы на неё. Удаление словами и строками (Ctrl, Alt, Cmd) остаётся браузеру: это правка текста вокруг, а не самой конструкции. При наведении подсветка темнеет, а курсор — стрелка: конструкция кликабельна, и это видно до клика. В readonly и disabled клик не работает — там нет ни отклика, ни курсора. Форматирование обходится с ними как с одним символом — выделили текст с переменной и сделали жирным, получится **раз {ИМЯ} два**, а не три отдельных куска. Подсветка — это обычные span, которые сериализация отбрасывает, поэтому в значение она не попадает: там остаётся ровно {ИМЯ} и [раз|два].

Список переменных задаётся опцией (она же включает персонализацию):

new MessageEditor(elem, {
    variables: [{ key: "ИМЯ", name: "Имя подписчика" }, { key: "ГОРОД" }],
});

либо атрибутом data-variables поля-носителя — когда разметку отдаёт сервер и передавать список неоткуда:

<textarea data-variables="ИМЯ, ГОРОД"></textarea>

<textarea data-variables='[{"key":"ИМЯ","name":"Имя подписчика"},"ГОРОД"]'></textarea>

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

Переменную, набранную или вставленную названием, поле само приводит к ключу: {Имя подписчика} становится {ИМЯ}. И в поле, и в списке она показана названием — набирают её с экрана, тем, что видно, — а подставить название нечем: в сообщение уходит ключ, и получателю такая переменная ушла бы скобками наружу. Подмена происходит сразу, пока пишущий видит результат, и на экране от неё ничего не меняется: там снова название. Регистр и лишние пробелы в написанном не важны (название — человеческий текст, а не код), а объявленный ключ важнее чужого названия: написанное, совпавшее с ключом, остаётся как есть.

Переменная с ключом, которого нет в объявленном списке, помечается в тексте отдельно — заливкой и волнистым подчёркиванием (span.variable.unknown), с подсказкой в title. Подставить её нечем: на отправке она уйдёт получателю скобками наружу, а выглядит при этом ровно как рабочая, и опечатка в ключе замечается уже по отправленному сообщению.

Это ошибка значения, а не оформления, поэтому она же останавливает отправку формы: контрол объявляет её полю-носителю через setCustomValidity, дальше решает браузер — как и с нативным required. Пока список объявлен, подпись невалидности на поле принадлежит контролу: свою приложению придётся держать где-то ещё. По destroy она снимается — иначе поле осталось бы в форме навсегда невалидным, а объяснить это было бы нечем. Список ключей отдаёт свойство unknownVariables:

editor.onChange(() => {
    const unknown = editor.unknownVariables; // ["ИМЯЯ"]
    hint.textContent = unknown.length ? `Нет таких переменных: ${unknown.join(", ")}` : "";
});

Пустой список — не ошибка: кнопка остаётся на месте, а окно объясняет, почему выбирать нечего. Текст задаётся опцией variablesEmpty или атрибутом data-variables-empty, по умолчанию — «Переменные не заданы.» (VARIABLES_EMPTY_TEXT). Причину знает приложение: переменные могут появиться после выбора аудитории, а могут быть не предусмотрены вовсе. Пока список пуст, ничего не проверяется и не помечается: набор ещё не известен, и чужой в нём не отличить от своего. Поля контрол тогда не касается вовсе — выставленную приложением подпись setCustomValidity в этом случае не стирает.

<textarea data-variables-empty="Переменные появятся после выбора аудитории."></textarea>

Настройка полей

Список переменных где-то ведётся, и из окна персонализации туда должно быть видно дорогу — особенно когда список пуст или нужного поля в нём не нашлось. Ссылка на настройку встаёт последней строкой окна, в обоих состояниях списка одинаково: при пустом это главный выход, при заполненном — запасной. Без объявленной настройки строки нет вовсе.

Где настройка живёт, знает приложение: строка — адрес перехода, и рисуется настоящая <a href> (работают средняя кнопка и «копировать адрес»); функция — действие хоста, SPA-переход или собственное окно, и рисуется кнопка в виде ссылки. Объявленная настройка — тоже согласие на персонализацию, как и объявленный список переменных.

new MessageEditor(elem, { variablesSetup: "/settings/fields" });
new MessageEditor(elem, { variablesSetup: () => app.nav("/settings/fields") });
<textarea data-variables-setup="/settings/fields" data-variables-setup-text="Управление полями"></textarea>

По нажатию окно закрывается молча — без возврата каретки в поле: фокус уходит на другой экран, и мигающая каретка в оставленном поле только сбивала бы с толку. Функция может попросить окно не трогать, вернув false, — например, когда открывает собственный слой поверх; действие при этом выполняется до закрытия. Подпись задаётся опцией variablesSetupText или атрибутом, по умолчанию — «Настроить поля» (VARIABLES_SETUP_TEXT).

Длина сообщения

У каналов есть лимиты на длину сообщения, а у сообщения с конструкциями нет одной длины: спинтакс уйдёт одним из вариантов, переменная развернётся в подставленное значение. Свойство messageLength отдаёт оценку — по видимому тексту, без маркеров форматирования, обрезанную по краям, как значение из getValue():

  • спинтакс [раз|два] считается самым длинным вариантом: в отправку уйдёт любой из них, и лимит обязан выдержать каждый;
  • переменная {ИМЯ} считается условной длиной подставляемого значения — по умолчанию 30 символов. Сколько на самом деле, знает приложение: задаётся опцией variableLength или атрибутом data-variable-length. Без персонализации {ИМЯ} — обычный текст и считается по буквам.

Вложенных конструкций нет: переменная внутри варианта спинтакса — часть его текста и считается по буквам, как и в проверке объявленных переменных.

<textarea data-variable-length="20"></textarea>

Своего вывода у длины пока нет: свой лимит приложение проверяет само, читая messageLength на onChange. Проверку формы длина не держит — оценка с конструкциями останавливала бы отправку по догадке. Считает ту же длину и отдельная функция messageLength(root, options), если содержимое пришло не из компонента.

API

| Метод | Описание | | --- | --- | | getValue(): string | Текущее значение (обрезанное по краям) | | setValue(value): void | Заменить значение | | hasValue(): boolean | Есть ли непустое значение | | validate(): boolean | Проверка по атрибутам поля-носителя, проставляет класс invalid | | unknownVariables | Ключи переменных из текста, которых нет в объявленном списке (в порядке появления) | | messageLength | Оценка длины сообщения: спинтакс — самым длинным вариантом, переменная — условной длиной (variableLength); считается на месте, по текущему содержимому | | sourceMode | Показан ли выход вместо плашки | | toggleSource(show?, focus?) | Переключить режим показа; без аргумента — на противоположный. Фокус по умолчанию не трогается, focus: true возвращает его в плашку вместе с кареткой (кроме disabled и readonly). Без включённого показа выхода не делает ничего | | onChange(handler) | Подписка на событие messageeditor-change | | editor | Доступ к встроенному RichEditor — выделение, вставка текста | | destroy(): void | Возвращает поле на место и освобождает ресурсы; открытое окно правки закрывается, своя подпись невалидности снимается |

Валидация штатная: ограничения объявлены на поле-носителе, решение принимает браузер — он же блокирует отправку формы. Контрол только отражает результат классом invalid. Своё ограничение у него одно — неизвестная переменная, и объявляется оно тем же способом, что и у остальных контролов кита, через setCustomValidity.

Пакет экспортирует и внутренние части, если они нужны отдельно: VariablesModal, RandomizerModal, parseVariables, buildVariable, buildSpintax, parseSpintax, messageLength, DEFAULT_VARIABLE_LENGTH, VARIABLES_EMPTY_TEXT, VARIABLES_SETUP_TEXT, типы VariablesSetup и LengthOptions.

Оформление

Разметка: корневой .ui-messageeditor.bubble (плашка) → редактируемый элемент .ui-richeditor и коробка кнопки .messageeditor-emoji-holder с кнопкой .messageeditor-emoji справа от него. Подсвеченные конструкции в тексте — span.variable и span.spintax; необъявленная переменная — span.variable.unknown.

Внутри конструкции символы самой разметки — скобки и разделитель вариантов — лежат в собственных обёртках span.mark (по умолчанию у каждой отступ в пиксель с обеих сторон), а между ними содержимое: варианты спинтакса обычным текстом, ключ переменной — тоже, либо в span.key, когда у неё есть название. Название показывается оформлением пустой обёртки span.label (content: attr(data-label)): узел с текстом попал бы в текст поля, а из него собирается значение сообщения. Текст конструкции от этого разбиения не меняется — сериализация вложенные обёртки отбрасывает, оставляя их текст.

При включённом показе выхода перед плашкой встаёт переключатель .messageeditor-modes с кнопками .messageeditor-mode (выбранная — .active), а после неё — панель .messageeditor-source. Видимость плашки и панели переключает класс source на корневом элементе, и вешает его сам компонент: сам режим он держит в себе, а из класса не читает — собственные классы поля-носителя переезжают на корневой элемент контрола, и class="source" в разметке поля включал бы режим, которого нет. По той же причине плашку прячет только класс режима при собранной панели (.source:has(> .messageeditor-source)).

Прокручивается текст, а не плашка: кнопка остаётся на месте, когда сообщение перерастает --messageeditor-maxheight.

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

Заливка по умолчанию своя, а не от полей ввода: белая плашка на белой странице держалась бы только на тени и почти не читалась бы.

Настраивается CSS-переменными:

| Переменная | По умолчанию | Что задаёт | | --- | --- | --- | | --messageeditor-fill | #e7f3ff | Заливка плашки | | --messageeditor-color | var(--input-color, #222) | Цвет текста | | --messageeditor-radius | 18px | Скругление плашки | | --messageeditor-minheight | var(--input-line-height, 1.2rem) | Высота содержимого пустой плашки (иначе она схлопывается в отступы) | | --messageeditor-maxheight | 220px | Высота, после которой появляется прокрутка | | --messageeditor-padding-tb | считается от --input-height | Отступы плашки сверху и снизу: такие, что пустая плашка ровно в высоту полей ввода кита, а строка стоит по центру. По горизонтали берётся --input-padding-lr, общий для полей ввода | | --messageeditor-gap | 8px | Расстояние от текста до кнопки смайлика | | --messageeditor-button-size | 32px | Размер кнопки смайлика | | --messageeditor-button-icon | 20px | Размер иконки на кнопке | | --messageeditor-button-color | var(--placeholder-color, #999) | Цвет иконки (в наведении и при открытой панели — акцент) | | --messageeditor-shadow | 0 1px 2px rgba(0,0,0,.16) | Тень плашки | | --messageeditor-border | var(--messageeditor-fill) | Рамка плашки — в цвет заливки, поэтому в покое не видна | | --messageeditor-border-width | 1px | Толщина рамки | | --messageeditor-border-focus | заливка, затемнённая на 12% | Рамка в фокусе | | --messageeditor-accent | var(--focus--input-border-color, #4a9eff) | Цвет иконки смайлика при наведении и открытой панели | | --messageeditor-spintax-fill | rgba(255,193,7,.28) | Подсветка спинтакса [раз\|два] в тексте | | --messageeditor-variable-fill | rgba(76,175,80,.24) | Подсветка переменной {ИМЯ} в тексте | | --messageeditor-variable-unknown-fill | rgba(244,67,54,.24) | Подсветка переменной, которой нет в объявленном списке | | --messageeditor-variable-unknown-mark | rgba(244,67,54,.8) | Волнистое подчёркивание под ней — отличие не только цветом | | --hover--messageeditor-spintax-fill | заливка, затемнённая на 12% | Подсветка спинтакса при наведении | | --hover--messageeditor-variable-fill | заливка, затемнённая на 12% | Подсветка переменной при наведении | | --hover--messageeditor-variable-unknown-fill | заливка, затемнённая на 12% | Подсветка неизвестной переменной при наведении | | --messageeditor-source-fill | var(--input-fill, #fff) | Заливка панели выхода — нейтральная, как у полей ввода: это не сообщение, а его разметка | | --messageeditor-source-border | var(--input-border-color, #dcdfe4) | Рамка панели выхода | | --messageeditor-source-color | var(--messageeditor-color) | Цвет текста разметки | | --messageeditor-source-font | системный моноширинный | Шрифт разметки | | --messageeditor-modes-gap | 6px | Расстояние от переключателя до плашки | | --messageeditor-quote-fill | заливка плашки, затемнённая на 6% | Подложка цитаты в сообщении — в тон плашки, а не серая по умолчанию редактора |

Классы состояний вешаются на корневой элемент: focused, invalid, disabled, readonly, source.