From d6ec5f2fe13bb1e3f4316f04591bf419f587c2bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 00:23:45 +0900
Subject: [PATCH] :v:

---
 gulpfile.ts                                  |   4 +-
 src/web/docs/api/endpoints/posts/create.yaml |   8 +-
 src/web/docs/api/endpoints/style.styl        |  12 +-
 src/web/docs/api/endpoints/view.pug          |  75 ++++-------
 src/web/docs/api/entities/post.yaml          | 124 +++++++++++++++++++
 src/web/docs/api/entities/style.styl         |   1 +
 src/web/docs/api/entities/view.pug           |  23 ++++
 src/web/docs/api/{endpoints => }/gulpfile.ts |  78 ++++++++++--
 src/web/docs/api/mixins.pug                  |  33 +++++
 src/web/docs/api/style.styl                  |  11 ++
 src/web/docs/layout.pug                      |  16 +++
 11 files changed, 305 insertions(+), 80 deletions(-)
 create mode 100644 src/web/docs/api/entities/post.yaml
 create mode 100644 src/web/docs/api/entities/style.styl
 create mode 100644 src/web/docs/api/entities/view.pug
 rename src/web/docs/api/{endpoints => }/gulpfile.ts (50%)
 create mode 100644 src/web/docs/api/mixins.pug
 create mode 100644 src/web/docs/api/style.styl
 create mode 100644 src/web/docs/layout.pug

