くりーむわーかー

プログラムとか。作ってて ・試しててハマった事など。誰かのお役に立てば幸いかと。 その他、いろいろエトセトラ。。。

Excel

Redmine エクセルの表をWikiの記法(textile)に変換

以前、タイトルの内容のものを書いたけど、あれは.net側に変換のモジュールを持たせてたので、ちゃんとRedmineのプラグインとして動くようにした。

ソースは↓に置いてあるので、使いたい方はご自由にお使いください。公開できるようなソースじゃないのですが、個人的に割と便利だったので上げておきます。テストとかはほぼしてないので、好きに直してください。

ソース

多分、大丈夫だと思うのですが、いちを「nokogiri」と「css_parser」が必要です。

動かすときは↓の感じです。

Wikiとかチケットとかの入力のところで、「E2T」ってボタンが出る。クリックでペースト用のエリアが出る。
sample01

エクセルの表をコピー。↓は国勢調査の何か。
sample02

エリアにペーストするとTextile形式に変換する。
sample03

そしたらそのままコピペでWikiとかの入力欄に貼り付け。
sample04

↓の感じになる。
sample05

完璧に復元はしてないけど、十分かなと。

ボタンのつけ方とか絶対いけてない。変換のロジックも書き殴ってそのままあげちゃってるのでご注意をば。。。

エクセルの表をRedmineのWikiテーブルにしたい

--追記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

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

問合せ