Пишем плагины для 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) мы увидим наше сообщение:

4xxmjqxzv3z7jfhqeauzmzuoxbe.png

Плагин 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-элемент контента, что-то вроде . Наша задача — отыскать все теги . Блоки кода помечаются Обсидианом как класс language-X, где X — имя вашего языка программирования, ну или в нашем случае это та самая пометка tab/tabs.

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

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

2jnuifjcqkvmap6pcer8dqki9qa.png

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

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

Плагин 2. Suggest TODO

Задача

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

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

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

iy7swmedfhjggihircdceun9imw.png

или даже так:

omn-jin1zwqbnjpq0z3k3w1bqr0.png

Оба вида списков должны поддерживаться. В случае с чек-боксами во внимание нужно принимать только незакрытые пункты. Хочется открыть заметку с 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-окошко с оповещением, которое всплывет на пару секунд в верхнем правом углу окна. Например, вот так:

v01mwnwccq6nwv2v1dla9_qmczo.png

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

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

8sxp9qfhxtwfejbrcbktrq9gupu.png

Чтобы создать свое модальное окно, нужно отнаследоваться от 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:

zzl7x7ylu1tgtwituhtxaknyjx4.png

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

laqiref6anow0xytcr21ly1wd-g.png

Плагин 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»-запроса, с возможностью быстрого перехода по файлам из сводки:

dlqddkyz1iiafixvb_1i7x6jeeq.png

У меня в тестовом хранилище не наберется 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:

sfh02oidhceoeixxaojgazez1ei.png

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 вида:

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

    oheiiucolnnlat3brybyvoosxc0.png

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

    Но у нас не работают ссылки! И как только я не пытался изголяться, чтобы заставить это работать: прописывал href, другие атрибуты; читал официальную документацию; дебажил ссылки в Obsidian и пытался добавлять в теги подсмотренные у 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
      
        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:

    onsye1e3ahnewh-sphwuius4sda.png

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

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

    hvm0m5i7rceofsuor2ysxwjks7m.png

    Готово!

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

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

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

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

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

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

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

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

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

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

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

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

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

    © Habrahabr.ru