Как стать автором
Обновить

Obsidian: исчерпывающее руководство по Templater

Уровень сложностиСредний
Время на прочтение38 мин
Количество просмотров5.9K

Предисловие

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


Слова благодарности

Перед тем как перейти к рассмотрению возможностей стороннего плагина Templater в Obsidian, хочу поблагодарить всех тех, кто прочел мою предыдущую статью, посвященную созданию примитивных сценариев для автоматизации своих заметок в Obsidian и тем, кто поучаствовал в обсуждении. Мне правда очень приятно осознавать, что те знания, которыми я обладаю - могут помочь кому-то еще.

Отдельно хочется сказать спасибо моей команде, ведь благодаря ей я и начал писать заметки про того как можно использовать Obsidian.


Слова извинений

Эта статья получилась достаточно большой, поэтому если вам вдруг тяжело или неинтересно читать - выключайте. Если у вас возникнет необходимость разобраться в той или иной функции - было бы здорово, если бы вы вспомнили, что существует такая статья. Я думаю, что у меня получилось собрать в ней самую важную и интересную информацию про Templater

Если я где-то что-то перепутал или не совсем уместно применил какие-либо термины, тоже простите - не специально.

И так, приступим...


Как шаблон влияет на заметки?

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

  • Даты: {{date}};

  • Времени: {{time}};

  • Названия заметки: {{title}};

  • Тела заметки (будет использоваться все содержимое заметки);

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

  • сможет реагировать на то как шаблон был вызван;

  • учитывать содержимое хранилища;

  • добавлять дополнительную логику обработки заметок как целиком, так и частично.

В свою очередь, в силу того, что Obsidian построен на Electron (и по-сути является браузером), то сообщество додумалось как можно обернуть это во благо пользователя.

Но за все в этой жизни нужно платить. Что я имею ввиду: взаимодействуя с заметками, мы фактически взаимодействуем с бизнес-логикой веб приложения: создание, редактирование и удаление каких-либо данных из хранилища должно быть обеспечено каким-то сценарием(скриптом), но каким? И тут мы подходим к главному - JavaScript это цена за индивидуальное рабочее пространство, которое мы можем настроить под себя.

Если ты не разработчик, поясню простыми словами: JavaScript — это язык, который определяет, как веб-приложение реагирует на ваши действия. Это своего рода «язык общения» между пользователем и программой. Он позволяет обрабатывать клики, ввод, события, данные — всё, что делает интерфейс интерактивным.

Хочу оговориться, что я не JS разработчик и в каких-то моментах могу ошибаться, если такое произойдет, поправьте меня - я за конструктивную критику и полезные знания.

Но причем здесь JavaScript и шаблоны?

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

Для наглядности, хочу показать несколько возможных сценариев ежедневного использования шаблонов:


Добавление заготовленных Callout-блоков

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

>[!info]- Пример
>Тут ваш текст

Но каждый раз писать эту конструкцию для меня неудобно, поэтому я сделал шаблон, который вставляет в текущую заметку в место, где стоит курсор готовый Callout-блок.

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

Но вернемся к поставленной задаче: реализация в этом случае - примитивная и не использует JS код.

>[!warning] Обратить внимание
>

А в настройках плагина Templater я просто зарегистрировал плагин, а затем привязал комбинацию горячих клавиш:

!

Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 1.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 1.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 2.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 2.png

И теперь, при нажатии комбинации Option + S в заметку вставляетс Callout-блок, который я использую регулярно в обсуждении материалов при совместном редактировании с моими друзьями.


Оборачивание выделенного текста в >

Но, после того как я добавил Callout-блок может возникнуть необходимость поместить в него какое-то количества текста и мне хотелось бы сделать это одной комбинацией клавиш и избежать построчного выделения.

Для этого у меня есть шаблон, который получает выделенный текст и вначале каждой строки ставит символ >. Это может быть полезно в том числе для создания "дополнительного" уровня вложенности, если вдруг я захочу использовать multi-column.

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

Код шаблона

<%*
let clip = tp.file.selection() ? tp.file.selection() : await tp.system.clipboard();
clip = clip.replace(/\n/g, '\n>');
tR +='>' + clip;
%>

Затем, по аналогии я также зарегистрировал шаблон в плагине и назначил ему комбинацию горячих клавиш, как в предыдущем примере.

Как это работает:

Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 3.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 3.png

Удаление уровня вложенности >

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

Он может быть полезен, когда необходимо убрать "лишний" уровень вложенности

Алгоритм добавления этого шаблона идентичен предыдущим: создали шаблон, зарегистрировали, присвоили комбинацию горячих клавиш - ничего не меняется.

Код шаблона

<%*
let clip = tp.file.selection() ? tp.file.selection() : await tp.system.clipboard();

clip = clip.replace(/^>+\s*/gm, '');

tR += clip;
%>

Как это работает

Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 4.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 4.png

Оборачивание выделенного текста в ```

Мне для работы с заметками часто необходимо оборачивать текст в блок кода и принцип реализации шаблона для этого практически идентичен:

Особенность этого шаблона в том, что если у вас в буфере есть какой-то текст - он будет вставлен в заметку и сразу обернут в ``` .

Алгоритм добавления этого шаблона идентичен предыдущим: создали шаблон, зарегистрировали, присвоили комбинацию горячих клавиш - ничего не меняется.

Код шаблона

<%*
let clip = tp.file.selection() ? tp.file.selection() : await tp.system.clipboard();
clip = `\`\`\`\n${clip}\n\`\`\``;
tR += clip;
%>

Как это работает?

Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 5.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 5.png

Знакомство с Templater

Репозиторий и документация

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

Терминология

Для лучшего погружения Автор предлагает использовать 5 определений:

  • Шаблон — это файл, содержащий команды;

  • Фрагмент текста, который начинается с открывающего тега <% и заканчивается закрывающим тегом %> — это то, что мы будем называть командой.

  • Функция — это объект, который мы можем вызвать внутри команды и который возвращает значение (строку замены);

  • Внутренние функции. Это предустановленные функции, встроенные в плагин. Например, tp.date.now — это внутренняя функция, которая возвращает текущую дату;

  • Пользовательские функции. Пользователи могут создавать собственные функции. Это могут быть системные команды или пользовательские скрипты;

  • Способ выполнения шаблона - это то, какой тип инициализации шаблона мы выбираем (создание нового файла на основе шаблона или работа с текущим открытым в режиме редактирования файлом);

  • Способ вызова шаблона - это то, как мы будем вызывать способ выполнения шаблона (через палитру команд, встроенную иконку плагина Templater на боковой (левой) панели, с помощью сторонних плагинов и т.д.).

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

Синтаксис

Templater использует собственный механизм шаблонов (rusty_engine) для объявления команды. Мне потребовалось некоторое время, чтобы привыкнуть к нему.

Главное, что нужно понимать - все функции Templater представляют собой объекты JavaScript, которые вызываются с помощью команды.

База для понимания

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

