Пишем плагины для Obsidian. Часть 1

от автора

Вступление

После шумихи с Notion все ринулись кто куда, но так сложилось, что по большей части все стали смотреть в сторону Obsidian. И Хабр заполонили статьи про Obsidian и про плагины для Obsidian.

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

А сейчас же я хотел бы заполнить сложившийся вакуум собственным гайдом.

Что мы будем делать

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

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

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

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

Вторая статья находится здесь.

Что нужно, чтобы написать плагин

Я долго не решался даже смотреть в сторону собственных плагинов, потому что думал, что для их написания необходим фронтенд-зоопарк из TypeScript, Electron, npm, Node.js или чего-то еще подобного.

Официально документация Obsidian настоятельно рекомендует использовать TypeScript и npm и использует только такой подход в своем GitHub с примерами. Я же в свою очередь утверждаю, что если вы хотите просто сесть и написать малюсенький плагин, у вас нет необходимости даже в этом стеке.

Четыре плагина которые мы напишем — это история про то как написать main.js к каждому из них. Ну, еще manifest.json, но там совсем пара строчек.

Краткий обзор API

У Obsidian есть официальная документация по разработке плагинов. Она содержит, как гайды по разным тематикам, так и полноценный API Reference, где перечислены все классы, доступные нам для разработки.

От себя могу сказать, что гайды достаточно поверхностные и вводят только в самый-самый курс дела, не вдаваясь куда-то глубоко. Рыться же в API Reference самостоятельно тоже порой бывает проблематично, когда сам недостаточно уверен в том, что ищешь. На худой конец у Obsidian есть Discord-канал, где комьюнити сможет помочь вам в чате plugin-dev.

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

Важные логические классы:

  • Plugin. От него наследуются, чтобы создать плагин

  • App. Это точка сбора всех крупных, значимых синглтонов, крутящихся в Obsidian

  • Vault для работы с папками и файлами хранилища

  • FileManager для более узкоспециализированной работы с папками и файлами хранилища. Иногда не угадаешь, где находится нужный тебе функционал: в Vault или FileManager

Важные UI-классы:

  • Notice. Для мелких popup-сообщений пользователю

  • Modal. Диалоговое окно, в т.ч. с возможностью ввода данных

  • ItemView. Кастомный гуй, куда можно рендерить любой HTML-контент

  • Workspace. Класс, заведующий всеми view на экране. Все вкладки, сплиты и прочее управляются через Workspace

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

const removePrefix = (value, prefix) =>     value.startsWith(prefix) ? value.slice(prefix.length) : value; 

Да, по странным стечениям обстоятельств написание плагинов для Obsidian часто требует удалять префикс из строки, а интерфейс String не может предложить нам ничего подходящего ¯\_(ツ)_/¯.

Скелет плагина

В самом минималистичном варианте, чтобы написать плагин вам нужно:

  • В вашем, желательно тестовом, Obsidian-vault’е найти папку .obsidian/plugins

  • Создать в этой папке папку с именем вашего плагина

  • Разместить в папке плагина файл manifest.json с информацией о плагине

  • Разместить в папке плагина файл main.js с кодом плагина

На самом деле как бы и все 🙂 Плагин сразу подхватится Obsidian, и его можно будет включить в настройках как обычный Community plugin.

Сперва всегда создаем файл manifest.json, где декларируем всю информацию о плагине:

{ "id": "test-habr-plugin", "name": "Test Habr Plugin", "version": "1.0.1", "minAppVersion": "1.0.0", "description": "Habr stronk", "author": "askepit", "authorUrl": "", "helpUrl": "", "isDesktopOnly": false } 

Я думаю, даже нет смысла описывать, что здесь происходит — все поля имеют хорошие говорящие названия. Вся указанная в манифесте информация будет использована Obsidian для отображения информации о вашем плагине в разделе настроек Community plugins.

Главное, не повторяйте мою ошибку! Я по глупости сперва указал в моих плагинах "isDesktopOnly": true будучи уверенным, что на мобильные устройства задеплоить рукописные плагины сложно, и я не буду этим заниматься. Спойлер — деплой на mobile не стоит практически ничего. При условии, конечно, что вы заблаговременно выставите "isDesktopOnly": false. Я же за свою оплошность поплатился долгим тупняком и даже просьбами о помощи в официальном Discord-канале, поскольку напрочь забыл про это манифест-поле. В общем, не повторяйте мою ошибку 🙂

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

