markdownのtextデータをhtmlタグにコンバートするツール「markdown_viewer」

2020年5月1日

テクノロジー

1日1ツール作成、100本ノックをやっているとか、いないとか、という状態の下駄です。

本日の謎掛け

「markdown」とかけまして・・・ 「買ったばかりの服」と、ときます。 そのココロは・・・ タグが付いていると、外したくなります。

text2markdownライブラリが欲しかった件

先日作成した、githubみたいなgitリポジトリ管理ツール「gitpage」で、 やはり、本家と同じ用に、README.mdを、リポジトリトップ画面に表示したいと考えて、markdown形式のテキストファイルを表示してみたが、 htmlタグに変換しなければ、見栄えがなんとも悪い。 似たような事を考える人はいるだろうから、ネットに変換ライブラリがたくさん落ちていると考えたのだが、 探しても、なかなかいいのが見つからない。 ほとんどの人が、markdownのcssだけをアップしていて、肝心のタグ変換は、テキストエディタのプラグインぐらいしか見つからなかった・・・ 探し方が緩かったのかもしれないが・・・ まあ、無いのであれば、自分で作ればいいかと考えて、サクッと1日かけて作ってみました。

ソースコード公開

;$$markdown_viewer = (function(){ var __options = { target : "body", style : "normal" }; var LIB = function(){}; // 起動scriptタグを選択 LIB.prototype.currentScriptTag = (function(){ var scripts = document.getElementsByTagName("script"); return this.currentScriptTag = scripts[scripts.length-1].src; })(); LIB.prototype.event = function(target, mode, func){ if (target.addEventListener){target.addEventListener(mode, func, false)} else{target.attachEvent('on' + mode, function(){func.call(target , window.event)})} }; LIB.prototype.urlinfo = function(uri){ uri = (uri) ? uri : location.href; var data={}; var urls_hash = uri.split("#"); var urls_query = urls_hash[0].split("?"); var sp = urls_query[0].split("/"); var data = { uri : uri , url : sp.join("/") , dir : sp.slice(0 , sp.length-1).join("/") +"/" , file : sp.pop() , domain : sp[2] ? sp[2] : "" , protocol : sp[0] ? sp[0].replace(":","") : "" , hash : (urls_hash[1]) ? urls_hash[1] : "" , query : (urls_query[1])?(function(urls_query){ var data = {}; var sp = urls_query.split("#")[0].split("&"); for(var i=0;i<sp .length;i++){ var kv = sp[i].split("="); if(!kv[0]){continue} data[kv[0]]=kv[1]; } return data; })(urls_query[1]):[] }; return data; }; LIB.prototype.upperSelector = function(elm , selectors) { selectors = (typeof selectors === "object") ? selectors : [selectors]; if(!elm || !selectors){return;} var flg = null; for(var i=0; i<selectors.length; i++){ for (var cur=elm; cur; cur=cur.parentElement) { if (cur.matches(selectors[i])) { flg = true; break; } } if(flg){ break; } } return cur; } // ゼロパディング LIB.prototype.zeroPadding = function(num , len){ var num_len = num.toString().length; if(num_len > len){ return num; } var zero_len = len - num_len; var zero_val = ""; for(var i=0; i<zero_len; i++){ zero_val += "0"; } return (zero_val + num.toString()) . slice( -1 * len); } LIB.prototype.construct = function(){ var lib = new LIB(); switch(document.readyState){ case "complete" : new MAIN;break; case "interactive" : lib.event(window , "DOMContentLoaded" , function(){new MAIN});break; default : lib.event(window , "load" , function(){new MAIN});break; } }; // 基本起動処理 var MAIN = function(options){ this.options = this.setOptions(options); this.setCSS(); this.set_markdown(); }; // options MAIN.prototype.setOptions = function(options){ var new_options = JSON.parse(JSON.stringify(__options)); if(options){ for(var i in options){ new_options[i] = options[i]; } } return new_options; }; // set-css MAIN.prototype.setCSS = function(){ if(document.querySelector("link[data-markdown-viewer='1']")){return} var myScript = new LIB().currentScriptTag; var href = myScript.replace(".js",".css"); var link = document.createElement("link"); link.setAttribute("data-markdown-viewer","1"); link.rel = "stylesheet"; link.href = href; var head = document.getElementsByTagName("head"); head[0].appendChild(link); }; // MarkDown-view MAIN.prototype.set_markdown = function(){ if(typeof this.options.target === "undefined" || !this.options.target){ this.options.target = "body"; } var targets = document.querySelectorAll(this.options.target); if(!targets || !targets.length){return;} for(var i=0; i<targets.length; i++){ this.md_convert(targets[i]); } }; MAIN.prototype.md_convert = function(elm){ if(!elm){return;} var text = elm.innerHTML; if(!text){return;} // console.log(text.replace(/\n/g,"---")); // marking elm.setAttribute("data-markdown" , "converted"); elm.setAttribute("data-markdown-style" , this.options.style); // convert var lines = text.split("\n"); for(var i=0; i<lines.length; i++){ // lines[i] = this.md_tag_escape(lines[i]); lines[i] = this.md_tag_br(lines[i]); lines[i] = this.md_tag_head(lines[i]); lines[i] = this.md_tag_emphasis(lines[i]); lines[i] = this.md_tag_link(lines[i]); lines[i] = this.md_tag_horizon(lines[i]); } text = lines.join("\n"); text = this.md_tag_list(text); text = this.md_tag_pre(text); text = this.md_tag_h1(text); text = this.md_tag_quote(text); text = this.md_tag_table(text); // view elm.innerHTML = text; }; // " $" -> <br> MAIN.prototype.md_tag_br = function(text){ if(text.match(/^(.*?) {2}$/)){ text = RegExp.$1 +"<br>"; } return text; }; // # MAIN.prototype.md_tag_head = function(text){ if(text.match(/^(.*?)#{6} (.*?)$/)){ text = RegExp.$1 +"<h6><a name='"+RegExp.$2+"'>"+ RegExp.$2 +"</a></h6>"; } else if(text.match(/^(.*?)# {5}(.*?)$/)){ text = RegExp.$1 +"<h5><a name='"+RegExp.$2+"'>"+ RegExp.$2 +"</a></h5>"; } else if(text.match(/^(.*?)# {4}(.*?)$/)){ text = RegExp.$1 +"<h4><a name='"+RegExp.$2+"'>"+ RegExp.$2 +"</a></h4>"; } else if(text.match(/^(.*?)# {3}(.*?)$/)){ text = RegExp.$1 +"<h3><a name='"+RegExp.$2+"'>"+ RegExp.$2 +"</a></h3>"; } else if(text.match(/^(.*?)# {2}(.*?)$/)){ text = RegExp.$1 +"<h2><a name='"+RegExp.$2+"'>"+ RegExp.$2 +"</a></h2>"; } else if(text.match(/^(.*?)# (.*?)$/)){ text = RegExp.$1 +"<h1><a name='"+RegExp.$2+"'>"+ RegExp.$2 +"</a></h1>"; } return text; }; // - MAIN.prototype.md_tag_list = function(text){ var lines = text.split("\n"); var flg = false; for(var i=0; i<lines.length; i++){ if(lines[i].match(/^(.*?)- (.*?)$/)){ if(flg === false){ lines[i] = "<ul>"+ RegExp.$1 +"<li>"+ RegExp.$2 +"</li>"; flg = true; } else{ lines[i] = RegExp.$1 +"<li>"+ RegExp.$2 +"</li>"; } } else if(flg === true){ lines[i] = "</ul>\n" + lines[i]; flg = false; } } return lines.join("\n"); } // > MAIN.prototype.md_tag_quote = function(text){ var lines = text.split("\n"); var flg = false; var datas = []; for(var i=0; i<lines.length; i++){ if(lines[i].match(/^([ \t]*?)((&gt;){1,})(.*?)$/)){ var count = RegExp.$2.length / 4; if(flg === false){ datas.push("<blockquote>"); flg = count; } else if(flg < count){ datas.push("<blockquote>"); flg = count; } else if(flg > count){ datas.push("</blockquote>"); flg = count; } lines[i] = RegExp.$1 + RegExp.$4; } else if(flg !== false){ datas.push("</blockquote>"); flg = false; } datas.push(lines[i]); } return datas.join("\n"); } // *..* , -..- MAIN.prototype.md_tag_emphasis = function(text){ if(text.match(/^(.*?) \*\*\*(.+?)\*\*\* (.*?)$/) || text.match(/^(.*?) \-\-\-(.+?)\-\-\- (.*?)$/)){ text = RegExp.$1 +"<strong><em>"+ RegExp.$2 +"</em></strong>" + RegExp.$3; } else if(text.match(/^(.*?) \*\*(.+?)\*\* (.*?)$/) || text.match(/^(.*?) \-\-(.+?)\-\- (.*?)$/)){ text = RegExp.$1 +"<strong>"+ RegExp.$2 +"</strong>" + RegExp.$3; } else if(text.match(/^(.*?) \*(.+?)\* (.*?)$/) || text.match(/^(.*?) \-(.+?)\- (.*?)$/)){ text = RegExp.$1 +"<em>"+ RegExp.$2 +"</em>" + RegExp.$3; } else if(text.match(/^(.*?) ~~(.+?)~~ (.*?)$/)){ text = RegExp.$1 +"<del>"+ RegExp.$2 +"</del>" + RegExp.$3; } return text; } // ``` || ~~~ MAIN.prototype.md_tag_pre = function(text){ var lines = text.split("\n"); var flg = false; for(var i=0; i<lines.length; i++){ if(lines[i].match(/^(.*?)```(.*?)$/)){ if(flg === false){ lines[i] = lines[i].replace("```","<pre>"); flg = true; } else{ lines[i] = lines[i].replace("```","</pre>"); flg = false; } } else if(lines[i].match(/^(.*?)~~~(.*?)$/)){ if(flg === false){ lines[i] = lines[i].replace("~~~","<pre>"); flg = true; } else{ lines[i] = lines[i].replace("~~~","</pre>"); flg = false; } } } return lines.join("\n"); } // ![..](..) MAIN.prototype.md_tag_link = function(text){ // image if(text.match(/^([ \t]*?)\!\[(.+?)\]\((.+?)\)(.*?)$/)){ var url = ""; // global if(RegExp.$3.match(/^[http|https]\:\/\//)){ url = RegExp.$3; } // local else{ var urlinfo = new LIB().urlinfo(); var uid = typeof urlinfo.query.uid !== "undefined" ? urlinfo.query.uid : ""; var id = typeof urlinfo.query.id !== "undefined" ? urlinfo.query.id : ""; var sp = RegExp.$3.split("."); var ext = sp.pop(); var branch = typeof urlinfo.query.branch !== "undefined" ? urlinfo.query.branch : ""; url = "image.php?mode=git&uid="+uid+"&id="+id+"&ext="+ext+"&branch="+branch+"&path="+ RegExp.$3 } text = RegExp.$1 + "<img src='"+ url +"' alt='"+ RegExp.$2 +"' />"; } // link else if(text.match(/^([ \t]*?)\[(.+?)\]\((.+?)\)(.*?)$/)){ text = RegExp.$1 + "<a href='"+ RegExp.$3 +"' title='"+ RegExp.$2 +"' />"+ RegExp.$2 +"</a>"; } return text; } // ***,---,___,=== MAIN.prototype.md_tag_horizon = function(text){ if(text.match(/^( \t*?)\*{3}( \t*?)$/) || text.match(/^( \t*?)\-{3}( \t*?)$/) || text.match(/^( \t*?)\_{3}( \t*?)$/) || text.match(/^( \t*?)\={3}( \t*?)$/)){ text = RegExp.$1 +"<hr>"+ RegExp.$2; } return text; } // == MAIN.prototype.md_tag_h1 = function(text){ var lines = text.split("\n"); var flg = false; var newDatas = []; for(var i=lines.length-1; i>=0; i--){ if(flg !== false){ if(lines[i].match(/^([ \t]*?)[\<](.*?)$/) || lines[i].match(/^[^ \ta-zA-Z0-9]/)){ newDatas.unshift("<h1 name='"+flg+"'>"+flg+"</h1>"); newDatas.unshift(lines[i]); flg = false; } else{ flg = lines[i] + flg; } } if(flg === false && lines[i].match(/^([ \t]*?)\=\=([ \t]*?)$/)){ flg = ""; } else if(flg !== false && i==0){ newDatas.unshift("<h1 name='"+flg+"'>"+flg+"</h1>"); break; } else if(flg === false){ newDatas.unshift(lines[i]); } } return newDatas.join("\n"); } // table MAIN.prototype.md_tag_table = function(text){ var lines = text.split("\n"); var flg = false; var newDatas = []; var normals = []; for(var i=0; i<lines.length; i++){ if(lines[i].match(/^([ \t]*?)\|/)){ normals.push(lines[i]); flg = true; } else if(flg !== false){ if(normals.length === 1){console.log(1); newDatas.push(normals[0]); } else if(normals.length >= 2 && normals[1].indexOf("-") == -1){ newDatas.concat(normals); } else if(normals.length >= 2){ var settings = this.set_table_setting(normals[1]); newDatas.push("<table>"); // header newDatas.push(this.set_table_cell(normals[0] , settings , "th")); // datas for(var j=2; j<normals.length; j++){ if(!normals[j]){continue;} newDatas.push(this.set_table_cell(normals[j] , settings , "td")); } newDatas.push("</table>"); } normals = []; flg = false; } else{ newDatas.push(lines[i]); } } return newDatas.join("\n"); } MAIN.prototype.set_table_setting = function(line){ var cells = line.split("|"); var data = [cells[0]]; for(var i=1; i<cells.length-1; i++){ data[i] = (function(cell){ switch(cell){ case ":--" : return "left"; case "--:" : return "right"; case ":--:" : return "center"; default : return ""; } })(cells[i]); } return data; }; MAIN.prototype.set_table_cell = function(line,settings,tag){ var cells = line.split("|"); var data = ""; for(var i=1; i<cells.length-1; i++){ data += "<"+tag+" class='"+settings[i]+"'>"+cells[i]+"</"+tag+">"; } return "<tr>"+data+"</tr>"; }; return MAIN; })(); /* Normal */ [data-markdown="converted"]{ /* white-space : pre-wrap; word-break : break-all; */ background-color:white; } /* [data-markdown="converted"]:after, [data-markdown="converted"]:before { display: table; content: ""; } [data-markdown="converted"]:after { clear: both; } */ /* [data-markdown="converted"]>:first-child { margin-top: 0; } [data-markdown="converted"]>:last-child { margin-bottom: 0; } */ [data-markdown="converted"] a { background-color: initial; color: #0366d6; text-decoration: none; font-size : 14px; } [data-markdown="converted"] a:active, [data-markdown="converted"] a:hover { outline-width: 0; text-decoration: underline; } [data-markdown="converted"] a:not([href]) { color: inherit; text-decoration: none; } [data-markdown="converted"] strong { font-weight: inherit; font-weight: bolder; font-weight: 600; } [data-markdown="converted"] h1, [data-markdown="converted"] h1 > a { /* font-size: 2em; */ margin: .67em 0; font-size: 32px; /* font-size: 2em; */ } [data-markdown="converted"] h1, [data-markdown="converted"] h2, [data-markdown="converted"] h3, [data-markdown="converted"] h4, [data-markdown="converted"] h5, [data-markdown="converted"] h6{ margin-top: 8px; margin-bottom: 8px; margin-top: 8px; margin-bottom: 16px; font-weight: 600; line-height: 1.25; } [data-markdown="converted"] h1, [data-markdown="converted"] h2{ font-weight: 600; padding-bottom: .3em; border-bottom: 1px solid #eaecef; } [data-markdown="converted"] h2, [data-markdown="converted"] h2 > a { font-size: 24px; font-size: 1.5em; } [data-markdown="converted"] h3, [data-markdown="converted"] h3 > a { font-size: 20px; font-size: 1.25em; } [data-markdown="converted"] h3, [data-markdown="converted"] h4, [data-markdown="converted"] h3 > a, [data-markdown="converted"] h4 > a { font-weight: 600; } [data-markdown="converted"] h4, [data-markdown="converted"] h4 > a { font-size: 16px; font-size: 1em; } [data-markdown="converted"] h5, [data-markdown="converted"] h5 > a { font-size: 14px; font-size: .875em; } [data-markdown="converted"] h5, [data-markdown="converted"] h6, [data-markdown="converted"] h5 > a, [data-markdown="converted"] h6 > a { font-weight: 600; } [data-markdown="converted"] h6, [data-markdown="converted"] h6 > a { font-size: 12px; font-size: .85em; color: #6a737d; } [data-markdown="converted"] blockquote, [data-markdown="converted"] details, [data-markdown="converted"] dl, [data-markdown="converted"] ol, [data-markdown="converted"] p, [data-markdown="converted"] pre, [data-markdown="converted"] table, [data-markdown="converted"] ul { margin-top: 0; margin-bottom: 16px; } [data-markdown="converted"] img { border-style: none; max-width: 100%; box-sizing: initial; background-color: #fff; } [data-markdown="converted"] img[align=right] { padding-left: 20px; } [data-markdown="converted"] img[align=left] { padding-right: 20px; } [data-markdown="converted"] code, [data-markdown="converted"] kbd, [data-markdown="converted"] pre { font-family: monospace,monospace; font-size: 1em; } [data-markdown="converted"] code { padding: .2em .4em; margin: 0; font-size: 85%; background-color: rgba(27,31,35,.05); border-radius: 3px; } [data-markdown="converted"] pre { word-wrap: normal; } [data-markdown="converted"] pre>code { padding: 0; margin: 0; font-size: 100%; word-break: normal; white-space: pre; background: transparent; border: 0; } [data-markdown="converted"] pre code { display: inline; max-width: auto; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background-color: initial; border: 0; } [data-markdown="converted"] hr { box-sizing: initial; height: 0; overflow: visible; height: 0; margin: 15px 0; overflow: hidden; background: transparent; border: 0; border-bottom: 1px solid #dfeaf5; border-bottom-color: #eee; height: .25em; padding: 0; margin: 24px 0; background-color: #e1e4e8; border: 0; } [data-markdown="converted"] hr:after, [data-markdown="converted"] hr:before { display: table; content: ""; } [data-markdown="converted"] hr:after { clear: both; } [data-markdown="converted"] input { font: inherit; margin: 0; overflow: visible; } [data-markdown="converted"] [type=checkbox] { box-sizing: border-box; padding: 0; } [data-markdown="converted"] * { box-sizing: border-box; } [data-markdown="converted"] input { font-family: inherit; font-size: inherit; line-height: inherit; } [data-markdown="converted"] input::-webkit-inner-spin-button, [data-markdown="converted"] input::-webkit-outer-spin-button { margin: 0; -webkit-appearance: none; appearance: none; } [data-markdown="converted"] :checked+.radio-label { position: relative; z-index: 1; border-color: #0366d6; } [data-markdown="converted"] table { border-spacing: 0; border-collapse: collapse; display: block; width: 100%; overflow: auto; } [data-markdown="converted"] table th { font-weight: 600; } [data-markdown="converted"] td, [data-markdown="converted"] th { padding: 0; font-size:14px; } [data-markdown="converted"] table td, [data-markdown="converted"] table th { padding: 6px 13px; border: 1px solid #b6c9db; } [data-markdown="converted"] table td.left, [data-markdown="converted"] table th.left { text-align : left; } [data-markdown="converted"] table td.center, [data-markdown="converted"] table th.center { text-align : center; } [data-markdown="converted"] table td.right, [data-markdown="converted"] table th.right { text-align : right; } [data-markdown="converted"] table tr { background-color: #fff; border-top: 1px solid #c6cbd1; } [data-markdown="converted"] table tr:nth-child(2n) { background-color: #dfeaf5; } [data-markdown="converted"] p { margin-top: 0; margin-bottom: 10px; } [data-markdown="converted"] blockquote { margin: 0; padding: 0 1em; color: #6a737d; border-left: .25em solid #dfeaf5; font-size:14px; } [data-markdown="converted"] blockquote>:first-child { margin-top: 0; } [data-markdown="converted"] blockquote>:last-child { margin-bottom: 0; } [data-markdown="converted"] ol, [data-markdown="converted"] ul { padding-left: 0; margin-top: 0; margin-bottom: 0; padding-left: 2em; } [data-markdown="converted"] ol ol, [data-markdown="converted"] ul ol { list-style-type: lower-roman; } [data-markdown="converted"] ol ol ol, [data-markdown="converted"] ol ul ol, [data-markdown="converted"] ul ol ol, [data-markdown="converted"] ul ul ol { list-style-type: lower-alpha; } [data-markdown="converted"] ol ol, [data-markdown="converted"] ol ul, [data-markdown="converted"] ul ol, [data-markdown="converted"] ul ul { margin-top: 0; margin-bottom: 0; } [data-markdown="converted"] li { word-wrap: break-all; font-size : 14px; } [data-markdown="converted"] li>p { margin-top: 16px; } [data-markdown="converted"] li+li { margin-top: .25em; } [data-markdown="converted"] dl { padding: 0; } [data-markdown="converted"] dl dt { padding: 0; margin-top: 16px; font-size: 1em; font-style: italic; font-weight: 600; } [data-markdown="converted"] dl dd { padding: 0 16px; margin-bottom: 16px; } [data-markdown="converted"] dd { margin-left: 0; } [data-markdown="converted"] code, [data-markdown="converted"] pre { font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size: 14px; } [data-markdown="converted"] pre { margin-top: 0; margin-bottom: 0; padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #dfeaf5; border-radius: 3px; } /* Style : Dark */ [data-markdown="converted"][data-markdown-style="dark"]{ /* white-space : pre-wrap; word-break : break-all; */ background-color:#012340; color:white; } [data-markdown="converted"][data-markdown-style="dark"] pre { background-color: #033E8C; } [data-markdown="converted"][data-markdown-style="dark"] *{ color:white; } [data-markdown="converted"][data-markdown-style="dark"] table td, [data-markdown="converted"][data-markdown-style="dark"] table th { border: 1px solid white; background-color:#033E8C; } [data-markdown="converted"][data-markdown-style="dark"] table tr:nth-child(2n) td, [data-markdown="converted"][data-markdown-style="dark"] table tr:nth-child(2n) th{ background-color: #3271c2; }

使い方

1. 上記ライブラリ、jsとcssを同じ階層に設置して、jsをscriptタグで読み込みます。 <script src="markdown_viewer/src/main.js"></script> 2. htmlの下の方に、下記のように、markdown表示させたいエレメントのselectorでオブジェクト起動します。 <script> new $$markdown_viewer({ target : ".markdown", style : "normal" }); </script> styleには、「normal」以外に「dark」モードを用意していますが、色味などが気に入らない人は、自分で任意のstyle値にして、cssファイルの下部にあるstyle色指定の箇所をコピーして作ってもらえれば、反映されます。

ライセンスと配布

基本的にMITです。 githubにおいているので、気になる人は、cloneしてお使いください。 https://github.com/yugeta/markdown_viewer 全ての機能を再現できているわけではないので、要望があれば、プルリクくださいませ。

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。