チュートリアル

Markdownでブログコンテンツが書けるCLIクライアントを作ろう!(その1)

ECHOPFのJavaScript SDKを使ってMarkdown記法で書いたドキュメントをブログにアップロードできるソフトウェアを作ります。JavaScript SDKはNode.jsで利用します。

作るもの

作るのはコマンドラインで使うECHOPF向けのブログクライアントです。記法として開発者の間でよく知られているMarkdownを採用します。例えば次のように使います。

初期設定コマンド

初期設定ファイルを生成します。

echopf-blog init .

新しい記事を書くテンプレートを生成

ブログ記事を書くためのテンプレートを生成します。

echopf-blog new

ブログを投稿

ブログ記事を投稿します。

echopf-blog push 生成したMarkdownファイル.md

ブログ記事をダウンロード

クラウドにあるブログ記事のデータをローカルコンピュータ内にダウンロードします。

echopf-blog pull

必要なもの

Node.js

Node.jsはサーバサイド、ローカルコンピュータ上で動作するJavaScriptエンジンになります。Node.jsを使うことでJavaScriptを使ってWebアプリケーションであったり、ローカルコンピュータ上で動作するソフトウェアが開発できるようになります。

ダウンロードはNode.jsの公式プロジェクトサイトから行えます。インストーラーを実行するだけで完了します。

JavaScript SDK

ECHOPFのJavaScript SDKが必要です。こちらからダウンロードできます。

JavaScript SDK

なお、このチュートリアルは作成時点での最新版である1.2.6に対応しています。ファイルをダウンロードしたら解凍し、解凍したフォルダの中にある ECHO.min.js を利用します。

下準備

まずインスタンスやメンバーの追加を行っていきます。これらはすべてECHOPFの管理画面上で行う作業です。

サイトアカウントの作成

サイトアカウントを作成する際にはREST APIを提供しているライセンスプランを用いてください。具体的にはポータルサイト、ECサイト、アプリケーションバックグラウンドになります。スタンダードサイトではREST APIがありませんので注意してください。

新規サイトアカウント作成

ブログインスタンスの作成

サイトアカウントを作り、管理画面にログインします。

管理画面

まず最初にブログインスタンスを作成します。インスタンス管理を開きます。そして、中央上部にあるプラスアイコンをクリックします。

インスタンス管理

出てきたインスタンスの中で、ブログを選択してください。

インスタンス作成

モーダルウィンドウが出てきますので、以下のように情報を入力します。

項目
インスタンスID information
インスタンス名 お知らせ
所属ページ Home
メインメニューに表示 表示する

入力したら生成ボタンを押します。下の画像のようにインスタンスが追加されたら完了です。

ブログインスタンスの追加完了

APIアプリの作成

REST APIを扱うためのAPIアプリを作成します。設定メニューの中にあるAPIアプリ管理を選択します。

APIアプリ管理

画面中央にあるプラスアイコンをクリックします。そうするとモーダルウィンドウが表示されます。アプリケーション名は適当で構いませんが、今回は「InformationClient」としておきます。

APIアプリを追加

そうすると X-ECHO-APP-ID と X-ECHO-APP-KEY という二つの文字列が生成されます。このIDとキーを使ってREST APIを操作しますので覚えておきましょう。

APIキーの生成完了

メンバー管理

ブログインスタンスにアクセスするメンバーを作成します。インスタンス管理に移動し、プラスアイコンをクリックします。そしてメンバーをクリックします。

インスタンスの追加

モーダルウィンドウが出てきますので、以下のように情報を入力します。

メンバーインスタンスの設定

項目
インスタンスID blogger
インスタンス名 ブロガー
所属ページ Home
メインメニューに表示 表示する

生成ボタンをクリックして、インスタンスの一覧に追加されれば完了です。

メンバーを作成する

左側のメニューに追加されたブロガーメニューのメンバー管理を選択します。

メンバー管理の選択

