--追記2017/5/21
こっちにRedmineのプラグイン的な何かを置きました。

RedmineのWikiで表的なまとめを作ろうとするとtextile形式で作ることになるんだけど、ちょっと見た目にこりだしたりすると、とたんにえらいもの書かないといけなくなる。なので、エクセルでまとめた表をぱっと貼り付けられると便利。なんかいいプラグインないかなーと探してみたけど、グッとくるものがいないので、とりあえず作った。

まずはエクセルの表をコピーしてテキストエリアあたりに貼り付け⇒セルのフォーマット情報も含めて内容を取得する。Webサイトで。これにはClipboradApiを使用。下の感じ。

document.querySelector('#pasteArea').addEventListener('paste', function (e) {
    console.log(e.clipboardData.getData('text/html'));
});

クリップボードのデータを取得するとき、だいたい”text/plain”で指定するんだけど、htmlで指定すると、セルのフォーマット情報も含んだHTML形式で取得できる。↓の感じ。

sample01

で、あとはこのHTMLを解析してTextile形式に変換すれば良いという寸法です。ホントはこの流れをRedmineのプラグインで作りたかったんだけど、Ruby触るの久しぶり過ぎて、すごい時間かかりそうだったので、.netに逃げました。.netでやる場合、HTMLの解析は「HtmlAgilityPack」を使用する。Nugetから取得。

とりあえずで作ったのは、TextAreaにエクセルの表をコピペすると、サーバ側にそのHTMLをテキストで投げて、サーバ側で解析して結果を返す的なAPI。

で困ったことにHTMLをテキストで投げると、.netのセキュリティのチェックにデフォルトで引っかかる。なので、アノテーションでそのチェックを外してあげる。↓の感じ。

[HttpPost]
[ValidateInput(false)]//コレ
public ActionResult Excel2Textile(int id , FormCollection cols)
{
    string resultStr = Conv(cols);
    return Json(resultStr, JsonRequestBehavior.AllowGet);
}

そしたら、HTMLを解析する。まず、HTMLのテキストをAgilityPackに食わせる。

    StringBuilder wkSb = new StringBuilder();
    HtmlDocument htmlDoc = new HtmlDocument();
    htmlDoc.LoadHtml(excelTableHTML);

