├── .gitignore ├── image └── result.png ├── static ├── image │ ├── head.png │ └── title.png ├── css │ ├── post.css │ ├── style.css │ ├── about.css │ └── github-markdown.css └── js │ ├── index.js │ ├── components.js │ ├── utils.js │ ├── about.js │ ├── hacker.js │ ├── handler.js │ └── terminal.js ├── cli ├── server.js ├── publish.js ├── init.js ├── index.js ├── build.js └── post.js ├── index.js ├── package.json ├── src ├── config.js ├── server │ └── server.js ├── database │ └── database.js └── parser │ └── parser.js ├── data.json ├── layout ├── post.template ├── about.template └── index.template ├── document.md ├── README.md └── post ├── firstblog.md ├── geekblog2.md ├── geekblog1.md └── geekblog3.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | node_modules 3 | *.log 4 | build -------------------------------------------------------------------------------- /image/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Easonzero/GeekBlog/HEAD/image/result.png -------------------------------------------------------------------------------- /static/image/head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Easonzero/GeekBlog/HEAD/static/image/head.png -------------------------------------------------------------------------------- /static/image/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Easonzero/GeekBlog/HEAD/static/image/title.png -------------------------------------------------------------------------------- /cli/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-8. 3 | */ 4 | "use strict"; 5 | const Server = require('../src/server/server'); 6 | 7 | module.exports = ()=>{ 8 | new Server().listen('127.0.0.1',4000); 9 | }; 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-7. 3 | */ 4 | exports.Server = require('./src/server/server'); 5 | exports.Parser = require('./src/parser/parser'); 6 | exports.Database = require('./src/database/database'); 7 | exports.Define = require('./src/config'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geek-blog", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "github-api": "^2.3.0", 6 | "github-markdown-css": "^2.4.1", 7 | "showdown": "^1.4.4" 8 | }, 9 | "bin": { 10 | "geekcli": "cli/index.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-8. 3 | */ 4 | module.exports = { 5 | indexPage:'index.html',//首页文件名 6 | indexTmpl:'index.template',//首页模板 7 | postTmpl:'post.template',//post文件模板 8 | post:'post',//post目录 9 | layout:'layout',//layout目录 10 | static:'static',//静态文件目录 11 | build:'build',//构建目录 12 | data:'data.json'//数据文件目录 13 | }; -------------------------------------------------------------------------------- /cli/publish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-11. 3 | */ 4 | "use strict"; 5 | const exec = require('child_process').exec; 6 | const cmd_cd = 'cd build'; 7 | const cmd_add = 'git add .'; 8 | const cmd_commit = `git commit -m "update"`; 9 | const cmd_push = `git push origin master`; 10 | 11 | module.exports = ()=>{ 12 | exec(`${cmd_cd}&&${cmd_add}&&${cmd_commit}&&${cmd_push}`,function(err,stdout,stderr){ 13 | if(err){ 14 | console.log(stderr); 15 | }else{ 16 | console.log('success!'); 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | {"name":"Easonzero","github":"https://github.com/Easonzero","zhihu":"https://www.zhihu.com/people/wang-yi-51-8","friendship-link":[{"name":"Estanball","link":"http://wangbicong.cn","summary":"聪哥哥的伊斯坦堡"},{"name":"Chestnutheng","link":"http://chestnutheng.cn","summary":"专注NLP\\ML的技术博客"},{"name":"Shawn","link":"http://shawnzeng.com","summary":"一个爱捣鼓前端的产品汪"}],"posts":[{"title":"从零到一,图形引擎(1)","tag":["计算机图形学","前端"],"date":"2017-1-5","path":"cg1.html"},{"title":"前端实现geek感爆棚的数码流动画","tag":["前端"],"date":"2016-11-16","path":"geekblog3.html"},{"title":"使用h5基本的canvas实现完整的terminal","tag":["前端"],"date":"2016-11-13","path":"geekblog2.html"},{"title":"写一个简单的博客生成器","tag":["Node"],"date":"2016-11-12","path":"geekblog1.html"},{"title":"GeekBlog--重建博客之路","tag":["Geek","Node","前端"],"date":"2016-11-11","path":"firstblog.html"}]} 2 | 3 | -------------------------------------------------------------------------------- /cli/init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-9. 3 | */ 4 | "use strict"; 5 | const fs = require('fs'); 6 | const {Define} = require('../index'); 7 | 8 | const defaultJson = `{ 9 | "name":"Your name", 10 | "github":"Your github link", 11 | "zhihu":"Your zhihu link", 12 | "posts":[], 13 | "friendship-link":[] 14 | }`; 15 | 16 | module.exports = ()=>{ 17 | for(let path in Define){ 18 | if(path=='data'){ 19 | fs.exists(`./${Define[path]}`,(exist)=>{ 20 | if(!exist) fs.writeFile(`./${Define.data}`,defaultJson); 21 | }); 22 | } 23 | else if(path.includes('Tmpl')||path.includes('Page')) continue; 24 | else 25 | fs.exists(`./${Define[path]}`,(exist)=>{ 26 | if(!exist) fs.mkdir(`./${Define[path]}`); 27 | }); 28 | } 29 | 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | let init = require('./init'); 4 | let build = require('./build'); 5 | let post = require('./post'); 6 | let server = require('./server'); 7 | let publish = require('./publish'); 8 | 9 | switch(process.argv[2]){ 10 | case 'init'://初始化命令 11 | init(); 12 | break; 13 | case 'build'://构建命令 14 | build(); 15 | break; 16 | case 'post'://管理文章命令 17 | post(process.argv[3],process.argv[4]); 18 | break; 19 | case 'server'://开启服务器命令 20 | server(); 21 | break; 22 | case 'publish': 23 | publish(); 24 | break; 25 | default: 26 | console.log(` 27 | Usage: geekcli 28 | 29 | Commands: 30 | init Create a new GeekBlog folder. 31 | build Write all files of project into ./build. 32 | post Manage posts. 33 | server Start server. 34 | `) 35 | } 36 | -------------------------------------------------------------------------------- /layout/post.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 |

TITLE:{{title}}


11 |

DATE:{{date}}


12 |

TAG:{{tag}}


13 |
14 |
15 | {{content}} 16 |
17 | 18 | Top 19 | 20 | 21 | Return 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /document.md: -------------------------------------------------------------------------------- 1 | ## 环境搭建 2 | 3 | 1. clone本项目,并确保本机拥有node和npm 4 | 2. 执行`sudo npm install -g` => 用于初始化全局命令,此时可以在终端执行geekcli命令 5 | 6 | ## 项目结构 7 | 8 | - \bin 生成的博客 9 | - \cli 命令回调部分的源码(前端) 10 | - \layout 模板文件 11 | - \post 文章 12 | - \src 提供细节处理部分的源码(后端) 13 | - \static 博客的静态文件,包括css、js、图片等 14 | - data.json 数据 15 | 16 | ## 项目概述 17 | 18 | 项目作为博客生成器实则分为两部分,一部分是博客管理、另一部分是博客生成: 19 | 20 | 博客管理部分处理流程如下: 21 | 1. 用户输入命令 22 | 2. 程序查询命令,并执行相应的回调方法 23 | 3. 在回调中执行相应的业务逻辑 24 | 4. 业务逻辑调用后端封装的接口,具体实现完全在后端完成。 25 | 26 | 博客生成部分处理流程如下: 27 | 1. 遍历模板文件,检查`{{...}}`语法,并根据其中内容访问`data.json`,复制其中相对应的内容替换`{{...}}` 28 | 2. 根据目录结构,将模板文件和其对应的css、js、图片文件复制到`\bin`文件夹下 29 | 30 | ## 博客使用 31 | 32 | 博客的文章存放在`\post`、资源文件存放在`\static`、,模板文件存放在`\layout`。其中博客的风格自定义主要由模板文件和资源文件决定。模板文件实质上就是html,只不过会根据`data.json`插入一些数据,这些数据的插入与css和js的编写完全无关,使用者正常的编写js和css文件即可。唯一需要注意的是js可以直接访问data.json中的任何数据。 33 | 34 | 博客管理方面提供如下命令使用: 35 | 36 | 初始化: 37 | 38 | ```shell 39 | geekcli init #初始化博客生成器,初始化后各文件夹都是空的,你可以将原项目中`\static`和`\layout`文件夹中的内容复制到你的博客对应文件夹下后再作修改 40 | geekcli build #构建、生成博客 41 | geekcli server #运行调试服务器,通过127.0.0.1:4000可以本地访问生成的博客 42 | ``` 43 | 44 | 发布文章: 45 | 46 | ```shell 47 | geekcli post create ${文章路径} #创建文章 48 | geekcli post update ${文章路径} #更新文章 49 | geekcli post delete ${文章路径} #删除文章 50 | ``` 51 | -------------------------------------------------------------------------------- /static/css/post.css: -------------------------------------------------------------------------------- 1 | * { 2 | position:relative; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html,body{ 8 | width:100%; 9 | height:100%; 10 | } 11 | 12 | header{ 13 | width:80%; 14 | text-align:left; 15 | color:white; 16 | background-color: rgb(10,10,10); 17 | padding: 10px; 18 | } 19 | 20 | footer{ 21 | margin-top: 40px; 22 | position: absolute; 23 | width:80%; 24 | text-align:center; 25 | color:white; 26 | background-color: rgb(10,10,10); 27 | padding: 10px; 28 | } 29 | 30 | footer a{ 31 | text-decoration:none; 32 | color:white 33 | } 34 | 35 | .cloud-tie-wrapper{ 36 | margin-top:88px; 37 | padding: 10px; 38 | width:80%; 39 | } 40 | 41 | .return,.top{ 42 | z-index:9999; 43 | text-decoration:none; 44 | color:white; 45 | background-color: rgb(10,10,10); 46 | } 47 | 48 | .fix-button{ 49 | width:50px; height:50px; 50 | text-align:center; font-size:small; 51 | line-height:50px; border:1px solid #999; 52 | position:fixed; right:50px; bottom:50px; 53 | border-radius: 50%; 54 | border-bottom-color:#333; 55 | border-right-color:#333; 56 | cursor:pointer; 57 | } 58 | 59 | .top{ 60 | margin-right: 100px; 61 | } 62 | 63 | .center{ 64 | left:50%; 65 | transform:translate(-50%); 66 | } 67 | 68 | .markdown-body{ 69 | margin-top:40px; 70 | width:80%; 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeekBlog 2 | 3 | [![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE) 4 | [![Badge](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu/#/zh_CN) 5 | 6 | A Lite and Geek-Style Blog Generator 7 | 一个`轻量的`、`极客风格`的博客生成器 8 | 9 | [geekblog展示博客](http://easonzero.github.io) 10 | 11 | 为什么说是极客风格呢?该博客生成器的默认主题采用终端风格,打造酷炫的geek风页面 12 | 而且不单单只是风格,默认主题上的终端是完全可以使用的... 13 | ![结果](./image/result.png) 14 | 15 | ### Installation 16 | 说实话写的好累啊- - 17 | 这个东西就不挂在npm上了 18 | 想用的人请自行clone,使用方法往下读 19 | 20 | ### Run 21 | 22 | clone之后首先要在根目录下初始化全局命令: 23 | 24 | ```shell 25 | sudo npm install -g 26 | ``` 27 | 28 | 初始化: 29 | 30 | ```shell 31 | geekcli init #初始化博客生成器 32 | geekcli build #构建、生成博客 33 | geekcli server #运行调试服务器,通过127.0.0.1:4000可以本地访问生成的博客 34 | ``` 35 | 36 | 发布文章: 37 | 38 | ```shell 39 | geekcli post create ${文章路径} #创建文章 40 | geekcli post update ${文章路径} #更新文章 41 | geekcli post delete ${文章路径} #删除文章 42 | ``` 43 | 44 | ps.`data.json`文件中管理数据,目前可以在里面设置博客名、知乎链接、github链接、友链 45 | ### Themes DIY 46 | 47 | 首先,最后生成的页面是由完全静态的html、js、css文件组成,这意味着只要你会前端,就可以随意制作自己的主题。 48 | 49 | 在这个前提下,GeekBlog提供了简单的模板功能,目前可以定制统一的文章`post`界面风格和主页`index`界面风格,之后会开放更多的模板,所有模板在layout文件夹下。 50 | 51 | 在使用模板时需要注意的是,GeekBlog为模板提供了数据支持: 52 | 53 | * 模板中可以使用`{{}}`语法,在html中插入相应的数据,也可以通过在js中直接调用data获得所有数据。 54 | * 在post模板中,可以调用所有与本文章相关的数据 55 | * 在post之外的模板中,可以调用所有数据 56 | * 可调用的数据键名和数据格式请参考`./data.json`中的内容 57 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-6. 3 | */ 4 | const http = require('http'); 5 | const url = require('url'); 6 | const fs = require('fs'); 7 | const Define = require('../config'); 8 | 9 | const mimetype = { 10 | 'txt': 'text/plain', 11 | 'html': 'text/html', 12 | 'css': 'text/css', 13 | 'xml': 'application/xml', 14 | 'json': 'application/json', 15 | 'js': 'application/javascript', 16 | 'jpg': 'image/jpeg', 17 | 'jpeg': 'image/jpeg', 18 | 'gif': 'image/gif', 19 | 'png': 'image/png', 20 | 'svg': 'image/svg+xml', 21 | 'ico': 'image/ico' 22 | }; 23 | 24 | class Server{ 25 | //初始化服务器 26 | constructor(){ 27 | this.server = http.createServer(function (req, res) { 28 | let {pathname} = url.parse(req.url); 29 | let path = `./build${pathname != '/'?pathname:`/${Define.indexPage}`}`; 30 | fs.exists(path, function(exists){ 31 | if(exists){ 32 | let file = fs.createReadStream(path); 33 | 34 | res.writeHead(200, { 35 | 'Content-Type': mimetype[path.split('.').pop()] || 'text/plain' 36 | }); 37 | 38 | file.on('data', res.write.bind(res)); 39 | file.on('close', res.end.bind(res)); 40 | file.on('error', (err)=>console.log(err)); 41 | } 42 | }); 43 | }); 44 | } 45 | //监听端口 46 | listen(ip,port){ 47 | console.log(`The server is listening ${ip}:${port}...`); 48 | this.server.listen(port, ip); 49 | } 50 | } 51 | 52 | module.exports = Server; 53 | -------------------------------------------------------------------------------- /layout/about.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About Me 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |

About Me

Easonzero

14 |
15 |
16 |

17 | test1 18 |

19 |
20 |
21 |
22 |
23 |

24 | test2 25 |

26 |
27 |
28 |
29 |
30 |

31 | test3 32 |

33 |
34 |
35 |
36 |
37 |

38 | test3 39 |

40 |
41 |
42 |
43 | 44 | Top 45 | 46 | 47 | Return 48 | 49 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | let content = $('.terminal'); 2 | let terminalCanvas = $('#terminal-canvas'); 3 | let bgCanvas = $('#bg-canvas'); 4 | 5 | let height = content.css('height'); 6 | let width = content.css('width'); 7 | 8 | terminalCanvas.attr('height',height); 9 | terminalCanvas.attr('width',width); 10 | bgCanvas.attr('width',window.screen.width); 11 | bgCanvas.attr('height',window.screen.height); 12 | 13 | const canvas = jQuery('#terminal-canvas')[0]; 14 | const canvasBg = jQuery('#bg-canvas')[0]; 15 | const ctx = canvas.getContext('2d'); 16 | const ctxBg = canvasBg.getContext('2d'); 17 | //添加终端 18 | const terminal = new Terminal( 19 | ctx, 20 | parseInt(height.substr(0,height.length-2)), 21 | parseInt(width.substr(0,width.length-2)), 22 | 'Eason@blog' 23 | ); 24 | //添加背景数码流 25 | const hacker = new Hacker( 26 | ctxBg, 27 | window.screen.width, 28 | window.screen.height 29 | ); 30 | 31 | terminal.render(); 32 | terminal.input('help'); 33 | terminal.input('$${Enter}'); 34 | 35 | content.on("mousewheel DOMMouseScroll", function (e) { 36 | 37 | let delta = (e.originalEvent.wheelDelta && (e.originalEvent.wheelDelta > 0 ? 1 : -1)) || // chrome & ie 38 | (e.originalEvent.detail && (e.originalEvent.detail > 0 ? -1 : 1)); // firefox 39 | 40 | if (delta > 0) { 41 | terminal.scroll("up"); 42 | } else if (delta < 0) { 43 | terminal.scroll("down"); 44 | } 45 | }); 46 | 47 | let over = false; 48 | $('.head-pic-a').mouseover(function(){ 49 | if(!over){ 50 | over = true; 51 | terminal.input('$${Clear}'); 52 | terminal.input('clear'); 53 | terminal.input('$${Enter}'); 54 | setTimeout(()=>{ 55 | over = false; 56 | terminal.input('hello'); 57 | terminal.input('$${Enter}'); 58 | },300); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /src/database/database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-8. 3 | */ 4 | const fs = require('fs'); 5 | const Define = require('../config'); 6 | 7 | class Database{ 8 | constructor(path){ 9 | this.json = JSON.parse(fs.readFileSync(path)); 10 | this.path = path; 11 | } 12 | //添加post 13 | addpost({title,tag,path}){ 14 | let date = new Date(); 15 | let _date = `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}`; 16 | for(let index in this.json.posts) { 17 | if (this.json.posts[index].path == path) { 18 | this.json.posts[index] = { 19 | title:title,tag:tag,date:_date,path:path 20 | }; 21 | return this; 22 | } 23 | } 24 | console.log(title,tag,path) 25 | this.json.posts.unshift({ 26 | title:title,tag:tag,date:_date,path:path 27 | }); 28 | return this; 29 | } 30 | //去除post 31 | rmpost({path}){ 32 | for(let index in this.json.posts){ 33 | if(this.json.posts[index].path == path) { 34 | let file = `./build/${this.json.posts[index].path}`; 35 | fs.exists(file,(exist)=>{ 36 | if(exist){ 37 | fs.unlinkSync(file); 38 | } 39 | }); 40 | this.json.posts.splice(index,1); 41 | } 42 | } 43 | return this; 44 | } 45 | //得到post 46 | getpost({path}){ 47 | for(let index in this.json.posts) { 48 | if (this.json.posts[index].path == path) { 49 | return this.json.posts[index]; 50 | } 51 | } 52 | return false; 53 | } 54 | //写入文件 55 | flush(){ 56 | let content = JSON.stringify(this.json); 57 | fs.writeFile(this.path,content); 58 | } 59 | } 60 | 61 | module.exports = Database; 62 | -------------------------------------------------------------------------------- /static/js/components.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-4. 3 | */ 4 | class Component { 5 | constructor(ctx,color,width,height){ 6 | this.ctx = ctx; 7 | this.color = color; 8 | this.width = width; 9 | this.height = height; 10 | } 11 | draw(x,y){console.log('need override the method of drawing!')}; 12 | } 13 | 14 | class Cursor extends Component{ 15 | constructor(ctx,color,width,height){ 16 | super(ctx,color,width,height) 17 | this.active = true; 18 | this.flag = true; 19 | this.time = 0; 20 | this.speed = 36; 21 | } 22 | 23 | draw(x,y){ 24 | if(this.active){ 25 | this.time++; 26 | if(this.time>=this.speed){ 27 | this.time=0; 28 | this.flag = !this.flag; 29 | } 30 | 31 | if(this.flag){ 32 | ctx.clearRect(x,y,this.width,this.height); 33 | } else{ 34 | ctx.fillStyle = this.color; 35 | ctx.fillRect(x,y,this.width,this.height); 36 | } 37 | } 38 | }; 39 | } 40 | 41 | class Prompt extends Component{ 42 | constructor(ctx,color,width,height,user){ 43 | super(ctx,color,width,height) 44 | this.user = user; 45 | this.fontWidth = ctx.measureText(this.user).width; 46 | if(this.fontWidth > this.width*7/8-20) this.width = (this.fontWidth+20)*8/7; 47 | } 48 | 49 | draw(x,y){ 50 | ctx.fillStyle = this.color; 51 | ctx.beginPath(); 52 | ctx.moveTo(x,y); 53 | ctx.lineTo(x+this.width*7/8,y); 54 | ctx.lineTo(x+this.width,y+this.height/2); 55 | ctx.lineTo(x+this.width*7/8,y+this.height); 56 | ctx.lineTo(x,y+this.height); 57 | ctx.lineTo(x,y); 58 | ctx.fill(); 59 | 60 | ctx.fillStyle = 'white'; 61 | ctx.fillText(this.user, x+(this.width*7/8-this.fontWidth)/2, y+this.height*3/5); 62 | }; 63 | } -------------------------------------------------------------------------------- /static/js/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-4. 3 | */ 4 | class Utils{ 5 | static giveMeChar(keycode, keyboardEvent) { 6 | var resultChar = '', 7 | charRelated = String.fromCharCode(keycode); 8 | 9 | if ((keycode>=48) && (keycode<=57) 10 | || (keycode>=65) && (keycode<=90) 11 | || (keycode>=97) && (keycode<=122)) { 12 | 13 | resultChar = charRelated; 14 | 15 | if (keyboardEvent.shiftKey) { 16 | switch (keycode) { 17 | case 57: 18 | resultChar = '('; 19 | break; 20 | case 48: 21 | resultChar = ')'; 22 | break; 23 | } 24 | } else if(keyboardEvent.ctrlKey){ 25 | switch (keycode){ 26 | case 85: 27 | resultChar = '$${Clear}'; 28 | break; 29 | } 30 | } else { 31 | resultChar = resultChar.toLowerCase(); 32 | } 33 | } else { 34 | switch (keycode) { 35 | case 32: 36 | resultChar = " "; 37 | break; 38 | case 47: 39 | resultChar = "/"; 40 | break; 41 | case 126: 42 | resultChar = "~"; 43 | break; 44 | case 189: 45 | resultChar = "-"; 46 | break; 47 | case 190: 48 | resultChar = "."; 49 | break; 50 | case 191: 51 | resultChar = "/"; 52 | break; 53 | case 220: 54 | resultChar = "\\"; 55 | break; 56 | case 222: 57 | resultChar = "'"; 58 | break; 59 | default: 60 | resultChar = ""; 61 | break; 62 | } 63 | } 64 | 65 | return resultChar; 66 | } 67 | } -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | position:relative; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | a{ 8 | text-decoration:none; 9 | color:white 10 | } 11 | 12 | html,body{ 13 | height:100%; 14 | width:100% 15 | } 16 | 17 | header,section,footer{ 18 | z-index:999 19 | } 20 | 21 | header{ 22 | height:50%; 23 | width:100% 24 | } 25 | 26 | footer{ 27 | height:10%; 28 | width:100% 29 | } 30 | 31 | .center{ 32 | left:50%; 33 | transform:translate(-50%); 34 | } 35 | 36 | .content{ 37 | height:40%; 38 | width:100%; 39 | } 40 | 41 | .terminal{ 42 | height:100%; 43 | width:50%; 44 | background-color: rgba(10,10,10,0.9); 45 | border-top:1px solid; 46 | border-bottom:1px solid; 47 | } 48 | 49 | .hide { 50 | display: none; 51 | } 52 | 53 | .bg { 54 | width:100%; 55 | height:100%; 56 | z-index:-1; 57 | position:fixed; 58 | background-color: rgb(10,10,10); 59 | top:0; 60 | } 61 | 62 | .head-pic-a{ 63 | height:35%; 64 | position:absolute; 65 | top:10px; 66 | left:10px; 67 | } 68 | 69 | .head-pic{ 70 | height:100%; 71 | position:absolute; 72 | } 73 | 74 | .head-pic-a:hover{ 75 | height:36%; 76 | } 77 | 78 | .head-content{ 79 | width:100%; 80 | height:80%; 81 | } 82 | 83 | .title{ 84 | position:absolute; 85 | top:55%; 86 | height:25% 87 | } 88 | 89 | .bottom{ 90 | position:absolute; 91 | height:50%; 92 | top:50%; 93 | color:white 94 | } 95 | 96 | .links-of-author{ 97 | position:absolute; 98 | top:40%; 99 | height:70%; 100 | width:10%; 101 | display: block; 102 | left:10px; 103 | } 104 | 105 | .links-of-author-item{ 106 | margin-top: 10px; 107 | padding:10px; 108 | width:100%; 109 | background-color: rgba(10,10,10,0.8); 110 | float:left; 111 | } 112 | 113 | .item-name { 114 | text-align:center 115 | } 116 | 117 | .item:before{ 118 | display: inline-block; 119 | vertical-align: middle; 120 | margin-right: 3px; 121 | content: " "; 122 | width: 4px; 123 | height: 4px; 124 | border-radius: 50%; 125 | background: #429f4e; 126 | } -------------------------------------------------------------------------------- /src/parser/parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-7. 3 | */ 4 | const fs = require('fs'); 5 | const Define = require('../config'); 6 | const showdown = require('showdown'), 7 | converter = new showdown.Converter(); 8 | 9 | class Parser{ 10 | //解析文件 11 | static mdparse(md){ 12 | let html = converter.makeHtml(md); 13 | return new Promise((resolve)=>{ 14 | resolve(html); 15 | }); 16 | } 17 | //解析模板 18 | static tmplparse(tmpl,o){ 19 | let tmpldata = fs.readFileSync(tmpl).toString(); 20 | let state = 0,result='',cmd=''; 21 | for(let i=0;ilet data = ${JSON.stringify(o)}`; 49 | }else{ 50 | let indexs = content.split('.'); 51 | let tmp = o; 52 | for(let i=0;i{ 66 | resolve(result); 67 | }); 68 | } 69 | } 70 | 71 | module.exports = Parser; -------------------------------------------------------------------------------- /post/firstblog.md: -------------------------------------------------------------------------------- 1 | 鄙人再一次搭了博客,虽说生命不息,折腾不止,但是作为一个程序员,当然从来都不是个喜欢重复性工作的人。 2 | 可我还是花费了两三天的时间重新设计、编码实现了如今的博客,大抵有着如下几条原由: 3 | ### :厌恶限制 4 | 我知道无论是Hexo、又或者Jekyll都极为便利的为博客爱好者提供了舒适的写博客的方式。使用他们,既可以节省时间,又可以轻易的用着人家写好的主题,甚至在敲着git add、git commit的时候还能自觉geek、油然而生一种自豪感。 5 | 事实上,我最开始也是使用着这些完善、精美的框架,可是很快我意识到了一个问题: 6 | Hexo这类框架自然很好,它们是为了减少维护博客时重复性的劳动而生的,但是为什么我在使用的时候感觉如此疲惫? 7 | 我需要了解这个框架的各种设定,也需要根据他的数据形式调整我的设计方案,或者说我是在不断的向一个框架做妥协。 8 | 我开始明白,我需要的是一个足够自由足够简洁的框架,我只需要框架为我完成重复性的工作,重来不需要框架影响我的设计思路。 9 | 于是我删掉了之前在Hexo框架下所写的所有代码,另起炉灶,完成了这个叫做GeekBlog的极简框架。事实上这个框架所做的只是写文件复制之类操作,简化了管理博客文章的过程,至于其他的,随你怎么做。 10 | 于是在这个框架下,我完成了如今这个此前只是存在于我的脑海中的博客-一个满满geek感的博客。 11 | ### :或许有点爱耍酷 12 | 玩技术如果玩的像你那些循循善诱的老师,或者乖巧听话的好好学生,那还不如捧着笔记本去看几集电视剧来的有趣。 13 | 有人说现在有种程序员叫做面向复制粘贴的程序员,其实复制粘贴几行代码真的不可耻,减少重复性工作是理所应当的,难道非要把一模一样的代码敲上一遍么。 14 | 可恨的是,学着技术却没有想法,学技术只是拿来理解自己复制粘贴下来的代码的人。 15 | 编程和美术、和这世界上大部分创造性工作都是类似的,允许重复、学习重复、但是重复的根本是为了让自己的想法更好的展现出来。 16 | 学画画的人往往要学会画线,总是要追求画出来的线又直又干净,于是一天又一天那样挥动着手臂排列出密密麻麻的线条,画到后来就好像一个精密的机器在机械作业。 17 | 每每想到这个画面,不自觉地就会想,程序员有节奏的啪啪啪地按着ctrl+C、ctrl+V,是不是也像是面无表情的机器人呢? 18 | 我不是机器人,我想要耍酷,想要把脑海里的想法涂抹到屏幕上。所以,每画一笔,都爽到颤栗。 19 | ### :难以接受过去的自己 20 | 人总是会不断进步的,如果有一天回过头去,发现现在还不如以前,那真是失败透顶。 21 | 但是,曾经的自己比现在的自己差劲固然可以理解,但是因此而接受曾经做的那些糟糕的作品,那就说不过去了。 22 | 我大一的时候就在gitpage上搭建过博客,那时也是颇费了一番手脚,将网上收罗来的各种插件东拼西凑到一起,别说设计风格,光是解决因为整合而出现的各种bug就已经焦头烂额了。 23 | 此外,博客的内容也是令人难以接受,回顾自己曾经写的博客,流水账似的内容,少有独立思考的东西,真是惨不忍睹。 24 | 所以啊,理所当然,我重做了博客,也删去了所有的文章。接下来也许仍然难以保持频繁的更新,但是这里我要给我自己一个承诺,自己都不满意的博客,绝不放上来丢人现眼。 25 | 26 | 呐,废话暂时就这么多,接下来介绍下GeekBlog 27 | 28 | # GeekBlog 29 | 30 | ![结果](https://raw.githubusercontent.com/Easonzero/GeekBlog/master/image/result.png) 31 | 32 | ### Installation 33 | 说实话写的好累啊- - 34 | 这个东西就不挂在npm上了 35 | 想用的人请自行clone,使用方法往下读 36 | 37 | ### Run 38 | 39 | 初始化: 40 | 41 | ```shell 42 | geekcli init #初始化博客生成器 43 | geekcli build #构建、生成博客 44 | geekcli server #运行调试服务器,通过127.0.0.1:4000可以本地访问生成的博客 45 | ``` 46 | 47 | 发布文章: 48 | 49 | ```shell 50 | geekcli post create ${文章路径} #创建文章 51 | geekcli post update ${文章路径} #更新文章 52 | geekcli post delete ${文章路径} #删除文章 53 | ``` 54 | 55 | ps.`data.json`文件中管理数据,目前可以在里面设置博客名、知乎链接、github链接、友链 56 | ### Themes DIY 57 | 58 | 首先,最后生成的页面是由完全静态的html、js、css文件组成,这意味着只要你会前端,就可以随意制作自己的主题。 59 | 60 | 在这个前提下,GeekBlog提供了简单的模板功能,目前可以定制统一的文章`post`界面风格和主页`index`界面风格,之后会开放更多的模板,所有模板在layout文件夹下。 61 | 62 | 在使用模板时需要注意的是,GeekBlog为模板提供了数据支持: 63 | 64 | * 模板中可以使用`{{}}`语法,在html中插入相应的数据,也可以通过在js中直接调用data获得所有数据。 65 | * 在post模板中,可以调用所有与本文章相关的数据 66 | * 在post之外的模板中,可以调用所有数据 67 | * 可调用的数据键名和数据格式请参考`./data.json`中的内容 68 | -------------------------------------------------------------------------------- /layout/index.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{name}}'s Blog 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{data.json}} 20 | 21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 48 |
49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
Powered By 57 | GeekBlog 58 |
59 |
60 |
61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /post/geekblog2.md: -------------------------------------------------------------------------------- 1 | 这段时间我和terminal相爱相杀,前一段时间开发了一个项目叫做[TerminalCanvas](https://github.com/Easonzero/TerminalCanvas),这是一个在linux终端上提供画布接口的node模块,支持监听按键事件和动画,有兴趣的同学可以戳进去瞅瞅。最近呢开发这个GeekBlog,又搞出了一个前端版的terminal。可能是身为一个程序员的原因,觉得图形界面普通无趣,terminal才是最佳的交互方式嘛~所以才不断的跟terminal死磕。 2 | 3 | ## 一个前端terminal的结构 4 | 5 | 从功能上来说,terminal需要具备接收用户输入,执行回调方法,返回数据显示在前端这三样功能。从组件划分上考虑呢,至少包括了提示头、游标、命令以及响应。基于以上需求,这里定了如下结构 6 | 7 | - class Terminal 8 | - input(ch);//接受用户输入接口 9 | - render();//渲染方法 10 | - listen();//监听按键事件方法 11 | - cmdHandler(cmd);//命令回调方法 12 | - class Cursor 13 | - draw(x,y);//绘制 14 | - class Prompt 15 | - draw(x,y);//绘制 16 | 17 | 应该算是很清晰吧,一个terminal在初始化的时候调用监听方法设置按键监听器,发生按键事件则将字码转换为字符传入input方法,input方法主要是调用cmdHandler获得回显内容,然后计算所有组件的位置,通过render方法重新渲染整个界面。 18 | 19 | ## 命令及响应的数据设计 20 | 21 | 呐,因为canvas是没有办法换行绘制的,所以必须要在数据结构上将行划分出来,所以数据结构如下: 22 | ```js 23 | { 24 | value:[]//每行的值 25 | line://总行数 26 | } 27 | ``` 28 | 29 | ## 组件位置计算 30 | 31 | 组件位置计算是为渲染函数减少计算量的重要一环。 32 | 计算位置的时候主要需要考虑三种情况:输入回车时、输入删除键时,输入其他字符时。 33 | 34 | - 输入回车时即为命令响应,需要调用cmdHandler获得回显内容,然后将回显内容转换为统一的数据结构存入响应数组中。最后在命令数组中压入一个空命令即可。游标的位置直接置为末行,水平初位置即可 35 | 36 | - 输入删除键的时候情况复杂一些,需要考虑删除操作是否会造成行数减一,如果当前字符是一行的首字符且当前行不是首行,则行数减一,游标上移一行,当前行的内容去掉最后一个字符,同时游标水平位置减去最后一个字符的宽度即可。 37 | 38 | - 输入其他字符的时候需要考虑当前字符是否是最后一个字符,如果是就命令数组最后一个命令的行数加一,最后一行内容加上该字符,游标下移一行,水平置为初位置即可。 39 | 40 | ## 渲染流程设计 41 | 42 | 一开始做的时候我是不停的重绘全部的Terminal内容,但是考虑一下,其实动画就一个光标闪动效果,没有必要搞成这样对吧,于是采用了如下做法: 43 | 44 | ```js 45 | self = this; 46 | 47 | function cursorAnimate(){ 48 | requestAnimationFrame(cursorAnimate) 49 | let last = self.curY0&&i<=this.res.length){ 66 | //遍历命令和响应数组进行渲染 67 | } 68 | 69 | ``` 70 | 71 | 这一点主要是通过记录当前已经绘制的行数,比较命令行能够容纳的最大行数来实现 72 | 73 | - 保证可输入的行永远在命令行窗口内部 74 | 75 | ```js 76 | if(i>0) 77 | while(k{ 9 | let database = new Database(`./${Define.data}`); 10 | //输出除了post之外的所有layout文件 11 | fs.readdir(`./${Define.layout}/`,(err,paths)=>{ 12 | for(let file of paths){ 13 | let info = fs.statSync(`./${Define.layout}/${file}`); 14 | if (!info.isDirectory()&&file!==Define.postTmpl) { 15 | Parser.tmplparse(`./${Define.layout}/${file}`,database.json) 16 | .then((html)=>fs.writeFileSync(`./${Define.build}/${file.split('.')[0]}.html`,html)); 17 | } 18 | } 19 | }); 20 | //输出post文件 21 | fs.readdir(`./${Define.post}/`,(err,paths)=>{ 22 | for(let file of paths){ 23 | let info = fs.statSync(`./${Define.post}/${file}`); 24 | if (!info.isDirectory()) { 25 | fs.readFile(`./${Define.post}/${file}`, (err,data)=>{ 26 | let md = data.toString(); 27 | Parser.mdparse(md).then((html)=>{ 28 | let post = database.getpost({path:`${file.split('.')[0]}.html`}); 29 | if(post){ 30 | post.content = html; 31 | return Parser.tmplparse(`./${Define.layout}/${Define.postTmpl}`,post); 32 | } 33 | }).then((html)=>{ 34 | fs.writeFileSync(`./${Define.build}/${file.split('.')[0]}.html`,html); 35 | }) 36 | }); 37 | } 38 | } 39 | }); 40 | //输出静态文件 41 | (function copy(src=`./${Define.static}`,dst=`./${Define.build}`){ 42 | fs.readdir( src, function( err, paths ) { 43 | if (err) { 44 | throw err; 45 | } 46 | paths.forEach((path)=>{ 47 | let _src = `${src}/${path}`, 48 | _dst = `${dst}/${path}`, 49 | readable, writable; 50 | fs.stat(_src,(err, st)=>{ 51 | if (err) { 52 | throw err; 53 | } 54 | if (st.isFile()) { 55 | readable = fs.createReadStream(_src); 56 | writable = fs.createWriteStream(_dst); 57 | readable.pipe(writable); 58 | } 59 | else if (st.isDirectory()) { 60 | fs.exists( _dst, function( exists ){ 61 | if(!exists) { 62 | fs.mkdir( _dst ); 63 | } 64 | }); 65 | return copy(_src,_dst); 66 | } 67 | }); 68 | }); 69 | }); 70 | })(); 71 | }; 72 | -------------------------------------------------------------------------------- /static/js/about.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 17-4-24. 3 | */ 4 | let centerX = parseFloat($('.head-pic').css('left')); 5 | let centerY = parseFloat($('.head-pic').css('top')); 6 | let rectstr = `
`; 7 | 8 | let keys = [ 9 | 'hit在读学生','热爱图形学的懒癌重症患者','easonzero','小一,男,未婚待业','不服我可以撩我啊','曾经搞点android,现在玩玩前端(雾', 10 | 'qq451114984','技术要简洁,demo要炫酷','Stay foolish?yes,I do','做人要谦逊,做事要漂亮' 11 | ]; 12 | 13 | let tweens = [ 14 | (i,total)=>{return [900+Math.cos(Math.PI*i/total)*380,0]}, 15 | (i,total)=>{return [900+Math.abs(i-total/2+1)*80,0]}, 16 | (i,total)=>{return [(i%2==0)?700:1280,0]}, 17 | (i,total)=>{return [980*((i=i/total-1)*i*i*i*i+1)+520,0]}, 18 | (i,total)=>{ 19 | let s = 1.70158; 20 | if ((i/=total/2) < 1) return [980/2*(i*i*(((s*=(1.525))+1)*i - s)) + 520,0]; 21 | return [980/2*((i-=2)*i*(((s*=(1.525))+1)*i + s) + 2) + 520,0]; 22 | }, 23 | (i,total)=>{ return [-980*((i=i/total-1)*i*i*i-1)+520,0]}, 24 | (i,total)=>{ return [Math.sqrt(460*460-(i-total/2+0.5)*(i-total/2+0.5)*980*980/(total*total))*2,0]}, 25 | (i,total)=>{ return [900+Math.abs(i-total/2+1)*80,(i%2==0)?-50:50]} 26 | ]; 27 | 28 | for(let i=0;i<10;i++){ 29 | let rect = $(rectstr); 30 | let tween = tweens[7](i,10); 31 | rect.addClass(`rect${i+1}`).css('height','40px').css('width',`${tween[0]}px`).css('top',`${i*50}px`).css('transform',`translate(${-50+tween[1]}%)`); 32 | $('header').append(rect); 33 | } 34 | 35 | let fIndex = Math.round(Math.random()*(keys.length-1-3)); 36 | let thetas = Math.random()*Math.PI*2, 37 | rs = Math.random()*100+100; 38 | let thetad = Math.random()*Math.PI*2, 39 | rd = 200+Math.random()*200; 40 | for(let i=0;i<4;i++){ 41 | $(`.des_${i+1}`) 42 | .css('top',`${centerY+Math.sin(thetas)*rs+Math.sin(thetad+i*Math.PI/2)*rd}px`) 43 | .css('left',`${centerX+Math.cos(thetas)*rs+Math.cos(thetad+i*Math.PI/2)*rd}px`); 44 | 45 | $(`.des_${i+1} > div > .des_c_t`).text(keys[fIndex+i]); 46 | } 47 | 48 | setInterval(()=>{ 49 | let index = Math.round(Math.random()*(tweens.length-1)); 50 | for(let i=0;i<10;i++){ 51 | let tween = tweens[index](i,10); 52 | $(`.rect${i+1}`).css('height','40px').css('width',`${tween[0]}px`).css('top',`${i*50}px`).css('transform',`translate(${-50+tween[1]}%)`); 53 | } 54 | },5000); 55 | 56 | setInterval(()=>{ 57 | let index = Math.round(Math.random()*(keys.length-1-3)); 58 | let thetas = Math.random()*Math.PI*2, 59 | rs = Math.random()*100+100; 60 | let thetad = Math.random()*Math.PI*2, 61 | rd = 200+Math.random()*200; 62 | for(let i=0;i<4;i++){ 63 | $(`.des_${i+1}`) 64 | .css('top',`${centerY+Math.sin(thetas)*rs+Math.sin(thetad+i*Math.PI/2)*rd}px`) 65 | .css('left',`${centerX+Math.cos(thetas)*rs+Math.cos(thetad+i*Math.PI/2)*rd}px`); 66 | $(`.des_${i+1} > div > .des_c_t`).text(keys[index+i]); 67 | } 68 | },2000); 69 | -------------------------------------------------------------------------------- /post/geekblog1.md: -------------------------------------------------------------------------------- 1 | 呐,其实在GeekBlog我下力气写的东西就仨,一个是简单的博客生成器,一个是前端的终端,一个是数码流背景。待我一个一个慢慢道来... 2 | 3 | ##博客生成器的结构 4 | 5 | 一个博客生成器在需求上无非就是提供给使用者简单的接口来创建博客,管理文章。 6 | 呐,分析这两个需求,创建一个博客就是根据模板和配置文件生成一堆静态文件,可以想到流程大概是使用者发送一个命令,出发一个命令回调,然后博客生成器去找项目中的配置文件和模板文件,经过解析模块生成了一堆html、js、css数据,然后把这些数据写入build文件夹。 7 | 而管理文章则是通过使用者写的md文档去更新或者生成对于的文章html,当然删除也是可以的。这个流程也大抵是通过一个命令回调,更新项目数据,然后将md文档和对应的文章模板联系起来进行解析,解析完了把数据写入build文件夹 8 | 根据这两个需求,我定的结构是如下的样子 9 | 10 | - cli 11 | - 命令1回调.js//一个命令回调 12 | - 命令2回调.js 13 | - ....js 14 | - index.js//命令入口 15 | - src 16 | - server//调试服务器模块 17 | - databse//数据管理模块 18 | - parser//解析器模块 19 | - config.js//参数文件 20 | 21 | 这个结构呢主要是将核心操作与终端管理解耦,降低代码的复杂程度。这个结构当然是非常简单的,相比与hexo那种插件化的结构显得非常的辣鸡,但是之所以写的这么简单呢,是因为考虑到我的需求是需要一款极简的博客生成器,只需要帮我完成日常的博客管理就好,所以功能相比hexo进行了极大的缩减,在这种情况下做什么插件化结构完全是多此一举。项目起初做架构设计的时候要结合需求,不要盲目的套用设计模式,设计模式是为了降低开发成本的,不要舍本逐末。 22 | 23 | ##Cli 24 | 25 | cli是client的缩写,对于博客生成器而言,命令行的处理就相当于前端:接收事件,转发事件,回显。 26 | 这里必须要感恩node,node是天生可以做命令行应用的,且看: 27 | ```js 28 | #!/usr/bin/env node 29 | console.log(process.argv) 30 | ``` 31 | 只要在我们正常写的node代码前加上一句`#!/usr/bin/env node`,就可以直接被终端执行,在代码中调用`process.argv`接可以获得命令参数了。 32 | 需要注意的是process.argv是一个数组,索引为1的值是命令,后面的索引对应的值就是参数。 33 | 最后,在package.json文件中添加如下语句: 34 | ```js 35 | "bin": { 36 | "geekcli": "cli/index.js" 37 | } 38 | ``` 39 | 然后使用npm link命令,则可以把`geekcli`设置为全局命令,同时把这个命令与`cli/index.js`文件关联起来。 40 | 41 | ##解析器 42 | 43 | 核心部分其实最重要的就是解析器部分了,其他的无非只是一些文件操作而已。 44 | 45 | 解析器的输入是模板文件和博客数据。原理是基于有限状态机遍历模板文件的每一个字符,进入到命令状态就记录命令,并在跳出命令状态的时候执行同步命令,将命令回调返回的数据插入到结果数据中,然后继续执行解析。 46 | 这里使用一个第三方库叫`markdown`,这个库的作用是将md文件转化为html,通过这个库可以把md文件当作直接的数据插入到模板请求md文件的位置。 47 | 48 | ```js 49 | switch (state){ 50 | case 0: 51 | if(ch=='{') state++; 52 | else result+=ch; 53 | break; 54 | case 1: 55 | if(ch=='{') 56 | state++; 57 | else { 58 | result+='{'+ch; 59 | state = 0; 60 | } 61 | break; 62 | case 2: 63 | if(ch=='}') 64 | state++; 65 | else { 66 | cmd+=ch; 67 | } 68 | break; 69 | case 3: 70 | if(ch=='}') { 71 | state = 0; 72 | let content = cmd.replace(/^[\s ]+|[\s ]+/g, ""); 73 | if(content==Define.data){ 74 | result += ``; 75 | }else{ 76 | let indexs = content.split('.'); 77 | let tmp = o; 78 | for(let i=0;i{ 25 | resolve(value); 26 | }) 27 | }, 28 | 'article':function([cmd,id]){ 29 | let value = ''; 30 | if(id){ 31 | if(id>data.posts.length-1) value = `No match with id:${id}!`; 32 | else { 33 | value = 'Waiting...\nFinding...'; 34 | setTimeout(()=>window.location.href=data.posts[id].path,500); 35 | } 36 | }else{ 37 | for(let index in data.posts){ 38 | value += `id '${index}' : title '${data.posts[index].title}'`+'\n'; 39 | } 40 | value+=`type 'article ' to read the article by id!`; 41 | } 42 | return new Promise((resolve)=>{ 43 | resolve(value); 44 | }) 45 | }, 46 | 'tag':function([cmd,id]){ 47 | let value = '',i=1; 48 | if(id){ 49 | if(id>__tag.length) value = `No match with id ${id}!`; 50 | else{ 51 | value += `TAG:${__tag[id-1]}`+'\n'; 52 | for(let index in data.posts){ 53 | for(let tag of data.posts[index].tag){ 54 | if(tag==__tag[id-1]){ 55 | value += `id '${index}' : title '${data.posts[index].title}'`+'\n'; 56 | } 57 | } 58 | } 59 | value+=`type 'article ' to read the article by id!`; 60 | } 61 | }else{ 62 | __tag.length=0; 63 | for(let index in data.posts){ 64 | for(let tag of data.posts[index].tag){ 65 | if(!value.includes(tag)){ 66 | __tag.push(tag); 67 | value += `id '${i++}' : TAG '${tag}'`+'\n'; 68 | } 69 | } 70 | } 71 | value+=`type 'tag ' to look through all article of the tag!`; 72 | } 73 | return new Promise((resolve)=>{ 74 | resolve(value); 75 | }) 76 | }, 77 | 'friendship':function([cmd,id]){ 78 | let value = ''; 79 | if(id){ 80 | if(id>data['friendship-link'].length-1) value = `No match with id:${id}!`; 81 | else { 82 | value = 'Waiting...\nFinding...'; 83 | setTimeout(()=>window.location.href=data['friendship-link'][id].link,500); 84 | } 85 | }else{ 86 | for(let index in data['friendship-link']){ 87 | value += `id '${index}' : ${data['friendship-link'][index].name}|${data['friendship-link'][index].summary}`+'\n'; 88 | } 89 | value+=`type 'friendship ' to visit their blogs!`; 90 | } 91 | return new Promise((resolve)=>{ 92 | resolve(value); 93 | }) 94 | }, 95 | 'hello':function([cmd]){ 96 | let value = ''; 97 | value=hello[Math.round(Math.random()*(hello.length-1))]; 98 | return new Promise((resolve)=>{ 99 | resolve(value); 100 | }) 101 | }, 102 | 'clear':function([cmd]){ 103 | return new Promise((resolve)=>{ 104 | resolve("${clear}"); 105 | }) 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /cli/post.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-7. 3 | */ 4 | "use strict"; 5 | const fs = require('fs'); 6 | const {Parser,Define,Database} = require('../index'); 7 | const readline = require('readline'); 8 | 9 | module.exports = (method,file)=>{ 10 | switch (method){ 11 | case 'create'://创建post 12 | const rl = readline.createInterface({ 13 | input: process.stdin, 14 | output: process.stdout 15 | }); 16 | fs.exists(file,(exist)=>{ 17 | if(exist){ 18 | let path = `${/\/.*\/(.*)\.md$/.exec(file)[1]}.html`; 19 | rl.question('title:',(answer)=>{ 20 | let title = answer; 21 | rl.question('tag:',(answer)=>{ 22 | let json = {title:title,tag:answer.split('|'),path:path}; 23 | let database = new Database(`./${Define.data}`); 24 | database.addpost(json).flush(); 25 | fs.readdir(`./${Define.layout}/`,(err,paths)=>{ 26 | for(let file of paths){ 27 | let info = fs.statSync(`./${Define.layout}/${file}`); 28 | if (!info.isDirectory()&&file!==Define.postTmpl) { 29 | Parser.tmplparse(`./${Define.layout}/${file}`,database.json) 30 | .then((html)=>fs.writeFileSync(`./${Define.build}/${file.split('.')[0]}.html`,html)); 31 | } 32 | } 33 | }); 34 | fs.readFile(file, (err,data)=>{ 35 | let md = data.toString(); 36 | Parser.mdparse(md).then((html)=>{ 37 | json.content = html; 38 | return Parser.tmplparse(`./${Define.layout}/${Define.postTmpl}`,json); 39 | }).then((html)=>{ 40 | fs.writeFileSync(`./${Define.build}/${path}`,html); 41 | }) 42 | }); 43 | rl.close(); 44 | }); 45 | }); 46 | }else{ 47 | console.log('Could not find the file!') 48 | } 49 | }); 50 | break; 51 | case 'update'://更新post 52 | let path = `${/\/.*\/(.*)\.md$/.exec(file)[1]}.html`; 53 | let database = new Database(`./${Define.data}`); 54 | let post = database.getpost({path:path}); 55 | 56 | fs.exists(file,(exist)=>{ 57 | if(exist){ 58 | fs.readFile(file, (err,data)=>{ 59 | let md = data.toString(); 60 | Parser.mdparse(md).then((html)=>{ 61 | post.content = html; 62 | return Parser.tmplparse(`./${Define.layout}/${Define.postTmpl}`,post); 63 | }).then((html)=>{ 64 | fs.writeFileSync(`./${Define.build}/${path}`,html); 65 | }) 66 | }); 67 | }else{ 68 | console.log('Could not find the file!') 69 | } 70 | }); 71 | break; 72 | case 'delete'://删除post 73 | fs.exists(file,(exist)=>{ 74 | if(exist){ 75 | fs.unlinkSync(file); 76 | } 77 | new Database(`./${Define.data}`).rmpost({path:`${/\/.*\/(.*)\.md$/.exec(file)[1]}.html`}).flush(); 78 | }); 79 | break; 80 | default: 81 | console.log('ERROR : post create/update/delete'); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /static/js/terminal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eason on 16-11-4. 3 | */ 4 | 5 | class Terminal{ 6 | constructor(ctx,height,width,user){ 7 | this.height = height; 8 | this.width = width; 9 | 10 | this.offset = 0; 11 | this.lineHeight = 20; 12 | this.lineLimit = Math.floor(height/this.lineHeight); 13 | this.fontSize = 15; 14 | ctx.font = `${this.fontSize}px serif`; 15 | 16 | this.cursor = new Cursor(ctx,'white',this.lineHeight/2,this.lineHeight); 17 | this.prompt = new Prompt(ctx,'blue',this.lineHeight*4,this.lineHeight,user); 18 | 19 | this.curX = this.prompt.width+10; 20 | this.curY = 1; 21 | 22 | this.cmd = []; 23 | this.res = []; 24 | this.ctx = ctx; 25 | 26 | this.cursorOffset = 4; 27 | 28 | this.history = 0; 29 | 30 | this.listen(); 31 | 32 | self = this; 33 | 34 | function cursorAnimate(){ 35 | requestAnimationFrame(cursorAnimate) 36 | let last = self.curY0&&i<=this.res.length){ 62 | let j=0,k=0; 63 | ctx.fillStyle = 'white'; 64 | 65 | if(this.cmd.length===0) { 66 | this.prompt.draw(0,0); 67 | break; 68 | } 69 | if(i>0) 70 | while(k{ 134 | switch(result){ 135 | case '${clear}': 136 | this.curX = this.prompt.width+10; 137 | this.curY = 1; 138 | this.cmd = []; 139 | this.res = []; 140 | this.render(); 141 | break; 142 | default: 143 | result = result.split('\n'); 144 | this.cmd.unshift({value:[''],line:1}); 145 | this.res.unshift({value:result,line:result.length}); 146 | this.curY+=1+this.res[0].line; 147 | this.curX = this.prompt.width+10; 148 | this.render(); 149 | } 150 | }); 151 | } 152 | else if(char=='$${Delete}') { 153 | let cmd = this.cmd[0]; 154 | this.history = 0; 155 | this.search = false; 156 | if(cmd.value[cmd.line-1].length===0&&cmd.line!=1){ 157 | cmd.line--; 158 | let c = cmd.value[cmd.line-1].charAt(cmd.value[cmd.line-1].length-1); 159 | cmd.value[cmd.line-1] = cmd.value[cmd.line-1].substr(0,cmd.value[cmd.line-1].length-1); 160 | this.curY--; 161 | this.curX = this.width; 162 | }else if(cmd.value[cmd.line-1].length!==0){ 163 | let c = cmd.value[cmd.line-1].charAt(cmd.value[cmd.line-1].length-1); 164 | cmd.value[cmd.line-1] = cmd.value[cmd.line-1].substr(0,cmd.value[cmd.line-1].length-1); 165 | this.curX -= this.ctx.measureText(c).width; 166 | } 167 | this.render(); 168 | }else if(char=='$${Up}'){ 169 | let cmd = this.cmd[0]; 170 | if(this.cmd.length===1) return; 171 | if(this.history==this.cmd.length-1) return; 172 | if(cmd.value[0].length==0||(this.history!=0&&!this.search)){ 173 | if(this.history==this.cmd.length-1) return; 174 | this.history++; 175 | this.curY += this.cmd[this.history].line-cmd.line; 176 | this.curX = this.prompt.width+10+this.ctx.measureText(this.cmd[this.history].value[this.cmd[this.history].value.length-1]).width; 177 | 178 | cmd.value.length = 0; 179 | for(let e of this.cmd[this.history].value){ 180 | cmd.value.push(e); 181 | } 182 | cmd.line = this.cmd[this.history].line; 183 | }else{ 184 | if(this.history===0) this.search = cmd.value[0]; 185 | for(let index=this.history+1;index0;index--){ 218 | if(this.cmd[index].value[0].startsWith(this.search)){ 219 | this.history = index; 220 | break; 221 | } 222 | } 223 | 224 | this.curY += this.cmd[this.history].line-cmd.line; 225 | this.curX = this.prompt.width+10+this.ctx.measureText(this.cmd[this.history].value[this.cmd[this.history].value.length-1]).width; 226 | 227 | cmd.value.length = 0; 228 | for(let e of this.cmd[this.history].value){ 229 | cmd.value.push(e); 230 | } 231 | cmd.line = this.cmd[this.history].line; 232 | } 233 | this.render(); 234 | }else if(char=='$${Clear}'){ 235 | this.cmd[0] = {value:[''],line:1}; 236 | this.curX = this.prompt.width+10; 237 | this.render(); 238 | }else if(char=='$${Tab}'){ 239 | for(let name in cmdHandler){ 240 | if(name.startsWith(this.cmd[0].value[0])){ 241 | this.cmd[0].value[0] = name; 242 | this.curX = this.prompt.width+10+this.ctx.measureText(name).width; 243 | this.render(); 244 | break; 245 | } 246 | } 247 | }else{ 248 | this.history = 0; 249 | this.search = false; 250 | let cmd = this.cmd[0]; 251 | let start = this.curX; 252 | if(this.curX>this.width) { 253 | start = 0; 254 | cmd.line++; 255 | this.curY++; 256 | cmd.value[cmd.line-1] = ''; 257 | } 258 | this.curX = start + this.ctx.measureText(char).width; 259 | cmd.value[cmd.line-1] += char; 260 | this.render(); 261 | } 262 | } 263 | 264 | cmdHandler(cmd){ 265 | cmd = cmd.replace(/(^\s*)|(\s*$)/g,''); 266 | cmd = cmd.replace(/[(^\s*)|(\s*$)]+/g,' ').split(/\s/); 267 | if(cmdHandler[cmd[0]]) 268 | return cmdHandler[cmd[0]](cmd); 269 | else 270 | return new Promise((resolve)=>{ 271 | resolve(` 272 | command not found:${cmd}! 273 | Type 'help' for more info! 274 | `); 275 | }) 276 | } 277 | } -------------------------------------------------------------------------------- /static/css/github-markdown.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: octicons-link; 3 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); 4 | } 5 | 6 | .markdown-body { 7 | -ms-text-size-adjust: 100%; 8 | -webkit-text-size-adjust: 100%; 9 | line-height: 1.5; 10 | color: #333; 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 12 | font-size: 16px; 13 | line-height: 1.5; 14 | word-wrap: break-word; 15 | } 16 | 17 | .markdown-body .pl-c { 18 | color: #969896; 19 | } 20 | 21 | .markdown-body .pl-c1, 22 | .markdown-body .pl-s .pl-v { 23 | color: #0086b3; 24 | } 25 | 26 | .markdown-body .pl-e, 27 | .markdown-body .pl-en { 28 | color: #795da3; 29 | } 30 | 31 | .markdown-body .pl-smi, 32 | .markdown-body .pl-s .pl-s1 { 33 | color: #333; 34 | } 35 | 36 | .markdown-body .pl-ent { 37 | color: #63a35c; 38 | } 39 | 40 | .markdown-body .pl-k { 41 | color: #a71d5d; 42 | } 43 | 44 | .markdown-body .pl-s, 45 | .markdown-body .pl-pds, 46 | .markdown-body .pl-s .pl-pse .pl-s1, 47 | .markdown-body .pl-sr, 48 | .markdown-body .pl-sr .pl-cce, 49 | .markdown-body .pl-sr .pl-sre, 50 | .markdown-body .pl-sr .pl-sra { 51 | color: #183691; 52 | } 53 | 54 | .markdown-body .pl-v { 55 | color: #ed6a43; 56 | } 57 | 58 | .markdown-body .pl-id { 59 | color: #b52a1d; 60 | } 61 | 62 | .markdown-body .pl-ii { 63 | color: #f8f8f8; 64 | background-color: #b52a1d; 65 | } 66 | 67 | .markdown-body .pl-sr .pl-cce { 68 | font-weight: bold; 69 | color: #63a35c; 70 | } 71 | 72 | .markdown-body .pl-ml { 73 | color: #693a17; 74 | } 75 | 76 | .markdown-body .pl-mh, 77 | .markdown-body .pl-mh .pl-en, 78 | .markdown-body .pl-ms { 79 | font-weight: bold; 80 | color: #1d3e81; 81 | } 82 | 83 | .markdown-body .pl-mq { 84 | color: #008080; 85 | } 86 | 87 | .markdown-body .pl-mi { 88 | font-style: italic; 89 | color: #333; 90 | } 91 | 92 | .markdown-body .pl-mb { 93 | font-weight: bold; 94 | color: #333; 95 | } 96 | 97 | .markdown-body .pl-md { 98 | color: #bd2c00; 99 | background-color: #ffecec; 100 | } 101 | 102 | .markdown-body .pl-mi1 { 103 | color: #55a532; 104 | background-color: #eaffea; 105 | } 106 | 107 | .markdown-body .pl-mdr { 108 | font-weight: bold; 109 | color: #795da3; 110 | } 111 | 112 | .markdown-body .pl-mo { 113 | color: #1d3e81; 114 | } 115 | 116 | .markdown-body .octicon { 117 | display: inline-block; 118 | vertical-align: text-top; 119 | fill: currentColor; 120 | } 121 | 122 | .markdown-body a { 123 | background-color: transparent; 124 | -webkit-text-decoration-skip: objects; 125 | } 126 | 127 | .markdown-body a:active, 128 | .markdown-body a:hover { 129 | outline-width: 0; 130 | } 131 | 132 | .markdown-body strong { 133 | font-weight: inherit; 134 | } 135 | 136 | .markdown-body strong { 137 | font-weight: bolder; 138 | } 139 | 140 | .markdown-body h1 { 141 | font-size: 2em; 142 | margin: 0.67em 0; 143 | } 144 | 145 | .markdown-body img { 146 | border-style: none; 147 | } 148 | 149 | .markdown-body svg:not(:root) { 150 | overflow: hidden; 151 | } 152 | 153 | .markdown-body code, 154 | .markdown-body kbd, 155 | .markdown-body pre { 156 | font-family: monospace, monospace; 157 | font-size: 1em; 158 | } 159 | 160 | .markdown-body hr { 161 | box-sizing: content-box; 162 | height: 0; 163 | overflow: visible; 164 | } 165 | 166 | .markdown-body input { 167 | font: inherit; 168 | margin: 0; 169 | } 170 | 171 | .markdown-body input { 172 | overflow: visible; 173 | } 174 | 175 | .markdown-body [type="checkbox"] { 176 | box-sizing: border-box; 177 | padding: 0; 178 | } 179 | 180 | .markdown-body * { 181 | box-sizing: border-box; 182 | } 183 | 184 | .markdown-body input { 185 | font-family: inherit; 186 | font-size: inherit; 187 | line-height: inherit; 188 | } 189 | 190 | .markdown-body a { 191 | color: #4078c0; 192 | text-decoration: none; 193 | } 194 | 195 | .markdown-body a:hover, 196 | .markdown-body a:active { 197 | text-decoration: underline; 198 | } 199 | 200 | .markdown-body strong { 201 | font-weight: 600; 202 | } 203 | 204 | .markdown-body hr { 205 | height: 0; 206 | margin: 15px 0; 207 | overflow: hidden; 208 | background: transparent; 209 | border: 0; 210 | border-bottom: 1px solid #ddd; 211 | } 212 | 213 | .markdown-body hr::before { 214 | display: table; 215 | content: ""; 216 | } 217 | 218 | .markdown-body hr::after { 219 | display: table; 220 | clear: both; 221 | content: ""; 222 | } 223 | 224 | .markdown-body table { 225 | border-spacing: 0; 226 | border-collapse: collapse; 227 | } 228 | 229 | .markdown-body td, 230 | .markdown-body th { 231 | padding: 0; 232 | } 233 | 234 | .markdown-body h1, 235 | .markdown-body h2, 236 | .markdown-body h3, 237 | .markdown-body h4, 238 | .markdown-body h5, 239 | .markdown-body h6 { 240 | margin-top: 0; 241 | margin-bottom: 0; 242 | } 243 | 244 | .markdown-body h1 { 245 | font-size: 32px; 246 | font-weight: 600; 247 | } 248 | 249 | .markdown-body h2 { 250 | font-size: 24px; 251 | font-weight: 600; 252 | } 253 | 254 | .markdown-body h3 { 255 | font-size: 20px; 256 | font-weight: 600; 257 | } 258 | 259 | .markdown-body h4 { 260 | font-size: 16px; 261 | font-weight: 600; 262 | } 263 | 264 | .markdown-body h5 { 265 | font-size: 14px; 266 | font-weight: 600; 267 | } 268 | 269 | .markdown-body h6 { 270 | font-size: 12px; 271 | font-weight: 600; 272 | } 273 | 274 | .markdown-body p { 275 | margin-top: 0; 276 | margin-bottom: 10px; 277 | } 278 | 279 | .markdown-body blockquote { 280 | margin: 0; 281 | } 282 | 283 | .markdown-body ul, 284 | .markdown-body ol { 285 | padding-left: 0; 286 | margin-top: 0; 287 | margin-bottom: 0; 288 | } 289 | 290 | .markdown-body ol ol, 291 | .markdown-body ul ol { 292 | list-style-type: lower-roman; 293 | } 294 | 295 | .markdown-body ul ul ol, 296 | .markdown-body ul ol ol, 297 | .markdown-body ol ul ol, 298 | .markdown-body ol ol ol { 299 | list-style-type: lower-alpha; 300 | } 301 | 302 | .markdown-body dd { 303 | margin-left: 0; 304 | } 305 | 306 | .markdown-body code { 307 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 308 | font-size: 12px; 309 | } 310 | 311 | .markdown-body pre { 312 | margin-top: 0; 313 | margin-bottom: 0; 314 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; 315 | } 316 | 317 | .markdown-body .octicon { 318 | vertical-align: text-bottom; 319 | } 320 | 321 | .markdown-body input { 322 | -webkit-font-feature-settings: "liga" 0; 323 | font-feature-settings: "liga" 0; 324 | } 325 | 326 | .markdown-body::before { 327 | display: table; 328 | content: ""; 329 | } 330 | 331 | .markdown-body::after { 332 | display: table; 333 | clear: both; 334 | content: ""; 335 | } 336 | 337 | .markdown-body>*:first-child { 338 | margin-top: 0 !important; 339 | } 340 | 341 | .markdown-body>*:last-child { 342 | margin-bottom: 0 !important; 343 | } 344 | 345 | .markdown-body a:not([href]) { 346 | color: inherit; 347 | text-decoration: none; 348 | } 349 | 350 | .markdown-body .anchor { 351 | float: left; 352 | padding-right: 4px; 353 | margin-left: -20px; 354 | line-height: 1; 355 | } 356 | 357 | .markdown-body .anchor:focus { 358 | outline: none; 359 | } 360 | 361 | .markdown-body p, 362 | .markdown-body blockquote, 363 | .markdown-body ul, 364 | .markdown-body ol, 365 | .markdown-body dl, 366 | .markdown-body table, 367 | .markdown-body pre { 368 | margin-top: 0; 369 | margin-bottom: 16px; 370 | } 371 | 372 | .markdown-body hr { 373 | height: 0.25em; 374 | padding: 0; 375 | margin: 24px 0; 376 | background-color: #e7e7e7; 377 | border: 0; 378 | } 379 | 380 | .markdown-body blockquote { 381 | padding: 0 1em; 382 | color: #777; 383 | border-left: 0.25em solid #ddd; 384 | } 385 | 386 | .markdown-body blockquote>:first-child { 387 | margin-top: 0; 388 | } 389 | 390 | .markdown-body blockquote>:last-child { 391 | margin-bottom: 0; 392 | } 393 | 394 | .markdown-body kbd { 395 | display: inline-block; 396 | padding: 3px 5px; 397 | font-size: 11px; 398 | line-height: 10px; 399 | color: #555; 400 | vertical-align: middle; 401 | background-color: #fcfcfc; 402 | border: solid 1px #ccc; 403 | border-bottom-color: #bbb; 404 | border-radius: 3px; 405 | box-shadow: inset 0 -1px 0 #bbb; 406 | } 407 | 408 | .markdown-body h1, 409 | .markdown-body h2, 410 | .markdown-body h3, 411 | .markdown-body h4, 412 | .markdown-body h5, 413 | .markdown-body h6 { 414 | margin-top: 24px; 415 | margin-bottom: 16px; 416 | font-weight: 600; 417 | line-height: 1.25; 418 | } 419 | 420 | .markdown-body h1 .octicon-link, 421 | .markdown-body h2 .octicon-link, 422 | .markdown-body h3 .octicon-link, 423 | .markdown-body h4 .octicon-link, 424 | .markdown-body h5 .octicon-link, 425 | .markdown-body h6 .octicon-link { 426 | color: #000; 427 | vertical-align: middle; 428 | visibility: hidden; 429 | } 430 | 431 | .markdown-body h1:hover .anchor, 432 | .markdown-body h2:hover .anchor, 433 | .markdown-body h3:hover .anchor, 434 | .markdown-body h4:hover .anchor, 435 | .markdown-body h5:hover .anchor, 436 | .markdown-body h6:hover .anchor { 437 | text-decoration: none; 438 | } 439 | 440 | .markdown-body h1:hover .anchor .octicon-link, 441 | .markdown-body h2:hover .anchor .octicon-link, 442 | .markdown-body h3:hover .anchor .octicon-link, 443 | .markdown-body h4:hover .anchor .octicon-link, 444 | .markdown-body h5:hover .anchor .octicon-link, 445 | .markdown-body h6:hover .anchor .octicon-link { 446 | visibility: visible; 447 | } 448 | 449 | .markdown-body h1 { 450 | padding-bottom: 0.3em; 451 | font-size: 2em; 452 | border-bottom: 1px solid #eee; 453 | } 454 | 455 | .markdown-body h2 { 456 | padding-bottom: 0.3em; 457 | font-size: 1.5em; 458 | border-bottom: 1px solid #eee; 459 | } 460 | 461 | .markdown-body h3 { 462 | font-size: 1.25em; 463 | } 464 | 465 | .markdown-body h4 { 466 | font-size: 1em; 467 | } 468 | 469 | .markdown-body h5 { 470 | font-size: 0.875em; 471 | } 472 | 473 | .markdown-body h6 { 474 | font-size: 0.85em; 475 | color: #777; 476 | } 477 | 478 | .markdown-body ul, 479 | .markdown-body ol { 480 | padding-left: 2em; 481 | } 482 | 483 | .markdown-body ul ul, 484 | .markdown-body ul ol, 485 | .markdown-body ol ol, 486 | .markdown-body ol ul { 487 | margin-top: 0; 488 | margin-bottom: 0; 489 | } 490 | 491 | .markdown-body li>p { 492 | margin-top: 16px; 493 | } 494 | 495 | .markdown-body li+li { 496 | margin-top: 0.25em; 497 | } 498 | 499 | .markdown-body dl { 500 | padding: 0; 501 | } 502 | 503 | .markdown-body dl dt { 504 | padding: 0; 505 | margin-top: 16px; 506 | font-size: 1em; 507 | font-style: italic; 508 | font-weight: bold; 509 | } 510 | 511 | .markdown-body dl dd { 512 | padding: 0 16px; 513 | margin-bottom: 16px; 514 | } 515 | 516 | .markdown-body table { 517 | display: block; 518 | width: 100%; 519 | overflow: auto; 520 | } 521 | 522 | .markdown-body table th { 523 | font-weight: bold; 524 | } 525 | 526 | .markdown-body table th, 527 | .markdown-body table td { 528 | padding: 6px 13px; 529 | border: 1px solid #ddd; 530 | } 531 | 532 | .markdown-body table tr { 533 | background-color: #fff; 534 | border-top: 1px solid #ccc; 535 | } 536 | 537 | .markdown-body table tr:nth-child(2n) { 538 | background-color: #f8f8f8; 539 | } 540 | 541 | .markdown-body img { 542 | max-width: 100%; 543 | box-sizing: content-box; 544 | background-color: #fff; 545 | } 546 | 547 | .markdown-body code { 548 | padding: 0; 549 | padding-top: 0.2em; 550 | padding-bottom: 0.2em; 551 | margin: 0; 552 | font-size: 85%; 553 | background-color: rgba(0,0,0,0.04); 554 | border-radius: 3px; 555 | } 556 | 557 | .markdown-body code::before, 558 | .markdown-body code::after { 559 | letter-spacing: -0.2em; 560 | content: "\00a0"; 561 | } 562 | 563 | .markdown-body pre { 564 | word-wrap: normal; 565 | } 566 | 567 | .markdown-body pre>code { 568 | padding: 0; 569 | margin: 0; 570 | font-size: 100%; 571 | word-break: normal; 572 | white-space: pre; 573 | background: transparent; 574 | border: 0; 575 | } 576 | 577 | .markdown-body .highlight { 578 | margin-bottom: 16px; 579 | } 580 | 581 | .markdown-body .highlight pre { 582 | margin-bottom: 0; 583 | word-break: normal; 584 | } 585 | 586 | .markdown-body .highlight pre, 587 | .markdown-body pre { 588 | padding: 16px; 589 | overflow: auto; 590 | font-size: 85%; 591 | line-height: 1.45; 592 | background-color: #f7f7f7; 593 | border-radius: 3px; 594 | } 595 | 596 | .markdown-body pre code { 597 | display: inline; 598 | max-width: auto; 599 | padding: 0; 600 | margin: 0; 601 | overflow: visible; 602 | line-height: inherit; 603 | word-wrap: normal; 604 | background-color: transparent; 605 | border: 0; 606 | } 607 | 608 | .markdown-body pre code::before, 609 | .markdown-body pre code::after { 610 | content: normal; 611 | } 612 | 613 | .markdown-body .pl-0 { 614 | padding-left: 0 !important; 615 | } 616 | 617 | .markdown-body .pl-1 { 618 | padding-left: 3px !important; 619 | } 620 | 621 | .markdown-body .pl-2 { 622 | padding-left: 6px !important; 623 | } 624 | 625 | .markdown-body .pl-3 { 626 | padding-left: 12px !important; 627 | } 628 | 629 | .markdown-body .pl-4 { 630 | padding-left: 24px !important; 631 | } 632 | 633 | .markdown-body .pl-5 { 634 | padding-left: 36px !important; 635 | } 636 | 637 | .markdown-body .pl-6 { 638 | padding-left: 48px !important; 639 | } 640 | 641 | .markdown-body .full-commit .btn-outline:not(:disabled):hover { 642 | color: #4078c0; 643 | border: 1px solid #4078c0; 644 | } 645 | 646 | .markdown-body kbd { 647 | display: inline-block; 648 | padding: 3px 5px; 649 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; 650 | line-height: 10px; 651 | color: #555; 652 | vertical-align: middle; 653 | background-color: #fcfcfc; 654 | border: solid 1px #ccc; 655 | border-bottom-color: #bbb; 656 | border-radius: 3px; 657 | box-shadow: inset 0 -1px 0 #bbb; 658 | } 659 | 660 | .markdown-body :checked+.radio-label { 661 | position: relative; 662 | z-index: 1; 663 | border-color: #4078c0; 664 | } 665 | 666 | .markdown-body .task-list-item { 667 | list-style-type: none; 668 | } 669 | 670 | .markdown-body .task-list-item+.task-list-item { 671 | margin-top: 3px; 672 | } 673 | 674 | .markdown-body .task-list-item input { 675 | margin: 0 0.2em 0.25em -1.6em; 676 | vertical-align: middle; 677 | } 678 | 679 | .markdown-body hr { 680 | border-bottom-color: #eee; 681 | } 682 | --------------------------------------------------------------------------------