Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,76 @@ Qiita CLI、Qiita Preview から記事の削除はできません。

[Qiita](https://qiita.com)上で記事の削除を行なえます。

## Qiita CLI でスライドを管理する

Qiita CLI では、[Marp](https://marp.app/) 形式の markdown ファイルでスライドを作成し、Qiita に投稿できます。

> [!NOTE]
> スライド機能は、Qiita のベータ版を利用しているユーザーのみ利用できます。
> それ以外のユーザーが`pull`、`preview`、`publish`コマンドを実行した場合、スライドの同期は行われません。スライドを投稿しようとした場合はエラーになります。

### スライドファイルの配置について

1 つのスライドの内容は、1 つの markdown ファイル(◯◯.md)で管理します。
スライドファイルは記事ファイルと同じく`public`ディレクトリ内に含める必要があります。
frontmatter に`marp: true`があるファイルがスライド、ないファイルが記事として扱われます。

```console
.
└─ public
├── newArticle001.md
└── slides
├── newSlide001.md
└── newSlide002.md
```

ファイル名を指定せずに作成したスライドと、Qiita 上にだけあるスライドは`public/slides`ディレクトリに保存されます。

### スライドの作成

Qiita Preview 上の「新規スライド作成」ボタン、または以下のコマンドで新規スライドを作成できます。

```console
npx qiita new --slide スライドのファイルのベース名
```

作成されたスライドファイルの中身は次のようになっています。

```markdown
---
title: newSlide001 # スライドのタイトル
id: null # スライドを投稿した際に自動的にスライドのUUIDに変わります
updated_at: null # スライドを投稿した際に自動的にスライドの更新日時に変わります
description: "" # スライドの説明
ignorePublish: false # true: `publish`コマンドにおいて無視されます(Qiitaに投稿されません) / false: `publish`コマンドで処理されます(Qiitaに投稿されます)
marp: true
theme: default # Marp のテーマ
---

# Title

---

# Page 2
```

`title`、`id`、`updated_at`、`description`、`ignorePublish`以外の項目(`theme`、`paginate`など)は、Marp のディレクティブとしてそのままスライドに反映されます。
本文は`---`でページを区切ります。

### スライドの投稿・更新

Qiita Preview 上の「スライドを投稿する」ボタン、または記事と同じ`publish`コマンドで投稿・更新ができます。

```console
npx qiita publish スライドのファイルのベース名
```

`npx qiita publish --all`を実行すると、記事とあわせて全てのスライドを反映させます。`ignorePublish: true`のスライドは対象外です。

### スライドの同期

`pull`、`preview`コマンドを実行すると、記事とあわせて Qiita 上のスライドも`public`ディレクトリに同期されます。

## GitHub で記事を管理する

### GitHub の設定について
Expand All @@ -226,14 +296,14 @@ npx qiita help

### pull

記事ファイルを Qiita と同期します。
Qiita 上で更新を行い、手元で変更を行っていない記事ファイルのみ同期されます。
記事ファイル、スライドファイルを Qiita と同期します。
Qiita 上で更新を行い、手元で変更を行っていない記事ファイル、スライドファイルのみ同期されます。

```console
npx qiita pull
```

`--force`オプションを用いることで、強制的に Qiita 上の内容を記事ファイルに反映させます。
`--force`オプションを用いることで、強制的に Qiita 上の内容を記事ファイル、スライドファイルに反映させます。

```console
npx qiita pull --force
Expand Down
102 changes: 22 additions & 80 deletions src/commands/help.test.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,41 @@
import { config } from "../lib/config";
import { getHelpText, help } from "./help";

jest.mock("../lib/config");

const mockConfig = jest.mocked(config);

describe("getHelpText", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("when the experimental slide feature is disabled (default)", () => {
beforeEach(() => {
mockConfig.getUserConfig.mockResolvedValue({
includePrivate: false,
host: "localhost",
port: 8888,
experimentalSlideFeatureEnabled: false,
});
});

it("does not mention slides on the publish and pull lines", async () => {
const helpText = await getHelpText();

expect(helpText).toContain("publish <basename> ... 記事を投稿、更新\n");
expect(helpText).toContain(
"publish --all 全ての記事を投稿、更新\n",
);
expect(helpText).toContain(
"pull 記事ファイルをQiitaと同期\n",
);
});

it("tells how to enable the slide feature on the new --slide line", async () => {
const helpText = await getHelpText();

expect(helpText).toContain("実験的機能");
expect(helpText).toContain("experimentalSlideFeatureEnabled");
});
import { help, helpText } from "./help";

describe("helpText", () => {
it("mentions slides on the new, publish and pull lines", () => {
expect(helpText).toContain(
"new --slide [<basename>] ...\n 新しいスライドを追加\n",
);
expect(helpText).toContain(
"publish <basename> ... 記事、スライドを投稿、更新\n",
);
expect(helpText).toContain(
"publish --all 全ての記事、スライドを投稿、更新\n",
);
expect(helpText).toContain(
"pull 記事、スライドファイルをQiitaと同期\n",
);
});

describe("when the experimental slide feature is enabled", () => {
beforeEach(() => {
mockConfig.getUserConfig.mockResolvedValue({
includePrivate: false,
host: "localhost",
port: 8888,
experimentalSlideFeatureEnabled: true,
});
});

it("mentions slides on the publish and pull lines", async () => {
const helpText = await getHelpText();

expect(helpText).toContain(
"publish <basename> ... 記事、スライドを投稿、更新\n",
);
expect(helpText).toContain(
"publish --all 全ての記事、スライドを投稿、更新\n",
);
expect(helpText).toContain(
"pull 記事、スライドファイルをQiitaと同期\n",
);
});

it("does not explain how to enable the already enabled feature", async () => {
const helpText = await getHelpText();

expect(helpText).not.toContain("実験的機能");
expect(helpText).not.toContain("experimentalSlideFeatureEnabled");
});
it("does not describe the slide feature as experimental", () => {
expect(helpText).not.toContain("実験的機能");
expect(helpText).not.toContain("experimentalSlideFeatureEnabled");
});
});

describe("help", () => {
let logSpy: jest.SpyInstance;

beforeEach(() => {
jest.clearAllMocks();
mockConfig.getUserConfig.mockResolvedValue({
includePrivate: false,
host: "localhost",
port: 8888,
experimentalSlideFeatureEnabled: false,
});
logSpy = jest.spyOn(console, "log").mockImplementation();
});

afterEach(() => {
logSpy.mockRestore();
});

it("prints the help text", async () => {
await help();
it("prints the help text", () => {
help();

expect(logSpy).toHaveBeenCalledWith(await getHelpText());
expect(logSpy).toHaveBeenCalledWith(helpText);
});
});
47 changes: 8 additions & 39 deletions src/commands/help.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
import { config } from "../lib/config";

const buildHelpText = ({
experimentalSlideFeatureEnabled,
}: {
experimentalSlideFeatureEnabled: boolean;
}) => {
const newSlide = experimentalSlideFeatureEnabled
? ` new --slide [<basename>] ...
新しいスライドを追加`
: ` new --slide [<basename>] ...
新しいスライドを追加(実験的機能。qiita.config.jsonで
experimentalSlideFeatureEnabledをtrueにすると使用可能)`;
const publish = experimentalSlideFeatureEnabled
? "記事、スライドを投稿、更新"
: "記事を投稿、更新";
const publishAll = experimentalSlideFeatureEnabled
? "全ての記事、スライドを投稿、更新"
: "全ての記事を投稿、更新";
const pull = experimentalSlideFeatureEnabled
? "記事、スライドファイルをQiitaと同期"
: "記事ファイルをQiitaと同期";

return `USAGE:
export const helpText = `USAGE:
qiita <COMMAND> [<OPTIONS>]

COMMAND:
init 記事をGitHubで管理するための初期設定
login Qiita APIの認証認可
new [<basename>] ... 新しい記事を追加
${newSlide}
new --slide [<basename>] ...
新しいスライドを追加
preview コンテンツをブラウザでプレビュー
publish <basename> ... ${publish}
publish --all ${publishAll}
pull ${pull}
publish <basename> ... 記事、スライドを投稿、更新
publish --all 全ての記事、スライドを投稿、更新
pull 記事、スライドファイルをQiitaと同期
posting-campaigns 開催中の記事投稿キャンペーン一覧を表示(最大100件)
version Qiita CLIのバージョンを表示
help ヘルプを表示
Expand All @@ -52,16 +30,7 @@ OPTIONS:

詳細についてはReadme(https://github.com/increments/qiita-cli)をご覧ください
`;
};

export const getHelpText = async () => {
const userConfig = await config.getUserConfig();

return buildHelpText({
experimentalSlideFeatureEnabled: userConfig.experimentalSlideFeatureEnabled,
});
};

export const help = async () => {
console.log(await getHelpText());
export const help = () => {
console.log(helpText);
};
19 changes: 2 additions & 17 deletions src/commands/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { handleError } from "../lib/error-handler";
import { getHelpText } from "./help";
import { helpText } from "./help";
import { exec } from "./index";

jest.mock("../lib/error-handler");
Expand All @@ -14,7 +14,6 @@ jest.mock("./publish");
jest.mock("./pull");
jest.mock("./version");

const mockGetHelpText = jest.mocked(getHelpText);
const mockHandleError = jest.mocked(handleError);

describe("exec", () => {
Expand Down Expand Up @@ -45,27 +44,13 @@ describe("exec", () => {

describe("when the command is unknown", () => {
it("prints the help text and exits with 1", async () => {
mockGetHelpText.mockResolvedValue("help text");

await expect(exec("unknown-command", [])).rejects.toThrow(
ProcessExitError,
);

expect(errorSpy).toHaveBeenCalledWith("help text");
expect(errorSpy).toHaveBeenCalledWith(helpText);
expect(exitSpy).toHaveBeenCalledWith(1);
expect(mockHandleError).not.toHaveBeenCalled();
});

it("handles the error and still exits with 1 when getHelpText fails", async () => {
const error = new Error("config is broken");
mockGetHelpText.mockRejectedValue(error);

await expect(exec("unknown-command", [])).rejects.toThrow(
ProcessExitError,
);

expect(mockHandleError).toHaveBeenCalledWith(error);
expect(exitSpy).toHaveBeenCalledWith(1);
});
});
});
8 changes: 2 additions & 6 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { handleError } from "../lib/error-handler";
import { packageUpdateNotice } from "../lib/package-update-notice";
import { getHelpText, help } from "./help";
import { help, helpText } from "./help";
import { init } from "./init";
import { login } from "./login";
import { newArticles } from "./newArticles";
Expand Down Expand Up @@ -32,11 +32,7 @@ export const exec = async (commandName: string, commandArgs: string[]) => {
if (!isCommand(commandName)) {
console.error(`Unknown command '${commandName}'`);
console.error();
try {
console.error(await getHelpText());
} catch (err) {
await handleError(err as Error);
}
console.error(helpText);
process.exit(1);
}

Expand Down
Loading
Loading