記事一覧はこちら

Qiitaのメモ機能をローカルで使う方法

つまり、MarkDownのテキストファイルをHTMLに変換しローカルのjsでメニューを表示する imgTemp-2015-12-13-08-12-52 セットのmarkdown.7zファイル ただMarkdownをhtmlに変換するのはgithubAPIだからオンラインが必要だけど・・・

ローカルのマークダウン記法のテキストファイルをhtmlに変換する。Githubに認証なしで使えるMarkdownAPIがあるのでそれを使う。ので、必然的にフォーマットはGithubMarkdownになる。 Markdown | GitHub Developer Guide それだけだとcssは無いし、qiitaに特徴的なメニューが無い。 cssSass: Syntactically Awesome Style Sheetsで書く。変換はめんどいのでsassをコンパイル出来る単一jarファイルを作る方法 – FNBで書いた方法でcssに変換する。

メニューは、まずmarkdownをhtmlに変換する所で##の見出しの一覧を作って <h1>見出し</h1>のhtmlに仕込んでおく。

<div id="menu">
<a href="#user-content-ショートカットキー"><h1>ショートカットキー</h1></a>
<a href="#user-content-第二階層"><h2>第二階層</h2></a>
<a href="#user-content-コンソールで使える予約語"><h1>コンソールで使える予約語</h1></a>
<a href="#user-content-console.logで使えるフォーマット"><h1>console.logで使えるフォーマット</h1></a>
</div>

後はjsでスクロールイベントに合わせて色を変える。便利便利。

これでローカルでMarkdownの便利な書き方、モノはhtmlなので画像もOK、Qiitaの便利なメニューも使える。そして閲覧はオフラインで出来てプライベートにメモれる。markdownを変換する時にgithubに投げる必要があるけど。これで完璧なメモ環境が出来た。 imgTemp-2015-12-13-08-44-46imgTemp-2015-12-13-08-47-20 なんとか.mdを_convert.batにD&DでconvertedFilesにhtmlが出来る。ので、画像もconvertedFilesの中に置く。 .sassはsass.batにD&Dで.cssに変換する。

"use strict";
window.addEventListener("DOMContentLoaded", function(){
  var ss=new ScrollSpy(".anchor",10);
  document.addEventListener("scroll",function(){
    ss.onScroll();
  });
}, false);
class ScrollSpy {
  constructor(anchorQuery,offsetMargin) {
    this.anchorQuery=anchorQuery;
    this.offsetMargin=offsetMargin;
  }
  onScroll(){
    var index=this.find();
    var nodes=this.getMenuNodes();
    for(var i=0;i<nodes.length;i++){
      if(i==index){
        this.changeMenuAttribute(nodes[i],true);
      }else{
        this.changeMenuAttribute(nodes[i],false);
      }
    }
  }
  find(){
    var qs=document.querySelectorAll(this.anchorQuery);
    var tbs=[];
    for(var i=0;i<qs.length;i++){
      var rect = qs[i].getBoundingClientRect();
      if(this.offsetMargin<rect.top){
        continue;
      }
      tbs.push({"index":i,"top":rect.top,"name":decodeURIComponent(qs[i].getAttribute("href").substr(1))});
    }
    console.table(tbs);
    if(tbs.length==0){
      return 0;
    }else{
      return tbs[tbs.length-1].index;
    }
  }
  getMenuNodes(){
    return document.querySelectorAll("#menu h1,#menu h2,#menu h3,#menu h4,#menu h5,#menu h6");
  }
  changeMenuAttribute(node,flag){
    if(flag){
      node.classList.add("active");
    }else{
      node.classList.remove("active")
    }
  }
}
package org.fushihara.github.markdown;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringEscapeUtils;

import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Request.Builder;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

public class GithubMarkdown {
    static final Charset utf8 = Charset.forName("utf-8");
    final String tempReplaceKey;
    OkHttpClient httpClient = new OkHttpClient();