表示されたメンバー管理で中央上部にあるプラスアイコンをクリックします。そうするとユーザ追加するためのモーダルウィンドウが表示されますので、入力してください。ログインID、パスワード、名前、メールアドレスは任意のものを入力してください。

メンバーの追加

項目
ログインID 任意
パスワード 任意
パスワード(確認) 任意
ステータス 有効
グループ 指定しない
名前 任意
メールアドレス 任意

作成するとメンバー管理の一覧にメンバーが追加されます。これで完了です。

メンバーの追加完了

権限設定

左側にあるブログインスタンスの設定メニューを開きます。

設定メニューの選択

さらにアクセスコントロール(ACL)をクリックします。この機能を使って、メンバーやグループ単位に利用できる機能を制限できます。

ACLの選択

記事とカテゴリについて、先ほど作ったメンバー(画面上はブロガーとなっています)に対して一覧表示、個別表示、追加、編集、削除権限を付与します。

権限の設定

設定したら保存ボタンを押します。

投稿時のステータス設定

さらにブログインスタンスの設定メニューから、外部記事投稿設定をクリックします。これはREST APIを使って投稿した記事の公開、非公開を設定するものです。

外部記事投稿設定

モーダルウィンドウが表示されますので有効に指定します。

新規投稿ステータスの変更

保存を押せば完了です。


ここまでで集まった情報についてまとめます。

項目内容
ドメイン サイトアカウント作成時に入力したドメインです blog-sample.echopf.com
X-ECHO-APP-ID APIアプリで作成されたアプリIDです 5176c253e43db45a4eaf84cd36916ee7
X-ECHO-APP-KEY APIアプリで作成されたアプリキーです fb1e85828fd47c6b3342bce4a4fda021
ブログインスタンスID ブログインスタンスのIDです information
メンバーインスタンスID メンバーインスタンスのIDです blogger
ログインID メンバーインスタンスで追加したメンバーのログインIDです blogmember
パスワード メンバーインスタンスで追加したメンバーのパスワードです password

これらの情報を使っていきますのでメモしておいてください。

Node.jsアプリケーションのベースを作る

続いてNode.jsアプリケーションのベースを作成します。Windowsであればコマンドプロンプト、macOSやLinuxであればターミナルを開きます。開いたウィンドウで次のように入力します。

node -v

これで v7.9.0 といった文字が表示されればNode.jsがインストールされています。コマンドがありません、Command not foundといったメッセージが出る場合にはNode.jsがきちんとインストールされているか確認してください。

フォルダの作成

フォルダを作成し、その中に移動します。コマンドの細かな解説はしませんが、一行目がechopf-blogというフォルダの作成、二行目がechopf-blogの中に移動(cdはchange directoryの略です)という意味になります。

mkdir echopf-blog
cd echopf-blog

Node.jsアプリケーションの初期化

移動したら Node.js アプリケーションの初期化を行います。この時使うのはNode.jsアプリケーションのパッケージ(ライブラリ)管理システムであるnpmというコマンドになります。最後のドット「.」を忘れずに入力してください。

npm init .

このコマンドを入力すると対話型で入力を求められます。基本的にすべてエンターキーで構いません。全体像は次のようになるはずです。

$ npm init .
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (echopf-blog) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /path/to/echopf-blog/package.json:

{
  "name": "echopf-blog",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes) yes

この結果、package.json という設定ファイルが生成されます。

コマンドを受け付けられるようにする

例えば echopf-blog new や echopf-blog pull といったコマンドで操作できるようにします。そのためのライブラリとして Commander をインストールします。ライブラリのインストールは先ほど使った npm コマンドを使います。 --save と付けると自動的に package.json を更新してくれます。

npm install commander --save

さらに コマンドで使えるようにするために package.json を更新します。具体的には下記のキーを追加します。

{
  "name": "echopf-blog",
  ...(省略)
  "main": "index.js",
  // 追加ここから
  "bin": {
    "echopf": "./index.js"
  },
  // 追加ここまで
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...(省略)
}

index.js を作成する