Мое желание решить задачу здесь и сейчас мешало сконцентрироваться. Из-за отсутствия знаний и желания работать с JavaScript, по-началу я даже делал шаблоны исключительно с помощью ИИ, но потом понял, что такой подход не жизнеспособен - ИИ не знает о плагинах столько, сколько написано в документации и очень быстро я понял, что могу сделать все, что мне надо, если немного посидеть над документацией - база важна.

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

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

Синтаксис команды

Команда должна иметь как открывающий тег <%, так и закрывающий тег %>.

Например, чтобы система подставила текущее время нам необходимо использовать внутреннюю функцию tp.date.now() и полная команда с её использованием будет выглядеть так: <% tp.date.now() %>.

Сразу оговорюсь, что в таком виде открывающей тег имеет ограничение по размеру команды (что она может в себе вместить), т.е. всего 1 строку с командой.

Для многострочной команды нужно модифицировать открывающий тег: <%*, закрывающий тег остается таким же %>

Синтаксис функции

Иерархия объектов

Все функции Templater, будь то внутренняя или пользовательская функция, доступны в объекте tp . Можно сказать, что все наши функции являются дочерними по отношению к объекту tp . Чтобы получить доступ к «дочерней» функции объекта, мы должны использовать точечную нотацию .

Это означает, что вызов функции Templater всегда будет начинаться с tp.<дочерняя функция объекта>

Вызов функции

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

В качестве примера мы будем использовать tp.date.now() для вызова внутренней функции tp.date.now

Функция может иметь как обязательные, так и необязательные аргументы. Они указываются между открывающей и закрывающей скобками, например:

tp.date.now(arg1_value, arg2_value, arg3_value, ...)

Все аргументы должны быть указаны в правильном порядке.

Аргументы функции могут иметь разные типы. Вот неполный список возможных типов функций:

  • Тип string означает, что значение должно быть заключено в одинарные или двойные кавычки ("value" или 'value')

  • Тип number означает, что значение должно быть целым числом (15-5, ...)

  • booleanТип означает, что значение должно быть либо true, либо false (полностью в нижнем регистре), и ничего больше.

При вызове функции необходимо учитывать тип аргумента, иначе она не сработает.

Настройки

Общие настройки

Стоит, на всякий случай, оговориться, что настройки плагина индивидуальны для каждого вашего хранилища. Получить доступ к настройкам можно открыв настройки вашего хранилища и найти меню плагина Templater после того как вы его установите и "Включите".

Нам доступны следующие параметры:

  • Template folder location - файлы в этой папке будут доступны в качестве шаблонов. Для удобства, я так и назвал файл с шаблонами Templates;

  • Syntax Highlighting on Desktop добавляет подсветку синтаксиса для команд Templater в режиме редактирования;

  • Syntax Highlighting on Mobile добавляет подсветку синтаксиса для команд Templater в режиме редактирования на мобильных устройствах;

  • Automatic jump to cursor автоматически запускает tp.file.cursor после вставки шаблона. Это нужно для того, чтобы при вызове шаблона определялось место, в котором необходимо произвести взаимодействие шаблона с вашей заметкой;

  • Trigger Templater on new file creation Templater будет отслеживать событие создания нового файла и, если оно соответствует заданному вами правилу, заменять каждую команду, которую он находит в содержимом нового файла. Это делает его совместимым с другими плагинами, такими как встроенный плагин "Ежедневные заметки" и т.д.;

  • Trigger Template on new file creation - вы можете указать шаблон, который будет автоматически использоваться для выбранной папки и ее дочерних элементов, с помощью функции Folder Templates. Будет использоваться наиболее точное совпадение, поэтому порядок правил не имеет значения.

    Обратите внимание, что автор пишет, что он является взаимоисключающим с шаблонами регулярных выражений для файлов и поэтому включение одного параметра приведет к отключению другого;

  • Trigger Template on new file creation - вы можете указать регулярные выражения, на соответствие которым будет проверяться путь к новому файлу. Если регулярное выражение совпадает, автоматически используется соответствующий шаблон. Правила проверяются сверху вниз, и используется первое совпадение;

  • Startup Templates - это шаблоны, которые будут выполняться один раз при запуске Obsidian;

  • User Scripts Function - позволяет определить папку, в которой будут храниться JS скрипты. Это очень полезно, если вам необходимо переиспользовать один и тот же код и нет желания каждый раз определять одни и те же функции;

    Я часто использую такое для работы с формами. Например, мне это помогает с функциями createFileIfNotExists() и createFolderIfNotExists() и позволяет избежать лишнего дублирования

  • User System Command Functions - позволяет создавать пользовательские функции,связанные с системными командами.

Выше я писал, что шаблоны можно назначить на комбинации клавиш, но следует также упомянуть, что вызывать зарегистрированные шаблоны можно и через панель команд Ctr/Cmd + P, а также через QuickAddModalFormsCommander и MetaBind. Более подробно о вызовах с примерами, особенностями и лайфхаками вы найдете в соответствующем разделе.


Внутренние функции

Напомню, что мы договорились понимать под встроенными функциями - предустановленные функции, встроенные в плагин, доступ к которым мы можем получить обратившись к объекту tp.


Модуль приложения: tp.app

Благодаря tp.app мы можем получить доступ к дочерним функциям глобального объекта приложения.

Здесь app - это объект приложения (хранилища), в котором исполняется шаблон. Этот объект мы получаем по API от Obsidian в качестве интерфейса, поэтому в том числе автор плагина предлагает просто ознакомиться с оффициальной документацией.

Интерфейс app — это центральный объект в архитектуре Obsidian API. Он предоставляет доступ ко всем основным возможностям редактора и позволяет плагинам взаимодействовать с файлами, UI и поведением пользователя.

Интерфейс app представляет главное приложение Obsidian и предоставляет доступ к его ключевым компонентам, таким как:

  • файловая система (vault) - объект, управляющий файловой системой: чтение, запись, создание и удаление файлов и папок в хранилище;

  • пользовательский интерфейс (workspace) - этот объект отвечает за UI и окна редактора. Через него можно получить доступ к открытым заметкам, вкладкам, панелям и их состоянию;

  • плагины - включенные и, в целом, загруженные в хранилище;

  • метаданные заметок (metadataCache) - позволяет получить метаданные заметок (фронтматтер, ссылки, теги и т.д.), не читая содержимое файла напрямую;

  • события и настройки.


Модуль конфигурации: tp.config

Модуль config предоставляет доступ к внутренним настройкам и контексту исполнения шаблона. Это полезно при в работе с динамическими шаблонами, поведение которых зависит от способа запуска, целевого файла или пользовательских параметров.

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

Основные свойства и методы

  • tp.config.active_file? - возвращает объект TFile, представляющий активный файл в момент запуска Templater. Может быть undefined, если нет активного файла (например, при запуске шаблона из новой заметки);

  • tp.config.run_mode - определяет как должен быть исполнен шаблон: в текущей заметке или создать новый файл на основе шаблона;

  • tp.config.target_file - объект TFile представляет целевой файл, в который будет вставлен шаблон;

  • tp.config.template_file - объект TFile представляет собой файл шаблона.


