前回から作りはじめた「Web ページの英単語を ヌメロニム
化する Safari 拡張」の続きを、今回もやっていこうと思います。前回の流れで大凡の枠組みは仕上がったので、残るは実際の動きを作り上げていくところから。行き当たりばったりの制作なので、行き詰まるかもしれないですし、好調かもしれないですし。なにはともあれ今日の時間は Safari 拡張に的を絞っていろいろ見ていく回にしますね。どうぞよろしくお願いします。
———————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #294
00:00 開始 00:30 DEC って、Ethernet で有名な会社? 01:39 ヌメロニムで遊んでみる 02:36 Safari Extension の基本構成、おさらい 04:59 ここから、テキストをヌメロニム化していく 07:00 Safari Extension を円滑に開発するための準備 09:12 検出されていた構文エラーの修正 11:12 テキストのヌメロニム化処理 28:08 ヌメロニム完成、差し込んでみる 36:36 クロージング & 今日の間違い箇所を教えてもらった! ————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #294
さて、今日は i18n インターナショナリゼーションについて少し話してみたいと思います。i18n とは、アルファベットの「i」と「n」の間に 18 文字が並ぶことから名付けられたもので、インターナショナリゼーションを指します。この表記法は1985年に DEC(Digital Equipment Corporation)の技術書で初めて使われました。ちなみに、DEC は昔イーサネットアダプターで非常に有名だった会社です。DEC の話題で少し脱線してしまいましたが、昔はイーサネットと言えば DEC でした。
さて、次の話題に移ります。Swift で Web エクステンションを作成しました。Xcode を使用してホストアプリとエクステンションという二つのコンポーネントを作り、そのエクステンション側の実装を進めていきます。今回はネイティブコードを使わず、クライアントサイドで動く JavaScript を書いていきます。
まず、マニフェストファイルにボタンのタイトルや必要な権限などを設定しました。そして、バックグラウンドで動かすスクリプトとして、ボタンが押された時に実行されるコードを書きました。その際、フロント側のタブに対してメッセージを送るようにしました。
具体的には、ドキュメントボディをヌメロニム化するコードを書きました。テキストノードであれば、それをヌメロニムに変換し、テキストノードでなければそのファイルとノードに対して再帰的にヌメロニム化を行います。このようにして、全てのテキストノードをヌメロニム化できることを目指しています。
これを動作確認するためには、まず Safari の設定で「開発メニュー」を有効にし、「開発」メニューから「未署名の機能拡張」を選択します。次に、エクステンションのチェックを入れると、ヌメロニムのボタンが表示されるようになります。ボタンを押す前に、JavaScript コンソールを表示しておき、エラーメッセージが確認できるようにします。
また、バックグラウンドプロセスのメッセージを見るためには、「開発」メニューから「Web 機能拡張バックグラウンドコンテンツ」を選択し、ヌメロニムエクステンションに接続します。この方法で、バックグラウンドプロセスのメッセージを確認できます。
さらに、ネイティブのログ出力を確認するには、macOS の「コンソール」アプリケーションを使用します。「コンソール」アプリケーションで PC 名を選択し、「ストリーミングを開始」をクリックして、検索窓にヌメロニム(機能拡張の名前)を入力します。これにより、nslog などのネイティブのログ出力を確認することができるのです。
これで、フロント側、バックグラウンド側、ネイティブ側のログをそれぞれ確認しながら開発を進めることができます。 ネイティブ確認バックグラウンドを確認した後に、フォアグラウンド確認を行います。こういった準備をしておくと、Safariエクステンションの開発を順調に進めることができます。さて、さっそくエラーが出ていますね。ボタンを押す前にエンタックスエラーが出ているようです。他にもいろいろとエラーが出ていますが、これらは自分には関係ないURLのエラーですので気にしなくて大丈夫です。
ここにアノニマス関数のエラーが発生しています。これらはクライアントに読み込まれているソースに由来するものだと思われますが、とにかく括弧の位置がおかしいみたいです。これが原因で動かなくなっているようです。コンテンツ内の括弧位置を修正しましょう。例えば、エキストノード
だった場合にはコンソールにログを出すなどして、調査を進めます。しかし、ノードに何があったのかを確認するためには少し調べる必要があります。トムのノード
ではなく、エレメント
の方がタグを持っている可能性があります。
エレメント
にはタグがあるのかもしれませんが、エキストノード
にはタグがありません。ただ、タグネーム
があるので、これを使用してXcodeからタグネームを出力することができます。まずはこれを置いておいて、括弧の位置が多くて怪しいので修正します。
ノードのテキストコンテンツを取得しますが、これはテキストノードなのでタグネームは不要です。これを削除して、テキストノードだった場合の処理を進めます。単語の境界でマッチする場合に、サブパターンを利用して最初の文字、中間の文字、最後の文字を取得し、ファースト
、ミドル
、ラスト
という形で取得します。この部分が違っていた場合は後で修正を加えます。
中間のミドルの文字数を取得し、最初の文字と中間の文字数、そして最後の文字で処理を行うと、ヌメロニムが生成されるはずです。まずは置き換えずに、コンソールに出力するようにします。元のテキストも出力します。
元のテキストを出力する準備を行い、異なる場合には再帰的に処理を進めます。これで所望の処理が完了するはずです。では、ちょっと動作を試してみます。 これでビルドをかけます。ビルドが終わると、拡張が差し替えられるんですよ。拡張が差し替えられて、レスポンスがファインドでロードされると、メッセージを送っているんじゃなかったかな。まず、これを何とかしましょうか。
アップグラウンドで、この辺のレシーブリクエストはいいですね。コンテンツのほうかな。この辺ですね。センドメッセージ。グローバルのスコープで何かセンドメッセージしていますよね。これはテンプレートが勝手に入れてきたもので、これは消しちゃおうかな。ここまででオッケーですね。それではビルドをかけてみます。
なんかログをしていますかね。もう一回ビルドをかけて、読み込みしてみますよ。動いた、動いた。動いたけど、コンテンツにスキップなんですよね。そんな感じかな。さっきいろいろあって、ヌメロニムのやつか。文字を調べないと出てこない。しかしこっちはヌメロニムのはず。見つかりませんね。アダマティング(型)を間違っているのかもしれません。
とりあえず、いろいろできたけど本当かな。なんか怪しい感じがするけど、ちゃんと調べていますかね。これなんだろう。なんか変ですね。もう一回動かしてみたけど、アルファでもなくて余計なテキストをちょっと減らしてみました。テキストが見つかって、マッチした場合には出力する。ここがマッチしなかったですよね。で、マッチしなかったらコンテンツテキストをノートとして出しているけど、なんか変なの。アルファベットとか、その辺がいろいろ出てくると思ったんだけど。それから、前回出てきてなかったっけ、変なコードを入れなければね。まあちょっとやってみようか。
ここから、えーっとどこまで。この辺までかな。ここがいらなくて、テキストを取って、それの出力をするって言われたっけ。で、これでもだからいらないからってさ。そしてビルドをかけて、それで実行したときに、タクタクと出なかったけど、タクタクと動きましたか。そうそう、なんか出てくるでしょ。こんな感じで。これはなんだろ、いろいろありますね。でもまた出てきてないかな、クッキーに対する声明とかWikipediaについて。手の中にずらずらと。とにかくさっきよりいっぱいテキストが出ている気がします。それからそうか、逆中っぽいですね。ここ取れてるよね。こんな感じで、いろいろとできてますね。ちゃんとここまでオッケーですね。
じゃあ次、パターンマッチングをやるわけですが、ここでもダメなのかな。単語が合ってないか。ここマッチして、これが出てるのかからマッチしてない気がするんですよね。テキストに対してマッチしているんですかね。JavaScriptの適用権。エグゼクテスト、それとストリングの方のやつと。match
とか matchAll
とかがありますよね。matchAll
でマッチしたのが全体アップ事例だから、matchAll
で配列が取れるような感じかな。戻り値が1してなかった時はどうなんだ。何か書いてないか。いや書いてあるかもしれないけど、matchAll
をここまででちょっとログ出してみますかね。コンソールにログとしてマッチを出力してみます。
これでどうだろう。ビルドかけて、ここでログを1回捨てて、でピックをかけて、他の方ね。何か間違ってるっぽい。matchAll
はあるって書いてあったけど、コンソールワーニングアウトだと思うけど、ログにしてみようか。そして match
をプリントしてみましょう。それだけだよね、大丈夫ですよね。大丈夫な気がするけど、match
だとエラーはないですね。で、ピックをかけるけど、何も出なくなった。これを matchAll
といっても、そもそもビルド・コンパイルし直して動かすと動くよね。だからここが間違っているんでしょう。matchAll
が間違っている。でも matchAll
がないっていうことはないでしょうね、きっと。
こんなふうに、多分こんなふうに手探りで作っていくんだと思うんですけど、もっとすごい使い方を教えてくださいね。 とりあえず、matchAll
を使った正規表現のマッチングについて説明します。まず、文字列に対してmatchAll
メソッドを使用し、その結果を配列に変換しようと試みています。ただ、このコードがうまく動作していないので、他に問題がありそうです。具体的には、contentText
が文字列ではない可能性があります。
JavaScriptで文字列かどうかを調べるには、instanceof
やtypeof
を使用します。instanceof
はオブジェクトのインスタンスを確認するのに使いますが、typeof
であればもっと一般的に使われる方法です。以下のように書き換えます。
if (typeof contentText === 'string') {
// ここに文字列の場合の処理を書く
}
まずmatchAll
メソッドを試してみます。matchAll
は文字列とキャプチャグループを返すので、それを利用します。ただ、結果がイテレーターで返ってくるので、配列に変換するか、for...of
ループを使用して処理します。また、グローバルフラグを付けて正規表現を実行します。
以下がコードの例です。
let regex = /パターン/g;
let matches = contentText.matchAll(regex);
for (const match of matches) {
console.log(match);
}
この方法でうまくいくか確認します。イテレーターが取れているので、次に進みます。これを配列に変換する方法もありますが、ここではシンプルにfor...of
ループを使います。
以下が最終的なコードの例です。
let regex = /パターン/g;
let matches = contentText.matchAll(regex);
for (const match of matches) {
console.log(match);
}
これで、文字列に対するパターンマッチがうまくいくはずです。まず、contentText
が文字列であることを確認し、それに対して正規表現のmatchAll
を適用し、その結果をイテレーターとしてループ処理します。
このようにしていくと、matchAll
メソッドで文字列の中にマッチする部分すべてを取得し、その結果を使って必要な操作を続けられます。 これでマッチした1個1個に対してフィルタをかけて実行すると、間違えてリロードしてしまいましたが、今度は大丈夫ですね。すると、全然ダメなんだな、数が減ってない。エラーでも起こっているのかな?どこまでキャストになるのか、とりあえず全部消してみましょう。
ループの中でこれでログして、ログして7箇所かかっていいんじゃないかな。コマンドだけ。こう動かすと何やらチラチラ出ているので、やっぱりエラーっぽいですね。どこがエラーなんだろう。ここの mid
取っている時点でもうエラーなのかな。これかな?間違えた、あっコンソールログ消えちゃった。もう一回今のところにしてあげて、こうすると…これはこれでいいのか?
実はここまでは OK ですが、問題は links
かな。とりあえずここまで行って links
も取りたいな。ミドルの links
。とりあえず mid
まずこのハートマークの顔文字が出ているかどうかをまず確認したいんですが、出てる出てる、もういいですね。もういっぱい単語取得できてる。
あそこ数字戻しちゃうのか。この数字は両替したいですね。そうすると単語があって、単語は数字を含む。Wは数字を含んじゃうのか、頭マッチングで数字を含まない。regexp
って何だろう、JavaScript 正規表現でアルファベット。素直に範囲で表現しないとダメなのかな。こうなっちゃうのかな?AからZ、まあそうするか、AからZで大小文字を区別しない。こうしてビルドしてイントロを同型して6機の区別が8、はい。動かすと多分単語取れてますね。ってことは links
が間違ってた。
マスクフィルタで文字数取るのって何だっけ、length
だよね文字で取ったと length
、合ってるよね。これで length
合ってるよね。これ入れるとなんかおかしくなってない? length
一緒とか同じなのかな?本当?綺麗でしょ。Ctrl
+ L、どっちも綺麗だけど。まあいいや、こうすると動いたね。何だったんだろう、とりあえず動いた。いいやこれで文字数を取った。リンクのミックス、これをここに入れてあげると当てて6を消して動かすとできた。出ました。
これ何でしょう?わからないよね。これ何でしょう?50円、まあいいや、とにかくできた。いい感じですね。あとはこれを置き換えていくわけですよ。どう置き換えるというのかな?グローバルバーマッチングでもうパターンを置き換えちゃえばいいのか、matchAll
というか execute
だからテキストコンテンツだからグローバルでパターンでいいのか。置き換えればいいんだ。だからテキストをここ、パターンマッチングを置き換えにしてって読めるのか。
JavaScriptの場合の置き換えは replace
を使うことになるのかな。replace
を使ったときにサブパターンってだから一度ができるのかな。JavaScript とリプレイス、パターンサブパターンとかにすると、replace
でパターンと文字列だけど、サブパターンはまたやってみちゃおうか。テキストリプレイスパターンで置き換える文字列として $1
, $links
。links
を取らなきゃいけないってことは無理だね。そうするとグローバルバーマッチングにしちゃうと置き換えがちょっと面倒なのかな。
ループでパターンマッチを重ねて掛けていってリプレイスしていくのになるのかな。文字数はどうしようかな。文字数を導入していく。JavaScriptでその場所を置き換えていくってどうやるんだっけ?テキストが見つかって必ず決まったテキストは必ず同じヌメロニムになることを考えると何やっちゃえばいいのかな。端的にやっちゃえばいいのかな。見つかった文字列マッチを matchAll
してちょっと無茶苦茶やっちゃいますか?
あんまり論理的に微妙な結果するんで、実際に使うことがあるかわからないけど、実際に使ったときはもうちょっと検証したほうがいいと思うんですが、const
ソースとして match[0]
画面で取れるんで、これをリプレイスしてあげる。ここからテキストかテキストをリプレイスかける。text.replace
で、ソースに対して const
ヌメロニム、ヌメロニムイコールこれがヌメロニムですよね。 なので、このようにしてあげると良いと思いますが、いい技だと感じました。ここでは、Swiftのリプレイスについて話します。確か、関数も渡せたはずです。ファンクションスロットタイプリプレイスについてのサンプルには書いていないかもしれませんが、リプレイスメントでストリングを渡さないで関数を渡すという方法もあります。
この方法を使うと、その部分を置き換えることができます。リプレイサーとしてマッチしたものと、$1
、$2
、$3
というキャプチャグループを使うことができます。それで問題ありません。この方法で行こうと思います。やはり、コードはきれいな方が良いので、ヌメロニムは必要ありませんし、オールグループも全部不要かもしれません。とりあえず、ちょっとコメントで残しておきます。
少し弱気な感じですが、これでマッチオールではなく、オールパターンマッチになるので大丈夫です。具体的に、text.replace
関数で、pattern
にマッチングし、そこにファンクションを渡します。そして、そのファンクションをfirst
、middle
、last
で実装します。以下のような感じです:
text.replace(pattern, function (first, middle, last) {
let result = first + middle.length + last;
return result;
});
これを実行すると、結果をreturn
で返す形になります。次に、console.log
で出力してみます。これでログを確認すると、ログが消えてしまうのが面白いです。動きましたが、結果が思った通りではありませんでした。もしかすると、この部分ではないかもしれません。URLError
とか関係ないかもしれません。
うまくいったのかどうかは分からない状況ですが、text.match
の場合、あるいはテキストパターンが一致する場合にこうしないといけないかもしれません。そして、ビルドをかけてみて、それで動かすと、全然ヌメロニムが置き換えられていないことが分かりました。リプレイスが止まっていたのでしょう。
もう一度チェックして、例えばパスだけ見てみます。パスに問題がないか確認し、リプレイスがマッチしているかどうかを確認します。それから、もう一度ビルドをかけて確定します。この辺りを見直して、もし間違っていれば修正しなければなりませんが、今日はここまでにしましょう。
このような微妙な調整を繰り返して完成させる予定ですが、今日は時間が来たのでこれで終わりたいと思います。次回も続けていく予定ですので、次回またお会いしましょう。