では index.js を新規作成します。内容は以下のようになります。 一行目はコマンドとして動かすために必要な記述になります。

#!/usr/bin/env node

// commanderライブラリの読み込み
const program = require('commander');

// commanderの実行内容です
program
  .version('1.0.0') // バージョン番号
  .command('init [directory]') // コマンドの実行方法
  .description('初期設定ファイルを生成します。最初に実行してください。') // コマンドの説明
  // 処理を書きます
  .action((dir) => {
    console.log(`ディレクトリは ${dir} です`)
  });

// commanderでコマンドラインの引数を解釈
program.parse(process.argv);

ローカルで実行してみる

ではこの内容を試してみます。ローカルで書いた内容をそのまま実行するためには npm link というコマンドを使います。最後のドット「.」を忘れないでください。

npm link .

これが終わると echopf というコマンドが使えるようになっているでしょう。そして --help でコマンドの説明が表示できます。

$ echopf --help

  Usage: echopf [options] [command]


  Options:

    -V, --version  output the version number
    -h, --help     output usage information


  Commands:

    init [directory]  初期設定ファイルを生成します。最初に実行してください。

後はコマンドの内容を作っていきます。

ECHOPFのJavaScript SDKを配置する

ダウンロードしたECHOPFのZipファイルを解凍し、ECHO.min.jsをプロジェクトのルートディレクトリに配置します。このJavaScript SDKはNode.jsにも対応しています。

初期設定を行うコマンドの作成

初期設定で行うのは設定ファイルの生成です。echopf init . で行えるようにします。これにより毎回APIのIDやキーを入力することなくechopfコマンドが使えるようになります。さらに手入力で設定ファイルを作るのは面倒なので、コマンドでできるようにします。

ライブラリのインストール

まずコマンドを対話型にしてくれるライブラリ inquirer をインストールします。

npm install inquirer --save

次に設定ファイルに書かれるJSONが見やすく整形される pretty-data をインストールします。

npm install pretty-data --save

クラスの作成

メンテナンス、可読性を考慮してファイルを分割して開発します。次のような構成になります。

├── index.js
├── ECHO.min.js
└── libs
    ├── base.js  // 新規作成
    └── init.js  // 新規作成

base.js にはファイル書き込みなどの共通処理を記述します。そして init.js は初期化処理に必要なコードを記述します。

まず libs/base.js というファイルを作成します。内容はBlogBaseクラスの定義で、次のようになります。このBlogBaseクラスで行っているのは設定情報をクラス内部の変数として保存する処理になります。

const ECHOPF = require('../ECHO');
const path = require('path');
const fs = require('fs');

module.exports = (() => {
  class BlogBase {
    constructor(options) {
      // 記事を保存するディレクトリ
      this.dir = `${path.resolve(options.dir)}/entries/`;
      // 設定ファイルのパス
      this.configPath = `${path.resolve(options.dir)}/echopf.config.json`;
      
      // ECHOPFで用いる設定があれば保存します
      this.config = options.config;
      if (this.configPath) {
        try{
          // 設定ファイルを読み込みます
          this.config = require(this.configPath);
          // ECHOPFを初期化します
          ECHOPF.initialize(
            this.config.domain,
            this.config.applicationId,
            this.config.applicationKey
          );
          // 初期化したECHOPFを保存します
          this.ECHOPF = ECHOPF;
        } catch (e) {
          // 設定ファイルがない場合
        }
      }
    }
  }
  return BlogBase;
})();

libs/init.js というファイルを作成します。これはBlogInitというクラスを持ち、先ほど作成したBlogBaseを継承しています。内容は次のようになります。

const pd = require('pretty-data').pd;
const fs = require('fs');
const inquirer = require('inquirer');
const BlogInit = require('./libs/init');

module.exports = (() => {
  class BlogInit extends BlogBase {
    // コンストラクタ
    constructor(dir) {
      // BlogBaseクラスのコンストラクタを呼び出します
      super({ dir });
    }
    
    // 設定ファイルの存在チェックを行います
    checkConfig() {
    }
    
    // 設定ファイルを作成します
    createConfigFile() {
    }
    
    // 記事を保存するentriesというディレクトリを作成します
    createDirectory() {
    }
  }
  return BlogInit;
})();

ではここから checkConfig 、createConfigFile 、 createDirectory をそれぞれ作成していきます。

設定ファイルの存在チェック(checkConfig)

設定ファイルを誤って上書きしないよう存在を確認し、あれば開発者に上書き確認を行います。この時、対話型インタフェースを実現する inquirer が使えます。Node.jsのファイルチェック fs.access は非同期処理になりますので、 checkConfig 全体を非同期処理 Promise で囲んで処理します。

ファイルの存在を確認し、ユーザが上書きを no とした場合はreject処理にします。エラーコードとして exit を指定しておきます。

checkConfig() {
  const me = this;
  return new Promise((res, rej) => {
    fs.access(me.configPath, (err) => {
      // エラーがない = ファイルがない場合は処理を抜ける
      if (err) return res();
      // ファイルがある場合は確認
      inquirer.prompt([{
        name: 'file',
        message: '設定ファイルが存在します。上書きしていいですか?',
        type: 'confirm',
      }])
        // 選択した場合、answer.file に true または false が入ります
        .then(answer => (answer.file ? res() : rej({ code: 'exit' })));
    });
  });
}

設定ファイルの作成(createConfigFile)

設定ファイルの作成は、先ほどの checkConfig を呼び出し、問題ない(設定ファイルが存在しない、または上書きの許可が出ている)場合には設定内容を書き込みます。この設定内容は askConfig を実行して得られる内容になりますが、後述します。

設定内容はJSONで得られますが、その内容を pretty-data を使って見やすく整形します。そして設定ファイルのパス me.configPath に書き込みます。

createConfigFile() {
  const me = this;
  // 全体を非同期処理にします
  return new Promise((res, rej) => {
    me.checkConfig()
      .then(() => askConfig())
      .then((config) => {
        // 設定内容のJSONを見やすく整形
        const json = pd.json(config);
        // ファイルに書き込み
        fs.writeFile(me.configPath, json, err => (err ? rej(err) : res()));
      })
      .catch((err) => {
        // err.code === 'exit' なのは上書きを拒否した場合です
        // その場合はエラー出ないので res() を呼び出します
        // それ以外の場合はエラーオブジェクトをそのまま次の処理に送ります
        if (err.code === 'exit') res();
        rej(err);
      });
  });
}

設定内容を対話型にする

設定ファイルに書き込む内容は対話型インタフェースで取得するようにします。設定ファイルの存在チェックを行った時と同じく、inquirer が利用できます。

ちょっと長いですが、次のようになります。ドメイン、アプリケーションID、アプリケーションキー、インスタンスID(ブログとメンバー管理)、メンバー管理のログインIDとパスワードになります。そして簡易的に入力チェックも行います。

// 入力チェック
const alphabetValidate = (input, label) => (
  input.match(/^[a-zA-Z0-9\?\-\+\.\_]*$/) ? true : `${label}は英数字しか使えません。`
);

// 対話型で入力してもらう内容
const askConfig = () => {
  // ドメイン
  const questions = [{
    name: 'domain',
    message: 'ドメイン名(例:your-domain.echopf.com): ',
  }];
  // 他は入力チェックやメッセージの作り方が共通なのでまとめてしまいます
  const sections = [
    { name: 'applicationId', label: 'アプリケーションID' },
    { name: 'applicationKey', label: 'アプリケーションキー' },
    { name: 'blogInstanceId', label: 'ブログのインスタンスID' },
    { name: 'memberInstanceId', label: 'メンバー管理のインスタンスID' },
    { name: 'login_id', label: 'ログインID' },
    { name: 'password', label: 'パスワード' },
  ];
  for (let i = 0; i < sections.length; i += 1) {
    // 対話型の内容を作成します
    const section = sections[i];
    questions.push({
      name: section.name,
      message: `${section.label}: `,
      validate: input => alphabetValidate(input, section.label),
    });
  }
  return inquirer.prompt(questions);
};