Модуль даты: tp.date

Этот модуль содержит все внутренние функции, связанные с датами, но помимо прочего есть и ряд "самостоятельных".

  • tp.date.now - возвращает текущую дату;

  • tp.date.tomorrow - возвращает завтрашнюю дату;

  • tp.date.weekday - возвращает день недели;

  • tp.date.yesterday - возвращает вчерашнюю дату;

Стоит также оговориться, что Templater предоставляет вам доступ к объекту moment со всеми его функциями.

Примеры команд

// Текущая дата
<% tp.date.now() %>
// Текущая дата с форматом
<% tp.date.now("Do MMMM YYYY") %> // пример: "21 мая 2025"
// Неделя назад от сегодня
<% tp.date.now("YYYY-MM-DD", -7) %>
// Через неделю от сегодня
<% tp.date.now("YYYY-MM-DD", 7) %>
// Месяц назад от сегодня (ISO-длительность)
<% tp.date.now("YYYY-MM-DD", "P-1M") %>
// Через год от сегодня (ISO-длительность)
<% tp.date.now("YYYY-MM-DD", "P1Y") %>
// Дата в заголовке файла + 1 день (завтра)
<% tp.date.now("YYYY-MM-DD", 1, tp.file.title, "YYYY-MM-DD") %>
// Дата в заголовке файла - 1 день (вчера)
<% tp.date.now("YYYY-MM-DD", -1, tp.file.title, "YYYY-MM-DD") %>

// Завтрашняя дата
<% tp.date.tomorrow() %>
// Завтрашняя дата с форматом
<% tp.date.tomorrow("Do MMMM YYYY") %> // пример: "22 мая 2025"

// Понедельник текущей недели
<% tp.date.weekday("YYYY-MM-DD", 0) %>
// Следующий понедельник
<% tp.date.weekday("YYYY-MM-DD", 7) %>
// Понедельник недели, на которую указывает заголовок файла
<% tp.date.weekday("YYYY-MM-DD", 0, tp.file.title, "YYYY-MM-DD") %>
// Предыдущий понедельник относительно даты из заголовка файла
<% tp.date.weekday("YYYY-MM-DD", -7, tp.file.title, "YYYY-MM-DD") %>

// Вчерашняя дата
<% tp.date.yesterday() %>
// Вчерашняя дата с форматом
<% tp.date.yesterday("Do MMMM YYYY") %> // пример: "20 мая 2025"

Модуль файла: tp.file

Но в чем Templater по-настоящему хорош, так это во взаимодействии с файлами. Этот модуль содержит 16 функций:


Содержимое текущего файла: tp.file.content

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

Особенность в том, что мы не можем указать какой файл ему следует прочитать. Поэтому в таком случае, нам необходимо открыть "абстрактный файл", чтобы получить его путь, а затем используя await tp.app.vault.read(file) получить содержимое другого файла.

Пример:

<%*
const file = app.vault.getAbstractFileByPath("README.md");
if (tp.file.exists(file)) {
  const content = tp.app.vault.read("README.md");
  tR = content;
}
%>

Создание нового файла: tp.file.create_new

С помощью этой функции мы можем создать новый файл. Она принимает следующие аргументы:

  • template: TFile ⎮ string - содержимое нового файла. Может быть как результатом выполнения шаблона так и произвольным текстом; Для того, чтобы содержимое файла стало результатом исполнения шаблона в качестве аргумента необходимо передать вызов функции tp.file.find_tfile("Название шаблона")

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

  • filename?: string - название файла, которое будет присвоено при создании. Здесь нужно обратить внимание на то, что название файла можно писать без расширения файла, но нельзя указывать путь к папке - эта логика заложена в отдельном методе;

  • open_new: boolean = false - флаг нужно ли открыть заметку после создания. Поскольку флаг опциональный, то ставить false, если вы не хотите открывать файл - не нужно;

    Обратите внимание, что флаг хоть и является опциональным - он также является позиционным аргументом, поэтому, если вы не укажете значение флага false, а затем укажите путь к папке, где необходимо создать файл - указание пути к папке будет проигнорировано, а файл откроется;

  • folder?: TFolder | string - может принимать путь, строку, абстрактный путь или текущую директорию объекта TFolder.

Примеры:

  1. Просто создание файла:

    • Содержимое - строка MyFileContent

    • Название файла - MyFileName (файл будет создан в корне хранилища и иметь расширение .md)

    <%* await tp.file.create_new("MyFileContent", "MyFilename") %>
  2. Содержимое файла берется из шаблона:

    • Содержимое файла - шаблон MyTemplate

    • Название файла MyFileName(файл будет создан в корне хранилища и иметь расширение .md)

    <%* await tp.file.create_new(tp.file.find_tfile("MyTemplate"), "MyFilename") %>
  3. Просто создание файла и открытие его:

    • Содержимое файла - шаблон MyFileContent

    • Название файла MyFileName(файл будет создан в корне хранилища и иметь расширение .md)

    • Опциональный флаг true, который отвечает за то, что файл будет открыт после создания.

    <%* await tp.file.create_new("MyFileContent", "MyFilename", true) %>
  4. Создание файла в текущей папке, откуда вызывается шаблон и открытие его:

    • Содержимое файла - шаблон MyFileContent

    • Название файла MyFileName(файл будет создан в корне хранилища и иметь расширение .md)

    • Опциональный флаг false, который отвечает за то, что файл будет открыт после создания

    • Указание папки с помощью объекта TFile

    <%* await tp.file.create_new("MyFileContent", "MyFilename", false, tp.file.folder(true)) %>
  5. Создание файла в конкретной папке и открытие его:

    • Содержимое файла - шаблон MyFileContent

    • Название файла MyFileName(файл будет создан в корне хранилища и иметь расширение .md)

    • Опциональный флаг false, который отвечает за то, что файл будет открыт после создания

    • Указание папки с помощью указания полного пути

    <%* await tp.file.create_new("MyFileContent", "MyFilename", false, "Path/To/MyFolder") %>
    
  6. Создание файла в конкретной папке и открытие его:

    • Содержимое файла - шаблон MyFileContent

    • Название файла MyFileName(файл будет создан в корне хранилища и иметь расширение .md)

    • Опциональный флаг false, который отвечает за то, что файл будет открыт после создания

    • Указание папки с помощью объекта TFolder

    <%* await tp.file.create_new("MyFileContent", "MyFilename", false, app.vault.getAbstractFileByPath("MyFolder")) %>
  7. Cоздание файла и добавление wiki-ссылки в текущую заметку:

    • Содержимое - строка MyFileContent

    • Название файла - MyFileName (файл будет создан в корне хранилища и иметь расширение .md)

    [[<% (await tp.file.create_new("MyFileContent", "MyFilename")).basename %>]]

Дата создания файла: tp.file.creation_date

