├── .gitignore
├── assets
├── .DS_Store
└── images
│ ├── blog16.png
│ ├── blog32.png
│ ├── blog48.png
│ ├── blog128.png
│ ├── github-logo.svg
│ └── chrome-logo.svg
├── screenShots
├── cache.png
├── select.png
├── 20190420-160829.png
├── 20190420-160909.png
└── 20190420-161014.png
├── .babelrc
├── test
├── playground
│ └── url.js
└── axiosTest.js
├── src
├── components
│ └── TextEditor
│ │ ├── index.css
│ │ └── index.js
├── pages
│ ├── popup.js
│ ├── options.js
│ ├── editor.js
│ └── background.js
├── api
│ ├── mock.js
│ └── index.js
├── views
│ ├── editor
│ │ ├── editor-bar.js
│ │ ├── files.js
│ │ ├── index.js
│ │ └── style.css
│ └── option
│ │ ├── index.jsx
│ │ ├── auth.jsx
│ │ └── form.jsx
└── utils.js
├── manifest.json
├── README.md
├── webpack.config.js
├── package.json
└── note.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | dist
3 | node_modules
4 |
--------------------------------------------------------------------------------
/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/assets/.DS_Store
--------------------------------------------------------------------------------
/screenShots/cache.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/screenShots/cache.png
--------------------------------------------------------------------------------
/assets/images/blog16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/assets/images/blog16.png
--------------------------------------------------------------------------------
/assets/images/blog32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/assets/images/blog32.png
--------------------------------------------------------------------------------
/assets/images/blog48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/assets/images/blog48.png
--------------------------------------------------------------------------------
/screenShots/select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/screenShots/select.png
--------------------------------------------------------------------------------
/assets/images/blog128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/assets/images/blog128.png
--------------------------------------------------------------------------------
/screenShots/20190420-160829.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/screenShots/20190420-160829.png
--------------------------------------------------------------------------------
/screenShots/20190420-160909.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/screenShots/20190420-160909.png
--------------------------------------------------------------------------------
/screenShots/20190420-161014.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangbangde/chrome-github-blog/HEAD/screenShots/20190420-161014.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "plugins": ["@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-runtime"]
4 | }
5 |
--------------------------------------------------------------------------------
/test/playground/url.js:
--------------------------------------------------------------------------------
1 | const url = require('url');
2 | // 10.15.3 版本 node 才有 URL 对象
3 | console.log(url)
4 | // console.log(url.parse('https://developer.chrome.com/extensions/tabs#method-update'))
5 | // console.log(url.parse('callback://oauth-code/test?q=zcq'))
6 |
--------------------------------------------------------------------------------
/src/components/TextEditor/index.css:
--------------------------------------------------------------------------------
1 | #text-editor {
2 | width: 100%; height: 100%; overflow: auto; border: none; background-color: transparent;
3 | color: #b3b3b3; font-family: Georgia,Times New Roman,Times,Songti SC,serif;
4 | padding: 20px 40px 80px; font-size: 18px; font-weight: 400; line-height: 30px;
5 | white-space: pre-wrap; box-sizing: border-box; resize: none;
6 | }
7 | #text-editor:focus {-webkit-appearance: none; outline: none;}
8 |
--------------------------------------------------------------------------------
/src/pages/popup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDom from 'react-dom';
3 | const container = document.createElement('div');
4 | function View(){
5 | const styles = {
6 | info: {
7 | minWidth: '100px',
8 | margin: '24px'
9 | }
10 | }
11 | return (
正在初始化一些数据,请稍等...
)
12 | }
13 | ReactDom.render(, container);
14 | document.body.appendChild(container);
15 |
--------------------------------------------------------------------------------
/src/pages/options.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDom from 'react-dom';
3 | import { SnackbarProvider } from 'notistack';
4 | import Option from '@/views/option';
5 |
6 | window.background = chrome.extension.getBackgroundPage().actions;
7 | const container = document.createElement('div');
8 | const view = (
9 |
10 |
11 |
12 | );
13 | ReactDom.render(view, container);
14 | document.body.appendChild(container);
15 |
--------------------------------------------------------------------------------
/test/axiosTest.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | axios.interceptors.response.use(response => {
4 | console.log('interceptors', response);
5 | return response;
6 | }, error => {
7 | console.log('interceptors', error)
8 | return Promise.reject(error);
9 | })
10 |
11 | axios({
12 | url: 'https://www.baidu.com',
13 | transformResponse: [
14 | function (data) {
15 | console.log('transformResponse', data)
16 | return data;
17 | }
18 | ]
19 | })
20 |
--------------------------------------------------------------------------------
/src/pages/editor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDom from 'react-dom';
3 | import { SnackbarProvider } from "notistack";
4 | import Editor from '@/views/editor/index';
5 |
6 | window.background = chrome.extension.getBackgroundPage().actions;
7 | const container = document.createElement('div');
8 | const view = (
9 |
10 |
11 |
12 | );
13 | ReactDom.render(view, container);
14 | document.body.appendChild(container);
15 |
--------------------------------------------------------------------------------
/src/api/mock.js:
--------------------------------------------------------------------------------
1 | import Url from "url";
2 |
3 | const mock = {
4 | // 'get /authorizations': config => new Promise(resolve => {
5 | // resolve({
6 | // status: 200,
7 | // data: []
8 | // })
9 | // })
10 | }
11 |
12 | class Mock{
13 | constructor(adapter) {
14 | this.adapter = adapter;
15 | }
16 | test(config){
17 | let path = Url.parse(config.url).path;
18 | let key = `${config.method} ${path}`;
19 | if(mock[key]){
20 | return mock[key]();
21 | }else{
22 | return this.adapter(config)
23 | }
24 | }
25 | }
26 | export default Mock;
27 |
--------------------------------------------------------------------------------
/assets/images/github-logo.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BlogHub",
3 | "version": "0.0.1",
4 | "manifest_version": 2,
5 |
6 | "description": "在 chrome extension 中管理你的 GitHub page,打造触手可达的写作环境",
7 | "icons": {
8 | "16": "assets/images/blog16.png",
9 | "32": "assets/images/blog32.png",
10 | "48": "assets/images/blog48.png",
11 | "128": "assets/images/blog128.png"
12 | },
13 |
14 | "background": {
15 | "page": "background.html",
16 | "persistent": false
17 | },
18 | "browser_action": {
19 | "default_popup": "popup.html",
20 | "default_title": "GitHub pages 伴侣"
21 | },
22 | "options_page": "options.html",
23 | "options_ui": {
24 | "page": "options.html",
25 | "open_in_tab": false,
26 | "chrome_style": false
27 | },
28 | "permissions": [
29 | "tabs", "http://*/",
30 | "https://*/",
31 | "webNavigation",
32 | "activeTab",
33 | "declarativeContent",
34 | "storage"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 在 Chrome 中编写博客并推送到 github pages💯
2 |
3 | 正在努力 coding...
4 |
5 | 非常希望有感兴趣的朋友愿意加入和我一起快乐打码
6 |
7 | 技术栈: `chrome extension` `react` `material-ui` `axios` `webpack`
8 |
9 | 另外,喜欢的小伙伴给颗Star🌟吧
10 |
11 | 当前进度:
12 |
13 | - [x] Github 授权
14 | - [x] 仓库、分支等配置项
15 | - [x] 编辑器实时预览
16 | - [x] 浏览 Github 仓库文件
17 | - [x] 实时本地缓存
18 | - [x] 粘贴图片自动上传
19 | - [ ] 完善编辑器功能
20 | - [ ] 开放用户自定义抓取文章脚本
21 | - [ ] 测试
22 | - [ ] 皮肤定制
23 | - [ ] 发布
24 |
25 |
26 | 开发预览:
27 |
28 | 1. `npm install`
29 | 2. `npm build`
30 | 3. chrome 扩展管理中加载 `dist` 目录
31 |
32 | 
33 |
34 | 
35 |
36 | 
37 |
38 | 
39 |
40 | 
41 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const Entry = require('./build/entries');
3 | const CleanWebpackPlugin = require('clean-webpack-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin')
5 |
6 | let entry = Entry.entries();
7 | let htmlPlugins = Entry.htmlPlugins();
8 |
9 | module.exports = {
10 | mode: process.env.NODE_ENV || 'development',
11 | entry,
12 | output: {
13 | filename: 'js/[name].js',
14 | path: path.resolve(__dirname, 'dist')
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.css$/,
20 | use: ["style-loader", "css-loader"]
21 | },
22 | {
23 | test: /\.(js|jsx)$/,
24 | exclude: /(node_modules|bower_components)/,
25 | loader: 'babel-loader'
26 | }
27 | ]
28 | },
29 | devtool: 'inline-source-map',
30 | devServer: {
31 | contentBase: path.resolve(__dirname, 'dist')
32 | },
33 | plugins: [
34 | new CleanWebpackPlugin(),
35 | ...htmlPlugins,
36 | new CopyWebpackPlugin([
37 | 'manifest.json',
38 | {from: 'assets', to: 'assets'}
39 | ],{copyUnmodified: true})// 因为每次都会清理dist,所以要强制复制所有文件
40 | ],
41 | resolve: {
42 | extensions: ['.js', '.jsx'],
43 | alias: {
44 | '@': path.resolve('src'),
45 | '@assets': path.resolve('assets')
46 | }
47 | },
48 | optimization: {
49 | runtimeChunk: 'single',
50 | splitChunks: {
51 | // chunks: "all",
52 | cacheGroups: {
53 | vendors: {
54 | chunks: "all",
55 | test: /[\\/]node_modules[\\/]/,
56 | name: 'vendors',
57 | reuseExistingChunk: true
58 | }
59 | }
60 | }
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chrome-github-blog",
3 | "version": "0.0.1",
4 | "description": "在 Chrome 中编写博客并推送到 github pages。",
5 | "private": true,
6 | "scripts": {
7 | "build": "webpack",
8 | "watch": "webpack --watch",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "keywords": [
12 | "chrome extension",
13 | "blog",
14 | "markdown",
15 | "github pages"
16 | ],
17 | "author": "bangabngde",
18 | "license": "ISC",
19 | "devDependencies": {
20 | "@babel/cli": "^7.1.0",
21 | "@babel/core": "^7.1.0",
22 | "@babel/plugin-proposal-async-generator-functions": "^7.2.0",
23 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
24 | "@babel/plugin-transform-runtime": "^7.4.3",
25 | "@babel/preset-env": "^7.1.0",
26 | "@babel/preset-react": "^7.0.0",
27 | "babel-loader": "^8.0.5",
28 | "clean-webpack-plugin": "^2.0.1",
29 | "copy-webpack-plugin": "^5.0.2",
30 | "css-loader": "^2.1.1",
31 | "html-webpack-plugin": "^3.2.0",
32 | "style-loader": "^0.23.1",
33 | "webpack": "^4.29.6",
34 | "webpack-cli": "^3.3.0",
35 | "webpack-dev-server": "^3.2.1"
36 | },
37 | "dependencies": {
38 | "@babel/runtime": "^7.4.3",
39 | "@material-ui/core": "^3.9.3",
40 | "@material-ui/icons": "^3.0.2",
41 | "axios": "^0.18.0",
42 | "js-base64": "^2.5.1",
43 | "lodash": "^4.17.11",
44 | "marked": "^0.6.2",
45 | "notistack": "^0.6.1",
46 | "rc-tree": "^1.15.2",
47 | "react": "^16.8.4",
48 | "react-dom": "^16.8.4",
49 | "yaml": "^1.5.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/views/editor/editor-bar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withStyles } from '@material-ui/core/styles';
3 | import Avatar from '@material-ui/core/Avatar';
4 |
5 | import CloudUpload from '@material-ui/icons/CloudUploadOutlined';
6 | import FolderOpen from '@material-ui/icons/FolderOpen';
7 | import Laptop from '@material-ui/icons/Laptop';
8 | import Tooltip from "@material-ui/core/Tooltip";
9 | import FiberNew from '@material-ui/icons/FiberNew';
10 |
11 | class EditorBar extends React.Component {
12 | constructor(props){
13 | super(props);
14 | this.state = {};
15 | }
16 |
17 | render() {
18 | const { classes } = this.props;
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | const styles = theme => ({
41 | root: {
42 | backgroundColor: '#2e2e2e',
43 | borderColor: '#2e2e2e',
44 | padding: theme.spacing.unit
45 | },
46 | group: {
47 | display: 'flex',
48 | alignItems: 'center',
49 | },
50 | icon: {
51 | fontSize: 24,
52 | cursor: 'pointer',
53 | color: '#b3b3b3',
54 | margin: '0 16px',
55 | '&:hover': {
56 | color: '#4d4d4d'
57 | }
58 | }
59 | });
60 |
61 | export default withStyles(styles)(EditorBar);
62 |
--------------------------------------------------------------------------------
/src/views/option/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withSnackbar } from 'notistack';
3 | import CssBaseline from '@material-ui/core/CssBaseline';
4 | import Auth from './auth';
5 | import Form from './form';
6 | import CircularProgress from "@material-ui/core/CircularProgress";
7 |
8 | const styles = {
9 | globalProgress: {
10 | position: 'fixed', top: 0, right: 0, bottom: 0, left: 0, backgroundColor: 'rgba(0,0,0,.6)',
11 | display: 'flex', justifyContent: 'center', alignItems: 'center'
12 | },
13 | main: {
14 | backgroundColor: 'white',
15 | minHeight: '230px',
16 | padding: '24px'
17 | }
18 | }
19 |
20 | class Option extends React.Component {
21 | constructor(props){
22 | super(props);
23 | this.state = {
24 | loaded: false,
25 | authShow: false
26 | };
27 | }
28 |
29 | showLoading(){
30 | this.setState({
31 | progress:
32 |
33 |
34 | })
35 | }
36 | hideLoading(){
37 | this.setState({
38 | progress: null
39 | })
40 | }
41 |
42 | componentWillMount() {
43 | this.showLoading();
44 | background.validToken().then(res => {
45 | this.setState({
46 | authShow: !res.success,
47 | loaded: res.success
48 | })
49 | this.hideLoading();
50 | })
51 | }
52 |
53 | toast(message, variant='info'){
54 | this.props.enqueueSnackbar(message, {
55 | variant: variant,
56 | anchorOrigin: {
57 | vertical: 'top',
58 | horizontal: 'center',
59 | }
60 | });
61 | }
62 |
63 | onAuthSubmit(pm){
64 | this.showLoading();
65 | pm.then(() => {
66 | this.setState({authShow: false, loaded: true})
67 | this.hideLoading();
68 | }).catch( msg => {
69 | this.hideLoading();
70 | this.toast(msg, 'warn')
71 | });
72 | }
73 |
74 | onFormSubmit(pm){
75 | this.showLoading();
76 | pm.then(res => {
77 | this.hideLoading();
78 | if(res.success){
79 | this.toast('信息已保存', 'success')
80 | }else{
81 | this.toast(res.message, 'error')
82 | }
83 | }).catch( e => {
84 | this.hideLoading();
85 | this.toast(e.message, 'warn');
86 | });
87 | }
88 |
89 | componentDidMount() {
90 | // this.toast('componentDidMount', 'success');
91 | }
92 |
93 | render(){
94 | return (
95 |
96 |
97 | {this.state.progress}
98 |
99 | {this.state.authShow &&
}
100 | {this.state.loaded &&
}
101 |
102 |
103 |
104 | );
105 | }
106 | }
107 |
108 | export default withSnackbar(Option);
109 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import Mock from './mock';
3 | import { Base64 } from 'js-base64';
4 |
5 | axios.defaults.baseURL = 'https://api.github.com';
6 | axios.defaults.headers.common['Accept'] = 'application/vnd.github.v3+json';
7 | const mock = new Mock(axios.defaults.adapter);
8 | axios.defaults.adapter = config => mock.test(config);
9 | axios.defaults.validateStatus = function (status) {
10 | return status < 500; // Reject only if the status code is greater than or equal to 500
11 | }
12 | axios.interceptors.response.use(res => {
13 | if(res.data && res.data.message){
14 | res.success = false
15 | }else{
16 | res.success = true
17 | }
18 | console.log('AXIOS response:', res.config.url, res.success?'success':'error')
19 | return res;
20 | });
21 | export function setData(key, value) {
22 | axios[key] = value;
23 | if(key == 'token'){
24 | axios.defaults.headers.common['Authorization'] = 'token ' + value;
25 | }
26 | }
27 |
28 | export async function createAuthorization(username, password) {
29 | return axios({
30 | url: 'authorizations',
31 | method: 'post',
32 | auth: {username, password},
33 | data: {
34 | "scopes": ["repo"],
35 | "note": axios.note
36 | },
37 | });
38 | }
39 |
40 | export async function listAuthorizations(username, password) {
41 | return axios({
42 | url: 'authorizations',
43 | method: 'get',
44 | auth: {username, password}
45 | });
46 | }
47 |
48 | export async function deleteAuthorization(username, password, id) {
49 | return axios({
50 | url: 'authorizations/' + id,
51 | method: 'delete',
52 | auth: {username, password}
53 | });
54 | }
55 |
56 | export async function getUserInfo() {
57 | return axios({
58 | url: 'user',
59 | method: 'get'
60 | });
61 | }
62 |
63 | export async function getRepositories() {
64 | return axios({
65 | url: '/user/repos',
66 | method: 'get'
67 | });
68 | }
69 |
70 | export async function getBranches(repo, branch = '') {
71 | return axios({
72 | url: `/repos/${axios.login}/${repo}/branches/${branch}`,
73 | method: 'get'
74 | });
75 | }
76 |
77 | export async function createRepository(name) {
78 | return axios({
79 | url: '/user/repos',
80 | method: 'post',
81 | data:{name}
82 | });
83 | }
84 |
85 | export async function getContents(path = '', isRaw = false) {
86 | return axios({
87 | url: `repos/${axios.login}/${axios.repo}/contents/${path}`,
88 | method: 'get',
89 | headers: {
90 | Accept: isRaw ? 'application/vnd.github.v3.raw+json' : 'application/vnd.github.v3+json'
91 | },
92 | param: {ref: axios.branch}
93 | });
94 | }
95 |
96 | export async function updateContents(path, content, sha) {
97 | return axios({
98 | url: `repos/${axios.login}/${axios.repo}/contents/${path}`,
99 | method: 'put',
100 | data: {
101 | message: axios.message,
102 | content: Base64.encode(content),
103 | branch: axios.branch,
104 | sha
105 | }
106 | });
107 | }
108 |
109 | export async function uploadFiles(path, file) {
110 | return new Promise((resolve, reject) => {
111 | let reader = new FileReader();
112 | reader.readAsBinaryString(file);
113 | reader.onloadend = function(ev) {
114 | if(ev.target.error){
115 | reject(ev.target.error)
116 | }else{
117 | resolve(btoa(ev.target.result))
118 | }
119 | };
120 | }).then(content => {
121 | return axios({
122 | url: `repos/${axios.login}/${axios.repo}/contents/${path}`,
123 | method: 'put',
124 | data: {
125 | message: axios.message,
126 | content,
127 | branch: axios.branch,
128 | }
129 | });
130 | })
131 | }
132 |
133 | export {axios}
134 |
--------------------------------------------------------------------------------
/src/components/TextEditor/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./index.css"
3 | import {uuid, pathJoin} from "@/utils";
4 |
5 | // 功能键
6 | const KEYS = {
7 | META: 'metaKey',
8 | CTRL: 'ctrlKey',
9 | SHIFT: 'shiftKey',
10 | ALT: 'altKey'
11 | };
12 |
13 | /**
14 | * 判断组合键
15 | *
16 | * @param array keys
17 | * @param string key
18 | */
19 | const testCompKey = (keys, key) => {
20 | let fks = new Set(['metaKey', 'ctrlKey', 'shiftKey', 'altKey']);
21 | for (let fk of keys) {
22 | if(typeof fk == 'object'){
23 | let count = 0;
24 | for(let sfk of fk){
25 | if(event[sfk]) count++;
26 | fks.delete(sfk)
27 | }
28 | if(count != 1){
29 | return false;
30 | }
31 | }else{
32 | if(event[fk]){
33 | fks.delete(fk)
34 | }else{
35 | return false;
36 | }
37 | }
38 | }
39 | for(let fk of fks.keys()){
40 | if(event[fk]) return false;
41 | }
42 | return event.key.toLowerCase() == key;
43 | }
44 |
45 | function TextEditor(props) {
46 | let tabSize = props.tabSize || 2;
47 |
48 | function handleKeyDown(ev) {
49 | if(ev.key == 'Tab'){
50 | let selectionStart = ev.target.selectionStart;
51 | let start = ev.target.value.substring(0, selectionStart)
52 | let end = ev.target.value.substr(ev.target.selectionEnd)
53 | ev.target.value = start + Array(tabSize).fill(' ').join('') + end;
54 | ev.target.selectionStart = ev.target.selectionEnd = selectionStart+tabSize;
55 | ev.preventDefault();
56 | return;
57 | }
58 |
59 | // 自定义组合键
60 | if(testCompKey([[KEYS.CTRL, KEYS.META]], 's')){
61 | props.onKeyDown({
62 | type: 'save',
63 | value: event.target.value
64 | })
65 | ev.preventDefault();
66 | return;
67 | }
68 |
69 | // Force save Meta+Shift+s | Ctrl+Shift+s
70 | if(testCompKey([[KEYS.CTRL, KEYS.META], KEYS.SHIFT], 's')){
71 | props.onKeyDown({
72 | type: 'forceSave',
73 | value: event.target.value
74 | })
75 | ev.preventDefault();
76 | return;
77 | }
78 | // Cut line
79 | if(testCompKey([[KEYS.CTRL, KEYS.META]], 'x')){
80 | props.onKeyDown({
81 | type: 'cutLine',
82 | value: ''
83 | })
84 | ev.preventDefault();
85 | return;
86 | }
87 |
88 | }
89 |
90 | function handlePast(ev) {
91 | let file = event.clipboardData.files[0];
92 | if(!file) return;
93 | let name = uuid(8, 62) + file.name;
94 |
95 | // 上传图片并插入编辑器
96 | if(/^image\//.test(file.type)){
97 | let url = 'assets/images'
98 | let path = pathJoin(url, name)
99 | let imgStr = ``;
100 |
101 | let selectionStart = ev.target.selectionStart;
102 | let start = ev.target.value.substring(0, selectionStart)
103 | let end = ev.target.value.substr(ev.target.selectionEnd)
104 | let newStr = `${imgStr}\n` + end;
105 | ev.target.value = start + newStr;
106 | ev.target.selectionStart = ev.target.selectionEnd = selectionStart+newStr.length;
107 |
108 | props.onImageUploadState(path, 0);
109 | props.onChange(ev.target.value);
110 | background.uploadFiles(path, file).then(res => {
111 | if(res.success){
112 | props.onImageUploadState(path, 1);
113 | }else{
114 | props.onImageUploadState(path, 2, res.message);
115 | }
116 | }, err => {
117 | props.onImageUploadState(path, 2, err.message);
118 | })
119 | }
120 | event.preventDefault();
121 | }
122 |
123 | function onChangeListener() {
124 | props.onChange(event.target.value);
125 | }
126 | const view = (
127 |
128 |
136 |
137 | );
138 | return view;
139 | }
140 |
141 | export default TextEditor;
142 |
--------------------------------------------------------------------------------
/assets/images/chrome-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/background.js:
--------------------------------------------------------------------------------
1 | import {promisify, Store} from '../utils';
2 | import * as Api from '@/api';
3 |
4 | !function() {
5 | const data = {
6 | name: undefined,
7 | login: undefined,
8 | avatar: undefined,
9 | token: undefined, // *required
10 | note: 'chrome extension access token',
11 |
12 | repo: undefined, // *required
13 | branch: 'master', // "master" | "gh-pages"
14 | path: '/_posts', // "/" | "/docs/_posts" | "/_posts"
15 | message: 'by chrome extension',
16 | baseUrl: undefined,
17 |
18 | postsIndex: null, // @type array 缓存文章索引
19 |
20 | editorWidthPercent: 0.5,
21 |
22 | validated: false,
23 |
24 | // TODO:
25 | // categories: ['笔记', '随笔', '前端'],
26 | // tags: ['Javascript', 'css'],
27 | };
28 |
29 | async function check(){
30 | // 初始化数据
31 | await initData();
32 | let token = await validToken();
33 | if(!token.success){
34 | return token;
35 | }
36 | return await checkConfig();
37 | }
38 |
39 | /**
40 | * Init data
41 | *
42 | * @returns {Promise}
43 | */
44 | async function initData() {
45 | let apiData = new Set(['token', 'login', 'repo', 'path', 'branch', 'note', 'message']);
46 | Store.change(function (changes) {
47 | for(let key in changes){
48 | data[key] = changes[key].newValue;
49 | if(apiData.has(key)){
50 | Api.setData(key, data[key]);
51 | }
52 | }
53 | });
54 | Object.assign(data, await Store.get());
55 | apiData.forEach(key => {
56 | Api.setData(key, data[key]);
57 | })
58 | await dev();
59 | }
60 |
61 | async function dev(){
62 | let info = await promisify(chrome.management.getSelf)();
63 | if(info.installType == 'development'){
64 | console.log(data);
65 | Store.log();
66 | }
67 | }
68 |
69 | async function validToken(){
70 | if(!data.token) {
71 | return {success: false, message: 'token not set'}
72 | }
73 |
74 | // 通过获取用户信息判断token是否失效
75 | let user = await Api.getUserInfo();
76 | if(!user.success){
77 | await Store.remove('token')
78 | return {success: false, message: user.data.message};
79 | }
80 | return {success: true};
81 | }
82 |
83 | async function checkConfig(){
84 | if(!data.repo){
85 | return {success: false, message: 'repo not set'}
86 | }
87 |
88 | // 通过访问仓库检测配置是否有效
89 | let repo = await Api.getContents();
90 | data.validated = repo.success;
91 | return repo;
92 | }
93 |
94 | async function updateUserInfo(user){
95 | if(user){
96 | await Store.set({
97 | name: user.data.name,
98 | login: user.data.login,
99 | avatar: user.data.avatar_url,
100 | });
101 | user.success = true;
102 | }else{
103 | user = await Api.getUserInfo();
104 | if(user.success){
105 | await Store.set({
106 | name: user.data.name,
107 | login: user.data.login,
108 | avatar: user.data.avatar_url,
109 | });
110 | }
111 | }
112 | return user;
113 | }
114 |
115 | function getContents(path, isRaw) {
116 | return Api.getContents(path, isRaw);
117 | }
118 |
119 | function createContent(path, content, sha) {
120 | return Api.updateContents(path, content, sha);
121 | }
122 |
123 | /**
124 | * 登录
125 | *
126 | * @param args
127 | * @returns {Promise}
128 | */
129 | async function doLogin(...args){
130 | if(args.length == 1){
131 | await Store.set({
132 | token: args[0]
133 | });
134 | return await updateUserInfo();
135 | }else{
136 | // 查询旧的 PAT,若存在则删除
137 | let authorizations = await Api.listAuthorizations(...args);
138 | if(!authorizations.success){
139 | return authorizations;
140 | }
141 | let auth = authorizations.data.find(data => data.note == data.note);
142 | if(auth){
143 | let del = await Api.deleteAuthorization(...args, auth.id);
144 | if(!del){
145 | return del;
146 | }
147 | }
148 | // 创建新的 PAT
149 | auth = await Api.createAuthorization(...args, data.note)
150 | if(!auth.success){
151 | return auth;
152 | }
153 | await Store.set({
154 | token: auth.data.token
155 | });
156 | // 获取用户信息
157 | return await updateUserInfo();
158 | }
159 | }
160 |
161 | /**
162 | * 设置目标仓库
163 | *
164 | * @param repoName
165 | * @returns {Promise}
166 | */
167 | async function setRepo(repo, branch, path){
168 | let valid = await Api.getBranches(repo, branch);
169 | data.validated = valid.success;
170 | if(valid.success){ await Store.set({repo, branch, path}); }
171 | return valid;
172 | }
173 |
174 | function uploadFiles(path, file) {
175 | return Api.uploadFiles(path, file)
176 | }
177 |
178 | const Background = {
179 | doLogin,
180 | setRepo,
181 | getContents,
182 | createContent,
183 | checkConfig,
184 | validToken,
185 | uploadFiles,
186 | save: Store.set,
187 | get: Store.get,
188 | remove: Store.remove,
189 | get postsIndex(){ return data.postsIndex; },
190 | get avatar(){ return data.avatar; },
191 | get login(){ return data.login; },
192 | get repo(){ return data.repo; },
193 | get branch(){ return data.branch; },
194 | get path(){ return data.path; },
195 | get width(){ return data.editorWidthPercent; },
196 | get baseUrl(){ return data.baseUrl || 'https://' + data.repo; }
197 | }
198 |
199 | window.actions = Background;
200 |
201 | chrome.browserAction.onClicked.addListener(() => {
202 | console.log(data.validated)
203 | if(data.validated){
204 | chrome.tabs.create({
205 | url: 'editor.html'
206 | })
207 | }else{
208 | chrome.runtime.openOptionsPage();
209 | }
210 | });
211 |
212 | check().catch(err => {
213 | console.log(err);
214 | }).then(()=>{
215 | chrome.browserAction.setPopup({popup: ''});
216 | })
217 |
218 | chrome.tabs.onActivated.addListener(e=> {
219 | console.log(e.id)
220 | })
221 | }();
222 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * promise 风格转化,仅用于 Chrome extension API
3 | *
4 | * @param {Function} fn 异步 Chrome extension API
5 | * @param {Object} area 上下文对象,针对 chrome.storage 下的 get、set 等方法
6 | * @returns {function(...[*]): Promise}
7 | */
8 | let promisify = (fn, area) => function (...args) {
9 | return new Promise((resolve, reject) => {
10 | let callback = function (...args) {
11 | resolve(...args);
12 | }
13 | fn.apply(area || null, [...args, callback])
14 | });
15 | };
16 |
17 | let Store = {
18 | set: promisify(chrome.storage.sync.set, chrome.storage.sync),
19 | get: promisify(chrome.storage.sync.get, chrome.storage.sync),
20 | remove: promisify(chrome.storage.sync.remove, chrome.storage.sync),
21 | clear: promisify(chrome.storage.sync.clear, chrome.storage.sync),
22 | change: fn => chrome.storage.sync.onChanged.addListener(fn),
23 | log: () => chrome.storage.onChanged.addListener(changes => {
24 | for (let key in changes) {
25 | let storageChange = changes[key];
26 | console.log('Store:log: "%s" changed from "%s" to "%s".', key, storageChange.oldValue, storageChange.newValue);
27 | }
28 | })
29 | };
30 |
31 | /**
32 | * DateFormat(new Date(), "[ '北京时间' z yyyy-MM-dd a hh:mm:ss.SSSS EE]")
33 | * DateFormat("[ '北京时间' z yyyy-MM-dd a hh:mm:ss.SSSS EE]")
34 | * DateFormat()
35 | *
36 | * @param date
37 | * @param pattern
38 | * @returns {string}
39 | * @constructor
40 | */
41 | function DateFormat(date, pattern) {
42 | if(arguments.length == 1 && typeof date == 'string'){
43 | pattern = date;
44 | date = null
45 | }
46 | var _p = pattern || 'yyyy/MM/dd HH:mm:ss'
47 | date = date||new Date()
48 |
49 | var _PLACEGOLDER = '_@@_'
50 |
51 | var AP = ['AM', 'PM']
52 | var WEEKDAY = ['日','一','二','三','四','五','六', '礼拜']
53 |
54 | var textIgnore = []
55 | var _i = 0
56 |
57 | _p = _p.replace(/('.*')/g, function(match){
58 | textIgnore.push(match.substring(1, match.length-1) )
59 | return _PLACEGOLDER
60 | })
61 |
62 | var lengthHandler= function(str, length){
63 | str += ''
64 | var l = length - str.length
65 | if(l > 0) {
66 | return (Math.pow(10, l)+'').substr(1) + str
67 | }else {
68 | return str.substr(0-l, length)
69 | }
70 | }
71 |
72 | var patterns = {}
73 | //Year
74 | patterns['y+'] = function (match, offset, string){
75 | let target = date.getFullYear()
76 | return lengthHandler(target, match.length)
77 | }
78 | //Month in year
79 | patterns['M+'] = function (match, offset, string){
80 | let target = date.getMonth() + 1
81 | return lengthHandler(target, match.length)
82 | }
83 | //Day in month
84 | patterns['d+'] = function (match, offset, string){
85 | let target = date.getDate()
86 | return lengthHandler(target, match.length)
87 | }
88 | //Am/pm marker
89 | patterns['a'] = function (match, offset, string){
90 | let target = date.getHours()
91 | if(target >= 0 && target < 12){
92 | target = AP[0]
93 | }else{
94 | target = AP[1]
95 | }
96 | return target
97 | }
98 |
99 | //Hour in day (0-23)
100 | patterns['H+'] = function (match, offset, string){
101 | let target = date.getHours()
102 | return lengthHandler(target, match.length)
103 | }
104 |
105 | //Hour in am/pm (1-12)
106 | patterns['h+'] = function (match, offset, string){
107 | let target = date.getHours()
108 | target = (target > 12 || target == 0) ? Math.abs(target - 12) : target
109 | return lengthHandler(target, match.length)
110 | }
111 |
112 | //Minute in hour(0-59)
113 | patterns['m+'] = function (match, offset, string){
114 | let target = date.getMinutes()
115 | return lengthHandler(target, match.length)
116 | }
117 |
118 | //Seconds in minute (0-59)
119 | patterns['s+'] = function (match, offset, string){
120 | let target = date.getSeconds()
121 | return lengthHandler(target, match.length)
122 | }
123 |
124 | //Millisecond
125 | patterns['S+'] = function (match, offset, string){
126 | let target = date.getSeconds()
127 | return lengthHandler(target, match.length)
128 | }
129 |
130 | //Day name in week
131 | patterns['E+'] = function (match, offset, string){
132 | let target = date.getDay()
133 | let length = match.length
134 | if(length == 1){
135 | return WEEKDAY[target]
136 | }else if(length == 2){
137 | return (WEEKDAY[7]||'周') + WEEKDAY[target]
138 | }
139 |
140 | }
141 |
142 | //Time zone
143 | patterns['z+'] = function (match, offset, string){
144 | let target = date.getTimezoneOffset()
145 | if(target > 0){
146 | return '-' + (target/60)
147 | }
148 | return '+' + (-target/60)
149 | }
150 |
151 | for(var k in patterns) {
152 | _p = _p.replace(new RegExp(k, 'g'), patterns[k]);
153 | }
154 |
155 | _p = _p.replace(new RegExp(_PLACEGOLDER, 'g'), function(){
156 | return textIgnore[_i++]
157 | })
158 |
159 | return _p
160 | }
161 |
162 | function pathJoin(...paths) {
163 | return paths.map(path => path.replace(/(^\/)|(\/$)/g, ''))
164 | .filter(i => i!='').join('/')
165 | }
166 |
167 | /**
168 | * 生成指定位数&基数的UUID
169 | * @param len
170 | * @param radix (2-62)
171 | * @returns {string}
172 | */
173 | function uuid(len, radix) {
174 | let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
175 | let uuid = [], i;
176 | radix = radix || chars.length;
177 |
178 | if (len) {
179 | for (i = 0; i < len; i++)
180 | uuid[i] = chars[0 | Math.random()*radix];
181 | } else {
182 | // rfc4122, version 4 form
183 | var r;
184 |
185 | // rfc4122 requires these characters
186 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
187 | uuid[14] = '4';
188 |
189 | // Fill in random data. At i==19 set the high bits of clock sequence as
190 | // per rfc4122, sec. 4.1.5
191 | for (i = 0; i < 36; i++) {
192 | if (!uuid[i]) {
193 | r = 0 | Math.random()*16;
194 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
195 | }
196 | }
197 | }
198 | return uuid.join('');
199 | }
200 |
201 |
202 | export {
203 | promisify, Store, DateFormat, pathJoin, uuid
204 | }
205 |
--------------------------------------------------------------------------------
/src/views/option/auth.jsx:
--------------------------------------------------------------------------------
1 | import React, {Fragment} from 'react';
2 | import Button from '@material-ui/core/Button';
3 | import Typography from '@material-ui/core/Typography';
4 | import TextField from '@material-ui/core/TextField';
5 | import IconButton from '@material-ui/core/IconButton';
6 | import Input from '@material-ui/core/Input';
7 | import InputLabel from '@material-ui/core/InputLabel';
8 | import InputAdornment from '@material-ui/core/InputAdornment';
9 | import FormHelperText from '@material-ui/core/FormHelperText';
10 | import FormControl from '@material-ui/core/FormControl';
11 | import Visibility from '@material-ui/icons/Visibility';
12 | import VisibilityOff from '@material-ui/icons/VisibilityOff';
13 | import { withStyles } from '@material-ui/core/styles';
14 | import DialogTitle from "@material-ui/core/DialogTitle";
15 | import DialogContent from "@material-ui/core/DialogContent";
16 | import Files from "@/views/editor/files";
17 | import DialogActions from "@material-ui/core/DialogActions";
18 | import Dialog from "@material-ui/core/Dialog";
19 | import Link from '@material-ui/core/Link';
20 |
21 | const loginType = {
22 | PASSWORD: 'password',
23 | TOKEN: 'token'
24 | }
25 |
26 | class Auth extends React.Component {
27 | constructor(props) {
28 | super(props);
29 | this.state = {
30 | loginType: loginType.PASSWORD,
31 | username: 'chengqifw@gmail.com',
32 | password: 'cbd579923.github',
33 | token: '',
34 | errors: {},
35 | showPassword: false
36 | }
37 | this.background = chrome.extension.getBackgroundPage().actions;
38 | }
39 |
40 | changeType(loginType){
41 | this.setState({loginType})
42 | }
43 |
44 | handleChange(field){
45 | this.setState({[field]: event.target.value})
46 | }
47 |
48 | handleClickShowPassword(){
49 | let showPassword = !this.state.showPassword;
50 | this.setState({showPassword})
51 | }
52 |
53 | validate(params){
54 | for(let key of params){
55 | let val = this.state[key];
56 | if(!val){
57 | this.state.errors[key] = true;
58 | this.setState({errors: this.state.errors})
59 | return false;
60 | }else if(this.state.errors[key] == true){
61 | this.state.errors[key] = false;
62 | this.setState({errors: this.state.errors})
63 | }
64 | }
65 | return true;
66 | }
67 |
68 | submit(){
69 | let args = this.state.loginType == loginType.PASSWORD ? ['username', 'password'] : ['token'];
70 | if(this.validate(args)){
71 | this.props.onSubmit(new Promise((resolve, reject) => {
72 | let params = args.map(key => this.state[key]);
73 | this.background.doLogin(...params).then(res => {
74 | if(res.success){
75 | resolve();
76 | }else{
77 | reject(err.message);
78 | }
79 | }).catch(err => {
80 | reject(err.message);
81 | });
82 | }));
83 | }else{
84 | this.props.onSubmit(Promise.reject());
85 | }
86 | }
87 |
88 | genFieldsPWD(classes){
89 | return ;
126 | }
127 |
128 | genFieldsToken(classes){
129 | return ;
145 | }
146 |
147 | render() {
148 | // 在导出时使用 withStyles 注入的
149 | const { classes } = this.props;
150 | return
151 | {this.state.loginType == loginType.PASSWORD ? this.genFieldsPWD(classes) : this.genFieldsToken(classes)}
152 |
153 |
154 |
155 |
156 | }
157 | }
158 | const styles = theme => ({
159 | fieldset: {
160 | border: 'none', margin: 0, padding: 0
161 | },
162 | field: {marginBottom: theme.spacing.unit},
163 | linkBtn: {cursor: 'pointer'},
164 | actions: {textAlign: 'right', marginTop: 8}
165 | });
166 |
167 | export default withStyles(styles)(Auth);
168 |
--------------------------------------------------------------------------------
/src/views/option/form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from '@material-ui/core/Button';
3 | import Typography from '@material-ui/core/Typography';
4 | import TextField from '@material-ui/core/TextField';
5 | import IconButton from '@material-ui/core/IconButton';
6 | import Input from '@material-ui/core/Input';
7 | import InputLabel from '@material-ui/core/InputLabel';
8 | import InputAdornment from '@material-ui/core/InputAdornment';
9 | import FormHelperText from '@material-ui/core/FormHelperText';
10 | import FormControl from '@material-ui/core/FormControl';
11 | import Visibility from '@material-ui/icons/Visibility';
12 | import VisibilityOff from '@material-ui/icons/VisibilityOff';
13 | import { withStyles } from '@material-ui/core/styles';
14 | import Paper from '@material-ui/core/Paper';
15 |
16 | class Validate {
17 | constructor(props, config){
18 | this.fields = props;
19 | this.conf = Object.assign({
20 | passedKeys: false,
21 | breakFailed: true
22 | },config)
23 | }
24 | valid(){
25 | let keys, data;
26 | if(arguments.length == 0){
27 | throw new TypeError('missing parameter');
28 | }else if(arguments.length == 1){
29 | keys = Object.keys(this.fields)
30 | data = arguments[0]
31 | }else{
32 | keys = arguments[0]
33 | data = arguments[1]
34 | }
35 |
36 | let results = {};
37 | let size = 0;
38 | for(let key of keys){
39 | for(let checker of this.fields[key]){
40 | let method = checker[0];
41 | if(typeof method == 'string'){
42 | method = this[method];
43 | }
44 | let pass = method(data[key]);
45 | if(pass){
46 | if(this.conf.passedKeys) results[key] = undefined;
47 | }else{
48 | results[key] = checker[1];
49 | size++;
50 | break;
51 | }
52 | }
53 | }
54 | Object.defineProperty(results, 'size', {
55 | value: size,
56 |
57 | })
58 | this.handler(results)
59 | return results.size == 0;
60 | }
61 | setHandler(cb){
62 | this.handler = cb
63 | return this;
64 | }
65 | require(data){
66 | return data != null && data !== '';
67 | }
68 | }
69 |
70 | class Config extends React.Component {
71 | constructor(props) {
72 | super(props);
73 | this.state = {
74 | repo: background.repo || background.login + '.github.io',
75 | branch: background.branch,
76 | path: background.path,
77 | error: {},
78 | }
79 | Validate.prototype.branch = (data) => /^\w+$/.test(data);
80 | Validate.prototype.path = (data) => /^\/(\w+\/?)+$/.test(data);
81 | this.validate = new Validate({
82 | repo: [
83 | ['require','请填写 repo 字段'],
84 | ],
85 | branch: [
86 | ['require','请填写 branch 字段'],
87 | ['branch', '请填写合法分支名称']
88 | ],
89 | path: [
90 | ['require','请填写 path 字段'],
91 | ['path', '请填写合法路径']
92 | ]
93 | }).setHandler(error => {
94 | this.setState({error})
95 | })
96 | }
97 |
98 | handleChange(field){
99 | this.setState({[field]: event.target.value})
100 | }
101 | handleFocus(field){
102 | if(this.state.error[field]){
103 | delete this.state.error[field];
104 | this.setState({error: this.state.error});
105 | }
106 | }
107 |
108 | submit(){
109 | let args = ['repo', 'branch', 'path'];
110 | console.log('before submit', this.state)
111 | if(this.validate.valid(this.state)){
112 | this.props.onSubmit(background.setRepo(this.state.repo, this.state.branch, this.state.path));
113 | }
114 | }
115 |
116 |
117 | componentDidUpdate(preProps, preState, snapShot) {
118 | if(preProps.submit != this.props.submit && this.props.submit)
119 | this.submit();
120 | }
121 |
122 | render() {
123 | // 在导出时使用 withStyles 注入的
124 | const { classes } = this.props;
125 |
126 | return (
127 |
128 |
129 | GitHub page 参数设置
130 |
131 |
145 |
159 |
173 |
174 |
175 |
176 |
177 | );
178 | }
179 | }
180 | const styles = theme => ({
181 | root: {
182 | padding: '0 24px 24px'
183 | },
184 | fieldset: {
185 | border: 'none', margin: 0, padding: 0
186 | },
187 | field: {marginBottom: theme.spacing.unit},
188 | linkBtn: {float: 'right'},
189 | actions: {textAlign: 'right', marginTop: 8}
190 | });
191 |
192 | export default withStyles(styles)(Config);
193 |
--------------------------------------------------------------------------------
/note.md:
--------------------------------------------------------------------------------
1 | # webpack 配置
2 | ## 目标:
3 | 1. 源码是按模块分包(html、js、css 等在同一个目录),希望打包后抽出js、css、图片等资源到资源目录。
4 | 2. 使用Sass/Less
5 | 3. 使用 react 构建用户界面
6 | 4. 希望引入 eslint
7 | 5. 要能提取大文件
8 | 6. 有一个开发服务器跑着
9 | 7. 能打出可直接加载的开发包
10 | 8. 能打出可直接发布的生产包
11 |
12 | OK 就是这样。
13 |
14 |
15 |
16 | ## 配置指南
17 | ### 1. 配置入口(entry)
18 |
19 | #### context
20 | 使用 `context` 字段指定入口文件所处的目录的绝对路径。换句话说就是指定一个绝对路径作为基础目录,后面解析入口、loaders 都是相对这个目录进行的。
21 |
22 | 举个栗子:
23 |
24 | project
25 | ```
26 | webpack-demo
27 | |-src
28 | |-index.jsx
29 | ```
30 | config
31 | ```js
32 | // __dirname: /Users/Frank/webpack-demo/
33 | context: __dirname,
34 | entry: {
35 | index: './src/index.jsx'
36 | }
37 | ```
38 | 上面是webpack的默认配置,意思是在当前目录查找 './src/index.jsx'。
39 |
40 | 下面尝试改一下 context
41 | ```js
42 | context: '/Users/Frank/webpack-demo/src',
43 | entry: {
44 | index: './src/index.jsx'
45 | }
46 | ```
47 | 很显然这次 webpack 不会构建成功,错误信息是:`ERROR in Entry module not found: Error: Can't resolve './src/view.js' in '/Users/ylfe/Frank/webpack-demo/src'`
48 |
49 | 不难看出,webpack 是到 context 指定的路径查找 entry。
50 |
51 | #### entry
52 | 在web项目中,入口通常是在 html 中通过 script 标签引入的 js 脚本。
53 | 一般一个html只引入一个 js 脚本,其他资源都在这个js脚本里面通过import、require等引用,这可以充分发挥 webpack 的依赖分析、自动转换、打包等功能的优势。
54 |
55 | `entry` 的值可以是一个字符串、数组、对象或是函数。
56 | #### >字符串
57 | ```js
58 | entry: 'index.jsx'
59 | // 等价于
60 | entry: {
61 | main: 'index.jsx'
62 | }
63 | ```
64 | `chunk` 被命名为 `main`
65 |
66 | 看一下打包结果:
67 | ```js
68 | ...webpackBootstrap
69 |
70 | ({/***/ "./index.jsx":
71 | /*!******************!*\
72 | !*** ./index.jsx ***!
73 | \******************/
74 | /*! no static exports found */
75 | /***/ (function(module, exports) {
76 | eval("\n\n//# sourceURL=webpack:///./index.jsx?");
77 | /***/ })
78 | /******/ });
79 | ```
80 | #### >数组
81 | ```js
82 | entry: ['index.jsx']
83 |
84 | ```
85 | webpack 针对数组中的每一项进行依赖分析、打包,只生成一个 bundle 文件。
86 | `chunk` 被命名为 `main`
87 |
88 | 虽然只有一项,bundle内容也不同于上例:
89 | ```js
90 | ({
91 | /***/ "./index.jsx":
92 | /*!******************!*\
93 | !*** ./index.jsx ***!
94 | \******************/
95 | /*! no static exports found */
96 | /***/ (function(module, exports) {
97 | eval("\n\n//# sourceURL=webpack:///./index.jsx?");
98 | /***/ }),
99 | /***/ 0:
100 | /*!************************!*\
101 | !*** multi ./index.jsx ***!
102 | \************************/
103 | /*! no static exports found */
104 | /***/ (function(module, exports, __webpack_require__) {
105 | eval("module.exports = __webpack_require__(/*! ./index.jsx */\"./index.jsx\");\n\n\n//# sourceURL=webpack:///multi_./index.jsx?");
106 | /***/ })
107 | /******/ });
108 | ```
109 | #### >对象
110 | ```js
111 | entry: {
112 | index: 'index.jsx'
113 | },
114 | ```
115 | 每个键(key)会是 chunk 的名称,值描述了 chunk 的入口文件(在output中通过[name]取得)
116 |
117 | #### >函数
118 | ```js
119 | entry: () => new Promise(resolve => {
120 | setTimeout(()=>{
121 | resolve({
122 | index: './index.jsx'
123 | })
124 | })
125 | })
126 | ```
127 | 通过promise设置 字符串、数组、对象等。
128 |
129 | ### 2. 配置输出(output)
130 | 顶层的 `output` 字段用一系列的配置项来告诉 webpack 将它处理的资源输出到哪里。
131 | 因为 webpack4 把分包的活也揽了过来,所以这个配置就比较复杂了。
132 |
133 | #### filename
134 | ```js
135 | filename: '[name].js' // 默认值,name 是 chunk 的名称,默认为 main
136 | ```
137 | 这个选项指定输出 bundle 的名称,对于单个 入口项目, filename可以使用占位符也可以使用静态名称,
138 | 对于多入口项目则必须使用占位符以生成不同名称的 bundle。否则可以看到这样的错误信息:
139 | ```
140 | ERROR in chunk view [entry]
141 | ./js/bundle.js
142 | Conflict: Multiple chunks emit assets to the same filename ./js/bundle.js (chunks index and view)
143 | ```
144 | 虽然名字叫 filename ,其实也可以指定路径的(相对于path字段设置的路径)。如:`js/[chunkhash].js`
145 |
146 | ps: 常用占位符有
147 | * [name] 模块名称
148 | * [id] 模块标识符
149 | * [hash] 模块标识符的 hash
150 | * [chunkhash] chunk 内容的 hash
151 | * [contenthash] 使用`ExtractTextWebpackPlugin`提取出的资源的 hash
152 |
153 | 可以指定 hash 长度: [hash:16]、[chunkhash:10]、[contenthash:10] (默认20)
154 | 也可以通过字段 'output.hashDigestLength' 全局设置。
155 |
156 | ### module
157 | 指定如何处理项目中不同的模块
158 |
159 | #### noParse: 正则、正则数组、函数、字符串
160 | 指定不处理哪些模块。举个栗子:
161 | ```js
162 | module: {
163 | noParse: /jquery/
164 | }
165 | ```
166 | 其它写法:
167 | ```js
168 | noParse: [/jquery/]
169 | noParse: 'jquery'
170 | noParse: content => /jquery/.test(content)
171 | ```
172 | 这意味着即使在 `.src/index.jsx` 里面 'import' 了 jquery ,webpack 也不会把 jquery 打进 bundle,其实根本就没有去处理 jquery。
173 | > 既然忽略了 jquery,就不要再使用 import 等引用它了。测试发现如果 import 的目标模块被 noParse 匹配,webpack 不会转译这个文件的所有 import 语句。。。
174 |
175 | 忽略一些比较大的没采用模块化的库可以大大提升编译速度。
176 |
177 | ---
178 |
179 | #### rules 规则数组
180 | 这些规则能够修改模块的创建方式,如指定加载相关模块时应用的loader、修改解析器等。
181 |
182 | 一个规则包括三个部分:条件、结果、嵌套规则。
183 | * 条件:用于判断是否对资源使用此规则
184 | * 结果:输出要对资源应用的 loaders、解析选项。
185 | * 嵌套规则:嵌套属性写在 `rules` 和 `onOf` 下
186 |
187 | 资源有两个相关值供执行条件匹配测试:
188 |
189 | 1. resource: 被请求资源的模块文件的绝对路径。
190 | 2. issuer: 请求当前资源的模块(`import` 语句所在模块文件)的绝对路径。
191 |
192 | * 条件设置字段(匹配目标是上文的 `resource` )
193 | * test: 正则表达式|正则表达式数组,匹配特定条件
194 | * include: 字符串|字符串数组,匹配特定条件
195 | * exclude: 字符串|字符串数组,排除特定条件
196 | * and: 条件数组,必须匹配数组中的所有条件
197 | * or: 条件数组,匹配数组中任何一个条件
198 | * not: 条件数组,必须不匹配数组中的所有条件
199 | > 条件可以是这些之一:
200 | >* 字符串:输入必须以提供的字符串开始
201 | >* 正则表达式: test用
202 | >* 函数: 返回一个真值
203 | >* 条件数组: 至少一个条件匹配
204 | >* 对象: 每个属性定义的条件都要匹配
205 |
206 | * `Rule.issuer`(匹配目标是上文的 `resource` ): 类似 'Rule.test',只不过issuer的匹配对象是issuer
207 | * `Rule.test` : `Rule.resource.test` 的简写,提供此选项就不能再提供 `Rule.resource`
208 | * `Rule.exclude` : `Rule.resource.exclude` 的简写,提供此选项就不能再提供 `Rule.resource`
209 | * `Rule.include` : `Rule.resource.include` 的简写,提供此选项就不能再提供 `Rule.resource`
210 |
211 | * `Rule.use` : 字符串、字符串数组、对象数组、函数
212 | 字符串/数组
213 | ```js
214 | module: {
215 | rules: [
216 | //这俩是 use:[{loader: 'css-loader'}] 的简写形式
217 | {test: /\.css$/, use: ['css-loader']}
218 | {test: /\.css$/, use: 'css-loader'}
219 | ]
220 | }
221 | ```
222 | 对象数组
223 | ```js
224 | module: {
225 | rules: [
226 | {
227 | test: /\.css$/,
228 | use: {
229 | loader: String, // 必须
230 | options: {}, // 可选,传递给 loader 的参数
231 | query: {} // 废弃,使用 `options` 替代
232 | }
233 | }
234 | ]
235 | }
236 | ```
237 | 函数写法
238 | ```js
239 | /**
240 | info: {
241 | compiler: 当前webpack的compiler,
242 | issuer: 发起当前引用的模块的路径,
243 | realResource: 当前正在加载的模块的路径
244 | resource: 一般和 realResource 一致,除非在引用语句中使用 `!=!` 指定了资源名称?
245 | }
246 | */
247 | use: info => [
248 | {loader: 'style-loader'}, {loader: 'css-loader'}
249 | ]
250 | ```
251 |
252 | \# `options.ident`
253 |
254 | webpack 根据模块的资源、loaders 和 loaders 的 配置项(options)来为模块生成一个全局唯一的**模块标识符**。webpack并不能保证这个标识符百分百全局唯一,所以你也可以使用 `options.ident` 字段指定一个唯一标识符。
255 | > options 对象是经过 `JSON.stringifi()` 字符化后参与生成标识符的。所以存在这种情况,你将两个相同的 loader 应用了不同的配置(options),然后将这两个loader应用于某个资源。此时如果两个 options 经过 `JSON.stringifi()` 生成的字符串是相同的,那么 webpack 为此模块生成的 标识符(module identifier)就不能保证唯一了。
256 |
257 | * `Rule.rules` : 嵌套规则数组,在当前规则匹配时使用
258 | * `Rule.onOf` : 嵌套规则数组,在当前规则匹配时使用, 但是只应用匹配到的第一个规则
259 | * `Rule.resource` : 配置匹配条件,一般都用上述的简单方法
260 | * `Rule.resourceQuery` : 当前规则条件匹配成功是再进一步匹配参数。🌰
261 | ```js
262 | import Foo from './foo.css?inline'
263 | // 匹配成功,使用url-loader 处理 foo.css
264 | rules: [
265 | {
266 | test: /.css$/,
267 | resourceQuery: /inline/,
268 | use: 'url-loader'
269 | }
270 | ]
271 | ```
272 | * `Rule.type` : 'javascript/auto' | 'javascript/dynamic' | 'javascript/esm' | 'json' | 'webassembly/experimental'
273 | 设置匹配到的模块的类型,能够阻止 webpack 默认的规则和加载行为。比如json文件的加载,如果我们需要使用自定义的 json 加载器,需要如下设置:
274 | ```js
275 | {
276 | test: /.json$/,
277 | type: 'javascript/auto',
278 | loader: 'custom-json-loader'
279 | }
280 | ```
281 | * `Rule.loader` : `Rule.use: [{ loader }]` 的简写, 字符串、字符串数组
282 | * `Rule.loaders` : `Rule.use` 的别名,已废弃
283 | * `Rule.parse` : 配置解析器
284 | ```js
285 | parser: {
286 | amd: false, // disable AMD
287 | commonjs: false, // disable CommonJS
288 | system: false, // disable SystemJS
289 | harmony: false, // disable ES2015 Harmony import/export
290 | requireInclude: false, // disable require.include
291 | requireEnsure: false, // disable require.ensure
292 | requireContext: false, // disable require.context
293 | browserify: false, // disable special handling of Browserify bundles
294 | requireJs: false, // disable requirejs.*
295 | node: false, // disable dirname, filename, module, require.extensions, require.main, etc.
296 | node: {...} // reconfigure node layer on module level
297 | }
298 | ```
299 | * `Rule.enforce` : 指定 loader 的种类。 "pre" | "post" ,没有值代表 normal loader。
300 | ```js
301 | // 内联 loaders 和 ! 前缀都是不推荐使用的。
302 | // 禁用 rules 中配置的 normal loaders
303 | import { a } from '!./file1.js';
304 |
305 | // 禁用 rules 中配置的 normal loaders 和 preloaders
306 | import { b } from '-!./file2.js';
307 |
308 | // 禁用 rules 中配置的所有 loaders
309 | import { c } from '!!./file3.js';
310 | ```
311 |
312 | 还有一种loader:内联loader:`import Styles from 'style-loader!css-loader?modules!./styles.css';`
313 | > 使用`!`将资源中的 loader 分开;使用`!`为整个规则添加前缀可以覆盖配置中的所有loader定义。
314 |
315 | ### sourcemap
316 | ```js
317 | devtool: 'inline-source-map'
318 | ```
319 | ### 文件监听
320 | ```js
321 | npx webpack --watch
322 | // 或加入npm script
323 | {
324 | "watch": "webpack --watch"
325 | }
326 | ```
327 |
328 | ### webpack-dev-server
329 | 安装
330 | ```
331 | npm install -D webpack-dev-server
332 | ```
333 | 配置
334 | ```js
335 | devServer: {
336 | contentBase: './dist'
337 | }
338 | ```
339 | 启动脚本
340 | ```json
341 | "start": "webpack-dev-server --open"
342 | ```
343 | > devServer 编译结束后会将 bundle 保持在内存中,不会写到任何输出文件。
344 | > 更改任何源文件并保存,devServer会自动编译后重新加载。
345 | > ps. 如果 contentBase 目录下没有index.html,打开浏览器会看到目录结构。经过观察发现这些都是本地文件,不是内存中的数据。内存中的 bundle 目录是访问不了的。
346 |
--------------------------------------------------------------------------------
/src/views/editor/files.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withStyles } from '@material-ui/core/styles';
3 | import Button from "@material-ui/core/Button";
4 | import TextField from "@material-ui/core/TextField";
5 | import LinearProgress from '@material-ui/core/LinearProgress';
6 |
7 | import SnackbarContent from '@material-ui/core/SnackbarContent';
8 | import ErrorIcon from '@material-ui/icons/Error';
9 | import Refresh from '@material-ui/icons/Refresh';
10 | import IconButton from "@material-ui/core/IconButton";
11 | import DialogTitle from "@material-ui/core/DialogTitle";
12 | import DialogContent from "@material-ui/core/DialogContent";
13 | import DialogActions from "@material-ui/core/DialogActions";
14 | import Dialog from "@material-ui/core/Dialog";
15 | import {DateFormat, pathJoin} from "@/utils.js";
16 |
17 | class Files extends React.Component {
18 | constructor(props){
19 | super(props);
20 | let root = background.repo;
21 | let paths = props.path.split('/')
22 | paths.pop();
23 | this.state = {
24 | root,
25 | path: paths.join('/'),
26 | newFileName: DateFormat('yyyy-MM-dd-'),
27 | netErr: '',
28 | paths: paths
29 | };
30 | this.loading = false;
31 | this.getContents = (path, isRaw) => {
32 | this.loading = true;
33 | return background.getContents(path, isRaw).then(res => {
34 | this.loading = false;
35 | if(!path){
36 | this.setState({
37 | [this.state.root]: res.data,
38 | netErr: ''
39 | })
40 | }else{
41 | this.setState({
42 | [path]: res.data,
43 | netErr: ''
44 | })
45 | }
46 | }).catch(e => {
47 | this.loading = false;
48 | this.setState({
49 | netErr: e.message
50 | })
51 | })
52 | };
53 | }
54 |
55 | handleChange(){
56 | this.setState({newFileName: event.target.value})
57 | }
58 |
59 | onBtnCreateClickListener(){
60 | let {newFileName, path} = this.state;
61 | if(newFileName){
62 | if(!/^\d{4}-\d{2}-\d{2}/.test(newFileName) && path.substr(-6) == '_posts'){
63 | this.props.toast('文件名称必须是 yyyy-MM-dd-name 的格式', 'warning')
64 | return;
65 | }
66 | }else{
67 | newFileName = DateFormat("yyyy-MM-dd-HHmmss")
68 | }
69 | console.log(path, newFileName, pathJoin(path, newFileName))
70 | this.props.onCreate(pathJoin(path, newFileName+'.md'))
71 | }
72 | onFileSelectedListener(node){
73 | if(node && node.type == 'file'){
74 | if(node.name.substr(-2).toLowerCase() != 'md'){
75 | this.props.toast('我只编辑 Markdown 文档!', 'info')
76 | return;
77 | }
78 | this.props.onSelect(node)
79 | }else{
80 | this.setState({
81 | path: node.path,
82 | paths: node.path.split('/')
83 | })
84 | }
85 | }
86 | onLinkClickListener(index){
87 | let paths = this.state.paths.slice(0, index+1);
88 | if(!this.state.loading){
89 | this.setState({
90 | path: paths.join('/'),
91 | paths
92 | })
93 | }
94 | }
95 | onBtnCancelClickListener(){
96 | this.props.onCancel();
97 | }
98 | onBtnRefreshClickListener(){
99 | this.setState({
100 | netErr: ''
101 | })
102 | }
103 |
104 | render() {
105 | const { classes, path } = this.props;
106 | let iconDirectory = ;
107 | let iconFile = ;
108 |
109 | const genPath = () => this.state.paths.map((path, index, {length}) => (
110 |
115 | {path}/
116 |
117 | ));
118 |
119 | const genNodes = () => {
120 | if(this.state.netErr){
121 | return
122 |
126 |
127 | {this.state.netErr}
128 |
129 | }
130 | action={[
131 |
138 |
139 | ,
140 | ]}
141 | />
142 |
143 |
144 | }
145 | let path = this.state.path;
146 | let cache = this.state[path || this.state.root];
147 | if(cache){
148 | return cache.map(node => (
149 |
153 | {node.type == 'file' ? iconFile : iconDirectory}
154 | {node.name}
158 |
159 | ))
160 | }else{
161 | if(!this.loading){
162 | this.getContents(path)
163 | }
164 | return
165 | }
166 | }
167 |
168 | return (
169 |
209 | );
210 | }
211 | }
212 |
213 | const styles = theme => ({
214 | root: {
215 | minHeight: '40vh'
216 | },
217 | btnNew: { float: 'right', marginRight: 8},
218 | icon: {
219 | fill: 'rgba(3,47,98,.5)'
220 | },
221 | directories: {
222 | fontSize: '1.2em',
223 | listStyle: 'none',
224 | paddingBlockStart: '0',
225 | paddingInlineStart: '0',
226 | '&>li': {
227 | padding: 4
228 | }
229 | },
230 | fileName: {
231 | marginLeft: '8px',
232 | color: '#0366d6',
233 | cursor: 'pointer',
234 | '&:hover': {
235 | textDecoration: 'underline'
236 | }
237 | },
238 | path: {
239 | lineHeight: '32px',
240 | color: '#0366d6',
241 | cursor: 'pointer',
242 | '&:hover': {
243 | textDecoration: 'underline'
244 | }
245 | },
246 | errWrap: {
247 | display: 'flex',
248 | justifyContent: 'center',
249 | paddingTop: '160px'
250 | },
251 | error: {
252 | backgroundColor: theme.palette.error.dark,
253 | position: 'relative',
254 | margin: '0 auto'
255 |
256 | },
257 | message: {
258 | display: 'flex',
259 | alignItems: 'center',
260 | },
261 | iconErr: {
262 | fontSize: 20,
263 | opacity: 0.9,
264 | marginRight: theme.spacing.unit,
265 | },
266 | inputName: {
267 | width: '180px',
268 | marginLeft: '4px'
269 | }
270 | });
271 |
272 | export default withStyles(styles)(Files);
273 |
--------------------------------------------------------------------------------
/src/views/editor/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {withStyles} from '@material-ui/core/styles';
3 | import EditorBar from './editor-bar';
4 | import Files from './files';
5 | import TextEditor from "@/components/TextEditor/index";
6 | import Marked from "marked";
7 | import CssBaseline from '@material-ui/core/CssBaseline';
8 | import {DateFormat, pathJoin} from "@/utils";
9 | import CircularProgress from "@material-ui/core/CircularProgress";
10 | import {withSnackbar} from 'notistack';
11 | import Typography from "@material-ui/core/Typography";
12 | import Divider from "@material-ui/core/Divider";
13 | import List from "@material-ui/core/List";
14 | import ListItem from "@material-ui/core/ListItem";
15 | import ListItemIcon from "@material-ui/core/ListItemIcon";
16 | import ListItemText from "@material-ui/core/ListItemText";
17 | import Drawer from "@material-ui/core/Drawer";
18 | import './style.css'
19 | import Button from "@material-ui/core/Button";
20 | import Dialog from "@material-ui/core/Dialog";
21 | import DialogTitle from "@material-ui/core/DialogTitle";
22 | import DialogContent from "@material-ui/core/DialogContent";
23 | import DialogContentText from "@material-ui/core/DialogContentText";
24 | import DialogActions from "@material-ui/core/DialogActions";
25 | import Delete from '@material-ui/icons/Delete';
26 |
27 | function setListener() {
28 | let handleMouseMove = ev => {
29 | this.setState({editorWidth: ev.pageX});
30 | }
31 |
32 | window.onresize = ev => {
33 | if(this.state.editorWidthPercent){
34 | this.setState({
35 | editorWidth: document.body.offsetWidth * this.state.editorWidthPercent
36 | });
37 | }
38 | }
39 |
40 | document.body.addEventListener('mousedown', ev => {
41 | if(ev.target.id === 'resizable'){
42 | document.body.addEventListener('mousemove', handleMouseMove);
43 | }
44 | })
45 |
46 | document.body.addEventListener('mouseup', ev => {
47 | if(ev.target.id === 'resizable'){
48 | document.body.removeEventListener('mousemove', handleMouseMove);
49 | let percent = ev.pageX/document.body.offsetWidth;
50 | this.setState({
51 | editorWidth: ev.pageX,
52 | editorWidthPercent: percent
53 | });
54 | background.save({
55 | editorWidthPercent: percent
56 | })
57 | }
58 | })
59 | }
60 |
61 | class MDEditor extends React.Component {
62 | constructor(props){
63 | super(props);
64 | this.state = {
65 | showFiles: false,
66 | showDrawer: false,
67 | showAlert: false,
68 |
69 | editorWidth: document.body.offsetWidth * background.width,
70 | editorWidthPercent: background.width,
71 |
72 | source: undefined,
73 | result: undefined,
74 | // 当前打开文档相关信息
75 | path: undefined,
76 | sha: undefined,
77 | index: -1,
78 |
79 | postsIndex: background.postsIndex || [],
80 | };
81 | // 节流使用的变量
82 | this.saveInterval = 10000;
83 | this.lastSaveTime = 0;
84 | /**
85 | * @type { path: string,sha:string,lastModified:string }
86 | */
87 | this.post = null; // 指向 postsIndexList 成员
88 |
89 | const imgErrs = {};
90 | /**
91 | *
92 | * @param path 图片相对路径
93 | * @param state 上传状态:0 正在上传 1 上传成功 2 上传失败
94 | * @param errMsg
95 | */
96 | this.handleImageUpload = function (path, state, errMsg) {
97 | console.log(path, state, errMsg)
98 | switch (state) {
99 | case 0: imgErrs[path] = true; break;
100 | case 1: delete imgErrs[path];break;
101 | case 2: imgErrs[path] = errMsg;break;
102 | }
103 | if(state){
104 | this.parse(this.state.source);
105 | }
106 | }
107 | this.renderer = new Marked.Renderer();
108 | this.renderer.image = function(href, title, text){
109 | if (href === null) {
110 | return text;
111 | }
112 | if(imgErrs[href]){
113 | if(imgErrs[href] === true){
114 | return `${text}上传中...`
115 | }else{
116 | return `${text}上传失败, 请重试`
117 | }
118 | }
119 | if(!href.startsWith('http')){
120 | href = pathJoin(this.options.baseUrl, href)
121 | }
122 | var out = '
' : '>';
127 | return out;
128 | }
129 | Marked.setOptions({
130 | baseUrl: background.baseUrl
131 | })
132 | }
133 |
134 | parse(data) {
135 | // TODO:// 检查正则表达式,运行就卡死
136 | // let source = data.replace(/^---((\s+.*)*)\s---/, (match, p1) => {
137 | // info = p1;
138 | // return '';
139 | // })
140 | let index = data.indexOf('---', 3);
141 | if(index){
142 | return Marked(data.substr(index+3), {renderer: this.renderer})
143 | }
144 | return Marked(data, {renderer: this.renderer})
145 | }
146 |
147 | componentWillMount() {
148 | setListener.bind(this)();
149 | this.loadData();
150 | console.log('show time', this.state)
151 | }
152 |
153 | loadData(data){
154 | let date = DateFormat('yyyy-MM-dd')
155 | let defMeta = `---\nlayout: post\ntitle: ${date}\nsubtitle:\ndate: ${date}\ntags: ['note']\n---\n`;
156 | if(!data){ // 默认新文章
157 | data = {
158 | source: defMeta,
159 | path: pathJoin(background.path, DateFormat("yyyy-MM-dd-HHmmss.'md'"))
160 | }
161 | }else if(!data.source){ // 新建文章
162 | data.source = defMeta;
163 | }
164 | data.result = this.parse(data.source);
165 | this.setState(data);
166 | }
167 | onCreateFile(path){
168 | this.setState({showFiles: false})
169 | this.loadData({path});
170 | }
171 | onFileSelected(file){
172 | this.setState({showFiles: false});
173 | if(this.post && this.post.path == file.path) return;
174 | let index = this.state.postsIndex.findIndex(item => item.path == file.path)
175 | if(index != -1){
176 | this.post = this.state.postsIndex[index];
177 | this.toast('为你打开的是该文件的本地缓存,请先提交或删除此缓存', 'info');
178 | background.get(this.post.path).then(res => {
179 | this.loadData({
180 | path: this.post.path,
181 | sha: this.post.sha,
182 | source: res[this.post.path],
183 | postIndex: index
184 | })
185 | });
186 | }else{
187 | this.showLoading()
188 | background.getContents(file.path, true).then(res => {
189 | let source = null;
190 | if(typeof res.data == 'object'){
191 | source = atob(res.data.content);
192 | }else{
193 | source = res.data;
194 | }
195 | this.loadData({source, path: file.path, sha: file.sha})
196 | this.hideLoading();
197 | }).catch(e => {
198 | this.hideLoading()
199 | this.toast(e.message, 'error')
200 | })
201 | }
202 | }
203 | onOpenLocalFile(index){
204 | if(this.state.index == index) return;
205 | this.post = this.state.postsIndex[index];
206 | background.get(this.post.path).then(res => {
207 | this.loadData({
208 | path: this.post.path,
209 | sha: this.post.sha,
210 | source: res[this.post.path],
211 | index: index
212 | })
213 | });
214 | }
215 | handleUpdate(value){
216 | this.setState({source: value, result: this.parse(value)})
217 | this.cache(value)
218 | }
219 | handleKeyDown(ev){
220 | switch (ev.type) {
221 | case 'save':
222 | this.saveData()
223 | break
224 | }
225 | }
226 | cache(){
227 | if(Date.now() - this.lastSaveTime > this.saveInterval){
228 | this.lastSaveTime = Date.now();
229 | this.saveData();
230 | }
231 | }
232 | saveData(){
233 | background.save({
234 | [this.state.path]: this.state.source
235 | }).then(res => {
236 | let {path, sha, postsIndex} = this.state;
237 | if(this.post){
238 | this.post.lastModified = DateFormat();
239 | }else{
240 | this.post = postsIndex.find(item => item.path == path);
241 | if(this.post){
242 | this.post.lastModified = DateFormat();
243 | }else{
244 | this.post = {path,sha,lastModified: DateFormat()};
245 | postsIndex.unshift(this.post)
246 | }
247 | }
248 | this.setState({postsIndex});
249 | background.save({postsIndex});
250 | })
251 | }
252 | onFileCanceled(){
253 | this.setState({showFiles: false})
254 | }
255 | showDrawer(){
256 | this.setState({showDrawer: !this.state.showDrawer});
257 | }
258 | open(){
259 | this.setState({
260 | showFiles: true
261 | })
262 | }
263 | upload(){
264 | this.setState({
265 | showAlert: true
266 | })
267 | }
268 | handleClose(field){
269 | this.setState({
270 | [field]: false
271 | })
272 | }
273 | deletePostCache(index){
274 | if(index == this.state.index){
275 | this.loadData()
276 | }
277 | let path = this.state.postsIndex.splice(index, 1)[0].path;
278 | this.setState({postsIndex: this.state.postsIndex})
279 | background.save({
280 | postsIndex: this.state.postsIndex
281 | })
282 | background.remove(path);
283 | event.preventDefault();
284 | }
285 | uploadConfirm(){
286 | this.setState({
287 | showAlert: false
288 | })
289 | this.showLoading();
290 |
291 | background.createContent(this.state.path, this.state.source, this.state.sha).then(res => {
292 | this.toast('保存成功', 'success');
293 | this.setState({
294 | sha: res.data.content.sha
295 | })
296 |
297 | this.state.postsIndex.splice(this.state.index, 1);
298 | this.setState({postsIndex: this.state.postsIndex})
299 | background.save({postsIndex: this.state.postsIndex})
300 | background.remove(this.state.path);
301 | this.loadData();
302 | }).catch(err => {
303 | this.toast(err.message, 'error');
304 | }).then(()=>{
305 | this.hideLoading();
306 | })
307 | }
308 | showLoading(){
309 | this.setState({
310 | progress:
311 |
312 |
313 | })
314 | }
315 | hideLoading(){
316 | this.setState({
317 | progress: null
318 | })
319 | }
320 | toast(message, variant){
321 | // variant could be success, error, warning or info
322 | this.props.enqueueSnackbar(message, { variant });
323 | };
324 | render() {
325 | const {classes} = this.props;
326 | const {progress, showFiles, showDrawer, editorWidth, source, result, postsIndex} = this.state;
327 | return (
328 |
329 |
330 |
331 | {progress}
332 | {showFiles &&
}
339 |
347 |
348 |
349 | 这里是存储在扩展中还没有同步到Github的文章
350 |
351 |
352 |
353 | {postsIndex.map((posts, index) => (
354 |
357 | {}
358 |
361 |
362 | ))}
363 |
364 |
365 |
366 | this.loadData()}
369 | local={this.showDrawer.bind(this)}
370 | open={this.open.bind(this)}
371 | upload={this.upload.bind(this)}
372 | />
373 |
374 |
375 |
376 |
383 |
384 |
385 |
386 |
387 |
388 |
{this.state.path}
389 |
390 |
391 |
409 |
410 | );
411 | }
412 | }
413 |
414 | let drawerWidth = 320;
415 | const styles = theme => ({
416 | root: {
417 | height: '100vh', width: '100vw'
418 | },
419 | path: {
420 | position: 'absolute', padding: '2px 8px',
421 | left: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,.2)', color: '#b3b3b3'
422 | },
423 | drawer: {
424 | width: drawerWidth,
425 | flexShrink: 0,
426 | },
427 | drawerPaper: {
428 | width: drawerWidth,
429 | backgroundColor: '#2e2e2e'
430 | },
431 | drawerHeader: {
432 | padding: theme.spacing.unit
433 | },
434 | textColor: {color: '#b3b3b3'},
435 |
436 | content: {
437 | width: '100%', height: '100%',
438 | display: 'flex', flexDirection: 'column',
439 | },
440 | listItem: {
441 | fontSize: 50
442 | },
443 |
444 | editorBar: {
445 | flex: '0 0 auto'
446 | },
447 | editorWrapper: {
448 | flex: '1 1 0',
449 | overflow: 'hidden',
450 | color: '#b3b3b3',
451 | backgroundColor: '#3d3d3d',
452 | display: 'flex',
453 | flexDirection: 'row'
454 | },
455 | left: {
456 | flex: '0 0 auto',
457 | display: 'flex',
458 | flexDirection: 'column',
459 | overflow: 'hidden'
460 | },
461 | divider: {
462 | flex: '0 0 auto',
463 | width: '4px',
464 | backgroundColor: 'rgba(0,0,0,.6)',
465 | cursor: 'col-resize'
466 | },
467 | right: {
468 | flex: '1 1 0',
469 | padding: '20px 40px 80px',
470 | backgroundColor: '#4d4d4d',
471 | fontSize: 16,
472 | overflow: 'auto'
473 | },
474 | editor: {flex: '1 1 0'},
475 | globalProgress: {
476 | position: 'fixed', top: 0, right: 0, bottom: 0, left: 0, backgroundColor: 'rgba(0,0,0,.6)',
477 | display: 'flex', justifyContent: 'center', alignItems: 'center'
478 | },
479 | iconDelete: {
480 | color: theme.palette.secondary.main
481 | }
482 | })
483 |
484 | export default withSnackbar(withStyles(styles, { withTheme: true })(MDEditor));
485 |
--------------------------------------------------------------------------------
/src/views/editor/style.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 | /**
3 | * 1. Set default font family to sans-serif.
4 | * 2. Prevent iOS text size adjust after orientation change, without disabling
5 | * user zoom.
6 | */
7 | @import url("https://fonts.googleapis.com/css?family=Open+Sans:400,700");
8 | html {
9 | font-family: sans-serif;
10 | /* 1 */
11 | -ms-text-size-adjust: 100%;
12 | /* 2 */
13 | -webkit-text-size-adjust: 100%;
14 | /* 2 */ }
15 |
16 | /**
17 | * Remove default margin.
18 | */
19 | body {
20 | margin: 0; }
21 |
22 | /* HTML5 display definitions
23 | ========================================================================== */
24 | /**
25 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
26 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
27 | * and Firefox.
28 | * Correct `block` display not defined for `main` in IE 11.
29 | */
30 | article,
31 | aside,
32 | details,
33 | figcaption,
34 | figure,
35 | footer,
36 | header,
37 | hgroup,
38 | main,
39 | menu,
40 | nav,
41 | section,
42 | summary {
43 | display: block; }
44 |
45 | /**
46 | * 1. Correct `inline-block` display not defined in IE 8/9.
47 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
48 | */
49 | audio,
50 | canvas,
51 | progress,
52 | video {
53 | display: inline-block;
54 | /* 1 */
55 | vertical-align: baseline;
56 | /* 2 */ }
57 |
58 | /**
59 | * Prevent modern browsers from displaying `audio` without controls.
60 | * Remove excess height in iOS 5 devices.
61 | */
62 | audio:not([controls]) {
63 | display: none;
64 | height: 0; }
65 |
66 | /**
67 | * Address `[hidden]` styling not present in IE 8/9/10.
68 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
69 | */
70 | [hidden],
71 | template {
72 | display: none; }
73 |
74 | /* Links
75 | ========================================================================== */
76 | /**
77 | * Remove the gray background color from active links in IE 10.
78 | */
79 | a {
80 | background-color: transparent; }
81 |
82 | /**
83 | * Improve readability when focused and also mouse hovered in all browsers.
84 | */
85 | a:active,
86 | a:hover {
87 | outline: 0; }
88 |
89 | /* Text-level semantics
90 | ========================================================================== */
91 | /**
92 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
93 | */
94 | abbr[title] {
95 | border-bottom: 1px dotted; }
96 |
97 | /**
98 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
99 | */
100 | b,
101 | strong {
102 | font-weight: bold; }
103 |
104 | /**
105 | * Address styling not present in Safari and Chrome.
106 | */
107 | dfn {
108 | font-style: italic; }
109 |
110 | /**
111 | * Address variable `h1` font-size and margin within `section` and `article`
112 | * contexts in Firefox 4+, Safari, and Chrome.
113 | */
114 | h1 {
115 | font-size: 2em;
116 | margin: 0.67em 0; }
117 |
118 | /**
119 | * Address styling not present in IE 8/9.
120 | */
121 | mark {
122 | background: #ff0;
123 | color: #000; }
124 |
125 | /**
126 | * Address inconsistent and variable font size in all browsers.
127 | */
128 | small {
129 | font-size: 80%; }
130 |
131 | /**
132 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
133 | */
134 | sub,
135 | sup {
136 | font-size: 75%;
137 | line-height: 0;
138 | position: relative;
139 | vertical-align: baseline; }
140 |
141 | sup {
142 | top: -0.5em; }
143 |
144 | sub {
145 | bottom: -0.25em; }
146 |
147 | /* Embedded content
148 | ========================================================================== */
149 | /**
150 | * Remove border when inside `a` element in IE 8/9/10.
151 | */
152 | img {
153 | border: 0; }
154 |
155 | /**
156 | * Correct overflow not hidden in IE 9/10/11.
157 | */
158 | svg:not(:root) {
159 | overflow: hidden; }
160 |
161 | /* Grouping content
162 | ========================================================================== */
163 | /**
164 | * Address margin not present in IE 8/9 and Safari.
165 | */
166 | figure {
167 | margin: 1em 40px; }
168 |
169 | /**
170 | * Address differences between Firefox and other browsers.
171 | */
172 | hr {
173 | box-sizing: content-box;
174 | height: 0; }
175 |
176 | /**
177 | * Contain overflow in all browsers.
178 | */
179 | pre {
180 | overflow: auto; }
181 |
182 | /**
183 | * Address odd `em`-unit font size rendering in all browsers.
184 | */
185 | code,
186 | kbd,
187 | pre,
188 | samp {
189 | font-family: monospace, monospace;
190 | font-size: 1em; }
191 |
192 | /* Forms
193 | ========================================================================== */
194 | /**
195 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
196 | * styling of `select`, unless a `border` property is set.
197 | */
198 | /**
199 | * 1. Correct color not being inherited.
200 | * Known issue: affects color of disabled elements.
201 | * 2. Correct font properties not being inherited.
202 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
203 | */
204 | button,
205 | input,
206 | optgroup,
207 | select,
208 | textarea {
209 | color: inherit;
210 | /* 1 */
211 | font: inherit;
212 | /* 2 */
213 | margin: 0;
214 | /* 3 */ }
215 |
216 | /**
217 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
218 | */
219 | button {
220 | overflow: visible; }
221 |
222 | /**
223 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
224 | * All other form control elements do not inherit `text-transform` values.
225 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
226 | * Correct `select` style inheritance in Firefox.
227 | */
228 | button,
229 | select {
230 | text-transform: none; }
231 |
232 | /**
233 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
234 | * and `video` controls.
235 | * 2. Correct inability to style clickable `input` types in iOS.
236 | * 3. Improve usability and consistency of cursor style between image-type
237 | * `input` and others.
238 | */
239 | button,
240 | html input[type="button"],
241 | input[type="reset"],
242 | input[type="submit"] {
243 | -webkit-appearance: button;
244 | /* 2 */
245 | cursor: pointer;
246 | /* 3 */ }
247 |
248 | /**
249 | * Re-set default cursor for disabled elements.
250 | */
251 | button[disabled],
252 | html input[disabled] {
253 | cursor: default; }
254 |
255 | /**
256 | * Remove inner padding and border in Firefox 4+.
257 | */
258 | button::-moz-focus-inner,
259 | input::-moz-focus-inner {
260 | border: 0;
261 | padding: 0; }
262 |
263 | /**
264 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
265 | * the UA stylesheet.
266 | */
267 | input {
268 | line-height: normal; }
269 |
270 | /**
271 | * It's recommended that you don't attempt to style these elements.
272 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
273 | *
274 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
275 | * 2. Remove excess padding in IE 8/9/10.
276 | */
277 | input[type="checkbox"],
278 | input[type="radio"] {
279 | box-sizing: border-box;
280 | /* 1 */
281 | padding: 0;
282 | /* 2 */ }
283 |
284 | /**
285 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
286 | * `font-size` values of the `input`, it causes the cursor style of the
287 | * decrement button to change from `default` to `text`.
288 | */
289 | input[type="number"]::-webkit-inner-spin-button,
290 | input[type="number"]::-webkit-outer-spin-button {
291 | height: auto; }
292 |
293 | /**
294 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
295 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
296 | * (include `-moz` to future-proof).
297 | */
298 | input[type="search"] {
299 | -webkit-appearance: textfield;
300 | /* 1 */
301 | /* 2 */
302 | box-sizing: content-box; }
303 |
304 | /**
305 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
306 | * Safari (but not Chrome) clips the cancel button when the search input has
307 | * padding (and `textfield` appearance).
308 | */
309 | input[type="search"]::-webkit-search-cancel-button,
310 | input[type="search"]::-webkit-search-decoration {
311 | -webkit-appearance: none; }
312 |
313 | /**
314 | * Define consistent border, margin, and padding.
315 | */
316 | fieldset {
317 | border: 1px solid #c0c0c0;
318 | margin: 0 2px;
319 | padding: 0.35em 0.625em 0.75em; }
320 |
321 | /**
322 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
323 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
324 | */
325 | legend {
326 | border: 0;
327 | /* 1 */
328 | padding: 0;
329 | /* 2 */ }
330 |
331 | /**
332 | * Remove default vertical scrollbar in IE 8/9/10/11.
333 | */
334 | textarea {
335 | overflow: auto; }
336 |
337 | /**
338 | * Don't inherit the `font-weight` (applied by a rule above).
339 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
340 | */
341 | optgroup {
342 | font-weight: bold; }
343 |
344 | /* Tables
345 | ========================================================================== */
346 | /**
347 | * Remove most spacing between table cells.
348 | */
349 | table {
350 | border-collapse: collapse;
351 | border-spacing: 0; }
352 |
353 | td,
354 | th {
355 | padding: 0; }
356 |
357 | .highlight table td {
358 | padding: 5px; }
359 |
360 | .highlight table pre {
361 | margin: 0; }
362 |
363 | .highlight .cm {
364 | color: #999988;
365 | font-style: italic; }
366 |
367 | .highlight .cp {
368 | color: #999999;
369 | font-weight: bold; }
370 |
371 | .highlight .c1 {
372 | color: #999988;
373 | font-style: italic; }
374 |
375 | .highlight .cs {
376 | color: #999999;
377 | font-weight: bold;
378 | font-style: italic; }
379 |
380 | .highlight .c, .highlight .cd {
381 | color: #999988;
382 | font-style: italic; }
383 |
384 | .highlight .err {
385 | color: #a61717;
386 | background-color: #e3d2d2; }
387 |
388 | .highlight .gd {
389 | color: #000000;
390 | background-color: #ffdddd; }
391 |
392 | .highlight .ge {
393 | color: #000000;
394 | font-style: italic; }
395 |
396 | .highlight .gr {
397 | color: #aa0000; }
398 |
399 | .highlight .gh {
400 | color: #999999; }
401 |
402 | .highlight .gi {
403 | color: #000000;
404 | background-color: #ddffdd; }
405 |
406 | .highlight .go {
407 | color: #888888; }
408 |
409 | .highlight .gp {
410 | color: #555555; }
411 |
412 | .highlight .gs {
413 | font-weight: bold; }
414 |
415 | .highlight .gu {
416 | color: #aaaaaa; }
417 |
418 | .highlight .gt {
419 | color: #aa0000; }
420 |
421 | .highlight .kc {
422 | color: #000000;
423 | font-weight: bold; }
424 |
425 | .highlight .kd {
426 | color: #000000;
427 | font-weight: bold; }
428 |
429 | .highlight .kn {
430 | color: #000000;
431 | font-weight: bold; }
432 |
433 | .highlight .kp {
434 | color: #000000;
435 | font-weight: bold; }
436 |
437 | .highlight .kr {
438 | color: #000000;
439 | font-weight: bold; }
440 |
441 | .highlight .kt {
442 | color: #445588;
443 | font-weight: bold; }
444 |
445 | .highlight .k, .highlight .kv {
446 | color: #000000;
447 | font-weight: bold; }
448 |
449 | .highlight .mf {
450 | color: #009999; }
451 |
452 | .highlight .mh {
453 | color: #009999; }
454 |
455 | .highlight .il {
456 | color: #009999; }
457 |
458 | .highlight .mi {
459 | color: #009999; }
460 |
461 | .highlight .mo {
462 | color: #009999; }
463 |
464 | .highlight .m, .highlight .mb, .highlight .mx {
465 | color: #009999; }
466 |
467 | .highlight .sb {
468 | color: #d14; }
469 |
470 | .highlight .sc {
471 | color: #d14; }
472 |
473 | .highlight .sd {
474 | color: #d14; }
475 |
476 | .highlight .s2 {
477 | color: #d14; }
478 |
479 | .highlight .se {
480 | color: #d14; }
481 |
482 | .highlight .sh {
483 | color: #d14; }
484 |
485 | .highlight .si {
486 | color: #d14; }
487 |
488 | .highlight .sx {
489 | color: #d14; }
490 |
491 | .highlight .sr {
492 | color: #009926; }
493 |
494 | .highlight .s1 {
495 | color: #d14; }
496 |
497 | .highlight .ss {
498 | color: #990073; }
499 |
500 | .highlight .s {
501 | color: #d14; }
502 |
503 | .highlight .na {
504 | color: #008080; }
505 |
506 | .highlight .bp {
507 | color: #999999; }
508 |
509 | .highlight .nb {
510 | color: #0086B3; }
511 |
512 | .highlight .nc {
513 | color: #445588;
514 | font-weight: bold; }
515 |
516 | .highlight .no {
517 | color: #008080; }
518 |
519 | .highlight .nd {
520 | color: #3c5d5d;
521 | font-weight: bold; }
522 |
523 | .highlight .ni {
524 | color: #800080; }
525 |
526 | .highlight .ne {
527 | color: #990000;
528 | font-weight: bold; }
529 |
530 | .highlight .nf {
531 | color: #990000;
532 | font-weight: bold; }
533 |
534 | .highlight .nl {
535 | color: #990000;
536 | font-weight: bold; }
537 |
538 | .highlight .nn {
539 | color: #555555; }
540 |
541 | .highlight .nt {
542 | color: #000080; }
543 |
544 | .highlight .vc {
545 | color: #008080; }
546 |
547 | .highlight .vg {
548 | color: #008080; }
549 |
550 | .highlight .vi {
551 | color: #008080; }
552 |
553 | .highlight .nv {
554 | color: #008080; }
555 |
556 | .highlight .ow {
557 | color: #000000;
558 | font-weight: bold; }
559 |
560 | .highlight .o {
561 | color: #000000;
562 | font-weight: bold; }
563 |
564 | .highlight .w {
565 | color: #bbbbbb; }
566 |
567 | .highlight {
568 | background-color: #f8f8f8; }
569 |
570 | * {
571 | box-sizing: border-box; }
572 |
573 | body {
574 | padding: 0;
575 | margin: 0;
576 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
577 | font-size: 16px;
578 | line-height: 1.5;
579 | color: #606c71; }
580 |
581 | #skip-to-content {
582 | height: 1px;
583 | width: 1px;
584 | position: absolute;
585 | overflow: hidden;
586 | top: -10px; }
587 | #skip-to-content:focus {
588 | position: fixed;
589 | top: 10px;
590 | left: 10px;
591 | height: auto;
592 | width: auto;
593 | background: #e19447;
594 | outline: thick solid #e19447; }
595 |
596 | a {
597 | color: #1e6bb8;
598 | text-decoration: none; }
599 | a:hover {
600 | text-decoration: underline; }
601 |
602 | .btn {
603 | display: inline-block;
604 | margin-bottom: 1rem;
605 | color: rgba(255, 255, 255, 0.7);
606 | background-color: rgba(255, 255, 255, 0.08);
607 | border-color: rgba(255, 255, 255, 0.2);
608 | border-style: solid;
609 | border-width: 1px;
610 | border-radius: 0.3rem;
611 | transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
612 | .btn:hover {
613 | color: rgba(255, 255, 255, 0.8);
614 | text-decoration: none;
615 | background-color: rgba(255, 255, 255, 0.2);
616 | border-color: rgba(255, 255, 255, 0.3); }
617 | .btn + .btn {
618 | margin-left: 1rem; }
619 | @media screen and (min-width: 64em) {
620 | .btn {
621 | padding: 0.75rem 1rem; } }
622 | @media screen and (min-width: 42em) and (max-width: 64em) {
623 | .btn {
624 | padding: 0.6rem 0.9rem;
625 | font-size: 0.9rem; } }
626 | @media screen and (max-width: 42em) {
627 | .btn {
628 | display: block;
629 | width: 100%;
630 | padding: 0.75rem;
631 | font-size: 0.9rem; }
632 | .btn + .btn {
633 | margin-top: 1rem;
634 | margin-left: 0; } }
635 |
636 | .page-header {
637 | color: #fff;
638 | text-align: center;
639 | background-color: #159957;
640 | background-image: linear-gradient(120deg, #155799, #159957); }
641 | @media screen and (min-width: 64em) {
642 | .page-header {
643 | padding: 5rem 6rem; } }
644 | @media screen and (min-width: 42em) and (max-width: 64em) {
645 | .page-header {
646 | padding: 3rem 4rem; } }
647 | @media screen and (max-width: 42em) {
648 | .page-header {
649 | padding: 2rem 1rem; } }
650 |
651 | .project-name {
652 | margin-top: 0;
653 | margin-bottom: 0.1rem; }
654 | @media screen and (min-width: 64em) {
655 | .project-name {
656 | font-size: 3.25rem; } }
657 | @media screen and (min-width: 42em) and (max-width: 64em) {
658 | .project-name {
659 | font-size: 2.25rem; } }
660 | @media screen and (max-width: 42em) {
661 | .project-name {
662 | font-size: 1.75rem; } }
663 |
664 | .project-tagline {
665 | margin-bottom: 2rem;
666 | font-weight: normal;
667 | opacity: 0.7; }
668 | @media screen and (min-width: 64em) {
669 | .project-tagline {
670 | font-size: 1.25rem; } }
671 | @media screen and (min-width: 42em) and (max-width: 64em) {
672 | .project-tagline {
673 | font-size: 1.15rem; } }
674 | @media screen and (max-width: 42em) {
675 | .project-tagline {
676 | font-size: 1rem; } }
677 |
678 | .main-content {
679 | word-wrap: break-word; }
680 | .main-content :first-child {
681 | margin-top: 0; }
682 | @media screen and (min-width: 64em) {
683 | .main-content {
684 | max-width: 64rem;
685 | padding: 2rem 6rem;
686 | margin: 0 auto;
687 | font-size: 1.1rem; } }
688 | @media screen and (min-width: 42em) and (max-width: 64em) {
689 | .main-content {
690 | padding: 2rem 4rem;
691 | font-size: 1.1rem; } }
692 | @media screen and (max-width: 42em) {
693 | .main-content {
694 | padding: 2rem 1rem;
695 | font-size: 1rem; } }
696 | .main-content kbd {
697 | background-color: #fafbfc;
698 | border: 1px solid #c6cbd1;
699 | border-bottom-color: #959da5;
700 | border-radius: 3px;
701 | box-shadow: inset 0 -1px 0 #959da5;
702 | color: #444d56;
703 | display: inline-block;
704 | font-size: 11px;
705 | line-height: 10px;
706 | padding: 3px 5px;
707 | vertical-align: middle; }
708 | .main-content img {
709 | max-width: 100%; }
710 | .main-content h1,
711 | .main-content h2,
712 | .main-content h3,
713 | .main-content h4,
714 | .main-content h5,
715 | .main-content h6 {
716 | margin-top: 2rem;
717 | margin-bottom: 1rem;
718 | font-weight: normal;
719 | color: #159957; }
720 | .main-content p {
721 | margin-bottom: 1em; }
722 | .main-content code {
723 | padding: 2px 4px;
724 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
725 | font-size: 0.9rem;
726 | color: #567482;
727 | background-color: #f3f6fa;
728 | border-radius: 0.3rem; }
729 | .main-content pre {
730 | padding: 0.8rem;
731 | margin-top: 0;
732 | margin-bottom: 1rem;
733 | font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace;
734 | color: #567482;
735 | word-wrap: normal;
736 | background-color: #f3f6fa;
737 | border: solid 1px #dce6f0;
738 | border-radius: 0.3rem; }
739 | .main-content pre > code {
740 | padding: 0;
741 | margin: 0;
742 | font-size: 0.9rem;
743 | color: #567482;
744 | word-break: normal;
745 | white-space: pre;
746 | background: transparent;
747 | border: 0; }
748 | .main-content .highlight {
749 | margin-bottom: 1rem; }
750 | .main-content .highlight pre {
751 | margin-bottom: 0;
752 | word-break: normal; }
753 | .main-content .highlight pre,
754 | .main-content pre {
755 | padding: 0.8rem;
756 | overflow: auto;
757 | font-size: 0.9rem;
758 | line-height: 1.45;
759 | border-radius: 0.3rem;
760 | -webkit-overflow-scrolling: touch; }
761 | .main-content pre code,
762 | .main-content pre tt {
763 | display: inline;
764 | max-width: initial;
765 | padding: 0;
766 | margin: 0;
767 | overflow: initial;
768 | line-height: inherit;
769 | word-wrap: normal;
770 | background-color: transparent;
771 | border: 0; }
772 | .main-content pre code:before, .main-content pre code:after,
773 | .main-content pre tt:before,
774 | .main-content pre tt:after {
775 | content: normal; }
776 | .main-content ul,
777 | .main-content ol {
778 | margin-top: 0; }
779 | .main-content blockquote {
780 | padding: 0 1rem;
781 | margin-left: 0;
782 | color: #819198;
783 | border-left: 0.3rem solid #dce6f0; }
784 | .main-content blockquote > :first-child {
785 | margin-top: 0; }
786 | .main-content blockquote > :last-child {
787 | margin-bottom: 0; }
788 | .main-content table {
789 | display: block;
790 | width: 100%;
791 | overflow: auto;
792 | word-break: normal;
793 | word-break: keep-all;
794 | -webkit-overflow-scrolling: touch; }
795 | .main-content table th {
796 | font-weight: bold; }
797 | .main-content table th,
798 | .main-content table td {
799 | padding: 0.5rem 1rem;
800 | border: 1px solid #e9ebec; }
801 | .main-content dl {
802 | padding: 0; }
803 | .main-content dl dt {
804 | padding: 0;
805 | margin-top: 1rem;
806 | font-size: 1rem;
807 | font-weight: bold; }
808 | .main-content dl dd {
809 | padding: 0;
810 | margin-bottom: 1rem; }
811 | .main-content hr {
812 | height: 2px;
813 | padding: 0;
814 | margin: 1rem 0;
815 | background-color: #eff0f1;
816 | border: 0; }
817 |
818 | .site-footer {
819 | padding-top: 2rem;
820 | margin-top: 2rem;
821 | border-top: solid 1px #eff0f1; }
822 | @media screen and (min-width: 64em) {
823 | .site-footer {
824 | font-size: 1rem; } }
825 | @media screen and (min-width: 42em) and (max-width: 64em) {
826 | .site-footer {
827 | font-size: 1rem; } }
828 | @media screen and (max-width: 42em) {
829 | .site-footer {
830 | font-size: 0.9rem; } }
831 |
832 | .site-footer-owner {
833 | display: block;
834 | font-weight: bold; }
835 |
836 | .site-footer-credits {
837 | color: #819198; }
838 |
--------------------------------------------------------------------------------