記事を保存するディレクトリの作成

記事をクラウドから取得したら、entriesというディレクトリに保存することとします。そこで、フォルダを作成する処理を作ります。

// 記事を保存するentriesというディレクトリを作成します
createDirectory() {
  // ディレクトリのパス
  const dirName = this.dir;
  return new Promise((res, rej) => {
    // ディレクトリの存在確認
    fs.access(dirName, (err) => {
      // エラーがない = すでにフォルダが存在している
      if (!err) {
        return res(err);
      }
      // フォルダがなけれあ作成
      fs.mkdir(dirName, (err) => {
        err ? rej(err) : res();
      });
    });
  });
}

処理実行部の作成

ではこの init.js を読み込んで、処理を組み込みます。対象になるのは index.js です。

元々次のようになっていた部分を直します。

// commanderの実行内容です
program
  .version('1.0.0') // バージョン番号
  .command('init [directory]') // コマンドの実行方法
  .description('初期設定ファイルを生成します。最初に実行してください。') // コマンドの説明
  // 処理を書きます
  .action((dir) => {
    console.log(`ディレクトリは ${dir} です`)
  });

下記のように修正します。 libs/init.js を読み込んで、設定ファイルの作成 createConfigFile と ディレクトリの作成 createDirectory を実行します。

const BlogInit = require('./libs/init');
program
  .version('1.0.0') // バージョン番号
  .command('init [directory]') // コマンドの実行方法
  .description('初期設定ファイルを生成します。最初に実行してください。') // コマンドの説明
  // 処理を書きます
  .action((dir) => {
    // 設定ファイルの生成
    const init = new BlogInit(dir);
    init
      // 設定ファイルの作成
      .createConfigFile()
      .then(() =>
        // ディレクトリの作成
        init.createDirectory())
      .then(() => {
        console.log('初期設定が完了しました。続けて pull コマンドを実行してみましょう');
      })
      .catch((err) => {
        console.log(`エラーが発生しました。${JSON.stringify(err)}`);
      });
  });

ここまでで初期設定の処理が完了です。

コマンドを実行する

では初期設定コマンドを実行してみましょう。

$ echopf init .

次のように対話型で入力が求められれば成功です。入力内容はサンプルなので、ご自身のものを入力してください。

$ echopf init .
? ドメイン名(例:your-domain.echopf.com):  example.echopf.com
? アプリケーションID:  087...a73
? アプリケーションキー:  9d9...426
? ブログのインスタンスID:  information
? メンバー管理のインスタンスID:  blogger
? ログインID:  blogger
? パスワード:  Mf6...ZDW
初期設定が完了しました。続けて pull コマンドを実行してみましょう

完了すると、ディレクトリ構成が次のようになっているはずです。

├── ECHO.min.js
├── echopf.config.json
├── entries
├── index.js
├── libs
│   ├── base.js
│   └── init.js
├── node_modules
├── package-lock.json
└── package.json

echopf.config.json が設定ファイルで、次のような内容になっていれば問題ありません。値は例で、実際にはさきほど入力した情報が表示されるはずです。

{
  "domain": "example.echopf.com",
  "applicationId": "087...a73",
  "applicationKey": "9d9...426",
  "blogInstanceId": "information",
  "memberInstanceId": "blogger",
  "login_id": "blogger",
  "password": "Mf6...ZDW"
}

もう一度初期設定コマンドを実行すると、設定ファイルが存在するというメッセージが出るはずです。ここで Y を入力するともう一度設定内容を対話型に聞かれます。 N を押すとキャンセルされます。

$ echopf init .
? 設定ファイルが存在します。上書きしていいですか? No
初期設定が完了しました。続けて pull コマンドを実行してみましょう

管理画面で記事を投稿する

ECHOPFの管理画面にて、テストで一つ記事を投稿しておきます。これは次の「記事の取得処理」を行うためです。左のメニューからお知らせの記事追加を選択します。