В качестве аргумента эта функция принимает формат даты, по умолчанию формат: "YYYY-MM-DD HH:mm", но вы можете выбрать свой

Пример:

<% tp.file.creation_date() %>

Управление курсором: tp.file.cursor

По умолчанию, эта функция устанавливает курсор на месте команды, но в качестве аргумента она принимает порядковый номер order, который связывает "начало пары" и "конец пары".

Порядок перемещения различных курсоров, например, с 1 на 2, с 2 на 3 и так далее. Если вы укажете несколько значений tp.file.cursor с одинаковым порядком, редактор переключится на многокурсорный режим.

Очень удобная штука, если ваш шаблон предполагает некую "системность" и "последовательное" или наоборот "одновременное" заполнение данных.

Особенности "очереди" и "многокурсорного" режимов

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

Решение оказалось простым: нужно открыть горячие клавиши вашего хранилища и найти Templater: Jump to next cursor location (на MacOS по умолчанию Option + Tab).

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

Примеры:

  1. Эта команда просто установит курсор в указанном месте после выполнения шаблона:

    <% tp.file.cursor() %>
  2. Эта команда в свою очередь установит "чек-поинты" курсора после вставки шаблона:

    Название: <% tp.file.cursor(1) %>
    Описание: <% tp.file.cursor(2) %>
  3. Такой шаблон активирует "многокурсорный" ввод при вставке:

    Название: <% tp.file.cursor(1) %>
    Описание: <% tp.file.cursor(1) %>
  4. Такой шаблон комбинирует "многокурсорный ввод "

    Название: <% tp.file.cursor(1) %>
    Описание: <% tp.file.cursor(1) %>
    Итог1: <% tp.file.cursor(2) %>
    Итог2: <% tp.file.cursor(2) %>

Добавление данных после курсора: tp.file.cursor_append

Согласно документации - должно добавлять содержимое после курсора в файле. Но в качестве аргумента принимает только content типа string, т.е. мы не можем скомбинировать с другим шаблоном (как например это было при создании файла) или с "очередностью курсоров".

Возможно, я еще не преисполнился логикой этого плагина, но мне эта функция кажется бессмысленной по той причине, что я ее не понимаю. Более того, текст всегда вставляется не там где я ожидаю, а именно в начале шаблона - это странно

Пример

Тело шаблона:

Вот это начало шаблона

<% tp.file.cursor_append("Какой текст") %>

Результат:

Какой текстВот это начало шаблона


Проверка на существование объекта: tp.file.exists

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

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

Если объект существует, то возвращает булёво значение.

Примеры

  1. Проверка существования файла, указывая полный путь

<% await tp.file.exists("MyFolder/MyFile.md") %>
  1. Проверка существования текущего файла (для наглядности используются дочерние функции file)<% await tp.file.exists(tp.file.folder(true) + "/" + tp.file.title + ".md") %>


Получение данных о файле как об объекте TFile: tp.file.find_tfile

Как я упоминал ранее, полезно знать какие данные можно получить из интерфейса TFile, потому что мы получаем объект, который выведется в формате [object Object], в то время как если использовать свойства класса TFile, то мы получаем конкретные данные об объекте.

Свойства класса TFile:

  • basename - название объекта без расширение;

  • extension - расширение объекта;

  • name - полное название объекта с расширением;

  • path - путь к объекту;

  • stat - информация о файле. Поскольку stat имеет тип FileStats, то у него есть свои свойства, а именно:

    • ctime - время создания файла в миллисекундах;

    • mtime - время последнего редактирования файла в миллисекундах;

    • size - размер файла в байтах;

  • vault - информация о хранилище, в котором расположен файл. Поскольку vault имеет тип Vault, как следствие он наследует его свойства:

    • adapter с типом DataAdapter, в котором содержится исчерпывающая информация о хранилище;

    • configDir, в котором содержится информация о названии директории, в которой находятся конфигурационные файлы текущего хранилища;

Пример

Эта команда получает информацию о текущем файле как об объекте TFile

<% tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md"). %> 

А вот так мы можем получить:

  1. Название текущего файла без расширения:

    <% tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md").basename %> 
  2. Расширение текущего файла:

    <% tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md").extension %> 
  3. Название текущего файла с расширением:

    <% tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md").name %> 
  4. Путь к текущему файлу:

    <% tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md").path %> 
  5. Информацию о времени создания файла:

    <% tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md").stat.ctime %> 
  6. Информацию о времени последнего редактирования файла:

    <% tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md").stat.mtime %> 
  7. Информацию о размере файла:

    <% tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md").stat.size %> 

Получение данных о текущей папке: tp.file.folder

Эта функция получает путь к папке в рамках хранилища, для получения пути к папке в системе необходимо использовать функцию tp.file.path, о которой я расскажу дальше.

В качестве аргумента эта функция принимает флаг absolute типа Boolean, который определяет будет ли возвращен абсолютный путь или только название папки.

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

Пример

  1. Получение названия текущей папки:

    <% tp.file.folder() %>
  2. Получение полного пути до текущей папки в рамках хранилища

    <% tp.file.folder(true) %>
    

Добавление содержимого файла: tp.file.include

Свою предыдущую статью я начал с того, что пытался объяснить себе и читателю на чем базируется идея шаблонов, а именно на переиспользовании данных. И как раз дочерняя функция tp.file.include позволяет "добавлять" в заметку целиком или частично информацию из другой заметки.

Эта функция принимает аргумент include_link, который может быть представлен как в виде обычной строки/пути, так и в виде объекта TFile. Может указывать как на часть заметки (в виде ссылки на заголовок ^название заголовка), так и целиком на заметку.

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

Примеры

  1. Полное включение файла

    <% tp.file.include("[[README]]") %>
  2. Полное включение файла, но получаем как объект TFile

    <% tp.file.include(tp.file.find_tfile("TestNote")) %>
  3. Включение только секции файла

    <% tp.file.include("[[TestNote#Section1]]") %>
  4. Включение только блока файла

    <% tp.file.include("[[TestNote#^block1]]") %>

Получение даты последнего изменения файла: tp.file.last_modified_date

Получает дату последнего изменения текущего файла.

Эта функция принимает аргумент format и по умолчанию возвращает значения в таком виде: "YYYY-MM-DD HH:mm".

Но на мой взгляд эта функция бесполезна по двум причинам:

  1. Мы можем получить информацию о дате последнего изменения текущего файла используя функцию tp.file.find_tfile(tp.file.folder(true) + "/" + tp.file.title + ".md").stat.mtime - соглашусь, что last_modified_date короче, но есть и вторая причина;

  2. Мы не можем получить информацию о редактировании для какого-либо другого файла, т.е. если бы эта функция имела флаг current_file и по умолчанию стояло true, то эффективность этого метода на мой взгляд была бы сильно выше.

Пример

<% tp.file.last_modified_date() %>

Перемещение файла: tp.file.move

