読者です 読者をやめる 読者になる 読者になる

#ToDo 関連のメモ ( #sorashima )

「物の状態が最終的にこうなっていたら良いな一覧と、その為の手順を書き込む」アプリを使う上でのメモなど。(内容が古いまま、間違ったままもあるので注意。)広告が自動で挿入される無料版ブログサービスを利用しているので、PVが増えても一銭の得にもなりませぬ

[ 記事一覧へ ]

完了タスクを #Markdown で出力する #HandyFlowy の機能拡張スクリプト

HandyFlowy WorkFlowy

Taskmatorでタスク管理をしていた時は、完了したタスクツリーをメールシェア機能を利用して記録保存していた。しかし、WorkFlowyのiOSアプリにはエクスポートする機能が無い。

HandyFlowyには「Export TEXT」機能や、「Export to Evernote」という機能拡張スクリプトが最初から入っているが、希望する書式では出力してくれない。

Safariには「デスクトップ用サイトを表示」する機能もあるが、

  1. WorkFlowyをiPhoneSafariで開く
  2. 「デスクトップ用サイトを表示」でデスクトップ版に切り替える
  3. 狭い画面をパンしながらなんとかエクスポートする
  4. Draftsにペーストして置換などで整える

なんて面倒くさいことを毎日続けることにウンザリ。

面倒くさくいことは続かない。
続けたかったら、面倒くさくなくする仕組みを作らないといけない。
そこで、スクリプトDIYで作ることにした。

書式は、

  • 検索での最上位トピックは###ヘッダ(h3)
  • インデント付きリストでトピックツリーを表現し、インデントは半角スペース4文字で字下げ
  • ノートはトピックの行末に半角スペース2個と改行で始める
  • ノートの改行のある行の行末も半角スペース2文字と改行
  • 最終的にはEvernoteに保存するため、URLは自動的にリンクに変換されるので処理しない
  • square brackets式に、未完トピックは[ ]、完了トピックは[x]を行頭に付ける
  • 行末には「≫」で該当トピックへのリンク

Javascriptを書くなんて何年ぶりだろう?
埃をかぶったJavascriptの本を引っ張りだして、めくるページ毎に再発見に感動し、「値渡し」と「参照渡し」の違いなどでつまずいたりしながらスクリプトと格闘し、なんとか動くようにした。

var nest = 0;
var pId = "";
var done = false;
var name = "";
var note = "";
var tpcLst = [];

