├── .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 | ![logo](logo.png) 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 | ![logo](logo.png) 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 | 17 | 新しいタイトル 18 | name 19 | 20 | ** 新しい本文 21 | 22 | 2008-01-01T00:00:00.000Z 23 | 24 | 25 | no 26 | 27 | hoge/fuga 28 | ` ) 29 | }) 30 | 31 | test('article2xmlString (draft)', () => { 32 | const article: Article = { 33 | id: undefined, 34 | customUrl: 'hoge/fuga', 35 | title: '新しいタイトル', 36 | text: '\n ** 新しい本文\n ', 37 | date: new Date('2008-01-01T00:00:00Z'), 38 | categories: [ 'Scala' ], 39 | editedDate: new Date(), 40 | draft: true 41 | } 42 | expect(article2xmlString(article)).toMatch( ` 43 | 45 | 新しいタイトル 46 | name 47 | 48 | ** 新しい本文 49 | 50 | 2008-01-01T00:00:00.000Z 51 | 52 | 53 | yes 54 | 55 | hoge/fuga 56 | ` ) 57 | }) 58 | 59 | -------------------------------------------------------------------------------- /__test__/atomPubRequest.test.ts: -------------------------------------------------------------------------------- 1 | import AtomPubRequest from '../src/atomPubRequest' 2 | import { promises as fs } from 'fs' 3 | import path from 'path' 4 | 5 | jest.mock('request-promise-native', () => ( (req: any) => { 6 | if( req.uri === 'https://blog.hatena.ne.jp/user/blogId/atom/urlTail' ) { 7 | return 'dummy-xml-string' 8 | } 9 | if( req.uri === 'https://blog.hatena.ne.jp/user/blogId/atom/entry' && req.method === 'GET') { 10 | return fs.readFile( path.resolve( __dirname, 'xml-example', 'entry.xml' ) ) 11 | } 12 | if( req.uri === 'https://blog.hatena.ne.jp/user/blogId/atom/entry?page=1588813317' && req.method === 'GET') { 13 | return fs.readFile( path.resolve( __dirname, 'xml-example', 'entry__question__page=1588813317.xml' ) ) 14 | } 15 | if( req.uri === 'https://blog.hatena.ne.jp/user/blogId/atom/entry' && req.method === 'POST') { 16 | return fs.readFile( path.resolve( __dirname, 'xml-example', 'entry', '26006613566848996.xml' ) ) 17 | } 18 | if( req.uri === 'https://blog.hatena.ne.jp/user/blogId/atom/entry/26006613566848996' && req.method === 'PUT') { 19 | return fs.readFile( path.resolve( __dirname, 'xml-example', 'entry', '26006613566848996.xml' ) ) 20 | } 21 | })) 22 | 23 | 24 | const atomPubRequest = new AtomPubRequest('user','password','blogId') 25 | 26 | test('request', async () => { 27 | const res = (atomPubRequest as any).request('/urlTail', 'GET', undefined) 28 | expect(res).toBe('dummy-xml-string') 29 | }) 30 | 31 | test('fetchPageChain', async () => { 32 | const articles = await (atomPubRequest as any).fetchPageChain(null) 33 | 34 | expect( 35 | JSON.stringify(articles) 36 | ).toBe( 37 | JSON.stringify( require('./atomPubRequest.test/articles.json') ) 38 | ) 39 | 40 | }) 41 | 42 | test('fetchs', async () => { 43 | const articles = await atomPubRequest.fetchs() 44 | 45 | expect( 46 | JSON.stringify(articles) 47 | ).toBe( 48 | JSON.stringify( require('./atomPubRequest.test/articles.json') ) 49 | ) 50 | }) 51 | 52 | const article = { 53 | "id": "26006613566848996", 54 | "customUrl": "2010/01/01/000000", 55 | "title": "dummy", 56 | "date": new Date("2009-12-31T15:00:00.000Z"), 57 | "editedDate": new Date("2020-05-13T05:27:04.000Z"), 58 | "text": "dummy\n", 59 | "categories": [], 60 | "draft": false 61 | } 62 | 63 | test('fullUrl', () => { 64 | expect( atomPubRequest.fullUrl(article) ).toBe('https://blogId/2010/01/01/000000') 65 | }) 66 | 67 | test('post', async () => { 68 | const { id, title, customUrl, date, editedDate, text, categories, draft } = article 69 | const sendArticle = { 70 | title, customUrl, date, editedDate, text, categories, draft, 71 | id: undefined 72 | } 73 | const responsedArticle = await atomPubRequest.post(sendArticle) 74 | expect( responsedArticle ).toEqual(article) 75 | }) 76 | test('put', async () => { 77 | const responsedArticle = await atomPubRequest.put(article) 78 | expect( responsedArticle ).toEqual(article) 79 | }) 80 | -------------------------------------------------------------------------------- /__test__/atomPubRequest.test/articles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "26006613568876375", 4 | "customUrl": "hello-new", 5 | "title": "新規投稿記事", 6 | "date": "2019-09-16T16:37:00.000Z", 7 | "editedDate": "2020-05-16T14:32:25.000Z", 8 | "text": " TL;DR 初投稿に際して自分に向けたポエム\n\n記事を書くためにブログを作った。\n\nブログをはじめるにあたり、はじめるに至った理由を綴る。\n(自分に書く価値を再認識させることで、記事を書く動機を強める狙い)\n\n## ブログを書く意義\nasdfas\n### 文章執筆\n\nわかりやすく完結な文章を書くには推敲した上である程度時間を掛ける必要がある。\n文章の執筆能力を高めるためにブログを書くことにした。\n\n### 思考を整理する\n\n最近アウトプットすることの重要性に気づいた。\n\nアウトプットする事柄は、自分の中で理解し整理されているものでなければならない。\n今までの私は、こと情報系に関してインプットの機会が多かった。\nだからこそ自分の文章を公開することで、インプットした内容を整理する機会としたい。\n\n### 誰かのために\n\n私は開発していて詰まることがよくある。例えば「ライブラリを入れたけど動かない」「エラーを吐いて動かない」等。\n\nこのような私が開発していて詰まったことと解決策を記録することで、同じ問題を抱えている誰か(もしくは未来の私)に向けたヒントになればという思いがある。\n\n私が解決策を示す文章を書く手間が、私や誰かが問題解決にかける手間より小さいならば、とても価値のある記事がかけたということである。\n\n## まとめ\n\nそんなわけで、次の目的で思ったことや知ったことを書き留めていきたい。\n\n- 文章執筆の場\n- 思考を整理する場\n- 誰かのためにハウツーを記録する場\n\n \n", 9 | "categories": [ 10 | "ポエム" 11 | ], 12 | "draft": false 13 | }, 14 | { 15 | "id": "26006613568865965", 16 | "customUrl": "2020/05/13/122736", 17 | "title": "猫の広告文", 18 | "date": "2020-05-13T03:27:36.000Z", 19 | "editedDate": "2020-05-16T14:14:18.000Z", 20 | "text": "猫の広告文\n夏目漱石\n\n\n\n 吾輩は猫である。名前はまだない。主人は教師である。迷亭は美学者、寒月は理学者、いづれも当代の変人、太平の逸民である。吾輩は幸にして此諸先生の知遇を辱〔かたじけの〕ふするを得てこゝに其平生を読者に紹介するの光栄を有するのである。……吾輩は又猫相応の敬意を以て金田令夫人の鼻の高さを読者に報道し得るを一生の面目と思ふのである。……\n\n\n\n\n底本:「漱石全集 第十六巻」岩波書店 \n   1995(平成7)年4月19日発行\n初出:「東京朝日新聞」\n   1905(明治38)年11月15日\n※初出として掲げたもの以外にも、各紙に掲載された。それらの間には、わずかな異同がある。\n※底本のテキストは、「吾輩ハ猫デアル」の発行者の一人、服部国太郎宛葉書(天理大学附属天理図書館蔵)による。\n※亀甲かっこ〔〕付きのルビは底本編集部によるもので、現代仮名遣いである。\n(例)知遇を辱〔かたじけの〕ふするを得て\n入力:砂場清隆\n校正:小林繁雄\n2003年3月31日作成\n青空文庫作成ファイル:\nこのファイルは、インターネットの図書館、青空文庫(http://www.aozora.gr.jp/)で作られました。入力、校正、制作にあたったのは、ボランティアの皆さんです。\n", 21 | "categories": [], 22 | "draft": false 23 | }, 24 | { 25 | "id": "26006613566848996", 26 | "customUrl": "2010/01/01/000000", 27 | "title": "dummy", 28 | "date": "2009-12-31T15:00:00.000Z", 29 | "editedDate": "2020-05-13T05:27:04.000Z", 30 | "text": "dummy\n", 31 | "categories": [], 32 | "draft": false 33 | }, 34 | { 35 | "id": "26006613566779962", 36 | "customUrl": "2020/05/13/122622", 37 | "title": "星めぐりの歌", 38 | "date": "2020-05-13T03:26:22.000Z", 39 | "editedDate": "2020-05-13T03:26:22.000Z", 40 | "text": "# 星めぐりの歌\n\n宮澤賢治\n\n```\nあかいめだまの さそり\nひろげた鷲の  つばさ\nあをいめだまの 小いぬ、\nひかりのへびの とぐろ。\n\nオリオンは高く うたひ\nつゆとしもとを おとす、\nアンドロメダの くもは\nさかなのくちの かたち。\n\n大ぐまのあしを きたに\n五つのばした  ところ。\n小熊のひたいの うへは\nそらのめぐりの めあて。\n```\n\n\n\n底本:「【新】校本宮澤賢治全集 第六巻 詩5[#「5」はローマ数字、1-13-25] 本文篇」筑摩書房\n   1996(平成8)年5月30日初版第1刷発行\n入力:田中敬三\n校正:土屋隆\n2006年7月26日作成\n青空文庫作成ファイル:\nこのファイルは、インターネットの図書館、青空文庫(http://www.aozora.gr.jp/)で作られました。入力、校正、制作にあたったのは、ボランティアの皆さんです。\n", 41 | "categories": [], 42 | "draft": false 43 | }, 44 | { 45 | "id": "26006613566778012", 46 | "customUrl": "2020/05/13/122220", 47 | "title": "雨ニモマケズ HTML埋め込み", 48 | "date": "2020-05-13T03:22:20.000Z", 49 | "editedDate": "2020-05-13T03:22:20.000Z", 50 | "text": "```html\n\n\n\n\t\n\t\n\t\n\t宮澤賢治 〔雨ニモマケズ〕\n\t\n \n\t\n\t\n\t\n\n\n
\n