Для перемещения файла внутри хранилища используется функция tp.file.move, которая принимает 2 аргумента, которые могут быть TFile:

  • new_path - относительный путь к новому хранилищу без расширения файла. Примечание: новый путь должен включать папку и имя файла, например "/Notes/MyNote".

  • file_to_move -файл для перемещения, по умолчанию используется текущий файл.

Примеры

  1. Перемещение текущего файла

    <% await tp.file.move("/Обучение/Templater/" + tp.file.title) %>
  2. Перемещение текущего файла и переименование:

    <% await tp.file.move("/Обучение/Templater/Перемещение файлов") %>

Получение пути в системе: tp.file.path

Эта функция получает абсолютный путь к файлу в системе.

Принимает аргумент relative, который является флагом (по умолчанию `false).

  • Если установлено значение true, то извлекается только относительный путь внутри хранилища;

  • Если установлено значение false, то извлекается путь к файлу в рамках системы.

Примеры

  1. Извлекается абсолютный путь:

    <% tp.file.path() %>
    // Пример абсолютного пути: 	/Users/Vangeli/Documents/Obsidian/Obsidian/Home/Templates/test_template.md
  2. Извлекается относительный путь внутри хранилища:

    <% tp.file.path(true) %>
    // Пример относительного пути: Templates/test_template.md

Переименование файла: tp.file.rename

Эта функция позволяет изменить имя текущему файлу и принимает аргумент new_title типа string.

Опять же, на мой взгляд, было бы здорово получить 2-й аргумент и указывать какой файл мы хотим переименовать без его непосредственного открытия. Но что хорошо, что этот метод поддерживает конкатенацию строк и можно как-то взаимодействовать с текущим именем и добавлять какие-нибудь "идентификаторы"

Пример:

  1. Простое переименование файла

    <% await tp.file.rename("Файл с новым названием") %>
    
  2. Переименование файла с конкатенацией строк

    <% await tp.file.rename(tp.file.title + "1") %>
    

Получение выделенного текста из текущего файла: tp.file.selection

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

Она не принимает никаких аргументов, но за то возвращает выделенный текст:

Пример

<% tp.file.selection() %>

Получение тегов текущего файла: tp.file.tags

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

Я искренне не понимаю, почему эта функция не может принимать путь к файлу в качестве аргумента, которых тут, к слову, нет, а затем "под капотом" как-то скрыто открывать или использовать tp.app.metadataCache.getTags

Обратите внимание, что эта функция вызывается без ()

Пример

<% tp.file.tags %>

Получение имени текущего файла: tp.file.title

Вы уже видели много раз эту функцию, но это все потому, что получение названия текущего файла действительно часто используется. Можно сделать пользовательский скрипт или пользовательскую функцию для того, чтобы переиспользовать какие-то блоки кода, но все равно мы будем использовать tp.file.title если нам будет нужно получить название текущего файла - очень полезная функция, запомните.


Модуль Frontmatter: tp.frontmatter

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

Альтернатива: получение frontmatter из глобального объекта app

<% tp.app.metadataCache.getFileCache(file).frontmatter %>

Но у этого подхода есть ряд особенностей:

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

    <% tp.app.vault.getAbstractFileByPath('path') %>
  2. На выходе мы получим данные в виде [object Object] и для того, чтобы с ними взаимодействовать - их нужно распарсить:

Например так:

const file = app.vault.getAbstractFileByPath("README.md");

const cache = app.metadataCache.getFileCache(file);
const frontmatter = cache?.frontmatter;

  if (frontmatter) {
    tR = JSON.stringify(frontmatter, null, 2); 
  } else {
    tR = "Frontmatter не найден";
  }

Особенности при получении данных через tp.frontmatter:

  1. Если имя переменной в файле frontmatter содержит пробелы, вы можете сослаться на неё с помощью квадратных скобок следующим образом:

    <% tp.frontmatter["количество дней"] %>
    
  2. Чтобы получать тот результат, который вам нужен, рекомендую сохранять вывод в переменную, а затем по возможности выводить через tR

    <%*
    const количествоДней = tp.frontmatter.["количество дней"]? || "нет значений";
    tR = количествоДней;
    %>
    
    1. Чтобы обработать массив - нужно проитерироваться по массиву, а затем задать правило форматирования через .join

    ---
	список:
	- первый
	- второй
	---

	<%*
	const список = tp.frontmatter.список?.join(", ") || "нет значений";
	tR = список;
	%>

Модуль хуков: tp.hooks

Этот модуль предоставляет хуки, которые позволяют выполнять код при возникновении события Templater.

Перехватывает, когда все активно выполняемые шаблоны завершают работу. В большинстве случаев это будет один шаблон, если только вы не используете tp.file.include или tp.file.create_new.

При многократном вызове этого метода функции обратного вызова будут выполняться параллельно.

В качестве аргумента принимает callback_function - функцию, которая будет вызвана после завершения всех запущенных шаблонов.

Это может быть полезно для какой-то большой сложной логики, работы с ИИ ассистентами (например для исправления орфографических ошибок) или выстраивания целого "пайплайна" из шаблонов.

Мне очень нравится пример из документации, в нем рассматривается вызов линтера после завершения работы шаблона:

Пример

/ Update frontmatter after template finishes executing
<%*
tp.hooks.on_all_templates_executed(async () => {
  const file = tp.file.find_tfile(tp.file.path(true));
  await tp.app.fileManager.processFrontMatter(file, (frontmatter) => {
    frontmatter["key"] = "value";
  });
});
%>
// Run a command from another plugin that modifies the current file, after Templater has updated the file
<%*
tp.hooks.on_all_templates_executed(() => {
  tp.app.commands.executeCommandById("obsidian-linter:lint-file");
});
-%>

Модуль объекта Obsidian: tp.obsidian

Этот модуль предоставляет доступ ко всем функциям и классам Obsidian API. Это в основном полезно при написании скриптов.

Здесь вы можете найти все интерфейсы, с которыми можете взаимодействовать в рамках этого модуля.

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


Модуль системы: tp.system

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

1. Функция извлечения в буфер обмена: tp.system.clipboard

Эта функция извлекает содержимое буфера обмена. Важно оговориться, что это могут быть данные полученные как за пределами Obsidian, так и в нем.

Обратите внимание, что clipboard не только извлекает данные. Поместить данные в буфер можно:

  1. Нажав ctrl/cmd + c;

  2. Вызвав функцию tp.file.selection() - именно она сохраняет выделенный текст в буфер обмена

  3. Вызвав команду tp.system.clipboard() - в качестве единственной команды в файле, в таком случае, если буфер пуст - он поместит в буфер содержимое всего текущего файла, а затем вставит в текущем файле в позиции курсора;

Пример

<% tp.system.clipboard() %>

2. Функция вызова модального окна для ввода текста: tp.system.prompt

С помощью этой функции можно вызвать модальное окно. Его размеры, расположение, да и в целом, оформление редактировать нельзя.

Принимает 4 аргумента:

  1. prompt_text - текст, размещенный над полем ввода;

  2. default_value - значение по умолчанию для поля ввода;

  3. throw_on_cancel - выдает ошибку, если запрос отменен, вместо возврата значения null;

  4. multiline - если установлено значение true, поле ввода будет многострочным. По умолчанию false;

Обратите внимание, что аргументы позиционные, поэтому нужно следить за последовательностью.

Важно отметить, что для сохранения значения в переменной tp.system.prompt() требуется оператор await.

Примеры:

  1. Простое модальное окно без условий:

    <% tp.system.prompt("Вы еще тут?") %>
  2. Модальное окно без условий, с сохранением ответа в переменной:

      <%*
        const answer = await tp.system.prompt("Вы еще тут?");
        tR = answer;
      %>
  3. Модальное окно со значением ответа по умолчанию:

    <% tp.system.prompt("Понравилась ли вам эта статья?", "Возможно") %>
    
  4. Модальное окно с поддержкой многострочного ввода:

    <% tp.system.prompt("Расскажите о задачах, которые вы хотели бы решить с помощью `Templater`?", null, false, true) %>
    

3. Функция вызова модального окна для выбора из предложенных значений: tp.system.suggester

Эта функция похожа на предыдущую и у нее есть те же достоинства и те же недостатки.

Она принимает 5 аргументов:

  1. text_items - массив строк, представляющих текст, который будет отображаться для каждого элемента в подсказке. Это также может быть функция, которая сопоставляет элемент с его текстовым представлением;

  2. items - массив, содержащий значения каждого элемента в правильном порядке;

  3. throw_on_cancel - выдает ошибку, если запрос отменен, вместо возврата значения null;

  4. placeholder -строка-заполнитель для подсказки;

  5. limit - ограничение количества отображаемых элементов (полезно для повышения производительности при отображении больших списков).

Важно отметить, что для сохранения значения в переменной tp.system.suggester() требуется оператор await.

Примеры:

  1. Модальное окно с выпадающим списком, в котором содержатся пары "ключ/значение":

    <% tp.system.suggester(["Python", "JS", "RUST"], ["Python", "JS", "RUST"]) %>
  2. Модальное окно с выпадающим списком файлов:

    [[<% (await tp.system.suggester((item) => item.basename, app.vault.getMarkdownFiles())).basename %>]]
  3. Модальное окно с выпадающим списком по тегам:

    <% tp.system.suggester(item => item, Object.keys(app.metadataCache.getTags()).map(x => x.replace("#", ""))) %>
    
  4. Модальное окно с выпадающим списком, с сохранением ответа в переменной:

      <%*
        const answer = await tp.system.suggester(["Python", "JS", "RUST"], ["Python", "JS", "RUST"]);
        tR = answer;
      %>
    

Веб-модуль: tp.web

Этот модуль содержит функции, связанные с выполнением веб-запросов.

Какие-то из них "забавные", а некоторые на мой взгляд - прям очень важные.

1. Функция получения цитаты дня: tp.web.daily_quote

Эта функция относится, скорее, к забавным. Но может кому-то для оформления дашбордов и может прийтись по душе.

Она не принимает никаких аргументов и возвращает Callout-блок со случайной цитатой. Цитаты берутся из конкретного репозитория .

Пример:

<% tp.web.daily_quote() %>
//Цитата дня 22 мая 2025 г: 
//> [!quote] You must welcome change as the rule but not as your ruler.
//> — Denis Waitley

2. Функция получения случайного изображения: tp.web.random_picture

Эта функция, на мой взгляд, тоже относится к забавным, но кому-то может и будет полезна.

  • size - размер изображения в формате <width>x<height>;

  • query - ограничивает выбор фотографиями, соответствующими поисковому запросу. Можно указать несколько поисковых запросов, разделив их запятыми;

  • include_size - необязательный аргумент для включения указанного размера в ссылку на изображение в формате Markdown. По умолчанию - false.

Примеры:

  1. Случайное изображение

    <% tp.web.random_picture() %>
  2. Случайное изображение, соответствующее размерам

    <% tp.web.random_picture("200x200") %>
  3. Случайное изображение, соответствующее размерам и определенному промпту

    <% tp.web.random_picture("200x200", "landscape,water") %>

3. Функция отправки HTTP запроса: tp.web.request

Учитывая, что web как встроенный инструмент появился в Obsidian не так давно - сообщество еще не успело сделать какие-то полноценные инструменты для работы с вебом. Но, если честно, я сомневаюсь, что в этом есть какая-то необходимость.

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

Функция принимает 2 аргумента:

  • url - URL-адрес, к которому будет направлен HTTP-запрос;

  • path - путь в ответе JSON для извлечения конкретных данных.

Т.е. ограничения у этой функции такие, что ей нужно достучаться куда-то, где ей вернут json, чтобы получить конкретные данные из этого JSON-a.

Но, чтобы данные отобразились - нужно написать адаптер, который сможет преобразовать JSON из объекта в любой другой тип.

К тому же, мы не можем посылать POST запросы, для этого можно использовать обычный fetch - реализацию я прикреплю дальше.

Примеры:

  1. Простой запрос

    <% tp.web.request("https://jsonplaceholder.typicode.com/todos/1") %>
  2. Запрос с указанием пути для получения данных конкретного объекта

    <% tp.web.request("https://jsonplaceholder.typicode.com/todos", "0.title") %>
  3. Запрос с указанием пути для получения данных конкретного объекта и его "маппинг":

    <%*
    const data = await tp.web.request("https://jsonplaceholder.typicode.com/todos");
    const limited = data.slice(0, 5); // ограничиваем первыми 5 элементами, потому что их там больше 100
    tR = JSON.stringify(limited, null, 2);
    %>
    [
      {
        "userId": 1,
        "id": 1,
        "title": "delectus aut autem",
        "completed": false
      },
      {
        "userId": 1,
        "id": 2,
        "title": "quis ut nam facilis et officia qui",
        "completed": false
      },
      {
        "userId": 1,
        "id": 3,
        "title": "fugiat veniam minus",
        "completed": false
      },
      {
        "userId": 1,
        "id": 4,
        "title": "et porro tempora",
        "completed": true
      },
      {
        "userId": 1,
        "id": 5,
        "title": "laboriosam mollitia et enim quasi adipisci quia provident illum",
        "completed": false
      }
    ]
    

Альтернатива:

Но ничего нам не мешает делать curl или fetch запросы.

1. Простой fetch запрос:

<%*
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    title: "Пример",
    body: "Это POST-запрос через fetch",
    userId: 42
  })
});

if (!response.ok) {
  tR = `Ошибка: ${response.status}`;
} else {
  const data = await response.json();
  tR = JSON.stringify(data, null, 2);
}
%>

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

Поэтому я предпочитаю "переиспользовать" JS скрипты.

2. Пользовательский скрипт

Дальше мы рассмотрим как работают пользовательские функции, поэтому позволю забежать немного вперед:

Мы можем реализовать собственную функцию (например send_post, в котором можем отправлять post запросы) - это удобно и можно переиспользовать.

// Тело шаблона
<%*
const response = await tp.user.send_post(
 tp, "https://jsonplaceholder.typicode.com/posts",
 {
   title: "Пример",
   body: "Это отправка через пользовательский скрипт",
   userId: 123
 }
);

tR = JSON.stringify(response, null, 2);
%>
// Пример вывода:
// {
//   "title": "Пример",
//  " body": "Это отправка через пользовательский скрипт",
//   "userId": 123,
//   "id": 101
// }
// Пользовательская функция
const send_post = async (tp, url, payload = {}) => {
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload)
  });

  if (!res.ok) {
    throw new Error(`Ошибка ${res.status}: ${res.statusText}`);
  }

  return await res.json();
};

module.exports = send_post;

Бонус: Отправка данных в телеграм

У меня как-то стояла задача сделать оповещение в телеграм для совместной работы над проектом и тогда я понял, что данные можно с легкостью отправлять в телеграм.

Тогда я реализовал небольшой класс, который отправлял мои данные:

class TelegramBot {  
    constructor(botToken, chatId, messageThreadId) {  
        this.botToken = botToken;  
        this.chatId = chatId;  
        this.topics = topics;  
    }  
  
    async sendMessage(topic, message) {  
        console.log(`message to sent: ${message}`);  
        const messageThreadId = this.topics[topic];  
        if (!messageThreadId) {  
            console.error(`Топик "${topic}" не найден в словаре.`);  
            return;  
        }  
  
        const url = `https://api.telegram.org/bot${this.botToken}/sendMessage`;  
        const response = await fetch(url, {  
            method: "POST",  
            headers: { "Content-Type": "application/json" },  
            body: JSON.stringify({  
                chat_id: this.chatId,  
                message_thread_id: messageThreadId,  
                text: message,  
                parse_mode: "MarkdownV2",
            }),  
        });  
  
        if (!response.ok) {  
            const errorText = await response.text();  
            console.error(`Ошибка отправки сообщения в топик "${topic}":`, errorText);  
            throw new Error(errorText);  
        }  
  
        console.log(`Сообщение успешно отправлено в топик "${topic}"!`);  
    }  
}  

