JoplinのPluginを開発していく

05/03

モルモット

Develop Plugin for Joplin

Index

Take Control More Usefully

かなり便利なJoplin生活。
メモ帳革命が私に起きている。

ただ若干不便なことが。
それがBlogの更新。
動画のリンクをhexo-tag-owlで変換しているのだが、タグをいちいち書いているのが面倒くさい。
そんなとき、ショートカットキーで一発変換してくれたら超便利だなと。

あと、pixabayとかに代表される無料素材サイトからそれらしい画像を検索して勝手にダウンロードして添付するようなことできないかなと。
Office製品にある画像検索機能。そうアレ。

もうひとつ。ブラウザと同じように選択テキストを右クリックしてコンテキストメニューに「Google検索」と「Google翻訳」を追加したい。

そんなニッチなプラグインがあるはずもなく、そこら辺がちょっと不便。

It doesn’t exist, just make it exist

なけりゃ作るだけ。
ここを参考に作ってみようって話。

What’s required

要件としては

  1. エディタ上で選択テキストをショートカットキーでHeo Tag Owlのフォーマットに変換する
  2. エディタ上でショートカットキーでプロンプトを出して、検索ワードを入力、Pixabayで検索して出てきた候補を選択したら画像をダウンロードして添付する
  3. エディタ上で選択テキストを右クリックのコンテキストメニューから「Google検索」「Google翻訳」を選択できるようにする

こんな感じだろうか。
まずは勉強も兼ねて1番目のやつからやってみる。

What is Hexo Tag Owl?

そもそもこのHexo Tag Owlというのが何かを説明すると、Hexoというブログ管理ツールのプラグインで記事内に特殊タグを書くと、記事を投稿するときに自動的にHTMLタグに展開される仕組み。

タグはYouTube、ニコニコ動画などの動画、Giphyとかの静止画に対応しており、ユーザはタグの種類(youtube、niconico etc..)と、リソースのIDだけを指定すれば、自動的にHTMLタグに展開するという支援ツール。

これをJoplin上で編集しているときにURLを貼り付けて変換ボタンもしくはショートカットキーで展開してくれたら便利だなと。

たとえば

https://youtu.be/Bu1TGvs

みたいなURLがあったとしたら、これを

{% owl youtube Bu1TGvs %}

って変換してほしい。

Setup My Environment

開発環境を整える。
Windows 11 Proの環境でnodejsのバージョンが18.*。

%USERPROFILE%>cd Documents
%USERPROFILE%\Documents>npm i -g yo generator-joplin
%USERPROFILE%\Documents>mkdir develop\joplin-plugin-hexo-support
%USERPROFILE%\Documents>cd develop\joplin-plugin-hexo-support
%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>git init
%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>yo joplin

ここでエラーが出る。
エラー内容を見るとjoplinのバージョンが古いみたいな内容。

%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>type src\manifest.json
{
"manifest_version": 1,
"id": "net.yf-19.friedbis.joplin-plugin-hexo-support",
"app_min_version": "2.8.0",
"version": "1.0.2",
"name": "joplin-plugin-hexo-support",
"description": "add some buttons of blogging support for hexo",
"author": "friedbis",
"homepage_url": "https://superpack.yf-19.net",
"repository_url": "https://github.com/friedbis/joplin-plugin-hexo-support.git",
"keywords": ["shortcut", "toolbar", "icon", "menu"],
"categories": []
}
%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>

ここのmanifest.jsonファイルの「app_min_version」が環境に入れているJoplinのバージョンより高いから怒られているのか?
なので、今の2.7.15を入れたら怒られなくなった。
これで必要なアセットが用意される。

ほんで

src/index.ts

というファイルをいじることで実装できるようだ。
早速やってみよう。

Develop my plugin

正直全部自分で用意するのが億劫なので、違うプラグインを参考にしてみたい。
そこで見つけたのが、xardbaiz氏のjoplin-plugin-hackmdというプラグイン。
このプラグインを選んだ理由は以下のとおり。

  • エディタにアイコンを追加している
  • ショートカットキーを追加している
  • 設定画面に個別設定を追加している

これと同じことをしたかったので、参考に作ってみた。

index.ts

import joplin from 'api';
import { MenuItemLocation } from 'api/types';
import { ToolbarButtonLocation } from 'api/types';
import { settings } from "./settings";
import { actions, DTI_SETTINGS_PREFIX, ACTIVATE_ONLY_SETTING, hostList, DEFAULTID } from "./common";

function wrapSelectionWithStrings(selected: string|null){
// Determine host info and throw into subfunc
for(const host in hostList){
const objHost = hostList[host];
if(selected.search(objHost.hostname)>-1){
console.log('Detected ['+objHost.name+']');
return _wrapSelectionWithStrings(selected, objHost);
}
if(objHost.anotherHostname!==""&&selected.search(objHost.anotherHostname)>-1){
console.log('Detected ['+objHost.name+']');
return _wrapSelectionWithStrings(selected, objHost);
}
if(selected.search(objHost.name)>-1){
console.log('Detected ['+objHost.name+']');
return _wrapSelectionWithStrings(selected, objHost);
}
}
}

function _wrapSelectionWithStrings(selected: string|null, Host: any) {
if (!selected) selected = Host.defaultURL;

// Remove white space on either side of selection
const start = selected.search(/[^\s]/);
const end = selected.search(/[^\s](?=[\s]*$)/);
const core = selected.slice(start, end + 1);

// Translate URL <-> TagOWL
if (core.startsWith(Host.wrapString1) && core.endsWith(Host.wrapString2)) {
console.log('TagOWL -> URL');
const inside = core.slice(Host.wrapString1.length, core.length - Host.wrapString2.length);
const defaulturl = Host.defaultURL.replace(DEFAULTID, inside);
console.log('['+defaulturl+']');
return defaulturl;
} else {
console.log('URL -> TagOWL');
const mediaid=getMediaId(selected, Host.hostname, Host.queryString);
const tagowl=selected.slice(0, start) + Host.wrapString1 + mediaid + Host.wrapString2 + selected.slice(end + 1);
console.log('['+tagowl+']');
return tagowl;
}
}

function getMediaId(selected: string, hostname: string, queryString: string){
// Parse URL
let parse: URL;
try {
console.log('Parsing URL');
parse = new URL(selected);
}catch(err){
console.log('Error parsing URL');
return '**error**';
}

if(parse.hostname.search(hostname)<0){
console.log('Invalid hostname');
return '**error**';
}

// Parse mediaid
if(queryString!==""){
console.log('Parsing MediaID');
const start = parse.search.search(queryString);
if(start>-1){
const params = parse.searchParams;
const query = queryString.replace('=','').replace('?','');
return params.get(query);
}
}

// Parse pathname
console.log('Parsing pathname');
const pathArray = parse.pathname.split('/');
if(pathArray[pathArray.length-1]!==''){
if(hostname.search('giphy')>-1) //<- I DONT WANNA DO THIS
return pathArray[pathArray.length-2];
else
return pathArray[pathArray.length-1];
}else{
return pathArray[pathArray.length-2];
}
}

joplin.plugins.register({
onStart: async function() {
console.info('Hexo Support plugin started!');
await settings.register();
const activateOnlyIfEnabledInMarkdownSettings = await joplin.settings.value(ACTIVATE_ONLY_SETTING);

// process actions
for (const actionName in actions) {
const action = actions[actionName];

let activate = true;

if (activateOnlyIfEnabledInMarkdownSettings && actionName !== 'textStrikethrough') {
activate = await joplin.settings.globalValue(action.markdownPluginSetting);
}

joplin.commands.register({
name: actionName,
label: action.label,
enabledCondition: 'markdownEditorPaneVisible && !richTextEditorVisible',
iconName: action.iconName,
execute: async () => {
const selectedText = (await joplin.commands.execute('selectedText') as string);

const newText = wrapSelectionWithStrings(selectedText);

await joplin.commands.execute('replaceSelection', newText);
await joplin.commands.execute('editor.focus');
},
});
var toolbarIconEnabled = !(await joplin.settings.value(DTI_SETTINGS_PREFIX + actionName));
if (toolbarIconEnabled) {
joplin.views.toolbarButtons.create(actionName + 'Button', actionName, ToolbarButtonLocation.EditorToolbar);
}
joplin.views.menuItems.create(actionName + 'MenuItem', actionName, MenuItemLocation.Edit, { accelerator: action.accelerator });
}
},
});

としてみた。
あとは

src/settings.ts
src/common.ts

ここらへんを編集した。
ポイントとしては、common.tsにタグ変換のための情報をhostListとして保存している。
こうしておけば、あとから追加することもできる。

a part of common.ts

export const hostList = {
YouTube: {
name: 'youtube',
wrapString1: '{% owl youtube ',
wrapString2: ' %}',
defaultURL: 'https://www.youtube.com/watch?v=NNNNNNNN',
hostname: 'youtube.com',
anotherHostname: 'youtu.be',
queryString: 'v=',
},
DailyMotion: {
name: 'dailymotion',
wrapString1: '{% owl dailymotion ',
wrapString2: ' %}',
defaultURL: 'https://www.dailymotion.com/video/NNNNNNNN',
hostname: 'dailymotion.com',
anotherHostname: '',
queryString: '',
},
NicoNico: {
name: 'niconico',
wrapString1: '{% owl niconico ',
wrapString2: ' watch %}',
defaultURL: 'https://www.nicovideo.jp/watch/NNNNNNNN',
hostname: 'nicovideo.jp',
anotherHostname: 'nico.ms',
queryString: '',
},
vimeo: {
name: 'vimeo',
wrapString1: '{% owl vimeo ',
wrapString2: ' %}',
defaultURL: 'https://vimeo.com/NNNNNNNN',
hostname: 'vimeo.com',
anotherHostname: '',
queryString: '',
},
IMDB: {
name: 'imdb',
wrapString1: '{% owl imdb ',
wrapString2: ' %}',
defaultURL: 'https://www.imdb.com/title/NNNNNNNN/?ref_=ext_shr_lnk',
hostname: 'imdb.com',
anotherHostname: '',
queryString: '',
},
GIPHY: {
name: 'giphy',
wrapString1: '{% owl giphy ',
wrapString2: ' %}',
defaultURL: 'https://media.giphy.com/media/NNNNNNNN/giphy.gif',
hostname: 'giphy.com',
anotherHostname: '',
queryString: '',
},
Imgur: {
name: 'imgur',
wrapString1: '{% owl imgur ',
wrapString2: ' %}',
defaultURL: 'https://imgur.com/gallery/NNNNNNNN',
hostname: 'imgur.com',
anotherHostname: '',
queryString: '',
},
};

最終的な状況はここを参考にしていただければと。

Test my plugin

んで、テストをしたいときは、以下のようにする。

%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>npm run dist
%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>

これでdistフォルダにindex.jsとmanifest.jsonができるようだ。
ここの場所をプラグインの場所としてJoplinを設定するとテストができるようだ。
ただ環境を変える方がよいってことだったので、

C:\> "%USERPROFILE%\AppData\Local\Programs\Joplin\Joplin.exe" --env dev

DOS窓から上記コマンドを実行すると環境を変えられるようだ。

==現行の環境でやらない方がよいです。最悪データが削除される可能性があります。==

んで、開発用Joplinの[オプション] -> [プラグイン]を開いて、[詳細設定を表示]をクリックすると、「Development plugins」というテキストボックスがあるので、そこに今回の開発フォルダ内のdistフォルダを指定する。

%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support\dist

ここで一旦開発用joplinを閉じて、さっきのjoplinの起動方法で

C:\> "%USERPROFILE%\AppData\Local\Programs\Joplin\Joplin.exe" --env dev

開発用Joplinを再度起動すると、今回のプラグインを試すことができた。
デバッグのやり方はまた今度調べてみよう。

Deploy my plugin

さてテストも終わって、実際のプラグインをインストールする。
さっきテストするときのコマンドを実行すると、publishフォルダにjplファイルが出来上がる。

%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>npm run dist

> joplin-plugin-hexo-support@1.0.0 dist
> webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive

(node:20776) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead
(Use `node --trace-deprecation ...` to show where the warning was created)
Plugin archive has been created in %USERPROFILE%\Documents\develop\joplin-plugin-hexo-support\publish\net.yf-19.friedbis.joplin-plugin-hexo-support.jpl

%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>dir publish

2022/05/03 09:31 <DIR> .
2022/05/03 09:31 <DIR> ..
2022/05/03 09:31 11,776 net.yf-19.friedbis.joplin-plugin-hexo-support.jpl
2022/05/03 09:31 634 net.yf-19.friedbis.joplin-plugin-hexo-support.json
2 個のファイル 12,410 バイト
2 個のディレクトリ 179,803,062,272 バイトの空き領域

%USERPROFILE%\Documents\develop\joplin-plugin-hexo-support>

こんな感じ。
このjplファイルを実際の環境にインストールしたら使える。
なんて簡単なんだ。

今後

要件のところにも書いたけど、2つ目や3つ目も実装していくことにしよう。
Joplin面白い。


コメント: