├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── README_ja.md
├── __test__
├── article2fileString.test.ts
├── article2xml.test.ts
├── atomPubRequest.test.ts
├── atomPubRequest.test
│ └── articles.json
├── compare.test.ts
├── fileRequest.test.ts
├── findFiles.test.ts
├── loadConfig.test.ts
├── new.test.ts
├── pull.test.ts
├── push.test.ts
├── testDir.ts
├── tmp
│ └── .gitkeep
├── xml-example
│ ├── entry.xml
│ ├── entry
│ │ └── 26006613566848996.xml
│ └── entry__question__page=1588813317.xml
├── xml2articlesPage.test.ts
└── xml2articlesPage.test
│ ├── article0.txt
│ ├── article1.txt
│ ├── article2.txt
│ ├── article3.txt
│ ├── article4.txt
│ ├── article5.txt
│ ├── article6.txt
│ ├── article7.txt
│ ├── article8.txt
│ ├── article9.txt
│ └── feeds.xml
├── bin
└── gimonfu
├── example
├── .gimonfu.json
└── entry
│ ├── 2010
│ └── 01
│ │ └── 01
│ │ └── 000000.md
│ ├── 2020
│ └── 05
│ │ ├── 13
│ │ ├── 122148.md
│ │ ├── 122220.md
│ │ ├── 122622.md
│ │ └── 122736.md
│ │ ├── 06
│ │ └── 190022.md
│ │ └── 07
│ │ ├── 171242.md
│ │ ├── 171307.md
│ │ └── 171333.md
│ ├── NuxtTsV292.md
│ ├── draft.md
│ ├── hello-new.md
│ ├── hello.md
│ └── privateMethodTest-newPath.md
├── jest.config.js
├── logo.png
├── package-lock.json
├── package.json
├── src
├── article2fileString.ts
├── article2xml.ts
├── atomPubRequest.ts
├── compare.ts
├── fileRequest.ts
├── findFiles.ts
├── fixLineFeeds.ts
├── gitCommitDate.ts
├── index.ts
├── init.ts
├── loadConfig.ts
├── new.ts
├── ping.ts
├── pull.ts
├── push.ts
├── startup.ts
├── type
│ └── index.d.ts
└── xml2articlesPage.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: unit test
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | test-ubuntu:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Initialize
17 | run : yarn
18 |
19 | - name: Build
20 | run : yarn build:prod
21 |
22 | - name: Test
23 | run : yarn test
24 |
25 | test-windows:
26 | runs-on: windows-latest
27 |
28 | steps:
29 | - uses: actions/checkout@v2
30 |
31 | - name: Initialize
32 | run : yarn
33 |
34 | - name: Build
35 | run : yarn build:prod
36 |
37 | - name: Test
38 | run : yarn test
39 |
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | ignore/
4 | coverage/
5 | /__test__/tmp/**/*
6 | !/__test__/tmp/.gitkeep
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Keisuke Nakayama
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | English / [Japanese](README_ja.md)
2 |
3 | # gimonfu
4 |
5 | 
6 |
7 | gimonfu is a CLI tool to manage articles of Hatena-blog.
8 |
9 | gimonfu upload(download) markdown files to(from) Hatena-blog.
10 |
11 | ## Setup
12 |
13 | ```sh
14 | # Set the edit mode to Markdown. (Hatena-Blog > Settings > Basic Settings)
15 |
16 | $ yarn global add gimonfu
17 | # or $ npm install --global gimonfu
18 |
19 | $ mkdir blog
20 | $ cd blog
21 | $ gimonfu init
22 | ```
23 |
24 | ## Usage
25 |
26 | ### Register Credentials
27 |
28 | To register credentials to `.gimonfu.json`, you can run like this:
29 |
30 | ```sh
31 | $ gimonfu init
32 | ```
33 |
34 | Please execute this command before executing `$ gimonfu pull` or `$ gimonfu push`.
35 |
36 | Credentials are registered by creating a `.gimonfu.json` file in the current directory.
37 | You can also create `.gimonfu.json` manually without executing this command.
38 |
39 | ### Download Articles
40 |
41 | To download new or updated articles to `entry/` directory, you can run like this:
42 |
43 | ```sh
44 | $ gimonfu pull
45 | ```
46 |
47 | ### Upload Articles
48 |
49 | To upload new or updated articles in `entry/` directory, you can run like this:
50 |
51 | ```sh
52 | $ gimonfu push
53 | ```
54 |
55 | The relative path from the `entry/` directory is set as a custom URL with uploading.
56 |
57 | #### Judging posts/updates
58 |
59 | Post a new file that corresponds to the following.
60 |
61 | - YAML-Frontmatter doesn't have an article ID.
62 |
63 | Update the following files from an already published article.
64 |
65 | - YAML-Frontmatter has the article ID.
66 | - There are any changes.
67 | - The last update date is newer than the one already published.
68 |
69 | #### Overwriting a file
70 |
71 | When a new post/update is made, the contents of the file will also be overwritten to match the content of the post.
72 |
73 | For example, an ID is written into the YAML-Frontmatter when new posts are made, or they are deleted, if there are unnecessary fields in the YAML-Frontmatter before new posts/updates are made.
74 |
75 | ### Create New Draft Article
76 |
77 | To create a new draft article in the `entry/` directory, you can run like this:
78 |
79 | ```sh
80 | $ gimonfu new
81 | ```
82 |
83 | This will create a new markdown file with the following format:
84 | - File path: `entry/YYYY/MM/DD/HHMMSS.md` (using current timestamp)
85 | - Initial content includes YAML frontmatter with draft status
86 |
87 | ### Directory Structure
88 |
89 | ```
90 | .
91 | ├── .gimonfu.json
92 | └── entry
93 | ├── hello.md
94 | ├── 2020
95 | │ └── 05
96 | │ └── 09
97 | │ └── 10101010.md
98 | └── ...
99 | ```
100 |
101 | #### `.gimonfu.json`
102 |
103 | The format is like this:
104 |
105 | ```.gimonfu.json
106 | {
107 | "user_id" : "basd4g",
108 | "api_key" : "h0o1g2e3f4u5ga",
109 | "blog_id" : "basd4g.hatenablog.com"
110 | }
111 | ```
112 |
113 | This API key is fake.
114 |
115 | Please register your user id, API key, and blog id.
116 |
117 | #### `entry/`
118 |
119 | All articles are stored in the directory.
120 |
121 | The structure in the directory is the same as the end of the published article URL (expect for `.md` at the end of the file).
122 |
123 | This is accomplished by setting a custom URL for every post when it is posted/updated.
124 |
125 | #### Article File
126 |
127 | Each article is saved in markdown format under `entry/`.
128 |
129 | The format is like this:
130 |
131 | ```md
132 | ---
133 | title: Hello!
134 | date: 2019-09-16T16:37:00.000Z
135 | categories:
136 | - essay
137 | - happy day
138 | id: "26006613568876375"
139 | draft: true
140 | ---
141 | The body of the article continues below...
142 | ```
143 |
144 | The ID of the article is not needed for a new post.
145 | It is automatically appended when you post.
146 | And please do not make any changes.
147 |
148 | The line of `draft: true` is option.
149 | If it is not exist, it is expected public.
150 |
151 | ## Caution
152 |
153 | __USE GIT__
154 |
155 | A pull or push may create/overwrite/delete local files and published articles.
156 | It is highly recommended that you manage your article files in a version control system such as git in case of inadvertent deletion of the article.
157 |
158 | If you use git, you must not include `.gimonfu.json` in the repository, which contains credentials.
159 |
160 | ## GitHub Actions
161 |
162 | You can sync Hatena-blog and GitHub to use gimonfu on GitHub Actions.
163 |
164 | My blog is an example. ([Hatena-blog](https://basd4g.hatenablog.com) / [GitHub Repository](https://github.com/basd4g/basd4g.hatenablog.com))
165 |
166 | ## License
167 |
168 | MIT
169 |
170 | ## Author
171 |
172 | [basd4g](https://github.com/basd4g)
173 |
174 |
--------------------------------------------------------------------------------
/README_ja.md:
--------------------------------------------------------------------------------
1 | [English](README.md) / Japanese
2 |
3 | # gimonfu
4 |
5 | 
6 |
7 | gimonfuは、はてなブログの記事を管理する CLIアプリケーションです。
8 |
9 | 投稿記事を一括でMarkdownファイルとしてダウンロードしたり、編集/追加した記事を一括で公開したりできます。
10 |
11 | ## 準備
12 |
13 | ```sh
14 | # 編集モードをMarkdownモードに設定する。(はてなブログ > 設定 > 基本設定)
15 | # https://help.hatenablog.com/entry/editing-mode
16 |
17 | $ yarn global add gimonfu
18 | # or $ npm install --global gimonfu
19 |
20 | $ mkdir blog
21 | $ cd blog
22 |
23 | # はじめに認証情報を登録する
24 | $ gimonfu init
25 | ```
26 |
27 | ## 使い方
28 |
29 | ### `$ gimonfu init`
30 |
31 | 認証情報を登録します。
32 |
33 | `$ gimonfu pull`や`$ gimonfu push`を実行する前に行ってください。
34 |
35 | 認証情報の登録は、カレントディレクトリに`.gimonfu.json`ファイルを生成することで行われます。
36 | このコマンドを実行せずに、手動で`.gimonfu.json`を作成しても構いません。
37 |
38 | ### `$ gimonfu pull`
39 |
40 | 記事を`entry/`以下にダウンロードします。
41 |
42 | ダウンロードする先に既にファイルが存在するときは、公開された記事のほうが新しいときのみファイルを上書きします。
43 |
44 | ### `$ gimonfu push`
45 |
46 | `entry/`以下のファイルの中で新しいものを、新規投稿/更新します。
47 |
48 | このとき、entryディレクトリからの相対パスが、カスタムURLとして設定されます。
49 |
50 | #### 投稿/更新される記事の判断
51 |
52 | 次に該当するファイルを新規投稿します。
53 |
54 | - YAML-Frontmatter に記事idがない
55 |
56 | 次に該当するファイルを既に公開された記事から更新します
57 |
58 | - YAML-Frontmatter に記事IDがある
59 | - 既に公開された記事と比べて、変更がある
60 | - 既に公開された記事と比べて、最終更新日時が新しい
61 |
62 | #### ファイルの上書き
63 |
64 | 新規投稿/更新が行われると、ファイルの内容も投稿内容に合わせて上書きされます。
65 |
66 | 例えば、新規投稿時には YAML-Frontmatter にidが書き込まれたり、新規投稿/更新前に YAML-Frontmatte に不要なフィールドがあると削除されたりします。
67 |
68 | ### 新しい下書き記事の作成
69 |
70 | 新しい下書き記事を `entry/` ディレクトリに作成するには、次のコマンドを実行します。
71 |
72 | ```sh
73 | $ gimonfu new
74 | ```
75 |
76 | このコマンドは、現在時刻をファイル名として、`entry/YYYY/MM/DD/HHMMSS.md` というファイルを作成します。
77 |
78 | ### ディレクトリ構成
79 |
80 | ```
81 | .
82 | ├── .gimonfu.json
83 | └── entry
84 | ├── hello.md
85 | ├── 2020
86 | │ └── 05
87 | │ └── 09
88 | │ └── 10101010.md
89 | └── ...
90 | ```
91 |
92 | #### `.gimonfu.json`
93 |
94 | `$ gimonfu init`により作成され、 ブログIDや認証情報を記録したファイルです。
95 | APIキーを含むので流出しないように管理してください。
96 |
97 | `.gimonfu.json`のあるディレクトリを基準に記事をダウンロード/アップロードします。
98 |
99 | `.gimonfu.json`のあるディレクトリとそれより下位のディレクトリにいるとき、`$ gimonfu pull`と`$ gimonfu push`を実行できます。
100 |
101 | `.gimonfu.json`には、次のキーとそれに対応する値を含む必要が有ります。
102 |
103 | - "user_id" ... はてなのユーザID
104 | - "api_key" ... AtomPub APIキー (はてなブログ>設定>詳細設定 から入手できます)
105 | - "blod_id" ... ブログID (ブログのドメイン名。独自ドメインを利用している場合は、以前のドメインがブログID)
106 |
107 | ファイルの例を示します。必ず自分の認証情報に変えて利用してください。
108 |
109 | ```.gimonfu.json
110 | {
111 | "user_id" : "basd4g",
112 | "api_key" : "h0o1g2e3f4u5ga",
113 | "blog_id" : "basd4g.hatenablog.com"
114 | }
115 | ```
116 |
117 | #### `entry/`
118 |
119 | 記事を保存するディレクトリです。
120 | 初めて`$ gimonfu pull`を実行したとき、`.gimonfu.json`のあるディレクトリに作られます。
121 |
122 | 公開されるブログのURLの末尾と、entryディレクトリ内部の構造は一致します(ファイル末尾の拡張子`.md`を除く)。
123 | これは、記事の投稿/更新時にカスタムURLを設定することで実現しています。
124 |
125 | 例えば ブログ`https://basd4g.hatenablog.com` において、`.gimonfu.json`からの相対パスが`entry/firebase2hatenablog.md`であるファイルを新規投稿すると、カスタムURLが`https://basd4g.hatenablog.com/entry/firebase2hatenablog`となるように設定されます。
126 |
127 |
128 |
129 | #### 記事ファイル
130 |
131 | `entry/`以下に保存されるmarkdownファイルはそれぞれ記事ファイルです。
132 |
133 | ファイルは次のようなフォーマットです。YAML-Frontmatter 形式で始まります。
134 |
135 | ```md
136 | ---
137 | title: 記事タイトル
138 | date: 2019-09-16T16:37:00.000Z
139 | categories:
140 | - 1つ目のカテゴリ
141 | - 2つ目のカテゴリ
142 | id: "26006613568876375"
143 | draft: true
144 | ---
145 | 記事の本文が以下続きます。...
146 | ```
147 |
148 | YAML Frontmatterの項目は次の通り
149 |
150 | - title ... 記事タイトル (省略すると`No Title`というタイトルになります。)
151 | - date ... 投稿日時 (省略すると現在日時になります。)
152 | - categories ... カテゴリの配列
153 | - id ... 記事のID (変更を加えないでください)(新規投稿時はこの項目(行)は不要です。投稿時に自動で付加されます。)
154 | - draft ... 下書きか公開か (この行はオプションです。存在しないときは公開するものとして扱います。)
155 |
156 | ## 注意
157 |
158 | ### バージョン管理システムの利用
159 |
160 | pull や push を実行すると、ローカルのファイル群と公開されている記事群をそれぞれ、新規作成/上書き/削除します。
161 |
162 | 記事の不用意な削除に備えて、gitなどの **バージョン管理システムで記事ファイルを管理することを強く推奨します。**
163 |
164 | なおgitを使用する際、認証情報の含まれる`.gimonfu.json`はリポジトリに含めるべきではありません。
165 | 例えば次のように`.gitignore`にて除外する設定をすると、リポジトリに含めないようにできます。
166 |
167 | ```.gitignore
168 | .gimonfu.json
169 | ```
170 |
171 | ## License
172 |
173 | MIT
174 |
175 | ## Author
176 |
177 | [basd4g](https://github.com/basd4g)
178 |
--------------------------------------------------------------------------------
/__test__/article2fileString.test.ts:
--------------------------------------------------------------------------------
1 | import article2fileString from '../src/article2fileString'
2 |
3 | test('article2fileString', () => {
4 | const article: Article = {
5 | title: 'title-string',
6 | customUrl: '2020/05/12/today-blog',
7 | date: new Date('2020-05-12T22:55:00.000Z'),
8 | editedDate: new Date('2020-05-12T23:55:00.000Z'),
9 | id: '1234567890',
10 | categories: [ 'hoge', 'fuga' ],
11 | text: 'Hello!\nToday is 2020/5/12.\nBye~\n',
12 | draft: false,
13 | }
14 |
15 | const articleString = `---
16 | title: title-string
17 | date: 2020-05-12T22:55:00.000Z
18 | categories:
19 | - hoge
20 | - fuga
21 | id: "1234567890"
22 | draft: false
23 | ---
24 | Hello!
25 | Today is 2020/5/12.
26 | Bye~
27 | `
28 | // access to private method
29 | const fileString = article2fileString(article)
30 | expect(fileString).toBe(articleString)
31 | })
32 |
33 | test('article2fileString (draft)', () => {
34 | const article: Article = {
35 | title: 'title-string',
36 | customUrl: '2020/05/12/today-blog',
37 | date: new Date('2020-05-12T22:55:00.000Z'),
38 | editedDate: new Date('2020-05-12T23:55:00.000Z'),
39 | id: '1234567890',
40 | categories: [ 'hoge', 'fuga' ],
41 | text: 'Hello!\nToday is 2020/5/12.\nBye~\n',
42 | draft: true,
43 | }
44 |
45 | const articleString = `---
46 | title: title-string
47 | date: 2020-05-12T22:55:00.000Z
48 | categories:
49 | - hoge
50 | - fuga
51 | id: "1234567890"
52 | draft: true
53 | ---
54 | Hello!
55 | Today is 2020/5/12.
56 | Bye~
57 | `
58 | // access to private method
59 | const fileString = article2fileString(article)
60 | expect(fileString).toBe(articleString)
61 | })
62 |
63 | test('article2fileString (with colon in title)', () => {
64 | const article: Article = {
65 | title: 'title:string',
66 | customUrl: '2020/05/12/today-blog',
67 | date: new Date('2020-05-12T22:55:00.000Z'),
68 | editedDate: new Date('2020-05-12T23:55:00.000Z'),
69 | id: '1234567890',
70 | categories: [ 'hoge', 'fuga' ],
71 | text: 'Hello!\nToday is 2020/5/12.\nBye~\n',
72 | draft: false,
73 | }
74 |
75 | const articleString = `---
76 | title: "title:string"
77 | date: 2020-05-12T22:55:00.000Z
78 | categories:
79 | - hoge
80 | - fuga
81 | id: "1234567890"
82 | draft: false
83 | ---
84 | Hello!
85 | Today is 2020/5/12.
86 | Bye~
87 | `
88 | // access to private method
89 | const fileString = article2fileString(article)
90 | expect(fileString).toBe(articleString)
91 | })
92 |
93 | test('article2fileString (with colon and double quote in title)', () => {
94 | const article: Article = {
95 | title: 'title:strin"g',
96 | customUrl: '2020/05/12/today-blog',
97 | date: new Date('2020-05-12T22:55:00.000Z'),
98 | editedDate: new Date('2020-05-12T23:55:00.000Z'),
99 | id: '1234567890',
100 | categories: [ 'hoge', 'fuga' ],
101 | text: 'Hello!\nToday is 2020/5/12.\nBye~\n',
102 | draft: false,
103 | }
104 |
105 | const articleString = `---
106 | title: "title:strin\\"g"
107 | date: 2020-05-12T22:55:00.000Z
108 | categories:
109 | - hoge
110 | - fuga
111 | id: "1234567890"
112 | draft: false
113 | ---
114 | Hello!
115 | Today is 2020/5/12.
116 | Bye~
117 | `
118 | // access to private method
119 | const fileString = article2fileString(article)
120 | expect(fileString).toBe(articleString)
121 | })
122 |
123 | test('article2fileString (with double quote in title head)', () => {
124 | const article: Article = {
125 | title: '"title-string',
126 | customUrl: '2020/05/12/today-blog',
127 | date: new Date('2020-05-12T22:55:00.000Z'),
128 | editedDate: new Date('2020-05-12T23:55:00.000Z'),
129 | id: '1234567890',
130 | categories: [ 'hoge', 'fuga' ],
131 | text: 'Hello!\nToday is 2020/5/12.\nBye~\n',
132 | draft: false,
133 | }
134 |
135 | const articleString = `---
136 | title: "\\"title-string"
137 | date: 2020-05-12T22:55:00.000Z
138 | categories:
139 | - hoge
140 | - fuga
141 | id: "1234567890"
142 | draft: false
143 | ---
144 | Hello!
145 | Today is 2020/5/12.
146 | Bye~
147 | `
148 | // access to private method
149 | const fileString = article2fileString(article)
150 | expect(fileString).toBe(articleString)
151 | })
152 |
153 |
154 |
155 | test('article2fileString (with colon and double quote in categories', () => {
156 | const article: Article = {
157 | title: 'title-string',
158 | customUrl: '2020/05/12/today-blog',
159 | date: new Date('2020-05-12T22:55:00.000Z'),
160 | editedDate: new Date('2020-05-12T23:55:00.000Z'),
161 | id: '1234567890',
162 | categories: [ 'hog:"e', 'fuga:' ],
163 | text: 'Hello!\nToday is 2020/5/12.\nBye~\n',
164 | draft: false,
165 | }
166 |
167 | const articleString = `---
168 | title: title-string
169 | date: 2020-05-12T22:55:00.000Z
170 | categories:
171 | - "hog:\\"e"
172 | - "fuga:"
173 | id: "1234567890"
174 | draft: false
175 | ---
176 | Hello!
177 | Today is 2020/5/12.
178 | Bye~
179 | `
180 | // access to private method
181 | const fileString = article2fileString(article)
182 | expect(fileString).toBe(articleString)
183 | })
184 |
185 |
186 |
--------------------------------------------------------------------------------
/__test__/article2xml.test.ts:
--------------------------------------------------------------------------------
1 | import article2xmlString from '../src/article2xml'
2 |
3 | test('article2xmlString', () => {
4 | const article: Article = {
5 | id: undefined,
6 | customUrl: 'hoge/fuga',
7 | title: '新しいタイトル',
8 | text: '\n ** 新しい本文\n ',
9 | date: new Date('2008-01-01T00:00:00Z'),
10 | categories: [ 'Scala' ],
11 | editedDate: new Date(),
12 | draft: false
13 | }
14 | expect(article2xmlString(article)).toMatch( `
15 |