'use strict' var obsidian = require('obsidian')  class TestHabrPlugin extends obsidian.Plugin {     async onload() {      } }  module.exports = TestHabrPlugin 

Эта болванку вы можете смело копировать из одного своего плагина в другой. Класс, который мы отнаследовали от obsidian.Plugin — наша основная сущность, управляющая всем жизненным циклом нашего плагина.

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

async onload() { console.log('Habr stronk') } 

то при включении плагина в dev-tools консоль Обсидиана (Ctrl + Shift + I) мы увидим наше сообщение:

Плагин 1. Guitar tabs viewer

Итак, начинаем с самого простого и незначительного плагин для разминки и легкого вкатывания.

Задача

У меня есть записи с гитарными табулатурами, которые я храню в таком виде:

E|---------------------------------| B|---------------------------------| G|---------------------------------| D|-------3-5-4-3-------------------| A|-5---5---------5-3---3-0-0-3-3-4-| E|---------------------------------| 

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

E|—————————————————————————————————| B|—————————————————————————————————| G|—————————————————————————————————| D|———————3—5—4—3———————————————————| A|—5———5—————————5—3———3—0—0—3—3—4—| E|—————————————————————————————————| 

Бывают и другие линии — ASCII-символов для горизонтальных черточек предостаточно. Меня порядочно коробит этот визуальный раздрай. К тому же хочется как-то минимизировать само присутствие линий на табулатурном стане.

Поэтому я хочу, чтобы view-режим страницы с табулатурами приводил все известные мне виды тире к единому знаменателю, и в качестве этого знаменателя я выбрал символ Middle dot ·, как визуально занимающий самое малое место. Наиболее экстремальный вариант — это, конечно, пробел, но тогда все цифры просто «зависают в воздухе», и читабельность резко снижается.

В общем, хочу видеть все табулатуры вот так:

E|·································| B|·································| G|·································| D|·······3·5·4·3···················| A|·5···5·········5·3···3·0·0·3·3·4·| E|·································| 

Табулатуры в записях мы будем оформлять вот таким блоком кода:

```tab E|---------------------------------| B|---------------------------------| G|---------------------------------| D|-------3-5-4-3-------------------| A|-5---5---------5-3---3-0-0-3-3-4-| E|---------------------------------| ``` 

Плагин должен отрабатывать только для блоков с припиской tab или tabs.

Кстати, плюсану в карму тому, кто угадает, что это за песня 🙂

Реализация

Задача сводится к тому, что страница режиме просмотра должна подменять содержимое блоков кода с пометкой tab или tabs. В терминах Obsidian это называется markdown post-processing — когда ваш markdown уже отрендерен в HTML, и вы можете вклиниться в этот HTML перед самой отрисовкой страницы и подменить там элементы, как вам хочется.

У класса Plugin есть удобный метод registerMarkdownPostProcessor(), предназначенный как раз для этой операции, поэтому реализация плагина по сути тривиальна:

class GuitarTabsViewerPlugin extends obsidian.Plugin {     async onload() {         this.registerMarkdownPostProcessor((element, context) => {             const codeblocks = element.findAll('code')                    for (let codeblock of codeblocks) {                 const blockName = removePrefix(codeblock.className, 'language-')                 if (blockName != "tab" && blockName != "tabs") {                     continue                 }                  const targetSymbol = '·'                  codeblock.innerHTML = codeblock.innerHTML                     .replaceAll('-', targetSymbol) // minus sign                     .replaceAll('–', targetSymbol) // en-dash                     .replaceAll('—', targetSymbol) // em-dash                     .replaceAll('─', targetSymbol) // horizontal line                     .replaceAll('‒', targetSymbol) // figure dash             }         })     } } 

Что здесь происходит: element — это корневой html-элемент контента, что-то вроде <body>. Наша задача — отыскать все теги <code class="language-tab">. Блоки кода помечаются Обсидианом как класс language-X, где X — имя вашего языка программирования, ну или в нашем случае это та самая пометка tab/tabs.

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

Я вас поздравляю, наш первый плагин, делающий что-то осмысленное готов:

Вверху — табулатура в режиме редактирования, внизу — в режиме чтения.

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

Плагин 2. Suggest TODO

Задача

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

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

Как это должно работать: у нас есть заметка с пунктами, например:

или даже так:

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

Ответ с рекомендованным TODO вывести на экран в отдельном диалоговом окошке.

Реализация

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

function suggestTodoImpl(markdown) {     const todos = markdown.split("\n")         // find TODOs         .filter(line => {             if (line.startsWith('- [x]')) return false             return line.startsWith('- ') || line.startsWith('- [ ]')         })         // prettify TODOs         .map(line => removePrefix(removePrefix(line, '- [ ]'), '- ').trim())      if (todos.length === 0) {         return null     }      const randomLine = todos[Math.floor(Math.random() * todos.length)]     return randomLine } 

Имея на руках эту кор-логику можно приступать к самому плагину. Сначала дадим понять плагину, что мы хотим свой Command и иконку на Ribbon:

class TodoSuggestPlugin extends obsidian.Plugin {     async onload() {         this.addCommand({             id: 'Suggest-random-todo',             name: 'Suggest random TODO',             callback: () => {this.suggestTodo()}         })          this.addRibbonIcon('dice', 'Suggest random TODO', (evt) => {             this.suggestTodo()         })     } } 

Оба коллбека апеллируют к this.suggestTodo(), который нами еще не написан. Но мы это скоро исправим. В this.addCommand() мы регистрируем команду, которую сможем вызвать через Ctrl + P, а this.addRibbonIcon() добавит на левую панель иконку dice с изображением игральной кости. Оба действия приведут к одной логике:

async suggestTodo() { const activeView = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView) if (!activeView) { new Notice("No active note found!") return }  let content if (activeView.getMode() === "source") { // Editor mode: Get content from the editor const editor = activeView.editor content = editor.getValue() } else if (activeView.getMode() === "preview") { // Reading mode: Read content from the file const file = activeView.file content = await this.app.vault.read(file) }  if (!content) { new Notice("Could not read content!") return }  const todo = suggestTodoImpl(content)  if (!todo) { new Notice("No TODOs available!") return }  new ResultModal(this.app, todo).open() } 

Код, пусть и кажется большим, своей большей частью является прелюдией к строчке const todo = suggestTodoImpl(content). Задача же найти тот самый content сводится к тому, чтобы сначала найти активную view в нашем Obsidian, а затем извлечь из нее markdown.

Первое делается через this.app.workspace.getActiveViewOfType(obsidian.MarkdownView), второе же зависит от режима в котором сейчас находится view: чтения или редактирования. Поэтому мы и имеем это странное ветвление в попытках заполнить content.

Так же мы встречаем два новых для нас класса:

  • Notice

  • ResultModal

Notice — это маленькое popup-окошко с оповещением, которое всплывет на пару секунд в верхнем правом углу окна. Например, вот так:

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

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

Чтобы создать свое модальное окно, нужно отнаследоваться от Modal и описать в конструкторе класса наполнение окна:

class ResultModal extends obsidian.Modal {     constructor(app, todo) {         super(app)          this.setTitle('Your TODO')         this.setContent(todo)          new obsidian.Setting(this.contentEl)             .addButton((btn) =>                 btn                 .setButtonText('OK')                 .setCta()                 .onClick(() => {                     this.close()                 })             )     } } 

Кнопка OK просто закрывает модальное окно.

Плагин готов! Вы можете использовать его через Command palette, запустив команду Suggest TODO:

Или просто нажать на иконку на панели слева:

Плагин 3. Top-10 recent edited notes

Задача

У меня до последнего момента в Obsidian был установлен жирный плагин Dataview, на котором нельзя разве что запустить космический корабль. Плагин привносит в ваши заметки sql и JavaScript, чтобы вы могли жонглировать вашими данными и получать хитрые сводки и выжимки, как вам вашей душе будет угодно.

Мне все это вычислительное изобилие не нужно, и Dataview стоял у меня только с одной целью — иметь под рукой волшебную заметку со списком top-10 последних отредактированных в хранилище файлов. Вы просто создавали заметку вот c таким содержимым:

```dataview TABLE file.ctime as "Time Modified" SORT file.ctime DESC LIMIT 10 ``` 

и в режиме просмотра она показывала вам результат этого «sql»-запроса, с возможностью быстрого перехода по файлам из сводки:

У меня в тестовом хранилище не наберется 10 файлов, но суть вы поняли.

Такой список мне необходим, поскольку, не знаю, закономерно ли это для всех или нет, но

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

И да, я знаю о существовании core-плагина Quick Switcher, который можно вызвать по Ctrl + O. Его проблема в том, что он показывает топ недавно открывавшихся файлов, а это немного другое, и ломает мой флоу.

Итак, наш план:

  • Сделать top-10, но не в виде заметки, а в виде кастомной ui-страницы на правой панели — где находятся теги, содержание страницы, ссылки и т.п.

  • Сделать не фиксированный top-10, а скорее top-N, где N будет настраиваться в настройках плагина

  • Удалить тяжеловесный Dataview за ненадобностью

Реализация

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

  • Достаем список всех файлов хранилища

  • Сортируем их по дате изменения

  • Ограничиваем список N файлами

  • Куда-то отрисовываем результат в виде работающих ссылок на файлы

Так как мы планируем работать с файловым хранилищем в целом, нам нужен класс Vault, который как раз специализируется на таких задачах. У него есть метод Vault.getMarkdownFiles(), который выдаст нам список из объектов TFile. У этого класса в свою очередь можно достать информацию о дате последнего изменения файла: file.stat.mtime. Опишем свободную функцию для этой логики:

function getTopNFiles(plugin, n) { const files = plugin.app.vault.getMarkdownFiles().sort( (f1, f2) => { return f2.stat.mtime - f1.stat.mtime } )  if (files.length > n) { files.length = n }  return files } 

Остается понять, где этот код будет вызван. В текущем плагине мы хотим рендерить контент в особую панель справа. Это рендеринг в кастомный View. В Obsidian Docs есть хороший гайд, который рассказывает, как работать с View: как их объявлять, регистрировать в плагине, отображать и наполнять. Мы пойдем прямо по стопам этого гайда, и получим вот такой код:

class RecentEditedNotesPlugin extends obsidian.Plugin {     async onload() {         this.registerView(             VIEW_TYPE_RECENT_EDITED_NOTES,             (leaf) => new RecentEditedNotesView(leaf, this)         )         this.activateView()     }      async activateView() {         const { workspace } = this.app              let leaf = null         const leaves = workspace.getLeavesOfType(VIEW_TYPE_RECENT_EDITED_NOTES)              if (leaves.length > 0) {             // A leaf with our view already exists, use that             leaf = leaves[0]         } else {             // Our view could not be found in the workspace, create a new leaf             // in the right sidebar for it             leaf = workspace.getRightLeaf(false)             await leaf.setViewState({ type: VIEW_TYPE_RECENT_EDITED_NOTES, active: true })         }              // "Reveal" the leaf in case it is in a collapsed sidebar         workspace.revealLeaf(leaf)     } }  const VIEW_TYPE_RECENT_EDITED_NOTES = 'recent-edited-notes-view'  class RecentEditedNotesView extends obsidian.ItemView {     plugin = null      constructor(leaf, plugin) {         super(leaf)         this.plugin = plugin     }      getViewType() {         return VIEW_TYPE_RECENT_EDITED_NOTES     }      getDisplayText() {         return 'Recent edited notes'     }      async onOpen() {     } }  

Это все — служебная обвязка, списанная с гайда, чтобы объявить и отобразить на правой панели новый view с айди recent-edited-notes-view:

View пустой, мы будем его заполнять в методе async onOpen(), который пока что тоже пуст. Для начала подумаем о высокоуровневом поведении: когда и при каких условиях содержимое view должно обновляться?

  • На старте Obsidian

  • При изменении какого-то файла

  • При переименовывании какого-то файла

Так и запишем:

async onOpen() { this.plugin.app.vault.on('modify', (file) => { this.update() }) this.plugin.app.vault.on('rename', (file) => { this.update() })  this.update() } 

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

Мы практически на финишной прямой — осталось написать функцию update(), которая заполнит View нашим топом:

update() { const container = this.containerEl.children[1] container.empty()  container.createEl('h4', { text: 'Top-10 recent edited notes' })  const files = getTopNFiles(this.plugin, 10) const ul = container.createEl('ul')  for (const file of files) { const li = ul.createEl('li') const link = li.createEl('a', { text: file.basename }) } } 

Почему в this.containerEl.children[1] индекс 1, а не ноль — честно, не скажу, так было указано в гайде, и так работает, а экспериментировать я не стал 🙂

Функция добавляет на view заголовок, достает список файлов через getTopNFiles() и программно создает html вида:

<li>     <a>...</a>     <a>...</a>     <a>...</a>     ... </li> 

И мы получаем результат:

Опять-таки, у меня не 10 файлов, но вы поняли.

Но у нас не работают ссылки! И как только я не пытался изголяться, чтобы заставить это работать: прописывал href, другие атрибуты; читал официальную документацию; дебажил ссылки в Obsidian и пытался добавлять в теги <a> подсмотренные у Obsidian классы, такие как .internal-link — все было без толку. В итоге нашел ответ в Discord-канале — правильное работающее решение выглядит так:

link.addEventListener("click", (event) => { event.preventDefault() // Prevent default link behavior app.workspace.openLinkText(file.path, "", false) // Open the note }) 

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


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

class RecentEditedNotesSettingTab extends obsidian.PluginSettingTab {     plugin = null        constructor(app, plugin) {         super(app, plugin)         this.plugin = plugin     }        display() {         let { containerEl } = this          containerEl.empty()          new obsidian.Setting(containerEl)             .setName('List length')             .setDesc('How long is your list of recently edited notes')             .addText((text) =>                 text                 .setValue(this.plugin.settings.listLength)                 .onChange(async (value) => {                     this.plugin.settings.listLength = value                     await this.plugin.saveSettings()                 })             )     } }  const DEFAULT_SETTINGS = {     listLength: 10, }  class RecentEditedNotesPlugin extends obsidian.Plugin {     settings = null      async onload() {         await this.loadSettings()         this.addSettingTab(new RecentEditedNotesSettingTab(this.app, this))     ...     }      async loadSettings() {         this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())     }      async saveSettings() {         await this.saveData(this.settings)     } } 

Мы объявили вкладку с настройками RecentEditedNotesSettingTab, заполнили ее полем List length и зарегистрировали в плагине. Сохранение и загрузка так же учтены. Теперь мы в состоянии использовать эту настройку по назначению в нашем View. Мы изменим два места.

Первое:

-container.createEl('h4', { text: 'Top-10 recent edited notes' }) +container.createEl('h4', { text: `Top-${this.plugin.settings.listLength} recent edited notes` })  

Второе:

-const files = getTopNFiles(this.plugin, 10) +const files = getTopNFiles(this.plugin, this.plugin.settings.listLength) 

Теперь, если пойти в настройки, мы найдем наш плагин в графе Community plugins:

Это единственный плагин с настройками в моем тестовом хранилище, поэтому он здесь присутствует в гордом одиночестве.

Теперь пойдем в мой личный Vault и протестируем, как работает настройка, если изменить значение на 20:

Готово!

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

Промежуточные итоги

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

А что с мобильным Obsidian?

Если у вас есть Obsidian Sync, вы получите свои плагины сразу же после синхронизации устройств. Главное не забудьте про "isDesktopOnly": false в manifest.json! Иначе плагины будут показываться на мобильном устройстве, но будут отказываться включаться.

Если вы синхронизируетесь каким-то другим способом, просто добейтесь того, чтобы в папке .obsidian/plugins оказались папки ваших плагинов, и все так же станет работать.

А где четвертый плагин?

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

Где взять исходный код

После выпуска второй статьи я размещу ссылку на GitHub со всеми плагинами.

Как опубликовать плагин для сообщества Obsidian?

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

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

Часть 2

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как активно вы используете плагины?

12.5% Пользуюсь только core-плагинами7
64.29% Пользуюсь парой community-плагинов36
14.29% Мой Obsidian жестко нафарширован community-плагинами8
8.93% У меня есть в том числе самописные плагины (темы и css-стили не в счет)5

Проголосовали 56 пользователей. Воздержались 6 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/861230/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *