HTMLプレビュープラグイン

  1. 現在は2019年に作られたIEベースのものがリリースされていますが、markdownのプレビューワ?でwebview2ベースのものを開発されたみたいな記事を読んだ記憶があります。webview2ベースでプレビューできるプラグイン、できればマクロ経由で使えるものをお願いします。

     |  abeq  |  返信
  2. IE コンポーネントを使用した Web プレビュー プラグインですね。

    IE のサポートはすでに終了しているため、現在は WebView2 ベースで新たに開発を進めています。

    【参考】https://x.com/haijinboys/status/1984990483521966322

    一応、マクロから少しだけ操作できるようにはなっていますが、現在の仕様では、マクロからプレビュー側へデータを送信することしかできません。プレビュー側の HTML を取得するといったことはできません。

    また、以前と比べると HTML を手書きしてプレビューする利用シーンは減ってきているように感じていて、どの程度の需要があるのか判断に迷っているため、こちらの開発をどこまで優先するかは悩んでいるところです。

    Markdown プレビュー プラグインも開発中ですが、こちらは HTML のプレビューを目的としたものではないため、従来の Web プレビュー プラグインの後継という位置付けではありません。

    最近は AI が Markdown 形式で出力することも多くなり、Markdown プレビューの需要は高まってきていると感じているため、現在はこちらを優先して開発しています。

    もし WebView2 ベースの Web プレビュー プラグインをご希望でしたら、どのような用途でお使いになる予定なのか教えていただけると嬉しいです。今後の機能追加を検討する際の参考にさせていただきます。

     |  Kuro  |  返信
  3. こんにちわ、返信が遅くなって申し訳ありません。
    Markdownは機能が圧倒的に少ないのでもっと強力な簡易マークアップ言語を使いたいです。
    エディタ上でマークアップを入力してマクロを起動すると中でDOMに変換してプレビューに渡して結果を見る、ということがやりたいです。
    V8のjsが使えるということはエディタ上のすべてのデータに視覚的なプレゼンテーションを付けられるということです。プレビューがあればプレゼンテーションレイヤして使うことで現代のHypercardになり得るポテンシャルを感じています。
    こうなるともうエディターというジャンルを超えて唯一の存在になり得ると思います。
    あと今後主流がQuickJSになったとしてJScriptを廃止してもV8は廃止しないでください。JScriptのことは嫌いになってもV8のjsを嫌いにならないでください。

    ということで、よろしくお願いします。

     |  abeq  |  返信
  4. こんにちは。ご返信ありがとうございます。

    なるほど、より自由度の高いマークアップ言語を使って、Mery 上で入力したデータをマクロで DOM に変換し、プレビューに渡すという使い方ですね。

    そういうことであれば、現在開発中の WebView2 版 Web プレビュー プラグインで実現できるかもしれません。

    一応、検証では marked.js を使って Markdown を Web プレビュー プラグインに表示する、といった実験までは成功しています。

    ただ、現状ではマクロから Web プレビューへ一方向にデータを渡すことしかできないため、スクロール位置を同期させたり、Web プレビュー側のボタンクリックをマクロ側で受け取って処理したり、といったことはできません。

    そこまで実現しようとすると、秀丸エディタさんや EmEditor さんのように、エディターにブラウザを内蔵するかたちになってくるので、今のところ Mery としては、そこまでやるのはどうかな?といったところではありますね。

    ひとまず、Web プレビューのそういった使い方は面白そうなので、まずはプラグインとしてリリースしてみて、反応を見ながら検討していくかたちにしてみます。

    > プレビューがあればプレゼンテーションレイヤして使うことで現代のHypercardになり得るポテンシャルを感じています。

    なるほど、Mery のテキスト編集機能をデータ入力の部分として使い、V8 マクロでデータを加工・変換し、WebView2 のプレビューで自由に見せるという構成なら、確かにかなりいろいろなことができそうですね。

    > あと今後主流がQuickJSになったとしてJScriptを廃止してもV8は廃止しないでください。> JScriptのことは嫌いになってもV8のjsを嫌いにならないでください。

    V8 マクロについては、今のところ廃止する予定はありません。

    QuickJS は軽量で組み込みやすく、JavaScript の実行環境として非常に魅力的なので、今後はこちらを中心にしていく可能性はありますが、V8 には WebView2 と組み合わせて利用できることや、Node.js に近い豊富な JavaScript 環境を利用できることなど、QuickJS にはないメリットがあります。

    そのため、JScript (廃止の可能性) の扱いと V8 の扱いは分けて考えています。少なくとも現時点で V8 を廃止する予定はありませんので、ご安心ください。

    面白いアイデアをありがとうございました。

    現在、Mery v3.8.9 の開発を進めており、落ち着いたら「Markdown プレビュー」→「Markdown バー (Markdown 入力補助)」を予定していますので、Web プレビューのリリースはそのあとになってしまいます。

    Web プレビューは、需要がなさそうであればお蔵入りにしようと思っていたところでしたので、今回こういったご意見をいただけたのはありがたいです。

    今回いただいたアイデアも参考にしながら、Web プレビュー プラグインのリリースについて前向きに検討してみたいと思います。

     |  Kuro  |  返信
  5. >現代のHypercard
    仕組みとしてはOpenDocを思い出します。VS Codeでよくね? という気がしないでもないですが(笑)
    知ってる言語が使えるMeryとVS Codeの中間ぐらいのPharo(Squeakのフォーク)みたいなAll in Oneのプロダクトは、欲しいかも。
    https://pharo.org/

    > V8 には WebView2 と組み合わせて利用できることや、Node.js に近い豊富な JavaScript 環境を利用できることなど、QuickJS にはないメリットがあります。
    問題はMicrosoftがWebView2を見放しつつあること(笑)
    WebView2で作られたWindows機能や標準アプリを廃止したり、WinUI3を使いつつネイティブで作り直したりしているので。

    昨今のメモリ高騰化でMicrosoftでさえフットプリントの増大を気にし始めたことはよい傾向とは思うのですが、自分たちでWinUI3というドッグフードを食べ始めたことで、あまりのダメさ加減にようやく気付いたのかもしれません。

     |  enaka  |  返信
  6. Kuroさん、ありがとうございます。プレビューからエディタへの逆方向ができるとGUIエディタみたいなことができてまずますいいのですが難しそうですか。Mery側でリスナーを仕掛けてプレビュー側のイベントをキャッチするとか軽く連携する方法があるとよさそうですが。
    軽いMeryとデファクトスタンダードのブラウザ(コンポーネント)を使ってできるところによさがあると思うのでリリースを楽しみにしています。
    よろしくお願いします。

     |  abeq  |  返信
  7. >> enaka さん

    > 仕組みとしてはOpenDocを思い出します。VS Codeでよくね? という気がしないでもないですが(笑)

    同感です。わざわざ Mery でやらなくても…とは思うのですが、技術的な好奇心から、遊びで Web プレビューを作り始めて、マクロで中途半端に操作できるところまでやっちゃったものですから(笑)

    > 知ってる言語が使えるMeryとVS Codeの中間ぐらいのPharo(Squeakのフォーク)みたいなAll in Oneのプロダクトは、欲しいかも。

    おっ。Pharo って IDE なのかしら?と思ってインストールしてみたものの、なんぞこれ…

    何かを開発するためのものだということは分かったのですが、エディター的なものを確認するところまでたどり着けませんでした。

    > 問題はMicrosoftがWebView2を見放しつつあること(笑)
    > WebView2で作られたWindows機能や標準アプリを廃止したり、WinUI3を使いつつネイティブで作り直したりしているので。

    た…たしかに、WebView2 のバグも放置されっぱなしですし。なんだか不安になってきました。

    >> abeq さん

    ご返信ありがとうございます。

    > プレビューからエディタへの逆方向ができるとGUIエディタみたいなことができてまずますいいのですが難しそうですか。Mery側でリスナーを仕掛けてプレビュー側のイベントをキャッチするとか軽く連携する方法があるとよさそうですが。

    そうなんですよね。

    そもそも Mery のマクロには、Mery に常駐してイベントを監視するような仕組みは用意していないので、リスナーのような使い方はできないんです。

    …が、その後、考えていてちょっと妙案が思い浮かびました。

    Mery のマクロからプレビューに HTML を送信して表示するところまでは、すでにできています。

    では、逆にプレビュー側から Mery 側を操作できないか。

    これについて、Web プレビュー プラグインに Mery との橋渡しをするためのインターフェイスを用意しておいて、プレビューの HTML 内のスクリプトからそれを呼び出すことで、Mery 側のマクロを経由して Mery を操作する、という方法です。

    つまり、↓の onclick の部分みたいな書き方です。

    <html>
      <head></head>
      <body>
        <button onclick="runMacro('quickjs', 'alert(1);');">Button 1</button>
        <p>これはマクロから表示した HTML です。</p>
      </body>
    </html>
    

    たとえば、こんな流れです。

    1. マクロから上記のような HTML をプレビューに送信。
    2. プレビューに「Button 1」が表示される。
    3. 「Button 1」をクリックする。
    4. Mery 側でマクロとして alert(1); が実行される。

    実際には、WebView2 側からプラグインへメッセージを送り、プラグインから Mery 側のマクロを呼び出す、というような仕組みになると思います。

    この方法なら、プレビュー側から Mery の操作もできそうな気がします。

    たとえば、プレビュー上の要素をクリックしたら、対応する Markdown の位置に Mery のカーソルを移動する、といったこともできるかもしれません。

    これができると、単なる Web プレビューではなく、HTML を GUI として利用して、Mery と組み合わせてちょっとしたツールまで作れるようになるので、なかなか面白そうです。

     |  Kuro  |  返信
  8. > おっ。Pharo って IDE なのかしら?と思ってインストールしてみたものの、なんぞこれ…
    >
    > 何かを開発するためのものだということは分かったのですが、エディター的なものを確認するところまでたどり着けませんでした。

    Pascalに対するDelphiみたいなもので、魔改造AppleSmalltalkの開発環境兼実行環境です。Smalltalk-80にモダンな言語機能を拡張して、現在も活発に開発されています。
    VM単体だと何もできないので、イメージファイルもダウンロードして読み込んでください。クロスプラットフォームなので分けて配布されています。

    使い方はyoutubeでPharo,Squeak,Smalltalkあたりで検索して動画を見ればほとんどわかると思います。探せば小学校のプログラム教育向けのイメージファイルなどもあるので。

    > これについて、Web プレビュー プラグインに Mery との橋渡しをするためのインターフェイスを用意しておいて、プレビューの HTML 内のスクリプトからそれを呼び出すことで、Mery 側のマクロを経由して Mery を操作する、という方法です。
    :中略
    > 実際には、WebView2 側からプラグインへメッセージを送り、プラグインから Mery 側のマクロを呼び出す、というような仕組みになると思います。
    >
    > この方法なら、プレビュー側から Mery の操作もできそうな気がします。
    >
    > たとえば、プレビュー上の要素をクリックしたら、対応する Markdown の位置に Mery のカーソルを移動する、といったこともできるかもしれません。
    >
    > これができると、単なる Web プレビューではなく、HTML を GUI として利用して、Mery と組み合わせてちょっとしたツールまで作れるようになるので、なかなか面白そうです。

    こういったことを最初にできるようにしたのがSmalltalkで現在のWebの原型なんですよね。

     |  enaka  |  返信
  9. 横からすみません。
    名無しラズパイピコといいます。
    Meryでいつもお世話になっています。
    使用には、2.6.7と3.xの二つの系列のポータブル版をUSBに入れて持ち運び、作業先のオフラインのXP等の上で同時起動させての作業といった事を想定しています。

    スレの前の方でJScriptの話が出ていたのですが、
    実務の関係から、Meryのヴァージョン更新においてXP等の過去のOSのJScriptへの対応廃止ではなく後方互換の継続を求めます。

    また、個人的にXPにnode.jsを導入してMery側からWSHを通じて呼び出して、非WebView2なV8エンジン向けJSコードを実行させるマクロとか、
    技術的に色々楽しんでいます。

    &gt; ただ、QuickJS は ActiveScripting で使われる COM (IDispatch) 方式じゃないので、Mery のマクロ機能 (JScript9、V8 など) とは共存できず、マクロの実装を全部ゼロから作り直しですね…

    WebView2コントローラやQuickJSについてですが、XP等の過去のOSへの対応が難しいというのは理解しています。
    それでも、QuickJSについてはXPで動いたというような話をされていたので、高望みなのは理解していますが個人的にものすごく期待せずにはいられません。
    作業先のXPなどの過去のOS上においてQuickJS用マクロだけで完結するポータブル版Meryとか、十徳ナイフのような便利さを感じます。

    他方、作業してて思いますが、Active script化出来ない言語を上のNode.jsみたいにマクロで呼び出してMery上でデフォルトのJScript代わりに使ったり、
    WebView2コントローラを走らせる事が出来ない古いOSだったとしても、代替になるアプリケーションを使うよう変更する仕組みそのものをユーザがマクロで記述し最新版Meryで機能をためして使えるような、
    そんな都合の良い実装ってやっぱり無理でしょうか?

    Meryの各機能の"既定のプログラム"を、必要があればユーザーの書いたマクロで代替できる構造が可能なら、それが一番個人的に好ましいです。
    たとえばQuickJSですが、上記の構造が可能なら、マクロの言語としてどれを扱うかの部分のMery既定プログラムを、ユーザがマクロで記述して書き換えれるようにすれば、
    「Mery側でQuickJS向けに専用のプログラムコードを書いてQuickJSに依拠した構造になる」といった事を避け、祖結合を維持できるのではないかと考えます。
    マクロの記述だけで、QuickJSだけでなくMeryで対応していない言語を使えるようになったり、WebView2の使えない古いOSでもMeryを使ってAI経由でmarkdownのプレ表示をさせるようできたり、
    アプリケーションのネイティブコードを一から書くより実験的な対応には最適だと思います。
    あと、ユーザの身として技術的にすごく面白く感じます。

    実際にそう簡単にはいかないでしょうが、声が無ければ実現の可能性も存在しないので、まずは希望します。
    フレームワークな構造の仕組みのように、部分部分のMery既定プログラムをユーザのマクロで書き換える機能を望みます。

     |  名無しラズパイピコ  |  返信
  10. ご愛用いただきありがとうございます。

    > 実務の関係から、Meryのヴァージョン更新においてXP等の過去のOSのJScriptへの対応廃止ではなく後方互換の継続を求めます。

    JScript の廃止については、Microsoft の今後の方針としてその可能性が高いというだけで、まだ廃止が発表されたわけではありません。(現時点では VBScript の廃止のみ発表されています)

    また、仮に JScript が廃止されたとしても、過去の OS にすでに搭載されている JScript まで直ちに削除されるわけではないため、Mery 側で対応を変更しなければ、引き続き利用できる可能性は高いと思います。

    ただし、Mery は今後、Windows XP のサポートを終了する方向ですので、その点についてはご了承ください。

    理由としては、XP をターゲットにすると開発環境や使用できるツールが制約されること、新しい Windows API やライブラリを利用する際に XP 対応が制約になること、XP のための別実装や動作検証が増えて開発・保守の負担が大きくなることなど、いくつもあります。

    そのため、XP 対応を維持することで新しい機能の実装や、Mery 本体の開発・保守にまで制約が及ぶような状況は、今後避けたいと考えています。

    > ただ、QuickJS は ActiveScripting で使われる COM (IDispatch) 方式じゃないので、Mery のマクロ機能 (JScript9、V8 など) とは共存できず、マクロの実装を全部ゼロから作り直しですね…

    私もそこは懸念していました。

    QuickJS では、JScript のように ActiveXObject (COM オブジェクト) を標準では利用できないため、当初は既存のマクロがそのままでは動作しないケースがありました。

    しかし、Mery では現在、QuickJS からも ActiveXObject を利用できるようになっています。

    そのため、既存のマクロも、ある程度の修正は必要になるかもしれませんが、比較的少ない修正で QuickJS に移行できるケースもあるかと思います。

    > それでも、QuickJSについてはXPで動いたというような話をされていたので、高望みなのは理解していますが個人的にものすごく期待せずにはいられません。

    Windows XP で QuickJS が動作したというのは、初期の検証で使用した、本家の QuickJS ですね。

    当時、本家の QuickJS は更新が止まっていたこともあり、その後、Mery では、より活発にメンテナンスされている QuickJS-NG というフォークプロジェクトのほうを採用しました。

    QuickJS-NG は Windows 7 以降が必須となっているため、XP では動作しません。

    > 他方、作業してて思いますが、Active script化出来ない言語を上のNode.jsみたいにマクロで呼び出してMery上でデフォルトのJScript代わりに使ったり、
    > WebView2コントローラを走らせる事が出来ない古いOSだったとしても、代替になるアプリケーションを使うよう変更する仕組みそのものをユーザがマクロで記述し最新版Meryで機能をためして使えるような、
    > そんな都合の良い実装ってやっぱり無理でしょうか?

    残念ながら、Mery のマクロではそういったことはできません。

    そもそも、Mery のマクロは操作を自動化することを目的としたものであり、Mery の機能そのものをカスタマイズするために提供しているものではないためです。

    WebView2 の話に限って言えば、このスレッドで紹介した Web プレビューは、現在、プラグインというかたちで開発しています。

    そのため、マクロではできませんが、プラグインであれば、たとえば別の言語を中継するプラグインや、WebView2 の代わりに別のブラウザーへ内容を送信するといった機能も、比較的自由に作れるかと思います。

    > フレームワークな構造の仕組みのように、部分部分のMery既定プログラムをユーザのマクロで書き換える機能を望みます。

    技術的には実現できないことではありませんが、Mery としては、マクロをそこまで汎用的な仕組みにする予定はありません。

    これを実現しようとすると、単にマクロ API を追加するというより、Mery 内部のさまざまな処理を「ユーザーが差し替えられる部品」として設計し直す必要があります。

    そうなると、Mery そのものがかなり複雑になってしまいますし、本体の機能自体がマクロなどの外部の仕組みに依存する構造になってしまいます。

    特に、今後のサポート状況や仕様変更などが Mery 本体とは別に存在する JScript や QuickJS、WebView2 などの実行環境に Mery の本体機能を依存させることは、将来的な保守性や安定性の面でもリスクがあると考えています。

    私はそこに開発リソースを割くよりも、Mery 本体はできるだけシンプルに保ち、必要な機能はマクロやプラグイン、外部ツールなどから利用できるようにする方向のほうがよいと考えています。

    その意味では、Node.js を WSH 経由で呼び出して Mery から利用されている現在の方法は、かなり現実的なアプローチだと思います。

    「Mery が対応していない言語を Mery から使いたい」ということであれば、今後もマクロやプラグインから外部プログラムを利用する余地を広げていく、という方向なら現実的だと思います。

    ただ、「Mery の既定機能そのものをユーザーのマクロで自由に置き換えられるフレームワーク」のようなものについては、現時点では実装する予定はありません。

    貴重なご意見として参考にさせていただきます。ご提案ありがとうございました。

     |  Kuro  |  返信
  11. 横からの発言にも関わらず返信頂き、ありがとうございます。
    現時点で、提案したアイデアが日の目を見る可能性はない、というのは残念ですが、
    引き続き希望は持ちたいと思います。

    それと、やはりMeryのXPそのものへの対応の継続が厳しいという事に変わりないのは、しかたが無いと思います。
    いつかはXPで最新版のMeryが動かなくなるというのは、合理的は判断の結果ですから、間違っていると思う人はいないと思います。
    特に、XPを想定したMeryの開発環境の維持が問題なら、解決のしようがないのも理解できます。

    > 技術的には実現できないことではありませんが、Mery としては、マクロをそこまで汎用的な仕組みにする予定はありません。
    >
    > これを実現しようとすると、単にマクロ API を追加するというより、Mery 内部のさまざまな処理を「ユーザーが差し替えられる部品」として設計し直す必要があります。

    つまるところ、「新たな公開規格の策定とそのためのアプリケーション内部構造の変更を行う必要がある」ということで、直接的なアプリケーション開発とは又別の難しさが出てくるのはわかります。
    Meryで使われるマクロ用の言語をどのように扱うかのプログラムの部分だけでも、プログラム代替のためにユーザーが書いたマクロコードに任意で切り替えれる機能あるいは構造に出来れば、
    MeryのXP対応が実質終了してもマクロ言語においては旧バージョンのXP対応Meryで最新版と同じ事が試せて、
    ゆくゆくはVista対応終了の時も、7対応終了の時も同じように、プログラム代替マクロコードの記述だけで最新版と旧版を同じように扱える(他ユーザの方が書いたマクロがそのまま新旧Mery両方で問題なく動く・使える)ようになるんじゃないかと、
    期待せずにはいられません。
    実現させるなら当然、既定のプログラムから切り替えて動かす「プログラム代替のマクロコード」をユーザの各自の責任で書くことになりますし、
    おっしゃるとおり、Mery自体の開発や保守の必要リソースが永続的に増加する事等を含めて、難しいという事は理解しています。

    余談になりますが、ソフトウェアを使う側、特に非エンドユーザ界隈を見渡すと、
    ここ何十年の、「最新のものが良い」というパラダイムが今後継続しつづけるかどうか、わからなくなって来てるのではないかと思います。
    各地でそこそこ残っているオフラインの古いOSとかのように、
    現在のデファクトスタンダードとは異なる選択が、意図的に行われるケースをいくつか見ています。
    これが今後増えるかどうかは、社会そのものの進退によりますが、「昔は良かった」みたいな考えが非エンドユーザから多く出てくるなら、EOLなソフトウェアの対応をうたったサードパーティが次々でてきてもおかしくありません。
    2000とかMEはないでしょうが、古いLinuxとかWindows互換環境とか、ハードは近代的なのにソフトウェアはサードパーティがサポート継続しているEOLなソフト、
    みたいなチグハグな構成を前提としたユーザ間会話を目にする機会が多々あるやもしれません。
    それらが、愛着や郷愁かそれとも合理的な利か、一概に言うことも難しいと思います。

     |  名無しラズパイピコ  |  返信
  12. 新しい Web プレビュー プラグインのベータ版が完成したので、公開してみます。

    今回の Web プレビュー プラグインは、これまでのものからかなり大きく変わっています。

    • IE コンポーネントを廃止して WebView2 に変更
    • HTML / CSS / JavaScript を使った表示が可能
    • Mery のマクロから WebView2 を操作可能

    つまり、単に「Mery で Web ページを表示する」というだけではありません。

    Mery のマクロから HTML / CSS / JavaScript を使って、ちょっとした GUI を作るなんてこともできるようになっています。

    実際にこんなことができます。

    https://x.com/haijinboys/status/1984990483521966322

    そして、こちらは Web プレビュー プラグインを使って、マクロから UI を作っている例です。

    https://x.com/haijinboys/status/2087894504867315971

    一見すると、それなりに大がかりなものに見えるかもしれませんが、実はこれ、QuickJS を使った 1 本のマクロだけで動いています。

    HTML / CSS / JavaScript を書ける方なら、Mery のマクロからかなり自由に UI を作れるので、いろいろ面白いことができるのではないかなと思います。

    UI 作成のソースコードは 300 行ほどありますが、興味のある方がいらっしゃいましたら、お声がけください。そのうち公開するかもしれません。

    詳しい使い方や仕様については、同梱の README をご覧ください。

    今のところの目標は BOOTH (https://mery.booth.pm/) での公開です。

    正式公開まではもう少し時間がかかると思いますので、今回のベータ版については、二次配布などはせず、この投稿からダウンロードしてお試しいただけると助かります。

    とはいえ、まだベータ版ですので、しばらくはのんびり楽しみながら開発していく予定です。

    まずは、WebView2 に対応した新しい Web プレビュー プラグインを、ぜひ一度試してみてください。

    「こんなことができた」「こんな UI を作ってみた」「ここが動かない」など、いろいろ遊んでいただけるとうれしいです。

    動作検証にご協力いただける方や、バグを見つけられた方は、ご報告いただけると助かります。


    Web プレビュー プラグイン v3.0.0 (64 ビット版) [1,030,332 バイト 2026/09/09]
    https://www.haijin-boys.com/download/WebPreview-x64-3.0.0.zip

    ウイルスチェック:
    https://www.virustotal.com/#/file/61bc3b74ecf043e9a95652b9a390af540a3e4f10c88b8a61065015c80249cf72

     |  Kuro  |  返信
  13. > 新しい Web プレビュー プラグインのベータ版が完成したので、公開してみます。

    いろいろ楽しめそうです。
    細かいことですがアーカイブに含まれる WebView2Loader.dll は、Mery.exe の場所に既に同じものがあるので不要ではないでしょうか。

    なお、WebView2ローダーを使わず、直接呼び出すと少し起動が早くなります。以下にサンプルコードを見つけました。
    https://github.com/takazin11de/my_webview2

     |  enaka  |  返信
  14. > 新しい Web プレビュー プラグインのベータ版が完成したので、公開してみます。

    使ってみましたが、気が付いたことがあります。
    以前に質問した内容で、ローカルのドライブにある画像ファイルの表示です。

    <B><img src="C:\WIN_APL\BonsDiary64\Diary01\Drive\C\WIN_APL\MeryTestFile\image098.tif.jpg" width=320></B>

    以前の(ver2.4.0)のプラグインでは「画像」の表示が可能でしたが、今回(ver3.0.0)は画像の表示が出来ませんでした。

    個人的には画像の表示ができなくても問題は有りませんが、この変更は新しい仕様でしょうか?

    よろしくお願いします。

     |  大石剛司  |  返信
  15. > 以前の(ver2.4.0)のプラグインでは「画像」の表示が可能でしたが、今回(ver3.0.0)は画像の表示が出来ませんでした。

    WebView2のセキュリティ上の仕様っぽいですね。ローカルホスト名経由だと表示できるみたいです。

    https://github.com/MicrosoftEdge/WebView2Feedback/issues/642#issuecomment-736760565

     |  enaka  |  返信
  16. >> enaka さん

    ありがとうございます。

    WebView2Loader.dll については、Mery v3.4.1 未満では Mery 本体に含まれていないため、プラグイン側にも同梱しています。

    README では動作環境を v3.8.0 以降としているので、その点を踏まえたご指摘かな?と思いますが😅

    一応、v3.8.0 未満 (v2.6.7 など) でも、マクロからの操作には未対応ですが、本来の Web のプレビュー用途としては利用できます。

    そういった環境でも利用できるようにするため、WebView2Loader.dll を同梱しています。

    なので、README の動作環境については、「v3.8.0 以降を推奨」としたほうが良さそうですね。

    > なお、WebView2ローダーを使わず、直接呼び出すと少し起動が早くなります。以下にサンプルコードを見つけました。

    情報ありがとうございます。

    なかなか面白そうですね。どれくらい早くなるのかは気になるところですが、このコードを Delphi に移植するのは骨が折れそうです😱

    一応、WebView2Loader.dll (または WebView2Loader.lib の静的リンク) を使う方法は Microsoft のドキュメントでも案内されているので、こちらを使ったほうが、WebView2 の仕様変更などにも対応しやすいのかなと思います。

    それから、Mery では本体側でも V8 マクロの実行環境として WebView2 を利用していますし、Markdown プレビュー プラグインなど、他の機能でも WebView2 を利用しています。

    そのため、それぞれで直接呼び出すロジックを実装するよりも、Mery 本体や各プラグインで WebView2Loader.dll を共通して利用できるほうが、管理や実装の面でも扱いやすいかなと思います。

    >> 大石剛司さん

    早速お試しいただきありがとうございます。

    > 以前の(ver2.4.0)のプラグインでは「画像」の表示が可能でしたが、今回(ver3.0.0)は画像の表示が出来ませんでした。

    ローカルのドライブにあるファイル (file:///) についてですが、今回の WebView2 では、HTML からローカルファイルへアクセスする部分にセキュリティ上の制限があります。

    一応、アクセスできるようにする方法はあるのですが、セキュリティ面で懸念があるため、現時点では対応していません。

    というのも、HTML には <script></script> を記述できますので、ローカルファイルへのアクセスを許可すると、HTML 内の JavaScript から fetch("file:///C:/Users/...") のようにして、意図しないローカルファイルへのアクセスを試みることも考えられます。

    > 個人的には画像の表示ができなくても問題は有りませんが、この変更は新しい仕様でしょうか?

    はい。WebView2 への移行に伴う仕様上の変更となります。

    今後、どうしてもローカルファイルにアクセスしたいというご意見が多ければ対応を検討しますが、現時点では安全性を優先した仕様としています。

     |  Kuro  |  返信
  17. 面白そうですね、動作検証参加させていただきます!

    さっそくテキスト読み上げの UI を作ってみました。

    #fonticon = "\uE767", #4d82b8, #dddddd
    #language = "quickjs"
    #title = "テキスト読み上げ"
    
    const html = `<!DOCTYPE html>
    <html lang="ja">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>テキスト読み上げ</title>
    <style>
    :root {
    	color-scheme: light dark;
    }
    
    body {
    	background-color: light-dark(#fafdff, #0a1112);
    	color: light-dark(#404345, #b4bbbc);
    	line-height: 1.5;
    	overflow-wrap: anywhere;
    }
    
    h1 {
    	margin-block: 2rem 0;
    	font-size: 1.5em;
    }
    
    h2 {
    	margin-block: 2rem 0;
    	font-size: 1.25em;
    }
    
    menu {
    	list-style-type: none;
    	margin-block: 2rem;
    	padding-inline-start: 0;
    	display: flex;
    	flex-wrap: wrap;
    	gap: 1rem;
    }
    
    button > svg {
    	inline-size: 24px;
    	block-size: 24px;
    	vertical-align: middle;
    }
    
    form p {
    	display: flex;
    	gap: .5rem;
    }
    
    fieldset {
    	inline-size: fit-content;
    }
    
    fieldset > p {
    	margin-block: 0;
    }
    
    label,
    output {
    	text-wrap: nowrap;
    }
    </style>
    <script>
    "use strict";
    
    const utterance = new SpeechSynthesisUtterance();
    const availableVoices = new Map();
    
    const updateVoiceList = () => {
    	const voices = speechSynthesis.getVoices();
    
    	if (voices.length === 0) {
    		speechSynthesis.addEventListener("voiceschanged", updateVoiceList);
    		return;
    	}
    
    	const oldSelect = document.getElementById("voice");
    	const newSelect = document.createElement("select");
    	newSelect.id = "voice";
    	newSelect.name = "voice";
    	for (const voice of voices) {
    		if (voice.localService) {
    			const option = document.createElement("option");
    			option.value = voice.voiceURI;
    			option.textContent = voice.name;
    			newSelect.append(option);
    			availableVoices.set(voice.voiceURI, voice);
    		}
    	}
    	oldSelect.replaceWith(newSelect);
    };
    
    const speak = () => {
    	if (speechSynthesis.paused) {
    		document.getElementById("speak").toggleAttribute("disabled", true);
    		document.getElementById("stop").toggleAttribute("disabled", false);
    		speechSynthesis.resume();
    		return;
    	}
    
    	const settings = document.forms.settings;
    
    	utterance.voice = availableVoices.get(settings.voice.value);
    	utterance.volume = settings.volume.valueAsNumber;
    	utterance.rate = settings.rate.valueAsNumber;
    	utterance.pitch = settings.pitch.valueAsNumber;
    	document.getElementById("speak").toggleAttribute("disabled", true);
    
    	window.chrome.webview.postMessage({
    		type: "runMacro",
    		lang: "quickjs",
    		text:
    			"editor.ExecutePlugin('WebPreview.dll', mePluginUserMessage, 3, " +
    				"'utterance.text = ' + JSON.stringify(" + settings.targetText.value + ") + ';' + " +
    				"'speechSynthesis.speak(utterance);'" +
    			");"
    	});
    };
    
    const pause = () => {
    	if (speechSynthesis.speaking) {
    		speechSynthesis.pause();
    		document.getElementById("speak").toggleAttribute("disabled", false);
    		document.getElementById("stop").toggleAttribute("disabled", true);
    	}
    };
    
    const stop = () => {
    	speechSynthesis.cancel();
    	document.getElementById("speak").toggleAttribute("disabled", false);
    };
    
    const outputCurrentVolume = () => {
    	const settings = document.forms.settings;
    	settings.currentVolume.value = Number(settings.volume.value + "e+2");
    };
    
    const outputCurrentRate = () => {
    	const settings = document.forms.settings;
    	settings.currentRate.value = settings.rate.valueAsNumber.toFixed(2) + " 倍速";
    };
    
    const outputCurrentPitch = () => {
    	const settings = document.forms.settings;
    	let pitchValue = Number((Number(settings.pitch.value + "e+2") - 100) + "e-2");
    	if (pitchValue === 0) {
    		pitchValue = "±" + pitchValue;
    	} else if (pitchValue > 0) {
    		pitchValue = "+" + pitchValue;
    	}
    	settings.currentPitch.value = pitchValue;
    };
    
    const resetSettings = () => {
    	document.forms.settings.reset();
    	outputCurrentVolume();
    	outputCurrentRate();
    	outputCurrentPitch();
    };
    
    document.addEventListener("DOMContentLoaded", () => {
    	updateVoiceList();
    	utterance.addEventListener("end", () => document.getElementById("speak").toggleAttribute("disabled", false));
    	document.getElementById("speak").addEventListener("click", speak);
    	document.getElementById("pause").addEventListener("click", pause);
    	document.getElementById("stop").addEventListener("click", stop);
    	document.getElementById("volume").addEventListener("input", outputCurrentVolume);
    	document.getElementById("rate").addEventListener("input", outputCurrentRate);
    	document.getElementById("pitch").addEventListener("input", outputCurrentPitch);
    	document.getElementById("resetSettings").addEventListener("click", resetSettings);
    	outputCurrentVolume();
    	outputCurrentRate();
    	outputCurrentPitch();
    }, {once: true});
    </script>
    </head>
    <body>
    <h1><span aria-hidden="true">🔊 </span>テキスト読み上げ</h1>
    <menu>
    	<li>
    		<button id="speak" type="button">
    			<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
    				<title>読み上げ開始</title>
    				<path d="M8 5v14l11-7z"/>
    			</svg>
    		</button>
    	</li>
    	<li>
    		<button id="pause" type="button">
    			<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
    				<title>一時停止</title>
    				<rect x="6" y="6" width="4" height="12"/>
    				<rect x="14" y="6" width="4" height="12"/>
    			</svg>
    		</button>
    	</li>
    	<li>
    		<button id="stop" type="button">
    			<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
    				<title>停止</title>
    				<rect x="6" y="6" width="12" height="12"/>
    			</svg>
    		</button>
    	</li>
    </menu>
    <h2>設定</h2>
    <p>以下の設定はリアルタイムでは適用されません。設定変更後に読み上げを開始してください。</p>
    <form name="settings">
    	<fieldset>
    		<legend>読み上げ対象</legend>
    		<p>
    			<input id="documentText" type="radio" name="targetText" value="document.Text" checked>
    			<label for="documentText">文書全体</label>
    		</p>
    		<p>
    			<input id="selectionText" type="radio" name="targetText" value="document.selection.Text">
    			<label for="selectionText">選択範囲</label>
    		</p>
    	</fieldset>
    	<p>
    		<label for="voice"><span aria-hidden="true">🎤 </span>音声</label>
    		<select id="voice" name="voice"></select>
    	</p>
    	<p>
    		<label for="volume"><span aria-hidden="true">🔊 </span>音量</label>
    		<input id="volume" type="range" name="volume" min="0" max="1" step=".05" value="1">
    		<output for="volume" name="currentVolume"></output>
    	</p>
    	<p>
    		<label for="rate"><span aria-hidden="true">💨 </span>速度</label>
    		<input id="rate" type="range" name="rate" min=".25" max="3" step=".05" value="1">
    		<output for="rate" name="currentRate"></output>
    	</p>
    	<p>
    		<label for="pitch"><span aria-hidden="true">🎼 </span>音程</label>
    		<input id="pitch" type="range" name="pitch" min="0" max="2" step=".1" value="1">
    		<output for="pitch" name="currentPitch"></output>
    	</p>
    	<p><button id="resetSettings" type="button">設定を初期値に戻す</button></p>
    </form>
    </body>
    </html>`;
    
    if (editor.ExecutePlugin("WebPreview.dll", mePluginQueryStatus) === meStatusEnabled) {
    	editor.ExecutePlugin("WebPreview.dll", mePluginExecuteCommand);
    }
    editor.ExecutePlugin("WebPreview.dll", mePluginUserMessage, 2, html);
    

    X の投稿を拝見しながら手探りで作成したので何か間違ってるかもしれませんが、こちらでは意図したとおりに動いてくれてます。

     |  ucky  |  返信
  18. ありがとうございます!

    さっそく試していただいて、しかもここまでしっかり作っていただけるとは…

    テキスト読み上げの UI、こちらでも問題なく動作しています。

    WebPreview の postMessage → runMacro → QuickJS という流れも、こちらで想定していた使い方です。

    コードも拝見しましたが、特に問題なさそうです。

    しかし、X の投稿ではチラッと見えただけだと思いますが、postMessage とか、よく分かりましたね😳

    こういう実用的なマクロがあると、WebPreview の活用方法がイメージしやすくて助かります。

    私もサンプルということで、X に投稿したマクロを貼っておきます。

    「Mery Tool Box」という、何の役にも立たないマクロですが…

    # Web プレビュー プラグイン サンプル<!--
    #fonticon = "\uec7a"
    #language = "quickjs"
    #title = "Mery Tool Box"
    
    // Web プレビューからのイベント受信キー
    // マクロごとに独立したイベント領域を確保するため、マクロファイルのフルパスを使用
    const key = scriptFullName;
    
    // Web プレビューからのイベント取得
    const event = tag[key];
    
    if (event) {
    	// イベントの読み出し後に削除
    	tag[key] = null;
    	const data = JSON.parse(event);
    	switch (data.type) {
    		case "close":
    			// Web プレビューを閉じる
    			editor.executePlugin("WebPreview.dll", mePluginUserMessage, 0);
    		break;
    		case "insertDate":
    			// 今日の日付を挿入
    			document.selection.text = new Date().toLocaleDateString();
    			break;
    		case "insertEmoji":
    			// WebView 側で選択した絵文字を挿入
    			document.selection.text = data.value;
    			break;
    		case "insertColor":
    			// WebView 側で選択したカラーコードを挿入
    			document.selection.Text = data.value;
    			break;
    		case "sortLines":
    			// 行をソート
    			const sel = document.selection;
    			const isEmpty = sel.isEmpty;
    			if (isEmpty) {
    				sel.selectAll();
    			}
    			sel.text = sel.text.replace(/\n?$/, '').split('\n').sort().join('\n') + (sel.text.endsWith('\n') ? '\n' : '');
    			break;
    		case "showInfo":
    			// 文書情報を表示
    			alert(
    				"ファイル名: " + document.fullName + "\n" +
    				"文字数: " + document.text.length
    			);
    			break;
    		case "showEnvironment":
    			// WebView 側で取得した環境情報を表示
    			alert(
    				"Platform: " + data.value.platform + "\n" +
    				"Language: " + data.value.language + "\n" +
    				"CPU: " + data.value.cores + " cores\n" +
    				"DPI: " + data.value.pixelRatio
    			);
    			break;
    	}
    	editor.ExecuteCommandByID(2189);
    	quit();
    }
    
    // Web プレビューに表示する HTML
    const html = `
    <html>
      <head>
    		<style>
    			body {
    				background: #0d1117;
    				color: #f0f6fc;
    				font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
    				margin: 16px;
    			}
    			.buttons {
    				display: grid;
    				gap: 8px;
    				grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    			}
    			button {
    				background: #161b22;
    				border: 1px solid #30363d;
    				border-radius: 6px;
    				color: inherit;
    				cursor: pointer;
    				padding: 10px 12px;
    				white-space: nowrap;
    			}
    			button:hover {
    				background: #21262d;
    			}
    			.toolbox {
    				position: relative;
    			}
    			.closeButton {
    				background: transparent;
    				border: 0;
    				color: #8b949e;
    				cursor: pointer;
    				font-size: 20px;
    				height: 24px;
    				line-height: 24px;
    				padding: 0;
    				position: absolute;
    				right: 4px;
    				top: 4px;
    				width: 24px;
    			}
    			.closeButton:hover {
    				background: #30363d;
    				border-radius: 4px;
    				color: #ffffff;
    			}
    			#emojiPicker, #colorPicker {
    				background: #161b22;
    				border: 1px solid #30363d;
    				border-radius: 8px;
    				display: none;
    				margin-top: 8px;
    				padding: 10px;
    			}
    			.group {
    				display: flex;
    				flex-wrap: wrap;
    				gap: 4px;
    			}
    			.emoji {
    				align-items: center;
    				border-radius: 6px;
    				cursor: pointer;
    				display: flex;
    				flex: 0 0 36px;
    				font-size: 22px;
    				height: 36px;
    				justify-content: center;
    				transition: transform 0.1s;
    				width: 36px;
    			}
    			.emoji:hover {
    				background: #30363d;
    				transform: scale(1.15);
    			}
    			.color {
    				border-radius: 50%;
    				border: 2px solid transparent;
    				box-sizing: border-box;
    				cursor: pointer;
    				flex: 0 0 28px;
    				height: 28px;
    				transition: transform 0.1s;
    				width: 28px;
    			}
    			.color:hover {
    				border-color: #ffffff;
    				transform: scale(1.2);
    			}
    		</style>
    		<script>
    			const macroFile = ${JSON.stringify(scriptFullName)};
    
    			// Mery のマクロへイベントを送信
    			function fireEvent(type, value) {
    				const event = JSON.stringify({
    					type: type,
    					value: value
    				});
    				const script =
    					"tag[" + JSON.stringify(macroFile) + "] = " +
    					JSON.stringify(event) + ";";
    				window.chrome.webview.postMessage({
    					type: "runMacro",
    					lang: "quickjs",
    					text: script
    				});
    				window.chrome.webview.postMessage({
    					type: "runMacro",
    					lang: "quickjs",
    					file: macroFile
    				});
    			}
    
    			function closeToolBox() {
    				fireEvent("close");
    			}
    
    			function showEnvironment() {
    				fireEvent("showEnvironment", {
    					platform: navigator.platform,
    					language: navigator.language,
    					cores: navigator.hardwareConcurrency,
    					pixelRatio: window.devicePixelRatio
    				});
    			}
    
    			function toggleEmojiPicker() {
    				const picker = document.getElementById("emojiPicker");
    				picker.style.display =
    				getComputedStyle(picker).display === "none" ? "block" : "none";
    			}
    
    			function selectEmoji(emoji) {
    				fireEvent("insertEmoji", emoji);
    				document.getElementById("emojiPicker").style.display = "none";
    			}
    
    			function toggleColors() {
    				const picker = document.getElementById("colorPicker");
    				picker.style.display =
    				getComputedStyle(picker).display === "none" ? "block" : "none";
    			}
    
    			function selectColor(color) {
    				fireEvent("insertColor", color);
    				document.getElementById("colorPicker").style.display = "none";
    			}
    		</script>
    	</head>
    	<body>
    		<div class="toolbox">
    		<button class="closeButton" onclick="closeToolBox();">×</button>
    		<h2>✨ Mery Tool Box</h2>
    		<div class="buttons">
    			<button onclick="fireEvent('insertDate');">📅 今日の日付</button>
    			<button onclick="toggleEmojiPicker();">😀 絵文字</button>
    			<button onclick="toggleColors();">🎨 カラーコード</button>
    			<button onclick="fireEvent('sortLines');">🔀 行をソート</button>
    			<button onclick="fireEvent('showInfo');">📊 文書情報</button>
    			<button onclick="showEnvironment();">🖥️ 環境情報</button>
    		</div>
    		<div id="emojiPicker">
    			<div class="group">
    				<span class="emoji" title="😀" onclick="selectEmoji('😀')">😀</span>
    				<span class="emoji" title="😃" onclick="selectEmoji('😃')">😃</span>
    				<span class="emoji" title="😄" onclick="selectEmoji('😄')">😄</span>
    				<span class="emoji" title="😁" onclick="selectEmoji('😁')">😁</span>
    				<span class="emoji" title="😆" onclick="selectEmoji('😆')">😆</span>
    				<span class="emoji" title="😅" onclick="selectEmoji('😅')">😅</span>
    				<span class="emoji" title="😂" onclick="selectEmoji('😂')">😂</span>
    				<span class="emoji" title="🤣" onclick="selectEmoji('🤣')">🤣</span>
    				<span class="emoji" title="😊" onclick="selectEmoji('😊')">😊</span>
    				<span class="emoji" title="😇" onclick="selectEmoji('😇')">😇</span>
    				<span class="emoji" title="🙂" onclick="selectEmoji('🙂')">🙂</span>
    				<span class="emoji" title="😉" onclick="selectEmoji('😉')">😉</span>
    				<span class="emoji" title="😍" onclick="selectEmoji('😍')">😍</span>
    				<span class="emoji" title="🥰" onclick="selectEmoji('🥰')">🥰</span>
    				<span class="emoji" title="😘" onclick="selectEmoji('😘')">😘</span>
    				<span class="emoji" title="😎" onclick="selectEmoji('😎')">😎</span>
    				<span class="emoji" title="🤔" onclick="selectEmoji('🤔')">🤔</span>
    				<span class="emoji" title="😮" onclick="selectEmoji('😮')">😮</span>
    				<span class="emoji" title="😭" onclick="selectEmoji('😭')">😭</span>
    				<span class="emoji" title="😡" onclick="selectEmoji('😡')">😡</span>
    				<span class="emoji" title="👍" onclick="selectEmoji('👍')">👍</span>
    				<span class="emoji" title="👎" onclick="selectEmoji('👎')">👎</span>
    				<span class="emoji" title="👏" onclick="selectEmoji('👏')">👏</span>
    				<span class="emoji" title="🙌" onclick="selectEmoji('🙌')">🙌</span>
    				<span class="emoji" title="🙏" onclick="selectEmoji('🙏')">🙏</span>
    				<span class="emoji" title="💪" onclick="selectEmoji('💪')">💪</span>
    				<span class="emoji" title="👋" onclick="selectEmoji('👋')">👋</span>
    				<span class="emoji" title="👌" onclick="selectEmoji('👌')">👌</span>
    				<span class="emoji" title="✌️" onclick="selectEmoji('✌️')">✌️</span>
    				<span class="emoji" title="🤞" onclick="selectEmoji('🤞')">🤞</span>
    				<span class="emoji" title="🤟" onclick="selectEmoji('🤟')">🤟</span>
    				<span class="emoji" title="🤘" onclick="selectEmoji('🤘')">🤘</span>
    				<span class="emoji" title="❤️" onclick="selectEmoji('❤️')">❤️</span>
    				<span class="emoji" title="🧡" onclick="selectEmoji('🧡')">🧡</span>
    				<span class="emoji" title="💛" onclick="selectEmoji('💛')">💛</span>
    				<span class="emoji" title="💚" onclick="selectEmoji('💚')">💚</span>
    				<span class="emoji" title="💙" onclick="selectEmoji('💙')">💙</span>
    				<span class="emoji" title="💜" onclick="selectEmoji('💜')">💜</span>
    				<span class="emoji" title="🖤" onclick="selectEmoji('🖤')">🖤</span>
    				<span class="emoji" title="🤍" onclick="selectEmoji('🤍')">🤍</span>
    				<span class="emoji" title="💯" onclick="selectEmoji('💯')">💯</span>
    				<span class="emoji" title="✨" onclick="selectEmoji('✨')">✨</span>
    				<span class="emoji" title="⭐" onclick="selectEmoji('⭐')">⭐</span>
    				<span class="emoji" title="🔥" onclick="selectEmoji('🔥')">🔥</span>
    				<span class="emoji" title="🎉" onclick="selectEmoji('🎉')">🎉</span>
    				<span class="emoji" title="🎊" onclick="selectEmoji('🎊')">🎊</span>
    				<span class="emoji" title="🚀" onclick="selectEmoji('🚀')">🚀</span>
    				<span class="emoji" title="💡" onclick="selectEmoji('💡')">💡</span>
    				<span class="emoji" title="⚡" onclick="selectEmoji('⚡')">⚡</span>
    				<span class="emoji" title="🌈" onclick="selectEmoji('🌈')">🌈</span>
    				<span class="emoji" title="☀️" onclick="selectEmoji('☀️')">☀️</span>
    				<span class="emoji" title="🌙" onclick="selectEmoji('🌙')">🌙</span>
    				<span class="emoji" title="☕" onclick="selectEmoji('☕')">☕</span>
    				<span class="emoji" title="🎵" onclick="selectEmoji('🎵')">🎵</span>
    				<span class="emoji" title="📌" onclick="selectEmoji('📌')">📌</span>
    			</div>
    		</div>
    		<div id="colorPicker">
    			<div class="group">
    				<span class="color" title="#FF5252" style="background:#FF5252" onclick="selectColor('#FF5252')"></span>
    				<span class="color" title="#FF4081" style="background:#FF4081" onclick="selectColor('#FF4081')"></span>
    				<span class="color" title="#E040FB" style="background:#E040FB" onclick="selectColor('#E040FB')"></span>
    				<span class="color" title="#7C4DFF" style="background:#7C4DFF" onclick="selectColor('#7C4DFF')"></span>
    				<span class="color" title="#536DFE" style="background:#536DFE" onclick="selectColor('#536DFE')"></span>
    				<span class="color" title="#448AFF" style="background:#448AFF" onclick="selectColor('#448AFF')"></span>
    				<span class="color" title="#40C4FF" style="background:#40C4FF" onclick="selectColor('#40C4FF')"></span>
    				<span class="color" title="#18FFFF" style="background:#18FFFF" onclick="selectColor('#18FFFF')"></span>
    				<span class="color" title="#64FFDA" style="background:#64FFDA" onclick="selectColor('#64FFDA')"></span>
    				<span class="color" title="#69F0AE" style="background:#69F0AE" onclick="selectColor('#69F0AE')"></span>
    				<span class="color" title="#F44336" style="background:#F44336" onclick="selectColor('#F44336')"></span>
    				<span class="color" title="#E91E63" style="background:#E91E63" onclick="selectColor('#E91E63')"></span>
    				<span class="color" title="#9C27B0" style="background:#9C27B0" onclick="selectColor('#9C27B0')"></span>
    				<span class="color" title="#673AB7" style="background:#673AB7" onclick="selectColor('#673AB7')"></span>
    				<span class="color" title="#3F51B5" style="background:#3F51B5" onclick="selectColor('#3F51B5')"></span>
    				<span class="color" title="#2196F3" style="background:#2196F3" onclick="selectColor('#2196F3')"></span>
    				<span class="color" title="#03A9F4" style="background:#03A9F4" onclick="selectColor('#03A9F4')"></span>
    				<span class="color" title="#00BCD4" style="background:#00BCD4" onclick="selectColor('#00BCD4')"></span>
    				<span class="color" title="#009688" style="background:#009688" onclick="selectColor('#009688')"></span>
    				<span class="color" title="#4CAF50" style="background:#4CAF50" onclick="selectColor('#4CAF50')"></span>
    				<span class="color" title="#D32F2F" style="background:#D32F2F" onclick="selectColor('#D32F2F')"></span>
    				<span class="color" title="#C2185B" style="background:#C2185B" onclick="selectColor('#C2185B')"></span>
    				<span class="color" title="#7B1FA2" style="background:#7B1FA2" onclick="selectColor('#7B1FA2')"></span>
    				<span class="color" title="#512DA8" style="background:#512DA8" onclick="selectColor('#512DA8')"></span>
    				<span class="color" title="#303F9F" style="background:#303F9F" onclick="selectColor('#303F9F')"></span>
    				<span class="color" title="#1976D2" style="background:#1976D2" onclick="selectColor('#1976D2')"></span>
    				<span class="color" title="#0288D1" style="background:#0288D1" onclick="selectColor('#0288D1')"></span>
    				<span class="color" title="#0097A7" style="background:#0097A7" onclick="selectColor('#0097A7')"></span>
    				<span class="color" title="#00796B" style="background:#00796B" onclick="selectColor('#00796B')"></span>
    				<span class="color" title="#388E3C" style="background:#388E3C" onclick="selectColor('#388E3C')"></span>
    				<span class="color" title="#FF9800" style="background:#FF9800" onclick="selectColor('#FF9800')"></span>
    				<span class="color" title="#FFC107" style="background:#FFC107" onclick="selectColor('#FFC107')"></span>
    				<span class="color" title="#FFEB3B" style="background:#FFEB3B" onclick="selectColor('#FFEB3B')"></span>
    				<span class="color" title="#CDDC39" style="background:#CDDC39" onclick="selectColor('#CDDC39')"></span>
    				<span class="color" title="#8BC34A" style="background:#8BC34A" onclick="selectColor('#8BC34A')"></span>
    				<span class="color" title="#795548" style="background:#795548" onclick="selectColor('#795548')"></span>
    				<span class="color" title="#607D8B" style="background:#607D8B" onclick="selectColor('#607D8B')"></span>
    				<span class="color" title="#455A64" style="background:#455A64" onclick="selectColor('#455A64')"></span>
    				<span class="color" title="#37474F" style="background:#37474F" onclick="selectColor('#37474F')"></span>
    				<span class="color" title="#263238" style="background:#263238" onclick="selectColor('#263238')"></span>
    			</div>
    		</div>
    		</div>
    	</body>
    </html>
    `;
    
    // Web プレビューが起動していなければ起動
    if ((editor.executePlugin("WebPreview.dll", mePluginQueryStatus) & 2) === 0) {
    	editor.executePlugin("WebPreview.dll");
    }
    // HTML を Web プレビューに送信
    editor.executePlugin("WebPreview.dll", mePluginUserMessage, 2, html);
    

    個人的な工夫ポイントとしては、先頭の # Web プレビュー プラグイン サンプル<!-- で、マクロ実行前にソースコードそのものがプレビューに表示されないようにしているところ (笑)

    それから、プレビュー側のクリックイベントを tag 経由で QuickJS 側で拾っているところ。

    あとは、右上の閉じるボタンです。これを付けておけば、マクロの GUI を閉じて、通常の Web プレビューに戻せます。

    それから、WebPreview プラグインで何かを作るときに、デバッグに使える裏技を載せておきます。

    C:\Users\<UserName>\AppData\Roaming\Mery\Plugins\WebPreview\WebPreview.ini
    

    この WebPreview.ini の [WebPreview] セクションに Debug=1 を追記すると、WebView2 の開発者ツールを利用できるようになります。

    Web プレビュー部分を右クリックして、[開発者ツールで調査する] から開けます。

    しかし、いざ GUI が作れるとなっても、なかなか「何を作ればいいのか」が難しいところですね。

     |  Kuro  |  返信
  19. 大変参考になります。
    とりあえず一度プラグインで作ろうとして挫折した、マクロや外部ツールをサイドパネルからインクリメンタル検索で絞り込んで起動するコマンドランチャー的なものを作ろうとしています。
    登録したマクロや外部ツールが増えすぎて、ショートカットを割り当てたりポップアップメニューやメニューバーからたどるのが面倒になったという(笑)

    > しかし、いざ GUI が作れるとなっても、なかなか「何を作ればいいのか」が難しいところですね。

    ぱっと思いつくところだと、時計やカレンダーや天気などのウィジェット、電卓や付箋や辞書などのアクセサリ、メールなどの着信通知のダッシュボードでしょうか。

     |  enaka  |  返信
  20. サンプル公開ありがとうございます。参考になります。

    > 個人的な工夫ポイントとしては、先頭の # Web プレビュー プラグイン サンプル<!-- で、マクロ実行前にソースコードそのものがプレビューに表示されないようにしているところ (笑)

    # <script>document.body = document.createElement("body");</script>とする手もありますね。少し不格好ですが、この方法だと HTML 部分にコメントを書いてても大丈夫です。
    とはいえ、実際にマクロを実行するときはソースコード自体を開いてないことが多いと思うのであまり気にしなくていいかも?

    > それから、プレビュー側のクリックイベントを tag 経由で QuickJS 側で拾っているところ。

    確かにこの方がいいですね。処理が少なかったので HTML 側で書いたら引用符の中の引用符の中の引用符とか出てきて若干混乱しました……。

    開発者ツールを利用できるのは便利ですね。

    引き続き動作検証してみます。

     |  ucky  |  返信
  21. >> enaka さん

    > とりあえず一度プラグインで作ろうとして挫折した、マクロや外部ツールをサイドパネルからインクリメンタル検索で絞り込んで起動するコマンドランチャー的なものを作ろうとしています。

    コマンドランチャー、いいですね。

    Mery 側に登録されているマクロや外部ツールのコマンドを、マクロ側から動的に取得できない気がするので、そのあたりをどうやって実現するのか気になるところです😊

    > ぱっと思いつくところだと、時計やカレンダーや天気などのウィジェット、電卓や付箋や辞書などのアクセサリ、メールなどの着信通知のダッシュボードでしょうか。

    ウィジェット系も確かに面白そうです。

    「Mery でやる必要性」というところですと、Web プレビューは Zen モードでも使えるので、時計や付箋あたりは需要がありそうな気もします。

    個人的には、実際に試していないので分かりませんが、Mery の V8 マクロは起動するたびにそこそこ時間がかかるので、Web プレビューを一度起動しておけば常駐していることを利用して、そちらで V8 マクロを動かす、いわゆる「常駐 V8 エンジン」的な使い方もできないかな、と思っています。

    >> ucky さん

    > # <script>document.body = document.createElement(“body”);</script>とする手もありますね。

    これはいいですね。HTML コメントも書けるうえに、# や適当な文字列も表示されなくて、すごいアイデアです。

    > とはいえ、実際にマクロを実行するときはソースコード自体を開いてないことが多いと思うのであまり気にしなくていいかも?

    私は、なにげにこの「マクロのソースコードがプレビューに表示されてしまう」のが気になっていて、マクロ側の機能 (# 指令) として「Web プレビューに何も表示しない」みたいなものを用意しようかと思ったり、いや、でもプラグインに配慮した仕様を本体に追加するのは違うだろ、とか、ごねごね悩んでいました。

    > 確かにこの方がいいですね。処理が少なかったので HTML 側で書いたら引用符の中の引用符の中の引用符とか出てきて若干混乱しました……。

    なんとなく、そうなんだろうなぁとは思っていましたが、この方式の弱点ですね…

    私も JSON.stringify が入れ子になって、「なんで動かないんや!」って何度も泣きそうになりました。

    動作検証のご協力ありがとうございます。

    しかし、この Web プレビュー GUI 遊び、面白いのは面白いのですが、完全に Mery の閉じた世界の、あまりにもニッチな仕様なので、何の役に立つのかは私にもよく分かっていません😂

    そのあたりをご理解のうえ、どうか自己責任で沼にハマっていただければと思います🫠

     |  Kuro  |  返信
  22. > Mery 側に登録されているマクロや外部ツールのコマンドを、マクロ側から動的に取得できない気がするので、そのあたりをどうやって実現するのか気になるところです😊

    WibView2側でなければローカルファイルを読めるので、Mery.iniファイルを力技で取り込もうか、と考えています。
    Mery.iniは書き換えた部分を最後に追記するような汚いiniファイルではないのでパースは楽そうです。

    > しかし、この Web プレビュー GUI 遊び、面白いのは面白いのですが、完全に Mery の閉じた世界の、あまりにもニッチな仕様なので、何の役に立つのかは私にもよく分かっていません😂

    例えば拡張子qjs等にMeryを関連付けして、スクリプトを直接実行しWibView2画面を開 くことが出来れば、vbscriptとjscript代替の箱庭になれる気がします。
    Mery本体にMCPサーバー機能が出来たら、AIエージェントからMeryを操作するのも面白そうです。

     |  enaka  |  返信
スポンサーリンク