function tpcTrSrch(e) {
    function h() {
        var l = e.childNodes;
        for (var j = 0; j < l.length; j++) {
            tpcTrSrch(l[j])
        }
    }
    var g = e.nodeType;
    var f = e.nodeName;
    if (g == 1) {
        if (f == "DIV") {
            var d = e.attributes;
            var k = "";
            for (var b = 0; b < d.length; b++) {
                switch (d.item(b).nodeName) {
                    case "class":
                        var k = d.item(b).nodeValue;
                        break;
                    case "projectid":
                        pId = d.item(b).nodeValue.replace(/[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-([0-9a-f]+)/, "$1");
                        break;
                    default:
                        break
                }
            }
            switch (true) {
                case (k == ""):
                    h();
                    break;
                case (k.indexOf("project") >= 0):
                    done = false;
                    name = "";
                    note = "";
                    if (k.indexOf("done") >= 0) {
                        done = true
                    }
                    h();
                    break;
                case (k.indexOf("name") >= 0):
                    name = e.innerText.replace(/\n/, "");
                    break;
                case (k.indexOf("notes") >= 0):
                    var c = e.innerText;
                    note = (c.length == 1) ? "" : c;
                    var a = tpcLst.length;
                    tpcLst[a] = {};
                    tpcLst[a].nest = nest;
                    tpcLst[a].pId = pId;
                    tpcLst[a].done = done;
                    tpcLst[a].name = name;
                    tpcLst[a].note = note;
                    break;
                case (k == "children"):
                    nest++;
                    h();
                    nest--;
                    break;
                default:
                    h();
                    break
            }
        }
    }
}
var tpcLst2md = function() {
    var b = "";
    while (tpcLst.length > 0) {
        var d = tpcLst.shift();
        if (d.nest > 0) {
            if (d.nest == 1) {
                b += "### "
            } else {
                for (var a = 0; a <= d.nest - 3; a++) {
                    b += "    "
                }
                b += (d.done) ? "- [x] " : "- [ ] "
            }
            b += d.name + " [≫](https://workflowy.com/#/" + d.pId + ")"
        }
        if (d.note.length != 0) {
            b += "  \n";
            var c = d.note.split("\n");
            for (var a = 0; a < c.length; a++) {
                b += c[a];
                if (a != c.length - 1) {
                    b += "  \n"
                }
            }
        } else {
            b += "\n"
        }
    }
    return b
};
tpcTrSrch(document.getElementById("pageContainer"));
var text = tpcLst2md();
webkit.messageHandlers.CopyToClipboard.postMessage(text);
alert("クリップボードにコピーしました。");

スクリプトのインストールは慎重に。
スクリプトのインストール及びご使用は各自の自己責任でご利用ください。
スクリプトの使用によって、利用者および第三者に損害が発生したとしても、当方は一切責任を負わないものとします。

ScriptMaker等でインストール可能。

実際の動作

#WorkFlowy: Complete日付を指定してHandyFlowy起動するWorkflow - sorashimaのブログ でタスクの完了日付を指定してHandyFlowyを起動↓

スクリプトを実行しクリップボードの内容をDrafts4にペーストした図↓

([トピックのID]の部分は実際は16進数のID)

markdownプレビューすると↓

動作確認には aitatte氏の HandyFlowy上でJavaScriptやCSSの動作確認を手軽に行うスクリプトTester - aitatena を利用した。

ただ、Testerでは上手く動作したのに、ScriptMakerでイザ登録するとエラーを吐いて動かないことがよくあった。
動かなかったスクリプトJavascriptの圧縮ツールに通し、それをJavascriptの整形ツールに通して体裁を戻したら動いてくれた。
HTMLにスクリプトとして埋め込むのに対して、HandyFlowyの方は変数名とか、スクリプトの長さ(変数名や関数名が長ければそれだけサイズが大きくなる)とか、何か制限でもあるのだろうか?Testerでは動いただけに不思議だ。 *1

ちなみに圧縮ツールに掛ける前のソースは↓

var nest = 0;
var pId = "";
var done = false;
var name = "";
var note = "";
var tpcLst = [];

function tpcTrSrch(tNd) {

    function chldNdSrch() {
        // 全ての子DOMノードに対して同じ処理をする
        var tChld = tNd.childNodes;
        for (var i=0; i<tChld.length; i++) {
            tpcTrSrch(tChld[i]);
        }
    }

    var tNTyp = tNd.nodeType;
    var tNNm = tNd.nodeName;
//    var tNV = tNd.nodeValue;
    
    // エレメントノードなら
    if (tNTyp == 1) {
        // DIVエレメントなら
        if (tNNm == "DIV") {
            // class属性とprojectid属性を取得
            var tNdAttrs = tNd.attributes;
            var dCls = "";             
            for (var i=0; i<tNdAttrs.length; i++) {
                switch (tNdAttrs.item(i).nodeName) {
                    case "class":
                        var dCls = tNdAttrs.item(i).nodeValue;
                        break;
                    case "projectid":
                        pId = tNdAttrs.item(i).nodeValue.replace(/[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-([0-9a-f]+)/,"$1");
                        break;
                    default:
                        break;
                }
            }

            switch (true) {
                // classがないDIV
                case (dCls == ""):
                    // 全ての子ノードに対して同じ処理をする
                    chldNdSrch();
                    break;
                // projectクラスなら
                case (dCls.indexOf("project") >= 0):
                    done = false;
                    name = "";
                    note = "";
                    // mainTreeRootクラスなら
//                    if (dCls.indexOf("mainTreeRoot") >= 0) {}
                    // doneクラスなら
                    if (dCls.indexOf("done") >= 0) {
                        done = true;
                    }
                    // 全ての子ノードに対して同じ処理をする
                    chldNdSrch();
                    break;
                // nameクラスなら
                case (dCls.indexOf("name") >= 0):
                    name = tNd.innerText.replace(/\n/,"");
                    break;
                // noteクラスなら
                case (dCls.indexOf("notes") >= 0):
                    var noteTxt = tNd.innerText;
                    // noteクラスのDIVのinnerTextが改行一文字(ノートが無い場合)でなければ保存
                    note = (noteTxt.length == 1) ? "" : noteTxt;
                    var j = tpcLst.length;
                    tpcLst[j] = {};
                    tpcLst[j].nest = nest;
                    tpcLst[j].pId = pId;
                    tpcLst[j].done = done;
                    tpcLst[j].name = name;
                    tpcLst[j].note = note;
                    break;
                // childrenクラスなら
                case (dCls == "children"):
                    nest++;
                    // 全ての子ノードに対して同じ処理をする
                    chldNdSrch();
                    nest--;
                    break;
                 // 万が一に備えて
                default:
                    // 全ての子ノードに対して同じ処理をする
                    chldNdSrch();
                    break;
            }
        }
    }
}

var tpcLst2md = function ()  {
    var md  =  "";
    while (tpcLst.length > 0)  {
        var tp = tpcLst.shift();
        if (tp.nest > 0) {
            //トップレベルのtopicはレベル3のヘッダー
            if (tp.nest == 1) {
                md+="### ";
            } else {
                //半角スペース4文字でインデント
                for (var i = 0; i<= tp.nest - 3; i++) {
                    md += "    ";
                }
                //完了は[x]、未完は[ ]を行頭に付けてブレットリスト
                md += (tp.done) ? "- [x] " : "- [ ] ";
            }
            //topicの後にWorkFlowyへのリンク
            md += tp.name + " [≫](https://workflowy.com/#/" + tp.pId + ")";
        }
        //ノートがあれば
        if (tp.note.length != 0) {
            //topicの後にmarkdownの改行
            md+="  \n";
            var noteLines = tp.note.split("\n");
            for (var i=0; i < noteLines.length; i++) {
                md += noteLines[i];
                if (i != noteLines.length - 1) {
                    //ノートの最終行以外はmarkdownの改行
                    md += "  \n";
                }
            }
        } else {
            //topicの後に普通の改行
            md+="\n";
        }
    }
    return md;
}

tpcTrSrch(document.getElementById("pageContainer"));
var text = tpcLst2md();
//var url = "drafts4://x-callback-url/create?text=" + encodeURIComponent(text);
//window.open(url);
webkit.messageHandlers.CopyToClipboard.postMessage(text);
alert("クリップボードにコピーしました。");

*1:P.S.
aitatter様、アドバイスありがとうございます。
このカテゴリ専用のTwitterアカウントを持っていないのでこちらにてご無礼します。

ScriptMakerの.replace(/\n\s*/g,"")の部分を削除すれば直るかもです

ご指摘通り削除してみたところエラーにならなくなりました。
ありがとうごさいました。

広告を非表示にする