diff --git a/gulpfile.ts b/gulpfile.ts
index 0bc18dd7c..6807b6d57 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -48,7 +48,7 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
-require('./src/web/docs/api/endpoints/gulpfile.ts');
+require('./src/web/docs/api/gulpfile.ts');
 
 gulp.task('build', [
 	'build:js',
@@ -61,7 +61,7 @@ gulp.task('build', [
 gulp.task('rebuild', ['clean', 'build']);
 
 gulp.task('build:doc', [
-	'doc:endpoints',
+	'doc:api',
 	'doc:styles'
 ]);
 
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index 498a99159..5e2307dab 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -10,7 +10,7 @@ params:
     optional: true
     desc:
       ja: "投稿の本文"
-      en: "Text of a post"
+      en: "The text of your post"
   - name: "media_ids"
     type: "id(DriveFile)[]"
     optional: true
@@ -22,19 +22,19 @@ params:
     optional: true
     desc:
       ja: "返信する投稿"
-      en: "A post you want to reply"
+      en: "The post you want to reply"
   - name: "repost_id"
     type: "id(Post)"
     optional: true
     desc:
       ja: "引用する投稿"
-      en: "A post you want to quote"
+      en: "The post you want to quote"
   - name: "poll"
     type: "object"
     optional: true
     desc:
       ja: "投票"
-      en: "A poll"
+      en: "The poll"
     defName: "poll"
     def:
       - name: "choices"
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
index ab74e100b..07fb7ec2a 100644
--- a/src/web/docs/api/endpoints/style.styl
+++ b/src/web/docs/api/endpoints/style.styl
@@ -1,4 +1,4 @@
-@import "../../style"
+@import "../style"
 
 #url
 	padding 8px 12px
@@ -6,13 +6,3 @@
 	color #fff
 	background #222e40
 	border-radius 4px
-
-table
-	.name
-		font-weight bold
-
-	.name
-	.type
-	.optional
-		font-family Consolas, 'Courier New', Courier, Monaco, monospace
-
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 841ca8b3f..cebef9fa5 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -1,63 +1,30 @@
-doctype html
+extends ../../layout.pug
+include ../mixins
 
-mixin i18n(xs)
-	each text, lang in xs
-		span(class=`i18n ${lang}`)= text
+block title
+	| #{endpoint} | Misskey API
 
-mixin table(params)
-	table
-		thead: tr
-			th Name
-			th Type
-			th Optional
-			th Description
-		tbody
-			each param in params
-				tr
-					td.name= param.name
-					td.type
-						if param.kind == 'id'
-							| #{param.type} (
-							a(href=`/docs/api/entities/${param.entity}`)= param.entity
-							|  ID)
-						else if param.kind == 'entity'
-							| #{param.type} (
-							a(href=`/docs/api/entities/${param.entity}`)= param.entity
-							| )
-						else if param.kind == 'object'
-							| #{param.type} (
-							a(href=`#${param.defName}`)= param.defName
-							| )
-						else
-							= param.type
-					td.optional= param.optional.toString()
-					td.desc: +i18n(param.desc)
+block meta
+	link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
 
-html
-	head
-		meta(charset="UTF-8")
-		title #{endpoint} | Misskey API
-		link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
+block main
+	h1= endpoint
 
-	body
-		main
-			h1= endpoint
+	p#url= url
 
-			p#url= url
+	p#desc: +i18n(desc)
 
-			p#desc: +i18n(desc)
+	section
+		h2 Params
+		+propTable(params)
 
-			section
-				h2 Params
-				+table(params)
+		if paramDefs
+			each paramDef in paramDefs
+				section(id= paramDef.name)
+					h3= paramDef.name
+					+propTable(paramDef.params)
 
-				if paramDefs
-					each paramDef in paramDefs
-						section(id= paramDef.name)
-							h3= paramDef.name
-							+table(paramDef.params)
-
-			section
-				h2 Response
-				+table(res)
+	section
+		h2 Response
+		+propTable(res)
 
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
new file mode 100644
index 000000000..551f3b7c3
--- /dev/null
+++ b/src/web/docs/api/entities/post.yaml
@@ -0,0 +1,124 @@
+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: "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"
diff --git a/src/web/docs/api/entities/style.styl b/src/web/docs/api/entities/style.styl
new file mode 100644
index 000000000..bddf0f53a
--- /dev/null
+++ b/src/web/docs/api/entities/style.styl
@@ -0,0 +1 @@
+@import "../style"
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
new file mode 100644
index 000000000..f210582f1
--- /dev/null
+++ b/src/web/docs/api/entities/view.pug
@@ -0,0 +1,23 @@
+extends ../../layout.pug
+include ../mixins
+
+block title
+	| #{name} | Misskey API
+
+block meta
+	link(rel="stylesheet" href="/assets/docs/api/entities/style.css")
+
+block main
+	h1= name
+
+	p#desc: +i18n(desc)
+
+	section
+		h2 Properties
+		+propTable(props)
+
+		if propDefs
+			each propDef in propDefs
+				section(id= propDef.name)
+					h3= propDef.name
+					+propTable(propDef.params)
diff --git a/src/web/docs/api/endpoints/gulpfile.ts b/src/web/docs/api/gulpfile.ts
similarity index 50%
rename from src/web/docs/api/endpoints/gulpfile.ts
rename to src/web/docs/api/gulpfile.ts
index e375447c5..05567b623 100644
--- a/src/web/docs/api/endpoints/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -10,12 +10,15 @@ import * as pug from 'pug';
 import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 
-import config from './../../../../conf';
+import config from './../../../conf';
+
+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\((.+?)\)/);
+	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';
@@ -36,30 +39,53 @@ const parseParam = param => {
 	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 => {
-	const defs = [];
+	let defs = [];
 
 	params.forEach(param => {
 		if (param.def) {
 			defs.push({
 				name: param.defName,
-				params: param.def.map(p => parseParam(p))
+				params: sortParams(param.def.map(p => parseParam(p)))
 			});
 
 			const childDefs = extractDefs(param.def);
 
-			defs.concat(childDefs);
+			defs = defs.concat(childDefs);
 		}
 	});
 
 	return defs;
 };
 
-gulp.task('doc:endpoints', () => {
+gulp.task('doc:api', [
+	'doc:api:endpoints',
+	'doc:api:entities'
+]);
+
+gulp.task('doc:api:endpoints', () => {
 	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
@@ -72,10 +98,11 @@ gulp.task('doc:endpoints', () => {
 				endpoint: ep.endpoint,
 				url: `${config.api_url}/${ep.endpoint}`,
 				desc: ep.desc,
-				params: ep.params.map(p => parseParam(p)),
+				params: sortParams(ep.params.map(p => parseParam(p))),
 				paramDefs: extractDefs(ep.params),
-				res: ep.res.map(p => parseParam(p)),
-				resDefs: extractDefs(ep.res)
+				res: sortParams(ep.res.map(p => parseParam(p))),
+				resDefs: extractDefs(ep.res),
+				kebab
 			};
 			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
 				if (renderErr) {
@@ -94,3 +121,36 @@ gulp.task('doc:endpoints', () => {
 		});
 	});
 });
+
+gulp.task('doc:api:entities', () => {
+	glob('./src/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),
+				kebab
+			};
+			pug.renderFile('./src/web/docs/api/entities/view.pug', vars, (renderErr, html) => {
+				if (renderErr) {
+					console.error(renderErr);
+					return;
+				}
+				const htmlPath = `./built/web/docs/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/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
new file mode 100644
index 000000000..b302c7826
--- /dev/null
+++ b/src/web/docs/api/mixins.pug
@@ -0,0 +1,33 @@
+mixin propTable(props)
+	table.props
+		thead: tr
+			th Name
+			th Type
+			th Optional
+			th 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=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+								|  ID)
+							else
+								|  (ID)
+						else if prop.kind == 'entity'
+							|   (
+							a(href=`/docs/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= prop.optional.toString()
+					td.desc: +i18n(prop.desc)
diff --git a/src/web/docs/api/style.styl b/src/web/docs/api/style.styl
new file mode 100644
index 000000000..3675a4da6
--- /dev/null
+++ b/src/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
+
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
new file mode 100644
index 000000000..68ca9eb62
--- /dev/null
+++ b/src/web/docs/layout.pug
@@ -0,0 +1,16 @@
+doctype html
+
+mixin i18n(xs)
+	each text, lang in xs
+		span(class=`i18n ${lang}`)!= text
+
+html
+	head
+		meta(charset="UTF-8")
+		title
+			block title
+		block meta
+
+	body
+		main
+			block main