Subscribed unsubscribe Subscribe Subscribe

Islands in the byte stream

Technical notes by a software engineer

ExcelからtextareaにコピペするとHTMLのtableに変換するスニペット

ユースケース

  • Excelから社内ブログやWikiに表をコピペしたい

実装

とりあえず paste イベントをうけて処理するのでそのようにする。今回はCodeMirrorで制御されているtextareaなので、CodeMirrorを使ってないない場合は適宜読み替えてください。

ClipboardData

pasteで発行されるClipBoardEventに clipboardData: DataTransfer プロパティがあります。

spec: https://www.w3.org/TR/clipboard-apis/

// Web IDL
dictionary ClipboardEventInit : EventInit {
  DataTransfer? clipboardData = null;
};

こいつが items: DataTransferItemList をもっています。

// Web IDL
interface DataTransfer {
  attribute DOMString dropEffect;
  attribute DOMString effectAllowed;

  [SameObject] readonly attribute DataTransferItemList items;

  void setDragImage(Element image, long x, long y);

  /* old interface */
  [SameObject] readonly attribute DOMString[] types;
  DOMString getData(DOMString format);
  void setData(DOMString format, DOMString data);
  void clearData(optional DOMString format);
  [SameObject] readonly attribute FileList files;
}

なぜクリップボードの中身がリスト状の items になっているかというと、コピーしたものの表現が単一ではないからですね。

たとえば、ブラウザからのコピペはスタイルが適用されているので、 text/plain なプレインテキストと text/html なHTMLテキスト両方を含みます。画像の場合、参照URLがtext/plainで、画像のバイナリデータがimage/*で来ます。ただし、詳細はブラウザごとの違いも多く、たとえばLibreOfficeのスプレッドシートからのペーストは、Chromeだと画像化したtext/pngなitemが含まれますが、IEやFirefoxではテキストデータのみ存在します。

// Web IDL
interface DataTransferItemList {
  readonly attribute unsigned long length;
  getter DataTransferItem (unsigned long index);
}

さて、このitemsの要素は DataTransferItemで、クリップボードの中身の実体です。kindは文字列かファイルかという情報で、typeがMIME typeです。ただし、Firefoxなどitemsの存在しないブラウザもあります。まだ仕様が標準化されていないからでしょう。

// Web IDL
interface DataTransferItem {
  readonly attribute DOMString kind; // "string" or "file"
  readonly attribute DOMString type; // "text/plain", "image/png", ...
  void getAsString(FunctionStringCallback? _callback);
  File? getAsFile();
};

callback FunctionStringCallback = void (DOMString data);

ここで、ExcelのシートからChromeにペーストすると、 items の中身は text/plain, text/html, image/png が入っており、最後の画像データは該当部分のスクショになっています。Excelの要素をGitHub issuesのtextareaにExcelからコピペすると、画像が貼り付けられるのはそのためです。

画像のコピーの場合、itemstext/plain, image/png なので、text/htmlの有無が画像コピーとの違いとなります。

なので、「画像のコピーは画像としてアップロードして貼り付けて、Excelからのコピペは<table>...</table>に変換して貼り付ける」とするならば、以下のロジックでよさそうです。

  • text/plaintext/html のデータが存在して、 text/html のなかに table 要素が含まれるとき、その table 要素をペーストする

以上を加味すると、ペーストイベントをハンドルするイベントリスナは以下のようになるでしょう。

// JavaScript
var editor = CodeMirror.fromTextArea(...);

editor.on('paste', function(_, e) {
  // see https://html.spec.whatwg.org/multipage/interaction.html
  const clipboardData = e.clipboardData;

  const plainTextItem = clipboardData.getData('text/plain');
  const rtfItem = clipboardData.getData('text/rtf');
  const htmlItem = clipboardData.getData('text/html');
  const imageItem = (function (items) {
    if (!items) {
      return null;
    }

    for (let i = 0; i < items.length; i++) {
      if (items[i].type.startsWith('image/')) {
        return items[i];
      }
    }
    return null;
  })(clipboardData.items);

  console.log(clipboardData.types);
  console.log([plainTextItem, rtfItem, htmlItem, imageItem]);
  if (htmlItem && plainTextItem) {

    // rich contents, such as Excel and Power Point
    const html = new DOMParser().parseFromString(htmlItem, 'text/html');
    const tables = html.getElementsByTagName('table');

    if (tables.length) { // includes <table> tags
      const content = html.getElementsByTagName('body')[0].innerHTML;
      editor.replaceSelection(content);
      e.preventDefault();
      e.stopPropagation();
    } else {
      // TODO: ask users what to paste (HTML or plain text)
      // fallback to default
    }

  } else if (imageItem) {
    const blob = imageItem.getAsFile();
    const matched = blob.type.match(/^image\/(\w+)$/);
    if (matched) {
      const ext = matched[1]; // e.g. "png", "jpeg", or "gif"
      const filename = 'clipboard.' + ext;
      const formData = new FormData();
      formData.append('file', blob, filename);
      upload(formData);

      e.preventDefault();
      e.stopPropagation();
    }
  }
});

というわけで実装してみたものの、だいぶ複雑だしブラウザによる挙動の違いもあるのでちょっと使いづらい部分があります。

TSVやCSVをコードスニペットとして貼り付けるとtableとして表示する、というほうがいいかもしれないですね。

公開用DockerイメージにAndroid SDKを含めるのはライセンス違反という話

Android Software Development Kit License Agreementにこういう項目があります。

3.4 You may not use the SDK for any purpose not expressly permitted by the License Agreement. Except to the extent required by applicable third party licenses, you may not: (a) copy (except for backup purposes), modify, adapt, redistribute, decompile, reverse engineer, disassemble, or create derivative works of the SDK or any part of the SDK; or (b) load any part of the SDK onto a mobile handset or any other hardware device except a personal computer, combine any part of the SDK with other software, or distribute any software or device incorporating a part of the SDK.

まあ要は、変更したり再配布したりリバースエンジニアリングしたりするなってことですね。そして、Docker imageにAndroid SDKを含めて再配布するのはまさにライセンス違反というわけです。

DockerでAndroidアプリをビルドするのがそれなりに広まってきた様子ですが、Android SDKの含まれたイメージをDocker Hubで公開してはいけないということになります。

Android SDKライセンスが現状に追いついていないともいえるかもしれませんが、ライセンスはライセンスです。AndroidアプリをDockerでビルドしている各位はご注意ください。

私もうっかりDocker HubでSDK入のイメージを公開しているのですが、近日中に削除する予定です*1。これらに依存している場合はご注意ください。

削除予定のイメージ

*1:Dockerfileを公開することは問題ないので残します。

Makefileを自己文書化するハックを注入するスクリプト: inject-make-help

Re: Makefileを自己文書化する make2help | おそらくはそれさえも平凡な日々

ぼくもmakeはわりと使う方で、AndroidプロジェクトだろうとiOSプロジェクトだろうとよく使うコマンドセットをMakefileとして追加するのが大好きなんですが、self-documented Makefileは存在はしっていたもののセットアップが面倒でやっていませんでした。

じゃあ make2help はどうかというと、ただひとつのコマンドをインストールすると常self-documented Makefileを使えるという点では優れたアイデアなんですが、じゃあ今度はプロジェクトメンバーに make2help をインストールするように指示しないといけなくなってしまうのがイマイチだなあと。makeのメリットの一つは、敢えてインストールしなくてもだいたい入っているという点だと思うので。

というわけで、self-documented Makefile hackをMakefileに注入するスクリプトを書きました。まあホントはこのhackもgrep(1) とか awk(1) ではなく perl(1) で書き直したいところですが、とりあえずオリジナルのものを注入するだけです。すでに注入済みなら何もしません。

これなら、誰か一人が inject-make-help を実行したあとの Makefile をコミットしておけば誰でも make help が使えます。どうでしょう。

#!/usr/bin/env perl
# inject-make-help(1): To inject help hacks to the Makefile
use 5.10.0;
use strict;
use warnings;
use autodie;

# FIXME: use perl(1) instead of grep(1), sort(1), and awk(1)
my $help_task = q{
help:
    @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
} =~ s/^[ ]{4}/\t/xmsgr;

my $makefile = shift(@ARGV) // 'Makefile';

my $content = do {
    local $/;
    open my $fh, '<', $makefile;
    <$fh>;
};

if ($content !~ /\Q$help_task/xms) {
    $content .= $help_task;
    
    my($phony_tasks) = ($content =~ /^\.PHONY:([^\n]+)/xms);
    if (not defined $phony_tasks) {
        $content .= q{
.PHONY: help
} =~ s/^[ ]{4}/\t/xmsgr;
    } elsif ($phony_tasks !~ /\bhelp\b/xms) {
        $content =~ s/^(\.PHONY:[^\n]+)/$1 help/xms;
    }

    say "Inject help triks to $makefile";
    open my $fh, '>', $makefile;
    print $fh $content;
    close $fh;
}

ターゲットの Makefile はこんな感じ:

.DEFAULT_GOAL := help

foo: ## The foo task
  echo foo

bar: ## The bar task
  echo bar

これに対して inject-make-help をするとこんな感じになります:

.DEFAULT_GOAL := help

foo: ## The foo task
  echo foo

bar: ## The bar task
  echo bar

help:
   @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: help

.PHONY は既存のものがあればそれに対して help を追加します。

git branch の結果を時間順にソート: git branch --sort=-authordate

id:kazuho さんと「gitのbranchを消すべきか否か」という話をしていて、ぼくの「ローカルにせよリモートにせよbranchが増えすぎると目的のブランチを見つけられない」という意見に対して次のエントリを教えてもらったのでした。

git branch の結果を時間順にソート - kazuhoのメモ置き場

一理あるかもしれないと思ってこれをgitに組み込むためにgitのソースコードを眺めていたら、実はもうできるということを知りました。それがこれ:

# 新しいのが下
git branch --sort=authordate

# 新しいのが上
git branch --sort=-authordate

このソートに使えるフィールドは、 git branch --help を引くと "The keys supported are the same as those in git for-each-ref." といわれるので、 git for-each-ref --help を引くとそこにいろいろ書いてあります。

なおソースだとこのあたりで定義されているようです。

git/ref-filter.c at v2.8.4 · git/git · GitHub

はてなブログでRust対応のアップデートをしてほしい!

お題「シンタックス・ハイライト機能で対応してほしい言語」

Rust のシンタックス・ハイライトは対応されているようですが、この日記を書いた時点だと、extern, match, where などがハイライトされていなくて、対応バージョンが古いような気がします。シンタックス・ハイライトのアップデートをお願いします!

例:

// echo.rs
extern crate getopts;

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();

    let mut opts = getopts::Options::new();
    opts.optflag("n", "", "do not output the trailing newline");
    let matches = opts.parse(&args);

    match matches {
        Ok(m) => {
            show(&m.free);

            if !m.opt_present("n") {
                print!("\n");
            }
        }
        Err(_) => {
            show(args);
            print!("\n");
        }
    };
}

fn show<C: IntoIterator>(args: C)
    where C::Item: std::fmt::Display
{
    let mut i = 0;
    for arg in args {
        if i > 0 {
            print!(" ");
        }
        i += 1;
        print!("{}", arg);
    }
}

github.comのアカウントは仕事用と私用で分ける方がいいの?

一行まとめ:分ける理由・分けない理由両方あるのでどちらにすべきということはない

会社で github.com (not GHE) を採用するにあたって、アカウントについてガイドラインを制定すべきなのかどうか悩んでいます*1。とりあえず現状把握のためにアンケートを取ってみました。

だいたい 1対2くらいで「分けている」と「分けてない」に別れる結果となりました。私のアカウントは「分けていない」し、周りでも分けている人をあまり見かけなかったので、分けないのが普通と思っていたのですが、意外と分けている人が多く「分けない普通」とはいえないなというのが第一印象です。

分けないメリットは個人にも会社にもあると思います:

  • アカウント所有者にとっては管理が単純
    • github.com はアカウント切り替えが簡単ではない(2FAを有効にしていると特に)のでなおさら
  • 「仕事でOSSにコミットする」というケースを個人の成果として積み上げられる
  • 「プライベートでOSSにコミットする」というケースを会社のポジティブなイメージに繋げられる

一方で、分ける方も言い分はあります。

  • 仕事とプライベートの活動を混在させたくないという思いはあって当然だと考えられる
  • 実際、家にいる時も会社のリポジトリの通知やアクティビティが見えてしまうのは嫌だという意見を聞いた
  • アカウントを分けないと実名とネット上のアイデンティティが紐付けられやすくなり、プライバシーの問題が起きうる
    • 会社のorganizationを管理する側としては、アカウント名は自由でよくても見えるところに本名を設定してほしいという願いがある
    • その結果、「会社としては、本名とハンドルネームをひも付けてほしい」ということになる(それをルールにするかどうかは別として)
    • 「身バレしたくない」という個人の願いと「管理しやすい状態であってほしい」という会社の願いが矛盾する
      • その結果として生まれるのが アカウント管理台帳 だとしたら哀しい世界だが…
  • 一般論として、クラウドサービスのアカウントは個人用と会社用で分けるべきという考えもある
    • たとえば、Google accountなどは分かれていてしかるべき(たとえGoogle Apps for Workを採用していないとしても)
    • ただしこの点では github.com は「会社としても個人としても分けないメリットがある」という性質をもつので一般論を適用する必要はないと思っています

無難なとところとしては、会社としてはルールは作らず、GitHubのアカウント管理方法を説明して「どちらでもよい」とするしかないのかなという気がします。

ここで上げていない分ける理由、分けない理由もあると思うので、もし思いついたらコメントなどいただけると幸いです。

*1:https://help.github.com/articles/github-terms-of-service/ によると、複数のfree accountは認められないとありますが、会社用アカウントはfree accountではないと考えられるのでここではアカウントを分けることは利用規約に準拠していると解釈して話をしています。

Android Instant Apps FAQをざっと眺めた

Google I/O 2016で発表されたアプリをインストールしなくても使える "Android Instant Apps" ですが、技術が不明ということで様々な憶測を呼んでいます。

developer.android.com

これについては、すでにFAQがあっていくつかの疑問には答えられていました。

https://developer.android.com/topic/instant-apps/faqs.html

まとめると

  • Android 4.1から使える
  • プロジェクトごとに成果物(APK)は通常版とinstant版の2つのビルド設定を持つ
    • "Developers simply configure the project to create two build artifacts: the installable APK and the instant version"
  • instant版は通常版と同じAndroid APIが使えるが、バックグラウンドサービスなどは利用できない
  • パーミッションモデルはruntime permissionsを使う
    • 「古いデバイス用の追加作業は不明」と書いてあるが…?
    • Google Play servicesが処理を担うからパーミッションはいらないのかな?
  • publishは通常版とおなじくGoogle Play Developer Consoleで行う

これを読んでもよくわかりませんが、専用のbuild variantでAPKを作ってpublishする感じに見えますね。