記事の追加メニュー

記事を書き、保存してください。

記事の作成

既存の記事を取得する

ここから記事を作成するコマンド pull の作成に入ります。このコマンドはECHOPFのJavaScript SDKを使い、サーバからデータをダウンロードします。次のように実行します。

$ echopf pull

まず処理を実装する libs/pull.js を作成します。内容は次の通りです。 BlogBase を継承するクラスで、まだ中身はありません。

const BlogBase = require('./base');

module.exports = (() => {
  class BlogPull extends BlogBase {
    // 記事取得処理全体
    pull() {
    }
    
    // ブログ記事を取得
    fetchBlogEntries() {
    }
  }
  return BlogPull;
})();

そしてこのBlogPullクラスを扱う処理を index.js に記述します。

const BlogPull = require('./libs/pull');
program
  // コマンド pull を有効にします
  .command('pull')
  .description('サーバ上の記事をダウンロードします。')
  .action(() => {
    // BlogPullの処理
    const blogPull = new BlogPull({
      // 処理はカレントディレクトが対象
      dir: '.',
    });
    
    // 記事取得処理を実行します
    blogPull
      .pull()
      // 取得成功
      .then(() => {
        console.log('記事を取得しました');
      })
      // 取得失敗
      .catch((err) => {
        console.log(`エラーが発生しました。${JSON.stringify(err)}`);
      });
  });

記事の取得処理(fetchBlogEntries)

記事の取得処理 BlogPull.fetchBlogEntries は次のように、JavaScript SDKの Blogs を使います。そして find メソッドに対してブログインスタンスのIDを指定します。

コードとして書くと次のように書きます。

ECHOPF
  .Blogs
  .find("ブログのインスタンスID")
  .then((results) => {
    // 取得結果が入ります
  });

ではこの書き方を fetchBlogEntries に記述します。記事を取得する部分はネットワークを使うので全体を非同期処理 Promise で囲みます。

// ブログ記事を取得
fetchBlogEntries() {
  const me = this;
  return new Promise((res, rej) => {
    // ECHOPFのJavaScript SDKで記事を取得します
    me.ECHOPF
      .Blogs
      .find(me.config.blogInstanceId)
      // 記事取得が成功した場合
      .then((results) => {
        // 結果から記事だけを取り出します
        const entries = results.map((entry) => {
          if (!entry ||
              !entry.constructor ||
              entry.constructor.name !== 'EntryObject') return null;
          return entry;
        });
        // 処理成功に送ります
        res(entries);
      }, (err) => {
        // リソースが見つからない場合
        rej(err);
      });
  });
}

記事が取得された場合、 results の内容は次のようになります。記事 EntryObject 以外にも記事数であったり、次のページの有無と言った情報も入ってきますので、記事情報だけ取り出します。

List {
  '0': 
   EntryObject {
     instanceId: 'information',
     resourceType: 'entry',
     refid: '20171027063553',
     _data: 
      { refid: '20171027063553',
        title: 'バージョン1.0をリリースしました',
        description: '',
        keywords: '',
        robots: '',
        contents: [Object],
        link_status: 1,
        owner: null,
        modified: 2017-10-26T21:40:49.000Z,
        modified_user: 'atsushi@moongift.jp',
        created: 2017-10-26T21:40:49.000Z,
        published: 2017-10-26T21:35:00.000Z,
        categories: [Array],
        resource_type: 'entry',
        url_path: '/information/entry/20171027063553',
        url: 'http://example.echopf.com/information/entry/20171027063553' },
     _newACL: null,
     _currentACL: 
      ACL {
        _all: [Object],
        _allMembers: [Object],
        _specificGroups: {},
        _specificMembers: {} },
     _multipart: false },
  page: 1,
  prevPage: null,
  nextPage: null,
  pageCount: 1,
  count: 1,
  limit: 10,
  order: '*',
  asc: false,
  length: 1 }

記事取得処理全体(pull)