    public static void main(String[] args) throws FileNotFoundException, IOException, URISyntaxException {
        String markdownTextPath = "";
        String saveDirectory = "";
        String savePath = null;
        String templeteTxtPath = GithubMarkdown.getDefaultTempleteFilePath();
        for (String string : args) {
            if (string == null || string.equals("")) {
                continue;
            } else if (string.startsWith("-d:")) {
                saveDirectory = string.substring(3);
            } else if (string.startsWith("-t:")) {
                templeteTxtPath = string.substring(3);
            } else {
                markdownTextPath = string;
            }
        }
        if (markdownTextPath.equals("")) {
            System.out.println("GithubMarkdown.jar [-d:保存フォルダ] [-t:テンプレートテキスト] markdownテキストのパス");
            return;
        }
        if (saveDirectory.equals("")) {
            savePath = new File(new File(markdownTextPath).getParentFile(), new File(markdownTextPath).getName() + ".html").getPath();
        } else {
            savePath = new File(saveDirectory, new File(markdownTextPath).getName() + ".html").getPath();
        }
        System.out.println("version:" + System.getProperty("java.version"));
        System.out.println("load:" + markdownTextPath);
        System.out.println("save:" + savePath);
        GithubMarkdown ghm = new GithubMarkdown();
        String markdownText = GithubMarkdown.loadText(markdownTextPath);
        System.out.println("rawText:" + markdownText.length() + " chars");
        markdownText = ghm.syntaxHighlightKeepFileName(markdownText);
        String markdownHtmlRaw = ghm.convertMarkdown(markdownText);
        markdownHtmlRaw = ghm.syntaxHighlightRestoreFileName(markdownHtmlRaw);
        System.out.println("markDownText:" + markdownHtmlRaw.length() + " chars");
        String headText = GithubMarkdown.loadText(templeteTxtPath);
        String finalText = headText;
        finalText = finalText.replace("/*title*/", new File(markdownTextPath).getName());
        finalText = finalText.replace("/*body*/", markdownHtmlRaw);
        finalText = finalText.replace("/*menu*/", ghm.getMenuHtml(markdownText));
        ghm.saveText(savePath, finalText);
    }

    public GithubMarkdown() {
        this.tempReplaceKey = String.format("x%x", System.identityHashCode(this));
    }

    /**
     * 自分自身のjarファイル名の拡張子をtxtにしたパスを返す
     * 
     * @throws URISyntaxException
     * @throws IOException
     */
    private static String getDefaultTempleteFilePath() throws URISyntaxException, IOException {
        String myFile = new File(GithubMarkdown.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getAbsolutePath();
        if (!myFile.endsWith(".jar")) {
            throw new IOException(myFile);
        }
        String tempPath = myFile.substring(0, myFile.length() - 4) + ".txt";
        return tempPath;
    }

    /** ファイル名の構文があるqiita独自のシンタックスハイライトの構文からファイル名の部分を一旦移動させる */
    private String syntaxHighlightKeepFileName(String markdownRaw) {
        final Pattern pat = Pattern.compile("^(```[a-zA-Z0-9_]+?):(.+)", Pattern.MULTILINE);
        final Matcher matcher = pat.matcher(markdownRaw);
        final StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            // 一致したグループは matcher.group(n) で取得できる。
            // ここで replacement を加工する。
            String replacement = this.tempReplaceKey + ":" + matcher.group(2) + System.lineSeparator() + matcher.group(1);
            matcher.appendReplacement(sb, replacement);
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    private String syntaxHighlightRestoreFileName(String markdownHtml) {
        final Pattern pat = Pattern.compile("<p>" + this.tempReplaceKey + ":(.+?)</p>\\s\\s(<div[^\\>]+?>)([\\s\\S]+?)</div>");
        final Matcher matcher = pat.matcher(markdownHtml);
        final StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            // 一致したグループは matcher.group(n) で取得できる。
            // ここで replacement を加工する。
            String replacement = String.format("%2$s<div class=\"code-lang\">%1$s</div><div class=\"highlight-pre\">%3$s</div></div>", matcher.group(1).toString(), matcher.group(2).toString(), matcher.group(3).toString());
            matcher.appendReplacement(sb, replacement);
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    private static String loadText(String path) throws IOException {
        try {
            byte[] bytes = Files.readAllBytes(new File(path).toPath());
            return new String(bytes, utf8);
        } catch (FileNotFoundException e) {
            return "";
        }
    }

    private String getMenuHtml(String rawText) {
        StringBuilder sb = new StringBuilder();
        Pattern pat = Pattern.compile("\\s*(\\#+)\\s*(.+)");
        Matcher mat = pat.matcher(rawText);
        while (mat.find()) {
            String sharps = mat.group(1).toString().trim();
            String title = mat.group(2).toString().trim();
            sb.append(createMenuHtmlOneItem(sharps.length(), title));
            sb.append(System.lineSeparator());
        }
        return sb.toString();
    }

    private String createMenuHtmlOneItem(int level, String label) {
        return String.format(Locale.US, "<a href=\"%3$s\"><h%1$d>%2$s</h%1$d></a>", level, StringEscapeUtils.escapeHtml4(label), "#" + StringEscapeUtils.escapeXml11("user-content-" + label));
    }

    private void saveText(String path, String content) throws IOException {
        Files.write(new File(path).toPath(), content.getBytes(utf8), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
    }

    private String convertMarkdown(String rawText) throws IOException {
        Builder rb = new Request.Builder();
        // rb.addHeader("key", "value");
        rb.url("https://api.github.com/markdown/raw");
        RequestBody rbody = RequestBody.create(MediaType.parse("text/plain"), rawText);// 第二引数はbyte[]やfileの場合もある
        rb.post(rbody);
        Request request = rb.build();
        OkHttpClient client = new OkHttpClient();
        String result;
        Response response = client.newCall(request).execute();
        result = response.body().string();
        return result;
    }
}