// Пример вызова:
const message = MarkdownEscaper.escape(`📝 Создан новый файл: ${file.name}\n📂 Путь: ${file.path}`);
await telegramBot.sendMessage("create_files", message);

Хочу оговориться, что не использовал здесь пользовательский скрипт tp.user.send_post, потому что этот шаблон мог использоваться на нескольких устройствах и как следствие - нужно было бы проконтролировать, чтобы каждый настроил виртуальное окружение правильно, поэтому во всем нужно находить компромисс и не гнаться за "бест практис".


Пользовательские функции

В самом начале, я упоминал, что можно использовать js код, для его последующего переиспользования.

Пользовательские функции бывают двух видов:

  1. Пользовательские скрипты;

  2. Системные команды.

Пользовательские скрипты

В самом начале, я упомянул, что использую JS скрипты для переиспользования, потенциально повторяющегося кода.

Этот тип пользовательских функций позволяет вызывать скрипты JavaScript из файлов JavaScript и получать их выходные данные.

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

Особенность пользовательских функций в том, что они должны быть созданы в отдельных js файлах, которые:

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

  • функции должны соответствоать спецификации модулей CommonJS;

  • если внутри функции используется Templater или app, то необходимо также передать экземпляр класса tp

Пример

Содержимое шаблона с Templater командой