次に記事取得処理全体を処理する BlogPull.pull メソッドの実装です。先ほど作成した fetchBlogEntries の実行と、その結果 entries をファイルとして保存する saveEntries を実行します。 fetchBlogEntries などは非同期処理になりますので、pull メソッド全体も非同期処理 Promise で囲みます。

// 記事取得処理全体
pull() {
  const me = this;
  return new Promise((res, rej) => {
    me
      // 記事を取得します
      .fetchBlogEntries()
      // 取得した記事内容を保存します
      .then(entries => me.saveEntries(entries))
      // 保存が完了したら res を実行します
      .then(() => {
        res();
      })
      // エラーの場合です
      .catch((err) => {
        rej(err);
      });
  });
}

記事をファイルとして保存する(saveEntries)

サーバから取得した記事をMarkdownファイルとして保存します。この処理は受け取った内容を entryToMarkdown メソッドを使ってMarkdown化し、その内容を書き込みます。

** この処理は新しい記事を作成する場合にも使うので BlogBase クラスの中に定義します。**

// 記事をファイルとして保存する
saveEntries(entries) {
  const me = this;
  return new Promise((res, rej) => {
    for (let i = 0; i < entries.length; i += 1) {
      const entry = entries[i];
      const data = this.entryToMarkdown(entry);
      fs.writeFile(`${me.dir}/${entry.refid}.md`, data.join('\n'), err => (err ? rej(err) : ''));
    }
    res(true);
  });
}

ブログ記事のMarkdown化

ブログ記事のMarkdown化は、分かりやすくするために幾つかに分割処理しています。Markdownは次のような構造になります。

---
メタ情報
---
記事概要
<!--more-->
記事詳細

メタ情報は記事タイトルやキーワード、記事公開日時などに加えて、カテゴリ、ACL(アクセス権限)になります。このメソッドも BlogBase クラス内に定義します。

// 記事をMarkdown化
entryToMarkdown(entry) {
  // メタ情報の作成(ここから)
  let data = ['---'];
  data.push(`refid: ${entry.refid}`);

  const metas = [
    'title',
    'description',
    'keywords',
    'robots',
    'link_status',
    'owner',
    'published',
  ];
  data = data.concat(this.pushMeta(metas, entry));
  data = data.concat(this.pushCategories(entry.get('categories')));

  data = data.concat(this.pushAcl(entry.getACL()));

  data.push('---');
  // メタ情報の作成(ここまで)
  
  // 本文の処理
  data = data.concat(this.pushContent(entry.get('contents')));

  return data;
}

メタ情報の設定(pushMeta)

記事タイトルやキーワード、公開日時と言ったメタ情報を取得します。このメソッドも BlogBase クラス内に定義します。

// メタ情報の設定
pushMeta(metas, data) {
  return metas.map(meta => `${meta}: ${data.get(meta) || ''}`);
}

カテゴリの設定(pushCategories)

カテゴリ情報をリスト構造にします。この時の構造は次のようになります。

categories:
  - カテゴリID : カテゴリ名
  - カテゴリID : カテゴリ名
  - カテゴリID : カテゴリ名

処理 pushCategories の内容は次のようになります。このメソッドも BlogBase クラス内に定義します。

// カテゴリの設定
pushCategories(categories) {
  const data = [];
  if (!categories) return [];
  data.push('categories: ');
  for (let i = 0; i < categories.length; i += 1) {
    const category = categories[i];
    data.push(`  - ${category.get('refid')} : ${category.get('name')}`);
  }
  return data;
}

アクセス権限(ACL)の設定(pushAcl)

ACLは以下の4つが指定できます。

  • 全訪問者(未ログイン含む) : _all
  • 全ログインユーザ : _allMembers
  • 特定のグループ : _specificGroups
  • 特定のユーザ : _specificMembers

それぞれの権限設定について、4つのアクションに対して権限が指定できます。

  • 1件取得 : get
  • リスト取得 : list
  • 編集 : edit
  • 削除 : delete