で、あとはTableタグ拾って⇒TRタグ拾って⇒TDタグ拾ってを繰り返し。

    foreach (var rootnode in htmlDoc.DocumentNode.SelectNodes(@"//table"))
    {
        //テーブルのTRで回す
        foreach (var tmpnode in rootnode.SelectNodes(@"tr"))
        {
            //TDそれぞれで定義を付ける
            foreach (var tmpTdNode in tmpnode.SelectNodes(@"td"))
            {

"//table/tr"って書けば外側のループはいらないんだけど理由は後で。これでとりえあず、中身の内容は拾えるんだけど、スタイルはAgilityPackでは解析できないっぽい?なので別途、スタイルシートを解析するライブラリを使う。今回使ったのは「ExCss」。取得はNugetから。これを使ってClass定義とstyle属性の内容を解析する。

今回はそこまで厳密にテーブルを再現したいわけではないので、背景色・文字色・文字寄せ・セル幅くらいを再現する。スタイルの解析は下の感じで書く。

var parser = new Parser();
var stylesheet = parser.Parse(htmlDoc.DocumentNode.SelectSingleNode(@"//style").InnerHtml);

//Class名は.付きにしないと当たらない。ex: .HogeClass
var styleElm = stylesheet.StyleRules.Where(n => n.Value == wkclass).FirstOrDefault();
if (styleElm != null)
{
    foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
    {
        if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
        break;
    }
}

Class定義はこれでいいんだけど、タグ内のStyle属性やる場合、ExCssはクラス定義の形式じゃないと解析できない模様なので、ちょっと文字列足してあげる。↓の感じ。

//クラス名はなんでもよい
var tmppparse = parser.Parse(".hoge{" + tmpTdNode.Attributes["style"].Value.Trim() + "}");

これでだいたい出来上がりなんだけど、問題が一つ。画像とか図形が表にかかってると、マイクロソフトのオレオレ定義が入り込んでくる。↓の感じ。

sample02

あと、この定義が入ると、通常のtdタグの中にさらにtableタグが入り込んでくる。てんやわんや。なので、一番表のTableだけで処理したいので、最初のループで最初のtableタグのみ処理するように仕込んでる。

超絶やっつけで作ったやつの全体像は下の感じ。見せられたもんではないのですが、とりあえず。同じロジック2回書いてるし・・・あとで整理しないと。


//ExcelをRedmineのWiki用に変換する
public string ExcelTable2WikiTable(string excelTableDef)
{
    StringBuilder wkSb = new StringBuilder();
    HtmlDocument htmlDoc = new HtmlDocument();
    htmlDoc.LoadHtml(excelTableDef);
    string wkInnerText = "";

    var parser = new Parser();
    var stylesheet = parser.Parse(htmlDoc.DocumentNode.SelectSingleNode(@"//style").InnerHtml);

    foreach (var rootnode in htmlDoc.DocumentNode.SelectNodes(@"//table"))
    {
        //テーブルのTRで回す
        foreach (var tmpnode in rootnode.SelectNodes(@"tr"))
        {
            //TDそれぞれで定義を付ける
            foreach (var tmpTdNode in tmpnode.SelectNodes(@"td"))
            {
                if (tmpTdNode.InnerText.Contains("<!--[if gte vml 1]>"))
                {
                    HtmlDocument innerHtmlDoc = new HtmlDocument();
                    innerHtmlDoc.LoadHtml(tmpTdNode.OuterHtml);
                    foreach (var innerNode in innerHtmlDoc.DocumentNode.SelectNodes(@"//table/tr/td"))
                    {
                        wkInnerText = innerNode.InnerText;
                        break;
                    }
                }
                else
                {
                    wkInnerText = tmpTdNode.InnerText;
                }

                Dictionary<string, string> wkDic = new Dictionary<string, string>();
                string classsep = "";
                wkSb.AppendFormat("|");
                //列結合
                if (tmpTdNode.Attributes.Where(n => n.Name == "colspan").Count() > 0)
                {
                    wkSb.AppendFormat("\\{0}", tmpTdNode.Attributes["colspan"].Value.Trim());
                    classsep = ".";
                }
                //行結合
                if (tmpTdNode.Attributes.Where(n => n.Name == "rowspan").Count() > 0)
                {
                    wkSb.AppendFormat("/{0}", tmpTdNode.Attributes["rowspan"].Value.Trim());
                    classsep = ".";
                }
                //最初にスタイル属性に入ってる奴を処理
                if (tmpTdNode.Attributes.Where(n => n.Name == "style").Count() > 0)
                {
                    var tmppparse = parser.Parse(".hoge{" + tmpTdNode.Attributes["style"].Value.Trim() + "}");
                    var styleElm = tmppparse.StyleRules.Where(n => n.Value == ".hoge").FirstOrDefault();
                    if (styleElm != null)
                    {
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
                        {
                            if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "color"))
                        {
                            if (!wkDic.ContainsKey("color")) wkDic.Add("color", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "width"))
                        {
                            if (!wkDic.ContainsKey("width")) wkDic.Add("width", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "text-align"))
                        {
                            if (!wkDic.ContainsKey("text-align")) wkDic.Add("text-align", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                    }
                }
                //CLASSで指定されてる内容を処理
                if (tmpTdNode.Attributes.Where(n => n.Name == "class").Count() > 0)
                {
                    string wkclass = "." + tmpTdNode.Attributes["class"].Value.Trim();
                    var styleElm = stylesheet.StyleRules.Where(n => n.Value == wkclass).FirstOrDefault();
                    if (styleElm != null)
                    {
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
                        {
                            if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "color"))
                        {
                            if (!wkDic.ContainsKey("color")) wkDic.Add("color", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "width"))
                        {
                            if (!wkDic.ContainsKey("width")) wkDic.Add("width", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "text-align"))
                        {
                            if (!wkDic.ContainsKey("text-align")) wkDic.Add("text-align", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                    }
                }
                StringBuilder wkStyleStr = new StringBuilder();
                foreach (var tmpkey in wkDic.Keys)
                {
                    wkStyleStr.Append(tmpkey + ":" + wkDic[tmpkey] + ";");
                }
                if (wkDic.Count > 0) wkSb.Append("{" + wkStyleStr.ToString() + "}");
                wkSb.AppendFormat("{0}{1}", classsep, wkInnerText);
            }//td
            wkSb.Append("|" + Environment.NewLine);
        }//tr
        break;
    }//table
    
    return wkSb.ToString();
}

ちなみに、変換した結果をWikiに貼った結果は下。

sample03

けっこーちゃんと出来てる気がする。あー、ちゃんと列・行の結合にも対応出来てたりする。