〔雨ニモマケズ〕

\n

宮澤賢治

\n
\n
\n
\n

\n雨ニモマケズ
\n風ニモマケズ
\n雪ニモ夏ノ暑サニモマケヌ
\n丈夫ナカラダヲモチ
\n慾ハナク
\n決シテ瞋ラズ
\nイツモシヅカニワラッテヰル
\n一日ニ玄米四合ト
\n味噌ト少シノ野菜ヲタベ
\nアラユルコトヲ
\nジブンヲカンジョウニ入レズニ
\nヨクミキキシワカリ
\nソシテワスレズ
\n野原ノ松ノ林ノ\"※(「「蔭」の「陰のつくり」に代えて「人がしら/髟のへん」、第4水準2-86-78)\"
\n小サナ萓ブキノ小屋ニヰテ
\n東ニ病気ノコドモアレバ
\n行ッテ看病シテヤリ
\n西ニツカレタ母アレバ
\n行ッテソノ稲ノ朿ヲ[#「朿ヲ」はママ]負ヒ
\n南ニ死ニサウナ人アレバ
\n行ッテコハガラナクテモイヽトイヒ
\n北ニケンクヮヤソショウガアレバ
\nツマラナイカラヤメロトイヒ
\nヒドリノトキハナミダヲナガシ
\nサムサノナツハオロオロアルキ
\nミンナニデクノボートヨバレ
\nホメラレモセズ
\nクニモサレズ
\nサウイフモノニ
\nワタシハナリタイ
\n
\n南無無辺行菩薩
\n南無上行菩薩
\n南無多宝如来
\n南無妙法蓮華経
\n南無釈迦牟尼仏
\n南無浄行菩薩
\n南無安立行菩薩
\n
\n
\n
\n
\n
\n
\n
\n底本:「【新】校本宮澤賢治全集 第十三巻(上)覚書・手帳 本文篇」筑摩書房
\n   1997(平成9)年7月30日初版第1刷発行
\n※本文については写真版を含む本書によった。また、改行等の全体の体裁については、「【新】校本宮澤賢治全集 第六巻」筑摩書房1996(平成8)年5月30日初版第1刷発行を参照した。
\n入力:田中敬三
\n校正:土屋隆
\n2006年7月26日作成
\n2019年1月21日修正
\n青空文庫作成ファイル:
\nこのファイルは、インターネットの図書館、青空文庫(https://www.aozora.gr.jp/)で作られました。入力、校正、制作にあたったのは、ボランティアの皆さんです。
\n
\n
\n
\n
\n
\n
\n●表記について
\n\n
\n
\n
\n
\n●図書カード\n\n
\n\n\n```\n", 51 | "categories": [], 52 | "draft": false 53 | }, 54 | { 55 | "id": "26006613566777790", 56 | "customUrl": "2020/05/13/122148", 57 | "title": "雨ニモマケズ", 58 | "date": "2020-05-13T03:21:48.000Z", 59 | "editedDate": "2020-05-13T03:21:48.000Z", 60 | "text": "
\n

〔雨ニモマケズ〕

\n

宮澤賢治

\n
\n
\n
\n

\n雨ニモマケズ
\n風ニモマケズ
\n雪ニモ夏ノ暑サニモマケヌ
\n丈夫ナカラダヲモチ
\n慾ハナク
\n決シテ瞋ラズ
\nイツモシヅカニワラッテヰル
\n一日ニ玄米四合ト
\n味噌ト少シノ野菜ヲタベ
\nアラユルコトヲ
\nジブンヲカンジョウニ入レズニ
\nヨクミキキシワカリ
\nソシテワスレズ
\n野原ノ松ノ林ノ\"※(「「蔭」の「陰のつくり」に代えて「人がしら/髟のへん」、第4水準2-86-78)\"
\n小サナ萓ブキノ小屋ニヰテ
\n東ニ病気ノコドモアレバ
\n行ッテ看病シテヤリ
\n西ニツカレタ母アレバ
\n行ッテソノ稲ノ朿ヲ[#「朿ヲ」はママ]負ヒ
\n南ニ死ニサウナ人アレバ
\n行ッテコハガラナクテモイヽトイヒ
\n北ニケンクヮヤソショウガアレバ
\nツマラナイカラヤメロトイヒ
\nヒドリノトキハナミダヲナガシ
\nサムサノナツハオロオロアルキ
\nミンナニデクノボートヨバレ
\nホメラレモセズ
\nクニモサレズ
\nサウイフモノニ
\nワタシハナリタイ
\n
\n南無無辺行菩薩
\n南無上行菩薩
\n南無多宝如来
\n南無妙法蓮華経
\n南無釈迦牟尼仏
\n南無浄行菩薩
\n南無安立行菩薩
\n
\n
\n
\n
\n
\n
\n
\n底本:「【新】校本宮澤賢治全集 第十三巻(上)覚書・手帳 本文篇」筑摩書房
\n   1997(平成9)年7月30日初版第1刷発行
\n※本文については写真版を含む本書によった。また、改行等の全体の体裁については、「【新】校本宮澤賢治全集 第六巻」筑摩書房1996(平成8)年5月30日初版第1刷発行を参照した。
\n入力:田中敬三
\n校正:土屋隆
\n2006年7月26日作成
\n2019年1月21日修正
\n青空文庫作成ファイル:
\nこのファイルは、インターネットの図書館、青空文庫(https://www.aozora.gr.jp/)で作られました。入力、校正、制作にあたったのは、ボランティアの皆さんです。
\n
\n
\n
\n
\n
\n
\n●表記について
\n\n
\n
\n
\n
\n●図書カード\n\n
\n", 61 | "categories": [], 62 | "draft": false 63 | }, 64 | { 65 | "id": "26006613563419672", 66 | "customUrl": "2020/05/07/171333", 67 | "title": "マスタリングTCPIP 情報セキュリティ", 68 | "date": "2020-05-07T08:13:33.000Z", 69 | "editedDate": "2020-05-07T08:13:33.000Z", 70 | "text": "こっちは情報セキュリティ編\n", 71 | "categories": [], 72 | "draft": false 73 | }, 74 | { 75 | "id": "26006613563419525", 76 | "customUrl": "2020/05/07/171307", 77 | "title": "マスタリングTCPIP 入門", 78 | "date": "2020-05-07T08:13:07.000Z", 79 | "editedDate": "2020-05-07T08:13:07.000Z", 80 | "text": "こっちは入門\n", 81 | "categories": [], 82 | "draft": false 83 | }, 84 | { 85 | "id": "26006613563419396", 86 | "customUrl": "2020/05/07/171242", 87 | "title": "デザインパターン入門", 88 | "date": "2020-05-07T08:12:42.000Z", 89 | "editedDate": "2020-05-07T08:12:42.000Z", 90 | "text": "## テスト記事h2\n\nほげー\n", 91 | "categories": [], 92 | "draft": false 93 | }, 94 | { 95 | "id": "26006613563261569", 96 | "customUrl": "NuxtTsV292", 97 | "title": "Nuxt.js(v2.9.2)とTypeScriptの開発環境を作る。", 98 | "date": "2019-09-27T03:30:00.000Z", 99 | "editedDate": "2020-05-07T01:01:57.000Z", 100 | "text": "Nuxt.jsとTypeScriptで開発環境を作るときのまとめ。(2019/9/5時点)\n\nお急ぎの方は、\n記事中の作業を行ったものを[nuxt.ts-template](https://github.com/basd4g/nuxt.ts-template)としてGitHubのリポジトリに上げたので、cloneないしForkして使ってほしい。\n\n## 目指すもの\n\ncreate nuxt-app したときと同じ環境で、下記のものが使えること。\nすぐにNuxt.jsとTypeScriptを用いて開発を始められる環境を構築する。\n\n- Nuxt.js v2.9.2\n- TypeScript\n- ESLint\n- nuxt-property-decorator\n\n### nuxt-property-decorator (vue-property-decorator)\n\nNuxt.jsとTypeScriptを組み合わせるときは、nuxt-property-decorator(vue-property-decorator)の利用が推奨されている。^1\n\nもともとのNuxt.jsとは書き方が変わる。参考文献にいくつか載せてあるので、もともとの書き方と比較しながら慣れると良さそう。^2\n\n今回は最後にインストールするので、一つ前の[ESLint](#3-eslint)までで止まればこれを使わない環境も構築できる。\n\n## 手順\n\n大まかには次の流れで進む。\nいくつかハマりどころがあったので、私の環境でうまく行った手順を残しておく。\n\n(特にESLintに文句を言われることが多く、最初いきあたりばったりで進めていたら沢山のパッケージをインストールしてしまった。\nこの記事は、その後不要なものがあまり入らないようにやり直した内容の記録である。)\n\n1. yarn create nuxt-app\n2. TypeScriptを入れる\n3. vue-property-decoratorを入れる\n4. ESLintを入れる\n\n

\n\n### 1. create nuxt-app\n\nNuxt.jsで環境構築するときにお決まりのcreate-nuxt-app。\nyarnだと、 `$ yarn create nuxt-app hogehoge` とする。\n\n```sh\n$ yarn create nuxt-app nuxt.ts-template\n# yarn create v1.17.3\n# create-nuxt-app v2.10.1\n\n# 今回は次のように設定した\n# Project name, Project description, Author name は適宜\n# package manager : Yarn\n# UI framework : None\n# custom server framework : None (Recommended)\n# Nuxt.js modules :\n# test framework : None\n# rendering mode : Universal (SSR)\n# development tools :\n\n$ cd nuxt.ts-template/\n# 以下全てカレントディレクトリは移動しない。編集するファイル名もnuxt.ts-template/をカレントディレクトリとして表記する。\n\n$ yarn dev\n# 起動できることを確認。ブラウザからNuxt.jsのロゴが見えればオーケー。(以下の起動確認も同様。)\n\n```\n\n

\n\n### 2. Install TypeScript\n\nまず、TypeScriptに関するパッケージをインストールする。\n\n```sh\n$ yarn add -D @nuxt/typescript-build\n$ yarn add @nuxt/typescript-runtime @nuxt/types\n$ mv nuxt.config.js nuxt.config.ts\n$ touch tsconfig.json\n```\n\nTypeScriptに合わせて以下の設定ファイルを2つ、ソースを3つ書き換える。\n\n- `package.json`\n- `tsconfig.json` (新たに作成)\n- `nuxt.config.ts`\n- `index.vue`\n- `Logo.vue`\n\n`./package.json`\n\n```diff\n...\n \"scripts\": {\n- \"dev\": \"nuxt\",\n- \"build\": \"nuxt build\",\n- \"start\": \"nuxt start\",\n- \"generate\": \"nuxt generate\"\n+ \"dev\": \"nuxt-ts\",\n+ \"build\": \"nuxt-ts build\",\n+ \"start\": \"nuxt-ts start\",\n+ \"generate\": \"nuxt-ts generate\"\n },\n...\n```\n\n`./tsconfig.json`\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"module\": \"esnext\",\n \"moduleResolution\": \"node\",\n \"lib\": [\n \"esnext\",\n \"esnext.asynciterable\",\n \"dom\"\n ],\n \"esModuleInterop\": true,\n \"allowJs\": true,\n \"sourceMap\": true,\n \"strict\": true,\n \"noEmit\": true,\n \"baseUrl\": \".\",\n \"paths\": {\n \"~/*\": [\n \"./*\"\n ],\n \"@/*\": [\n \"./*\"\n ]\n },\n \"types\": [\n \"@types/node\",\n \"@nuxt/types\"\n ]\n },\n \"exclude\": [\n \"node_modules\"\n ]\n}\n```\n\n`./nuxt.config.ts`\n\n```diff\n+import {Configuration} from '@nuxt/types'\n\n-export default {\n+const nuxtConfig: Configuration = {\n...\n buildModules: [\n+ '@nuxt/typescript-build'\n ],\n...\n}\n+module.exports = nuxtConfig\n```\n\n`./pages/index.vue`\n\n```diff\n-\n+\n