<%* tp.user.create_file_if_not_exist(tp, 'README.md', 'Спишь?') %>;

Содержимое файла create_file_if_not_exist.js:

async function createFileIfNotExists(tp, filePath, content) {
    if (!(await tp.file.exists(filePath))) {
        await tp.file.create_newcreate(content, filePath);
    } else {
    }
}

module.exports = createFileIfNotExists;

2. Системные команды

Этот тип функций немного отличается от всего того, что мы с вами увидели в этой статье, по ряду причин:

  1. Создается и регистрируется он в настройках плагина в соответствующем разделе;

  2. Чтобы определить новую пользовательскую функцию системной команды, вам нужно задать имя функции, связанное с системной командой.

  3. Вы можете использовать внутренние функции в своей системной команде. Внутренние функции будут заменены до выполнения вашей системной команды.


Динамические команды

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

Чтобы объявить динамическую команду, добавьте знак «плюс +» после открывающего тега команды: <%+, при этом закрывающий тег не меняется: %>

Дата последнего изменения файла: <%+ tp.file.last_modified_date() %>

Но это рудиментарная функция и сам разработчик рекомендует использовать DataView, а я же советую не ограничиваться на этом и рассмотреть возможности MetaBind - не всегда и не во всем может быть полезно, но тем ни менее.


Способы вызова шаблонов

В этом блоке мне хотелось бы рассказать в чем разница между create temlate и insert template, почему некоторые шаблоны не работают, если вызвать их из режима "просмотра", а также как можно вызвать исполнение шаблона с помощью встроенных и сторонних инструментов.

Разница между create template и insert template

Какими бы говорящими названия не были - разницу понимать надо, т.к. поначалу у меня возникали трудности.

Особенности create template

Этот способ выполнения шаблона подразумевает, что новая заметка будет целиком сформирована из "шаблона". Т

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

При этом create template можно вызывать из режима просмотра, но важно чтобы шаблон мог получить все необходимые данные для интерполяции (например если вы используете tp.file.selection, то у вас могут возникнуть проблемы, потому что текст может быть не выделен).

Особенности instert template

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

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

Таким образом, шаблон реализует операции следующих типов:

  • навигация по хранилищу;

  • проверка существования файлов;

  • создание файлов;

  • запись в текущий файл.

Пример реализации такого скрипта

<%*
let created = 0;
let skipped = [];

for (let i = 1; i <= 100; i++) {
  const filename = `${i}.md`;
  const folder = "Тест";
  const fullpath = `${folder}/${filename}`;

  if (!tp.file.find_tfile(fullpath)) {
    await tp.user.create_file_if_not_exist(tp, fullpath, "Спишь?");
    created++;
  } else {
    skipped.push(filename);
  }
}

tR = `Создано файлов: ${created}\nПропущено (уже существовали): ${skipped.join(", ")}`;
%>

//Создано файлов: 96
//Пропущено (уже существовали): 1.md, 2.md, 4.md, 12.md

Способы вызова шаблонов:

Как я упоминал вначале статьи, шаблоны можно вызывать несколькими способами:

  1. С помощью палитры команд (Ctrl/Cmd + P или иконка палитры команд в боковом (левом) меню): для этого мы можем начать вводить как имя шаблона, а затем выбрать способ выполнения шаблона, так и ввести название плагина, а затем выбрать из зарегистрированных в нем шаблонов.

    Обратите внимание, что если у вас назначены горячие клавиши на конкретный способ выполнения конкретного шаблона, то такие комбинации отобразятся в палитре.

    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 6.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 6.png
  2. С помощью иконки на боковой (левой) панели управления: особенность этого способа, что мы получаем список зарегистрированных шаблонов и ничего больше.

    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 7.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 7.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 8.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 8.png

    UPD: напомню, что ваши шаблоны должны быть зарегистрированы в меню плагина

  3. С помощью инкапсуляции шаблонов: напомню, что нам доступна функция tp.file.include, которая позволяет получить данные из другого файла, а что нам мешает использовать в качестве другого файла - другой шаблон? Правильно, ничего не мешает.

    Можно также использовать tp.hooks для создания более хитрой и сложной логики ваших шаблонов, поэтому тут все упирается только в вашу фантазию и задачу.

  4. С помощью сторонних плагинов(QuickAddMetaBind): мы помним, что мы можем получить команды, зарегистрированные в плагине через tp.app.commands.commands, а затем использовать это в других плагинах

    Для понимания покажу как выглядит получение списка доступных команд:

    	<%*
    	const commands = Object.entries(app.commands.commands)
    	  .filter(([id, cmd]) => id.startsWith("templater"))
    	  .map(([id, cmd]) => `${id}: ${cmd.name}`);
    	
    	tR = commands.join("\n");
    	%>
    
    // Пример вывода:
    // templater-obsidian:insert-templater: Templater: Open insert template modal
    // templater-obsidian:replace-in-file-templater: Templater: Replace templates in the active file
    // templater-obsidian:jump-to-next-cursor-location: Templater: Jump to next cursor location
    // templater-obsidian:create-new-note-from-template: Templater: Create new note from template
    // templater-obsidian:Templates/Многострочный Callout.md: Templater: Insert Templates/Многострочный Callout.md
    // templater-obsidian:create-Templates/Многострочный Callout.md: Templater: Create Templates/Многострочный Callout.md
    

    Но в плагинах, которые мы можем использовать для вызова шаблонов это интегрировано в UI.

    Вызов с помощью QuickAdd

    Этот способ является одним из самых распространенных, т.к. он используется в связке с Commander, который позволяет добавлять QuickAdd команды на панель управления.

    Создание команды в QuickAdd

    Предполагается, что у нас в хранилище уже установлен плагин QuickAdd. Для создания команды нам необходимо:

    1. перейти в Настройки хранилища;

    2. откройте Меню плагина QuickAdd;

    3. в поле Templates укажите название команды

    4. нажмитеadd choise для регистрации команды;

    5. активируйте иконку молнии напротив добавленной команды;

    6. перейдите в настройки добавленной команды нажав на шестеренку;

    7. в поле Templates Path выберите ваш шаблон, который будет использоваться (обратите внимание, что все шаблоны в QuickAdd используют способ выполнения шаблона instert template);

    8. активируйте флаг в поле File Name Format;

    9. введите имя для временного файла в поле File Name, для того, чтобы плагин создал файл, который будет использован QuickAdd как файл из которого будет вызван шаблон;

    10. активируйте флаг в поле Create in folder;

    11. укажите дирректорию, в которой будет создан этот временный файл;

    12. в поле Set default behaviour if file already exist выберите опцию owerwrite the file это нам нужно для того, чтобы если какие-то данные записывались в этот временный файл, то при следующем вызове вашего шаблона они перезаписались и не занимали память и не вызывали никаких случайных конфликтов.

    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 9.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 9.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 10.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 10.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 11.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 11.png

    Создание команды/иконки на панели управления в Commander, на основе команды, зарегистрированной в QuickAdd

    Предполагается, что у нас в хранилище уже установлен плагин Commander. Для создания команды нам необходимо:

    1. перейти в Настройки хранилища;

    2. откройте Меню плагина Commander;

    3. выберите на какой панели вы хотите добавить иконку;

    4. нажмите кнопку "Добавить команду";

    5. введите название команды QuickAdd;

    6. нажмите "Сохранить" для регистрации команды;

    7. опционально: выберите как будет расположена иконка, какой у нее будет цвет и заголовок;

    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 12.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 12.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 13.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 13.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 14.png
    Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 14.png

    Вызов с помощью Commander, команды зарегистрированной на основе Templater

    Логика регистрации команды аналогична, той, что описана выше, но есть пара отличий. Давайте пройдемся по полному циклу создания команды в Commander на основе команды, зарегистрированный в Templater:

    Для создания команды нам необходимо:

    1. перейти в Настройки хранилища;

    2. откройте Меню плагина Commander;

    3. выберите на какой панели вы хотите добавить иконку;

    4. нажмите кнопку "Добавить команду";

    5. введите название команды Templater, учитывая способ выполнения команды (create template или insert template);

    6. нажмите "Сохранить" для регистрации команды;

    7. опционально: выберите как будет расположена иконка, какой у нее будет цвет и заголовок;

Вызов с помощью MetaBind

Функционал плагина MetaBind, как по мне, заслуживает в аналогичном подробном разборе - уж очень это сильный и интересный интсрумент. Он позволяет создавать разные формы ввода внутри заметки, при этом они могут динамически изменяться, но об этом я подробно расскажу в отдельной статье.

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

	```meta-bind-button
style: primary
label: Create Lecture Note
actions:
  - type: templaterCreateNote
    templateFile: "templates/Lecture Template.md"
    folderPath: Lectures
    fileName: "New Lecture Note - RENAME ME"
	```

К слову, для создания кнопок можно использовать как интерфейс плагина, так и написать содержимое вручную (признаться, я долго не знал об интерфейсе и писал все вручную).

Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 15.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 15.png

Мы также можем использовать команды, зарегистрированные в QuickAdd

	```meta-bind-button
	label: This is a button
	icon: ""
	hidden: false
	class: ""
	tooltip: ""
	id: ""
	style: primary
	actions:
	  - type: command
	    command: quickadd:choice:6bea8eb7-39b3-4cde-8546-dcdaf8defc05
	
	```
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 16.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 16.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 17.png
Салфетка/Vangeli/Obsidian 4 Templater/Медиа/Рис. 17.png

Заключение

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

Эта статья не вызовет сиюминутного интереса, но тем ни менее, хочется верить, что тот, кто захочет разобраться как устроен Templater - увидит эту статью. Ваши возможности при использовании этого инструмента зависят исключительно от вашего желания, во многом мой интерес к программированию возник благодаря ему. Я хотел автоматизировать какие-то рутинные задачи, а потом заигрался, выстраивая в Obsidian собственную систему управления задачами и ведения проектов (об этом вы можете почитать вот тут). И только потом, когда я научился решать большинство задач, которые могут возникнуть у меня или моих друзей - я успокоился в изучении всех его возможностей.

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

Буду рад, если в комментариях вы поделитесь практическими задачами, которые хотели бы решать с помощью Obsidian, или опишите интересные решения, которые вам уже удалось реализовать.

Мне также было бы приятно, если бы вы положительно оценили эту статью и участвовали в обсуждении.

Теги:
Хабы:
+27
Комментарии21

Публикации

Ближайшие события