summaryrefslogtreecommitdiff
path: root/src/server/web/docs/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/web/docs/api')
-rw-r--r--src/server/web/docs/api/endpoints/posts/create.yaml53
-rw-r--r--src/server/web/docs/api/endpoints/posts/timeline.yaml32
-rw-r--r--src/server/web/docs/api/endpoints/style.styl21
-rw-r--r--src/server/web/docs/api/endpoints/view.pug32
-rw-r--r--src/server/web/docs/api/entities/drive-file.yaml73
-rw-r--r--src/server/web/docs/api/entities/post.yaml173
-rw-r--r--src/server/web/docs/api/entities/style.styl1
-rw-r--r--src/server/web/docs/api/entities/user.yaml173
-rw-r--r--src/server/web/docs/api/entities/view.pug20
-rw-r--r--src/server/web/docs/api/gulpfile.ts188
-rw-r--r--src/server/web/docs/api/mixins.pug37
-rw-r--r--src/server/web/docs/api/style.styl11
12 files changed, 814 insertions, 0 deletions
diff --git a/src/server/web/docs/api/endpoints/posts/create.yaml b/src/server/web/docs/api/endpoints/posts/create.yaml
new file mode 100644
index 0000000000..5e2307dab4
--- /dev/null
+++ b/src/server/web/docs/api/endpoints/posts/create.yaml
@@ -0,0 +1,53 @@
+endpoint: "posts/create"
+
+desc:
+ ja: "投稿します。"
+ en: "Compose new post."
+
+params:
+ - name: "text"
+ type: "string"
+ optional: true
+ desc:
+ ja: "投稿の本文"
+ en: "The text of your post"
+ - name: "media_ids"
+ type: "id(DriveFile)[]"
+ optional: true
+ desc:
+ ja: "添付するメディア(1~4つ)"
+ en: "Media you want to attach (1~4)"
+ - name: "reply_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "返信する投稿"
+ en: "The post you want to reply"
+ - name: "repost_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "引用する投稿"
+ en: "The post you want to quote"
+ - name: "poll"
+ type: "object"
+ optional: true
+ desc:
+ ja: "投票"
+ en: "The poll"
+ defName: "poll"
+ def:
+ - name: "choices"
+ type: "string[]"
+ optional: false
+ desc:
+ ja: "投票の選択肢"
+ en: "Choices of a poll"
+
+res:
+ - name: "created_post"
+ type: "entity(Post)"
+ optional: false
+ desc:
+ ja: "作成した投稿"
+ en: "A post that created"
diff --git a/src/server/web/docs/api/endpoints/posts/timeline.yaml b/src/server/web/docs/api/endpoints/posts/timeline.yaml
new file mode 100644
index 0000000000..01976b0611
--- /dev/null
+++ b/src/server/web/docs/api/endpoints/posts/timeline.yaml
@@ -0,0 +1,32 @@
+endpoint: "posts/timeline"
+
+desc:
+ ja: "タイムラインを取得します。"
+ en: "Get your timeline."
+
+params:
+ - name: "limit"
+ type: "number"
+ optional: true
+ desc:
+ ja: "取得する最大の数"
+ - name: "since_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
+ - name: "until_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "指定すると、この投稿を基点としてより古い投稿を取得します"
+ - name: "since_date"
+ type: "number"
+ optional: true
+ desc:
+ ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
+ - name: "until_date"
+ type: "number"
+ optional: true
+ desc:
+ ja: "指定した時間を基点としてより古い投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
diff --git a/src/server/web/docs/api/endpoints/style.styl b/src/server/web/docs/api/endpoints/style.styl
new file mode 100644
index 0000000000..2af9fe9a77
--- /dev/null
+++ b/src/server/web/docs/api/endpoints/style.styl
@@ -0,0 +1,21 @@
+@import "../style"
+
+#url
+ padding 8px 12px 8px 8px
+ font-family Consolas, 'Courier New', Courier, Monaco, monospace
+ color #fff
+ background #222e40
+ border-radius 4px
+
+ > .method
+ display inline-block
+ margin 0 8px 0 0
+ padding 0 6px
+ color #f4fcff
+ background #17afc7
+ border-radius 4px
+ user-select none
+ pointer-events none
+
+ > .host
+ opacity 0.7
diff --git a/src/server/web/docs/api/endpoints/view.pug b/src/server/web/docs/api/endpoints/view.pug
new file mode 100644
index 0000000000..d271a5517a
--- /dev/null
+++ b/src/server/web/docs/api/endpoints/view.pug
@@ -0,0 +1,32 @@
+extends ../../layout.pug
+include ../mixins
+
+block meta
+ link(rel="stylesheet" href="/assets/api/endpoints/style.css")
+
+block main
+ h1= endpoint
+
+ p#url
+ span.method POST
+ span.host
+ = url.host
+ | /
+ span.path= url.path
+
+ p#desc= desc[lang] || desc['ja']
+
+ section
+ h2 %i18n:docs.api.endpoints.params%
+ +propTable(params)
+
+ if paramDefs
+ each paramDef in paramDefs
+ section(id= paramDef.name)
+ h3= paramDef.name
+ +propTable(paramDef.params)
+
+ if res
+ section
+ h2 %i18n:docs.api.endpoints.res%
+ +propTable(res)
diff --git a/src/server/web/docs/api/entities/drive-file.yaml b/src/server/web/docs/api/entities/drive-file.yaml
new file mode 100644
index 0000000000..2ebbb089ab
--- /dev/null
+++ b/src/server/web/docs/api/entities/drive-file.yaml
@@ -0,0 +1,73 @@
+name: "DriveFile"
+
+desc:
+ ja: "ドライブのファイル。"
+ en: "A file of Drive."
+
+props:
+ - name: "id"
+ type: "id"
+ optional: false
+ desc:
+ ja: "ファイルID"
+ en: "The ID of this file"
+ - name: "created_at"
+ type: "date"
+ optional: false
+ desc:
+ ja: "アップロード日時"
+ en: "The upload date of this file"
+ - name: "user_id"
+ type: "id(User)"
+ optional: false
+ desc:
+ ja: "所有者ID"
+ en: "The ID of the owner of this file"
+ - name: "user"
+ type: "entity(User)"
+ optional: true
+ desc:
+ ja: "所有者"
+ en: "The owner of this file"
+ - name: "name"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ファイル名"
+ en: "The name of this file"
+ - name: "md5"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ファイルのMD5ハッシュ値"
+ en: "The md5 hash value of this file"
+ - name: "type"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ファイルの種類"
+ en: "The type of this file"
+ - name: "datasize"
+ type: "number"
+ optional: false
+ desc:
+ ja: "ファイルサイズ(bytes)"
+ en: "The size of this file (bytes)"
+ - name: "url"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ファイルのURL"
+ en: "The URL of this file"
+ - name: "folder_id"
+ type: "id(DriveFolder)"
+ optional: true
+ desc:
+ ja: "フォルダID"
+ en: "The ID of the folder of this file"
+ - name: "folder"
+ type: "entity(DriveFolder)"
+ optional: true
+ desc:
+ ja: "フォルダ"
+ en: "The folder of this file"
diff --git a/src/server/web/docs/api/entities/post.yaml b/src/server/web/docs/api/entities/post.yaml
new file mode 100644
index 0000000000..f780263144
--- /dev/null
+++ b/src/server/web/docs/api/entities/post.yaml
@@ -0,0 +1,173 @@
+name: "Post"
+
+desc:
+ ja: "投稿。"
+ en: "A post."
+
+props:
+ - name: "id"
+ type: "id"
+ optional: false
+ desc:
+ ja: "投稿ID"
+ en: "The ID of this post"
+ - name: "created_at"
+ type: "date"
+ optional: false
+ desc:
+ ja: "投稿日時"
+ en: "The posted date of this post"
+ - name: "via_mobile"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "モバイル端末から投稿したか否か(自己申告であることに留意)"
+ en: "Whether this post sent via a mobile device"
+ - name: "text"
+ type: "string"
+ optional: true
+ desc:
+ ja: "投稿の本文"
+ en: "The text of this post"
+ - name: "media_ids"
+ type: "id(DriveFile)[]"
+ optional: true
+ desc:
+ ja: "添付されているメディアのID"
+ en: "The IDs of the attached media"
+ - name: "media"
+ type: "entity(DriveFile)[]"
+ optional: true
+ desc:
+ ja: "添付されているメディア"
+ en: "The attached media"
+ - name: "user_id"
+ type: "id(User)"
+ optional: false
+ desc:
+ ja: "投稿者ID"
+ en: "The ID of author of this post"
+ - name: "user"
+ type: "entity(User)"
+ optional: true
+ desc:
+ ja: "投稿者"
+ en: "The author of this post"
+ - name: "my_reaction"
+ type: "string"
+ optional: true
+ desc:
+ ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
+ en: "The your <a href='/docs/api/reactions'>reaction</a> of this post"
+ - name: "reaction_counts"
+ type: "object"
+ optional: false
+ desc:
+ ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
+ - name: "reply_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "返信した投稿のID"
+ en: "The ID of the replyed post"
+ - name: "reply"
+ type: "entity(Post)"
+ optional: true
+ desc:
+ ja: "返信した投稿"
+ en: "The replyed post"
+ - name: "repost_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "引用した投稿のID"
+ en: "The ID of the quoted post"
+ - name: "repost"
+ type: "entity(Post)"
+ optional: true
+ desc:
+ ja: "引用した投稿"
+ en: "The quoted post"
+ - name: "poll"
+ type: "object"
+ optional: true
+ desc:
+ ja: "投票"
+ en: "The poll"
+ defName: "poll"
+ def:
+ - name: "choices"
+ type: "object[]"
+ optional: false
+ desc:
+ ja: "投票の選択肢"
+ en: "The choices of this poll"
+ defName: "choice"
+ def:
+ - name: "id"
+ type: "number"
+ optional: false
+ desc:
+ ja: "選択肢ID"
+ en: "The ID of this choice"
+ - name: "is_voted"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "自分がこの選択肢に投票したかどうか"
+ en: "Whether you voted to this choice"
+ - name: "text"
+ type: "string"
+ optional: false
+ desc:
+ ja: "選択肢本文"
+ en: "The text of this choice"
+ - name: "votes"
+ type: "number"
+ optional: false
+ desc:
+ ja: "この選択肢に投票された数"
+ en: "The number voted for this choice"
+ - name: "geo"
+ type: "object"
+ optional: true
+ desc:
+ ja: "位置情報"
+ en: "Geo location"
+ defName: "geo"
+ def:
+ - name: "latitude"
+ type: "number"
+ optional: false
+ desc:
+ ja: "緯度。-90〜90で表す。"
+ - name: "longitude"
+ type: "number"
+ optional: false
+ desc:
+ ja: "経度。-180〜180で表す。"
+ - name: "altitude"
+ type: "number"
+ optional: false
+ desc:
+ ja: "高度。メートル単位で表す。"
+ - name: "accuracy"
+ type: "number"
+ optional: false
+ desc:
+ ja: "緯度、経度の精度。メートル単位で表す。"
+ - name: "altitudeAccuracy"
+ type: "number"
+ optional: false
+ desc:
+ ja: "高度の精度。メートル単位で表す。"
+ - name: "heading"
+ type: "number"
+ optional: false
+ desc:
+ ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。"
+ - name: "speed"
+ type: "number"
+ optional: false
+ desc:
+ ja: "速度。メートル / 秒数で表す。"
diff --git a/src/server/web/docs/api/entities/style.styl b/src/server/web/docs/api/entities/style.styl
new file mode 100644
index 0000000000..bddf0f53ab
--- /dev/null
+++ b/src/server/web/docs/api/entities/style.styl
@@ -0,0 +1 @@
+@import "../style"
diff --git a/src/server/web/docs/api/entities/user.yaml b/src/server/web/docs/api/entities/user.yaml
new file mode 100644
index 0000000000..a451a40807
--- /dev/null
+++ b/src/server/web/docs/api/entities/user.yaml
@@ -0,0 +1,173 @@
+name: "User"
+
+desc:
+ ja: "ユーザー。"
+ en: "A user."
+
+props:
+ - name: "id"
+ type: "id"
+ optional: false
+ desc:
+ ja: "ユーザーID"
+ en: "The ID of this user"
+ - name: "created_at"
+ type: "date"
+ optional: false
+ desc:
+ ja: "アカウント作成日時"
+ en: "The registered date of this user"
+ - name: "username"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ユーザー名"
+ en: "The username of this user"
+ - name: "description"
+ type: "string"
+ optional: false
+ desc:
+ ja: "アカウントの説明(自己紹介)"
+ en: "The description of this user"
+ - name: "avatar_id"
+ type: "id(DriveFile)"
+ optional: true
+ desc:
+ ja: "アバターのID"
+ en: "The ID of the avatar of this user"
+ - name: "avatar_url"
+ type: "string"
+ optional: false
+ desc:
+ ja: "アバターのURL"
+ en: "The URL of the avatar of this user"
+ - name: "banner_id"
+ type: "id(DriveFile)"
+ optional: true
+ desc:
+ ja: "バナーのID"
+ en: "The ID of the banner of this user"
+ - name: "banner_url"
+ type: "string"
+ optional: false
+ desc:
+ ja: "バナーのURL"
+ en: "The URL of the banner of this user"
+ - name: "followers_count"
+ type: "number"
+ optional: false
+ desc:
+ ja: "フォロワーの数"
+ en: "The number of the followers for this user"
+ - name: "following_count"
+ type: "number"
+ optional: false
+ desc:
+ ja: "フォローしているユーザーの数"
+ en: "The number of the following users for this user"
+ - name: "is_following"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "自分がこのユーザーをフォローしているか"
+ - name: "is_followed"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "自分がこのユーザーにフォローされているか"
+ - name: "is_muted"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "自分がこのユーザーをミュートしているか"
+ en: "Whether you muted this user"
+ - name: "posts_count"
+ type: "number"
+ optional: false
+ desc:
+ ja: "投稿の数"
+ en: "The number of the posts of this user"
+ - name: "pinned_post"
+ type: "entity(Post)"
+ optional: true
+ desc:
+ ja: "ピン留めされた投稿"
+ en: "The pinned post of this user"
+ - name: "pinned_post_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "ピン留めされた投稿のID"
+ en: "The ID of the pinned post of this user"
+ - name: "drive_capacity"
+ type: "number"
+ optional: false
+ desc:
+ ja: "ドライブの容量(bytes)"
+ en: "The capacity of drive of this user (bytes)"
+ - name: "host"
+ type: "string | null"
+ optional: false
+ desc:
+ ja: "ホスト (例: example.com:3000)"
+ en: "Host (e.g. example.com:3000)"
+ - name: "account"
+ type: "object"
+ optional: false
+ desc:
+ ja: "このサーバーにおけるアカウント"
+ en: "The account of this user on this server"
+ defName: "account"
+ def:
+ - name: "last_used_at"
+ type: "date"
+ optional: false
+ desc:
+ ja: "最終利用日時"
+ en: "The last used date of this user"
+ - name: "is_bot"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "botか否か(自己申告であることに留意)"
+ en: "Whether is bot or not"
+ - name: "twitter"
+ type: "object"
+ optional: true
+ desc:
+ ja: "連携されているTwitterアカウント情報"
+ en: "The info of the connected twitter account of this user"
+ defName: "twitter"
+ def:
+ - name: "user_id"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ユーザーID"
+ en: "The user ID"
+ - name: "screen_name"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ユーザー名"
+ en: "The screen name of this user"
+ - name: "profile"
+ type: "object"
+ optional: false
+ desc:
+ ja: "プロフィール"
+ en: "The profile of this user"
+ defName: "profile"
+ def:
+ - name: "location"
+ type: "string"
+ optional: true
+ desc:
+ ja: "場所"
+ en: "The location of this user"
+ - name: "birthday"
+ type: "string"
+ optional: true
+ desc:
+ ja: "誕生日 (YYYY-MM-DD)"
+ en: "The birthday of this user (YYYY-MM-DD)"
diff --git a/src/server/web/docs/api/entities/view.pug b/src/server/web/docs/api/entities/view.pug
new file mode 100644
index 0000000000..2156463dc7
--- /dev/null
+++ b/src/server/web/docs/api/entities/view.pug
@@ -0,0 +1,20 @@
+extends ../../layout.pug
+include ../mixins
+
+block meta
+ link(rel="stylesheet" href="/assets/api/entities/style.css")
+
+block main
+ h1= name
+
+ p#desc= desc[lang] || desc['ja']
+
+ section
+ h2 %i18n:docs.api.entities.properties%
+ +propTable(props)
+
+ if propDefs
+ each propDef in propDefs
+ section(id= propDef.name)
+ h3= propDef.name
+ +propTable(propDef.params)
diff --git a/src/server/web/docs/api/gulpfile.ts b/src/server/web/docs/api/gulpfile.ts
new file mode 100644
index 0000000000..37935413de
--- /dev/null
+++ b/src/server/web/docs/api/gulpfile.ts
@@ -0,0 +1,188 @@
+/**
+ * Gulp tasks
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as glob from 'glob';
+import * as gulp from 'gulp';
+import * as pug from 'pug';
+import * as yaml from 'js-yaml';
+import * as mkdirp from 'mkdirp';
+
+import locales from '../../../../../locales';
+import I18nReplacer from '../../../../build/i18n';
+import fa from '../../../../build/fa';
+import config from './../../../../conf';
+
+import generateVars from '../vars';
+
+const langs = Object.keys(locales);
+
+const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
+
+const parseParam = param => {
+ const id = param.type.match(/^id\((.+?)\)|^id/);
+ const entity = param.type.match(/^entity\((.+?)\)/);
+ const isObject = /^object/.test(param.type);
+ const isDate = /^date/.test(param.type);
+ const isArray = /\[\]$/.test(param.type);
+ if (id) {
+ param.kind = 'id';
+ param.type = 'string';
+ param.entity = id[1];
+ if (isArray) {
+ param.type += '[]';
+ }
+ }
+ if (entity) {
+ param.kind = 'entity';
+ param.type = 'object';
+ param.entity = entity[1];
+ if (isArray) {
+ param.type += '[]';
+ }
+ }
+ if (isObject) {
+ param.kind = 'object';
+ }
+ if (isDate) {
+ param.kind = 'date';
+ param.type = 'string';
+ if (isArray) {
+ param.type += '[]';
+ }
+ }
+
+ return param;
+};
+
+const sortParams = params => {
+ params.sort((a, b) => {
+ if (a.name < b.name)
+ return -1;
+ if (a.name > b.name)
+ return 1;
+ return 0;
+ });
+ return params;
+};
+
+const extractDefs = params => {
+ let defs = [];
+
+ params.forEach(param => {
+ if (param.def) {
+ defs.push({
+ name: param.defName,
+ params: sortParams(param.def.map(p => parseParam(p)))
+ });
+
+ const childDefs = extractDefs(param.def);
+
+ defs = defs.concat(childDefs);
+ }
+ });
+
+ return sortParams(defs);
+};
+
+gulp.task('doc:api', [
+ 'doc:api:endpoints',
+ 'doc:api:entities'
+]);
+
+gulp.task('doc:api:endpoints', async () => {
+ const commonVars = await generateVars();
+ glob('./src/server/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
+ if (globErr) {
+ console.error(globErr);
+ return;
+ }
+ //console.log(files);
+ files.forEach(file => {
+ const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+ const vars = {
+ endpoint: ep.endpoint,
+ url: {
+ host: config.api_url,
+ path: ep.endpoint
+ },
+ desc: ep.desc,
+ params: sortParams(ep.params.map(p => parseParam(p))),
+ paramDefs: extractDefs(ep.params),
+ res: ep.res ? sortParams(ep.res.map(p => parseParam(p))) : null,
+ resDefs: ep.res ? extractDefs(ep.res) : null,
+ };
+ langs.forEach(lang => {
+ pug.renderFile('./src/server/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
+ lang,
+ title: ep.endpoint,
+ src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/api/endpoints/${ep.endpoint}.yaml`,
+ kebab,
+ common: commonVars
+ }), (renderErr, html) => {
+ if (renderErr) {
+ console.error(renderErr);
+ return;
+ }
+ const i18n = new I18nReplacer(lang);
+ html = html.replace(i18n.pattern, i18n.replacement);
+ html = fa(html);
+ const htmlPath = `./built/server/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
+ mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+ if (mkdirErr) {
+ console.error(mkdirErr);
+ return;
+ }
+ fs.writeFileSync(htmlPath, html, 'utf-8');
+ });
+ });
+ });
+ });
+ });
+});
+
+gulp.task('doc:api:entities', async () => {
+ const commonVars = await generateVars();
+ glob('./src/server/web/docs/api/entities/**/*.yaml', (globErr, files) => {
+ if (globErr) {
+ console.error(globErr);
+ return;
+ }
+ files.forEach(file => {
+ const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+ const vars = {
+ name: entity.name,
+ desc: entity.desc,
+ props: sortParams(entity.props.map(p => parseParam(p))),
+ propDefs: extractDefs(entity.props),
+ };
+ langs.forEach(lang => {
+ pug.renderFile('./src/server/web/docs/api/entities/view.pug', Object.assign({}, vars, {
+ lang,
+ title: entity.name,
+ src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/api/entities/${kebab(entity.name)}.yaml`,
+ kebab,
+ common: commonVars
+ }), (renderErr, html) => {
+ if (renderErr) {
+ console.error(renderErr);
+ return;
+ }
+ const i18n = new I18nReplacer(lang);
+ html = html.replace(i18n.pattern, i18n.replacement);
+ html = fa(html);
+ const htmlPath = `./built/server/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
+ mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+ if (mkdirErr) {
+ console.error(mkdirErr);
+ return;
+ }
+ fs.writeFileSync(htmlPath, html, 'utf-8');
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/server/web/docs/api/mixins.pug b/src/server/web/docs/api/mixins.pug
new file mode 100644
index 0000000000..686bf6a2b6
--- /dev/null
+++ b/src/server/web/docs/api/mixins.pug
@@ -0,0 +1,37 @@
+mixin propTable(props)
+ table.props
+ thead: tr
+ th %i18n:docs.api.props.name%
+ th %i18n:docs.api.props.type%
+ th %i18n:docs.api.props.optional%
+ th %i18n:docs.api.props.description%
+ tbody
+ each prop in props
+ tr
+ td.name= prop.name
+ td.type
+ i= prop.type
+ if prop.kind == 'id'
+ if prop.entity
+ | (
+ a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+ | ID)
+ else
+ | (ID)
+ else if prop.kind == 'entity'
+ | (
+ a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+ | )
+ else if prop.kind == 'object'
+ if prop.def
+ | (
+ a(href=`#${prop.defName}`)= prop.defName
+ | )
+ else if prop.kind == 'date'
+ | (Date)
+ td.optional
+ if prop.optional
+ | %i18n:docs.api.props.yes%
+ else
+ | %i18n:docs.api.props.no%
+ td.desc!= prop.desc[lang] || prop.desc['ja']
diff --git a/src/server/web/docs/api/style.styl b/src/server/web/docs/api/style.styl
new file mode 100644
index 0000000000..3675a4da6f
--- /dev/null
+++ b/src/server/web/docs/api/style.styl
@@ -0,0 +1,11 @@
+@import "../style"
+
+table.props
+ .name
+ font-weight bold
+
+ .name
+ .type
+ .optional
+ font-family Consolas, 'Courier New', Courier, Monaco, monospace
+