処理は次のようになります。このメソッドも BlogBase クラス内に定義します。

// アクセス権限(ACL)の設定
pushAcl(acls) {
  if (!acls) return [];
  const data = [];
  const metas = [
    '_all',
    '_allMembers',
    '_specificGroups',
    '_specificMembers',
  ];
  data.push('acl:');
  for (let i = 0; i < metas.length; i += 1) {
    const meta = metas[i];
    if (Object.keys(acls[meta]).length === 0) {
      // 空だった場合
      data.push(`  ${meta}: `);
    } else {
      data.push(`  ${meta}:`);
      const acl = acls[meta];
      const subMetas = ['get', 'list', 'edit', 'delete'];
      for (let j = 0; j < subMetas.length; j += 1) {
        const key = subMetas[j];
        if (typeof acl[key] !== 'undefined') {
          data.push(`    ${key}: ${acl[key]}`);
        }
      }
    }
  }
  return data;
}

本文部分の作成(pushContent)

本文を作成する pushContent メソッドでは、内容と詳細を連結します。その際には HTML を Markdown に変換します。そのためのライブラリとして toMarkdown をインストールします。コマンドプロンプトやターミナルにて npm コマンドを実行します。

$ npm install to-markdown --save

そしてこのライブラリを libs/base.js にて読み込みます。

const toMarkdown = require('to-markdown');

pushContent ではHTMLをMarkdownに変換しつつ、画像のパスにドメインを追加します。例えば以下のような変換になります。

![](/path/to/image.png) -> ![](https://example.echopf.com/path/to/image.png)

そして、内容と詳細を <!--more--> で繋ぎます。これはWordPress風にしているだけで、他の区切り記号でも構いません。これは BlogBase クラスのコンストラクタで設定します。

constructor(options) {
  // :(省略)
  // 一番下に以下を追加
  this.bodySplit = '<!--more-->';
}

pushContent の内容は次のようになります。このメソッドも BlogBase クラス内に定義します。

pushContent(contents) {
  if (!contents) return [];
  const data = [];
  const contentType = ['main', 'detail'];
  for (let i = 0; i < contentType.length; i += 1) {
    const type = contentType[i];
    const content = contents[type];
    // HTMLをMarkdownに変換しつつ、画像のURLを変換します
    data.push(toMarkdown(content)
      .replace(/!\[(.*?)\]\((\/.*?)\)/g, `![$1](https://${this.config.domain}$2)`));
    // 内容の後に <!--more--> を追加します
    if (type === 'main') { data.push(`\n${this.bodySplit}\n`); }
  }
  return data;
}

pullコマンドを実行する

ここまでで pull コマンドが完成です。実際に実行してみましょう。

$ echopf-cli pull
記事を取得しました

そうして entries ディレクトリを見ると、Markdownファイルが作られているはずです。

$ ls entries/
20171027063553.md

そして内容は次のような形になります。

---
refid: 20171027063553
title: バージョン1.0をリリースしました
description: 
keywords: 
robots: 
link_status: 1
owner: 
published: Fri Oct 27 2017 06:35:00 GMT+0900 (JST)
categories: 
  - versionup : バージョンアップ
acl:
  _all:
    get: true
    list: true
    edit: false
    delete: false
  _allMembers:
  _specificGroups: 
  _specificMembers: 
---
ブログのCLIクライアントをバージョンアップしました。

### 1.0の主な特徴

*   データ取得に対応しました
*   設定ファイル作成に対応しました

<!--more-->

### 設定ファイルの作成に対応しました

設定ファイルの作成は次のように入力してください。

> echopf init .

一番最後のドット「.」を忘れないようにしてください。これは設定ファイルを作成するディレクトリになります。

ここまでの処理で初期設定を行うところと、記事を取得するまでの流れができあがりました。次回は新しい記事を作成、アップロードする処理を作ります。

なお、ここまでのソースコードはechopfcom/Echopf_Blog_CLI at v1にアップロードされています。実装時の参考にしてください。