├── .gitignore
├── LICENSE
├── README.md
├── admin
├── .gitignore
├── LICENSE
├── __tests__
│ └── preprocessor.js
├── dist
│ ├── font
│ │ ├── rango.eot
│ │ ├── rango.svg
│ │ ├── rango.ttf
│ │ └── rango.woff
│ └── index.html
├── gulpfile.js
├── lib
│ ├── actions.js
│ ├── api.js
│ ├── components
│ │ ├── app.jsx
│ │ ├── browser.jsx
│ │ ├── browserRow.jsx
│ │ ├── browserSidebar.jsx
│ │ ├── browserTable.jsx
│ │ ├── editor.jsx
│ │ ├── editorContent.jsx
│ │ ├── editorImages.jsx
│ │ ├── editorMetadata.jsx
│ │ ├── header.jsx
│ │ ├── imageItem.jsx
│ │ ├── inputCheckbox.jsx
│ │ ├── inputDropdown.jsx
│ │ ├── inputText.jsx
│ │ ├── inputTextarea.jsx
│ │ └── sortableItem.jsx
│ ├── main.jsx
│ └── stores
│ │ ├── app.js
│ │ ├── browser.js
│ │ ├── config.js
│ │ └── editor.js
├── package.json
└── style
│ ├── app
│ ├── _body.scss
│ ├── _browser.scss
│ ├── _editor.scss
│ └── _header.scss
│ ├── main.scss
│ ├── modules
│ ├── _buttons.scss
│ ├── _extends.scss
│ ├── _form.scss
│ ├── _functions.scss
│ ├── _icon_font.scss
│ ├── _mixins.scss
│ ├── _sortable.scss
│ └── _type.scss
│ └── vendor
│ ├── _normalize.scss
│ ├── codemirror
│ ├── _base16-light.scss
│ ├── _index.scss
│ └── _solarized.scss
│ └── jeet
│ ├── _functions.scss
│ ├── _grid.scss
│ ├── _settings.scss
│ └── index.scss
├── config.toml
├── docs
├── screenshot_1.jpg
└── screenshot_2.jpg
├── errors.go
├── handlers.go
├── handlers_test.go
├── logger.go
├── main.go
├── rangolib
├── asset.go
├── config.go
├── config_test.go
├── dir.go
├── dir_test.go
├── file.go
├── hugo.go
├── page.go
├── page_test.go
└── treecopier.go
├── router.go
├── routes.go
└── utils.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 | /rango
6 |
7 | # Folders
8 | _obj
9 | _test
10 | node_modules
11 |
12 | /admin/dist/*.css
13 | /admin/dist/*.js
14 |
15 | # Architecture specific extensions/prefixes
16 | *.[568vq]
17 | [568vq].out
18 |
19 | *.cgo1.go
20 | *.cgo2.c
21 | _cgo_defun.c
22 | _cgo_gotypes.go
23 | _cgo_export.*
24 |
25 | _testmain.go
26 |
27 | *.exe
28 | *.test
29 |
30 | # Hugo folders
31 | /content/**/*.md
32 | /static
33 | /public
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 George Czabania
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | rango
2 | =====
3 |
4 | A web frontend for [hugo](https://gohugo.io).
5 |
6 | It's designed to make it easy to manage a small site, even for people with
7 | little computer experience.
8 |
9 | 
10 |
11 | 
12 |
13 | ## Installation
14 |
15 | ```
16 | $ go get -u -v github.com/stayradiated/rango
17 | $ cd $GOPATH/src/github.com/stayradiated/rango
18 | $ cd admin
19 | $ npm install
20 | $ gulp
21 | $ cd ..
22 | $ go build
23 | $ ./rango
24 | ```
25 |
26 | ## Using with Apache
27 |
28 | Based on [this
29 | tutorial](http://www.jeffreybolle.com/blog/run-google-go-web-apps-behind-apache).
30 |
31 | 1. Create a folder named `admin` or `rango` or whatever.
32 | 2. Create a `.htaccess` inside that folder with the following content:
33 | 3. Enable apache modules: `proxy`, `proxy_http`, `rewrite`
34 |
35 | ```
36 | RewriteEngine on
37 | RewriteRule ^(.*)$ http://localhost:8080/$1 [P,L]
38 | ```
39 |
--------------------------------------------------------------------------------
/admin/.gitignore:
--------------------------------------------------------------------------------
1 | dist/js/*.js
2 | dist/css/*.css
3 | node_modules
4 | .svn
5 |
--------------------------------------------------------------------------------
/admin/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 George Czabania
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/admin/__tests__/preprocessor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var ReactTools = require('react-tools');
4 |
5 | module.exports = {
6 | process: function (src) {
7 | return ReactTools.transform(src);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/admin/dist/font/rango.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stayradiated/rango/71c995cba6cef7f0cb387f530474dd796302f4d6/admin/dist/font/rango.eot
--------------------------------------------------------------------------------
/admin/dist/font/rango.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/admin/dist/font/rango.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stayradiated/rango/71c995cba6cef7f0cb387f530474dd796302f4d6/admin/dist/font/rango.ttf
--------------------------------------------------------------------------------
/admin/dist/font/rango.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stayradiated/rango/71c995cba6cef7f0cb387f530474dd796302f4d6/admin/dist/font/rango.woff
--------------------------------------------------------------------------------
/admin/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |

19 |
23 |
24 | );
25 | },
26 |
27 | handleMouseDown: function (event) {
28 | // stop sortable from firing
29 | event.stopPropagation();
30 | },
31 |
32 | });
33 |
34 | module.exports = ImageItem;
35 |
--------------------------------------------------------------------------------
/admin/lib/components/inputCheckbox.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 | var PureRenderMixin = require('react/addons').addons.PureRenderMixin;
5 |
6 | var InputCheckbox = React.createClass({
7 | mixins: [
8 | PureRenderMixin,
9 | ],
10 |
11 | propTypes: {
12 | label: React.PropTypes.string.isRequired,
13 | },
14 |
15 | render: function () {
16 | return (
17 |
18 |
19 |
20 |
21 | );
22 | },
23 |
24 | });
25 |
26 | module.exports = InputCheckbox;
27 |
--------------------------------------------------------------------------------
/admin/lib/components/inputDropdown.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 | var PureRenderMixin = require('react/addons').addons.PureRenderMixin;
5 |
6 | var InputDropdown = React.createClass({
7 | mixins: [
8 | PureRenderMixin,
9 | ],
10 |
11 | propTypes: {
12 | label: React.PropTypes.string.isRequired,
13 | value: React.PropTypes.string.isRequired,
14 | onChange: React.PropTypes.func,
15 | },
16 |
17 | render: function () {
18 | return (
19 |
20 |
21 |
30 |
31 | );
32 | },
33 |
34 | });
35 |
36 | module.exports = InputDropdown;
37 |
--------------------------------------------------------------------------------
/admin/lib/components/inputText.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 | var PureRenderMixin = require('react/addons').addons.PureRenderMixin;
5 |
6 | var InputText = React.createClass({
7 | mixins: [
8 | PureRenderMixin,
9 | ],
10 |
11 | propTypes: {
12 | label: React.PropTypes.string.isRequired,
13 | value: React.PropTypes.string.isRequired,
14 | onChange: React.PropTypes.func,
15 | },
16 |
17 | render: function () {
18 | return (
19 |
20 |
21 |
27 |
28 | );
29 | },
30 |
31 | });
32 |
33 | module.exports = InputText;
34 |
--------------------------------------------------------------------------------
/admin/lib/components/inputTextarea.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 | var PureRenderMixin = require('react/addons').addons.PureRenderMixin;
5 |
6 | var InputTextarea = React.createClass({
7 | mixins: [
8 | PureRenderMixin,
9 | ],
10 |
11 | propTypes: {
12 | label: React.PropTypes.string.isRequired,
13 | value: React.PropTypes.string.isRequired,
14 | },
15 |
16 | getInitialState: function (props) {
17 | return {
18 | value: (props || this.props).value,
19 | };
20 | },
21 |
22 | componentWillReceiveProps: function (nextProps) {
23 | this.setState(this.getInitialState(nextProps));
24 | },
25 |
26 | render: function () {
27 | return (
28 |
29 |
30 |
35 |
36 | );
37 | },
38 |
39 | onChange: function (e) {
40 | var el = this.refs.input.getDOMNode();
41 | var value = el.value;
42 |
43 | this.setState({
44 | value: value,
45 | });
46 | },
47 |
48 | });
49 |
50 | module.exports = InputTextarea;
51 |
--------------------------------------------------------------------------------
/admin/lib/components/sortableItem.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 | var PureRenderMixin = require('react/addons').addons.PureRenderMixin;
5 | var SortableItemMixin = require('react-anything-sortable/SortableItemMixin');
6 |
7 | var ImageItem = React.createClass({
8 | mixins: [
9 | SortableItemMixin,
10 | PureRenderMixin,
11 | ],
12 |
13 | getDefaultProps: function () {
14 | return {
15 | className: '',
16 | }
17 | },
18 |
19 | render: function () {
20 | return this.renderWithSortable(
21 |
22 | {this.props.children}
23 |
24 | );
25 | },
26 |
27 | });
28 |
29 | module.exports = ImageItem;
30 |
--------------------------------------------------------------------------------
/admin/lib/main.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var jQuery = require('jquery');
4 | var fastclick = require('fastclick');
5 | var React = require('react');
6 | var Fluxxor = require('fluxxor');
7 |
8 | var actions = require('./actions');
9 | var App = require('./components/app');
10 | var AppStore = require('./stores/app');
11 | var ConfigStore = require('./stores/config');
12 | var EditorStore = require('./stores/editor');
13 | var BrowserStore = require('./stores/browser');
14 |
15 | jQuery(function () {
16 | fastclick(document.body);
17 |
18 | var stores = {
19 | App: new AppStore(),
20 | Config: new ConfigStore(),
21 | Editor: new EditorStore(),
22 | Browser: new BrowserStore(),
23 | };
24 |
25 | var flux = new Fluxxor.Flux(stores, actions);
26 | React.render(
, document.body);
27 | });
28 |
29 | // Trigger React dev tools
30 | if (typeof window !== 'undefined') {
31 | window.React = React;
32 | }
33 |
--------------------------------------------------------------------------------
/admin/lib/stores/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var jQuery = require('jquery');
4 | var Fluxxor = require('fluxxor');
5 | var Immutable = require('immutable');
6 |
7 | var api = require('../api');
8 |
9 | var AppStore = Fluxxor.createStore({
10 |
11 | actions: {
12 | OPEN_PATH: 'handleOpenBrowser',
13 | OPEN_DIRECTORY: 'handleOpenBrowser',
14 | OPEN_PAGE: 'handleOpenEditor',
15 | PUBLISH_SITE: 'handlePublishSite',
16 | },
17 |
18 | initialize: function () {
19 | this.state = Immutable.fromJS({
20 | route: 'BROWSER',
21 | });
22 | },
23 |
24 | handleOpenBrowser: function () {
25 | this.state = this.state.set('route', 'BROWSER');
26 | this.emit('change');
27 | },
28 |
29 | handleOpenEditor: function () {
30 | this.state = this.state.set('route', 'EDITOR');
31 | this.emit('change');
32 | },
33 |
34 | handlePublishSite: function () {
35 | api.publishSite().then(function (res) {
36 | console.log(res);
37 | });
38 | },
39 |
40 | });
41 |
42 | module.exports = AppStore;
43 |
--------------------------------------------------------------------------------
/admin/lib/stores/browser.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Immutable = require('immutable');
4 | var Fluxxor = require('fluxxor');
5 | var Path = require('path')
6 | var jQuery = require('jquery');
7 |
8 | var Rango = require('../api');
9 |
10 | var BrowserStore = Fluxxor.createStore({
11 |
12 | actions: {
13 | OPEN_PATH: 'handleOpenPath',
14 | OPEN_DIRECTORY: 'handleOpenDirectory',
15 |
16 | CREATE_PAGE: 'handleCreatePage',
17 | CREATE_DIRECTORY: 'handleCreateDirectory',
18 |
19 | UPDATE_DIRECTORY: 'handleUpdateDirectory',
20 |
21 | OPEN_PARENT_DIRECTORY: 'handleOpenParentDirectory',
22 | SELECT_FILE: 'handleSelectFile',
23 | DESELECT_ALL: 'handleDeselectAll',
24 | REMOVE_SELECTED_FILES: 'handleRemoveSelectedFiles',
25 | },
26 |
27 | initialize: function () {
28 |
29 | this.state = Immutable.fromJS({
30 |
31 | // a list storing the current path stored in an array
32 | // /usr/local/bin => ['usr', 'local', 'bin']
33 | path: [],
34 |
35 | // the directories and pages in the current path
36 | contents: [],
37 |
38 | // a set of selected pages and directories
39 | selected: Immutable.Set(),
40 |
41 | });
42 |
43 | // fetch contents of root directory
44 | this.fetchContents();
45 | },
46 |
47 | // get the current path as a string, also handles root directory properly
48 | getPath: function () {
49 | var path = this.state.get('path');
50 |
51 | if (path.size === 0) {
52 | return '/';
53 | } else {
54 | return path.join('/');
55 | }
56 | },
57 |
58 | // get contents of current path from server
59 | fetchContents: function () {
60 | var self = this;
61 | return Rango.readDir(this.getPath()).then(function (data) {
62 | self.state = self.state.merge({
63 | contents: Immutable.fromJS(data),
64 | selected: self.state.get('selected').clear(),
65 | });
66 | self.emit('change');
67 | });
68 | },
69 |
70 | // change the path
71 | handleOpenPath: function (newPath) {
72 | this.state = this.state.update('path', function (path) {
73 | if (newPath === '/') {
74 | return path.clear();
75 | }
76 | return Immutable.List(newPath.split('/'));
77 | });
78 |
79 | this.fetchContents();
80 | },
81 |
82 | // open a sub-directory
83 | handleOpenDirectory: function (dirName) {
84 | this.state = this.state.update('path', function (path) {
85 | return path.push(dirName);
86 | });
87 | this.fetchContents();
88 | },
89 |
90 | // open the parent directory
91 | handleOpenParentDirectory: function () {
92 | this.state = this.state.update('path', function (path) {
93 | return path.pop();
94 | });
95 | this.fetchContents();
96 | },
97 |
98 | // create a new directory on the server
99 | handleCreateDirectory: function () {
100 | var self = this;
101 |
102 | var name = window.prompt('Enter a name for the new directory');
103 | if (! name) { return; }
104 |
105 | return Rango.createDir(this.getPath(), name).then(function () {
106 | self.fetchContents();
107 | });
108 | },
109 |
110 | // create a new page on the server
111 | handleCreatePage: function () {
112 | var self = this;
113 |
114 | var name = window.prompt('Enter a title for the new page');
115 | if (! name) { return; }
116 |
117 | return Rango.createPage(this.getPath(), {
118 | metadata: {
119 | title: name,
120 | },
121 | content: ''
122 | }).then(function (res) {
123 | self.fetchContents();
124 | });
125 | },
126 |
127 | // rename a directory
128 | handleUpdateDirectory: function () {
129 | var selectedDir = this.state.get('selected').first();
130 | if (! selectedDir) { return; }
131 |
132 | var name = window.prompt('Enter a new name for this directory');
133 | if (! name) { return; }
134 |
135 | var self = this;
136 | var path = selectedDir.get('path');
137 |
138 | Rango.updateDir(path, {
139 | name: name,
140 | }).then(function (res) {
141 | self.fetchContents();
142 | });
143 | },
144 |
145 | // select a file
146 | handleSelectFile: function (file) {
147 | this.state = this.state.update('selected', function (selected) {
148 | return selected.clear().add(file);
149 | });
150 | this.emit('change');
151 | },
152 |
153 | // deselect all files
154 | handleDeselectAll: function () {
155 | this.state = this.state.update('selected', function (selected) {
156 | return selected.clear();
157 | });
158 | this.emit('change');
159 | },
160 |
161 | // delete the selected files
162 | handleRemoveSelectedFiles: function () {
163 | if (! window.confirm('Are you sure you want to delete the selected files?')) {
164 | return;
165 | }
166 |
167 | var self = this;
168 |
169 | var deferreds = this.state.get('selected').map(function (file) {
170 | var path = file.get('path');
171 | if (file.get('isDir')) {
172 | return Rango.deleteDir(path);
173 | } else {
174 | return Rango.deletePage(path);
175 | }
176 | }).toArray();
177 |
178 | jQuery.when.apply(jQuery, deferreds).done(function () {
179 | self.fetchContents();
180 | });
181 | },
182 |
183 | });
184 |
185 | module.exports = BrowserStore;
186 |
--------------------------------------------------------------------------------
/admin/lib/stores/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Immutable = require('immutable');
4 | var Fluxxor = require('fluxxor');
5 |
6 | var Rango = require('../api');
7 |
8 | var ConfigStore = Fluxxor.createStore({
9 |
10 | actions: {
11 | },
12 |
13 | initialize: function () {
14 | this.config = Immutable.fromJS({
15 | types: {
16 | default: {
17 | },
18 | },
19 | });
20 |
21 | // fetch config from server
22 | this.fetchConfig();
23 | },
24 |
25 | getState: function () {
26 | return {
27 | config: this.config,
28 | };
29 | },
30 |
31 | fetchConfig: function () {
32 | var self = this;
33 | return Rango.readConfig().then(function (config) {
34 | self.config = Immutable.fromJS(config);
35 | self.emit('change');
36 | });
37 | },
38 |
39 | });
40 |
41 | module.exports = ConfigStore;
42 |
--------------------------------------------------------------------------------
/admin/lib/stores/editor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Immutable = require('immutable');
4 | var Fluxxor = require('fluxxor');
5 | var path = require('path');
6 |
7 | var Rango = require('../api');
8 |
9 | var emptyPage = Immutable.fromJS({
10 | content: '',
11 | metadata: {},
12 | });
13 |
14 | var EditorStore = Fluxxor.createStore({
15 |
16 | actions: {
17 | OPEN_PAGE: 'handleOpenPage',
18 | SAVE_PAGE: 'handleSavePage',
19 | UPDATE_METADATA: 'handleUpdateMetadata',
20 | UPDATE_CONTENT: 'handleUpdateContent',
21 | UPLOAD_FILE: 'handleUploadFile',
22 | },
23 |
24 | initialize: function () {
25 |
26 | this.state = Immutable.fromJS({
27 |
28 | // path to the currently edited file
29 | path: '',
30 |
31 | // the directories and pages in the current path
32 | page: emptyPage,
33 |
34 | // if the page has changed since it was last loaded
35 | hasChanged: false,
36 |
37 | });
38 | },
39 |
40 | // fetch the page metadata and content from the server
41 | fetchPage: function () {
42 | var self = this;
43 |
44 | // empty content while we are waiting
45 | this.state = this.state.set('page', emptyPage);
46 | self.emit('change');
47 |
48 | Rango.readPage(this.state.get('path')).then(function (page) {
49 | self.state = self.state.set('page', Immutable.fromJS(page));
50 | self.state = self.state.set('hasChanged', false);
51 | self.emit('change');
52 | });
53 | },
54 |
55 | // open a page in the editor
56 | handleOpenPage: function (filename) {
57 | this.state = this.state.set('path', filename);
58 | this.fetchPage();
59 | },
60 |
61 | // save the current page back to the server
62 | handleSavePage: function () {
63 | var self = this;
64 |
65 | Rango.updatePage(
66 | this.state.get('path'),
67 | this.state.get('page').toJS()
68 | ).then(function (page) {
69 | self.state = Immutable.fromJS({
70 | path: page.path,
71 | hasChanged: false,
72 | page: {
73 | content: page.content,
74 | metadata: page.metadata,
75 | },
76 | })
77 | self.emit('change');
78 | })
79 | },
80 |
81 | handleUpdateMetadata: function (metadata) {
82 | this.state = this.state.setIn(['page', 'metadata'], metadata);
83 | this.state = this.state.set('hasChanged', true);
84 | this.emit('change')
85 | },
86 |
87 | handleUpdateContent: function (content) {
88 | this.state = this.state.setIn(['page', 'content'], content);
89 | this.state = this.state.set('hasChanged', true);
90 | this.emit('change');
91 | },
92 |
93 | handleUploadFile: function (file) {
94 | var self = this;
95 |
96 | Rango.createAsset(this.state.get('path'), file).then(function (result) {
97 | // TODO: handle errors...
98 | if (result.asset) {
99 | var asset = result.asset;
100 | var fullPath = path.join(asset.path, asset.name);
101 |
102 | self.state = self.state.updateIn(['page', 'metadata', 'images'], function (images) {
103 | if (images == null) {
104 | images = Immutable.List();
105 | }
106 | images = images.push(fullPath);
107 | return images;
108 | });
109 |
110 | self.state = self.state.set('hasChanged', true);
111 | self.emit('change');
112 | }
113 | });
114 | },
115 |
116 | });
117 |
118 | module.exports = EditorStore;
119 |
--------------------------------------------------------------------------------
/admin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "___",
3 | "version": "0.0.0",
4 | "description": "",
5 | "main": "dist/index.html",
6 | "scripts": {
7 | "test": "jest"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/stayradiated/___.git"
12 | },
13 | "author": "George Czabania",
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/stayradiated/___/issues"
17 | },
18 | "homepage": "https://github.com/stayradiated/___",
19 | "dependencies": {
20 | "codemirror": "^4.13.0",
21 | "fastclick": "^1.0.6",
22 | "fluxxor": "^1.5.2",
23 | "immutable": "^3.6.2",
24 | "jquery": "^2.1.3",
25 | "lodash": "^3.3.1",
26 | "marked": "^0.3.3",
27 | "moment": "^2.9.0",
28 | "react": "^0.12.2",
29 | "react-anything-sortable": "^0.1.1",
30 | "react-code-mirror": "^3.0.3",
31 | "react-dropzone": "^1.0.0"
32 | },
33 | "devDependencies": {
34 | "browser-sync": "^2.2.1",
35 | "browserify": "^9.0.3",
36 | "gulp": "^3.8.11",
37 | "gulp-autoprefixer": "2.1.0",
38 | "gulp-sass": "^1.3.3",
39 | "gulp-uglify": "^1.1.0",
40 | "jest-cli": "^0.4.0",
41 | "reactify": "^1.0.0",
42 | "vinyl-source-stream": "^1.0.0",
43 | "watchify": "^2.4.0"
44 | },
45 | "jest": {
46 | "testFileExtensions": [
47 | "json",
48 | "js",
49 | "jsx"
50 | ],
51 | "testPathIgnorePatterns": [
52 | "/node_modules/",
53 | "/preprocessor.js"
54 | ],
55 | "scriptPreprocessor": "
/__tests__/preprocessor.js",
56 | "unmockedModulePathPatterns": [
57 | "/node_modules/react"
58 | ]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/admin/style/app/_body.scss:
--------------------------------------------------------------------------------
1 | body {
2 | overflow-x: hidden;
3 | user-select: none;
4 | cursor: default;
5 |
6 | position: fixed;
7 | top: 0;
8 | bottom: 0;
9 | width: 100%;
10 |
11 | color: $color_fg;
12 | background: $color_bg;
13 | }
14 |
15 | .route {
16 | position: fixed;
17 | top: $header_height;
18 | bottom: 0;
19 | width: 100%;
20 | }
21 |
--------------------------------------------------------------------------------
/admin/style/app/_browser.scss:
--------------------------------------------------------------------------------
1 | .route.browser {
2 |
3 | > div {
4 | position: absolute;
5 | }
6 |
7 | .browser-sidebar {
8 | top: 0;
9 | left: 0;
10 | bottom: 0;
11 | width: 200px;
12 | }
13 |
14 | .browser-table {
15 | top: 0;
16 | left: 200px;
17 | right: 0;
18 | bottom: 0;
19 | }
20 | }
21 |
22 | .browser-sidebar {
23 | background: $color_b0;
24 | padding: 20px;
25 | box-sizing: border-box;
26 | border-right: 2px solid $color_b1;
27 |
28 | ul {
29 | padding: 0;
30 | margin: 0;
31 | }
32 |
33 | li {
34 | list-style-type: none;
35 | font-weight: $semibold;
36 | color: $color_c;
37 | font-size: 14px;
38 | line-height: 26px;
39 | cursor: pointer;
40 |
41 | &:hover {
42 | text-decoration: underline;
43 | }
44 | }
45 | }
46 |
47 | .browser-table {
48 | padding: 20px;
49 | overflow-y: auto;
50 |
51 | table {
52 | width: 100%;
53 | }
54 |
55 | th {
56 | color: $color_b3;
57 | }
58 |
59 | td {
60 | color: $color_f3;
61 | }
62 |
63 | td, th {
64 | text-align: left;
65 | font-size: 14px;
66 | line-height: 45px;
67 | }
68 |
69 | tr {
70 | border-bottom: 2px solid $color_b1;
71 | }
72 |
73 | tbody tr {
74 | cursor: default;
75 |
76 | &:hover {
77 | background: $color_b0;
78 | }
79 | }
80 |
81 | tr.selected {
82 | &, &:hover {
83 | background: $color_c;
84 |
85 | td {
86 | color: $color_bg;
87 | }
88 | }
89 |
90 | border-bottom-color: $color_bg;
91 | }
92 |
93 | td.name {
94 | color: $color_fg;
95 | font-weight: $semibold;
96 | font-size: 15px;
97 | }
98 |
99 | td.type .icon:before {
100 | font-size: 36px;
101 | color: $color_c;
102 | text-align: center;
103 | width: 50px;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/admin/style/app/_editor.scss:
--------------------------------------------------------------------------------
1 | $sidebar_width: 300px;
2 |
3 | .editor-metadata {
4 | position: absolute;
5 |
6 | top: 0;
7 | left: 0;
8 | bottom: 0;
9 | width: $sidebar_width;
10 |
11 | overflow-y: auto;
12 |
13 | background: $color_b0;
14 | padding: 20px;
15 | border-right: 2px solid $color_b1;
16 | box-sizing: border-box;
17 | }
18 |
19 | .editor-content {
20 | position: absolute;
21 |
22 | top: 0;
23 | left: $sidebar_width;
24 | right: 0;
25 | bottom: 0;
26 |
27 | box-sizing: border-box;
28 | }
29 |
30 | .editor-images {
31 | position: absolute;
32 | overflow-y: auto;
33 |
34 | top: 0;
35 | left: $sidebar_width;
36 | right: 0;
37 | bottom: 0;
38 |
39 | box-sizing: border-box;
40 |
41 | .images {
42 | padding: 20px;
43 | }
44 |
45 | .image {
46 | @include column(1/4, $cycle: 4);
47 |
48 | img {
49 | width: 100%;
50 | height: auto;
51 | }
52 | }
53 | }
54 |
55 | .editor-code {
56 | position: absolute;
57 | top: 0;
58 | left: 0;
59 | right: 0;
60 | bottom: 0;
61 |
62 | max-width: 800px;
63 | }
64 |
65 | .editor-preview {
66 | position: absolute;
67 | top: 0;
68 | left: 50%;
69 | width: 50%;
70 | bottom: 0;
71 |
72 | overflow-y: auto;
73 |
74 | padding: 0 1em;
75 | border-left: 2px solid $color_b1;
76 | box-sizing: border-box;
77 |
78 | font-size: 16px;
79 | line-height: 24px;
80 |
81 | blockquote {
82 | border-left: 4px solid $color_a;
83 | margin-left: 0;
84 | padding-left: 20px;
85 | }
86 |
87 | a {
88 | color: $color_c;
89 | }
90 |
91 | h2 {
92 | margin: 0;
93 | text-transform: none;
94 | font-size: 20px;
95 | font-weight: $bold;
96 | }
97 |
98 | }
99 |
100 | .CodeMirror {
101 | position: absolute;
102 |
103 | top: 0;
104 | left: 0;
105 | right: 0;
106 | bottom: 0;
107 |
108 | padding: 20px;
109 | height: auto;
110 |
111 | color: $color_f0;
112 | font-family: 'firamono', monospace;
113 | font-size: 14px;
114 | line-height: 24px;
115 | background: $color_bg;
116 | }
117 |
118 | .cm-header, .cm-strong {
119 | font-weight: bold;
120 | }
121 |
--------------------------------------------------------------------------------
/admin/style/app/_header.scss:
--------------------------------------------------------------------------------
1 | .app-header {
2 | $padding: 20px;
3 | $border: 2px;
4 | $line_height: $header_height - ($padding * 2) - $border;
5 |
6 | background: $color_fg;
7 | padding: $padding;
8 | border-bottom: 2px solid $color_b1;
9 | box-sizing: border-box;
10 |
11 | position: fixed;
12 | top: 0;
13 | width: 100%;
14 |
15 | @include cf();
16 |
17 | nav {
18 | float: left;
19 | }
20 |
21 | h1 {
22 | margin: 0;
23 | }
24 |
25 | h1, span {
26 | $sep_width: 20px;
27 |
28 | color: $color_bg;
29 | font-size: 20px;
30 | float: left;
31 | font-weight: $bold;
32 | position: relative;
33 |
34 | padding: 0 10px;
35 | margin-right: $sep_width;
36 |
37 | transition: background 0.15s;
38 |
39 | &:hover {
40 | background: $color_f0;
41 | }
42 |
43 | &:active {
44 | box-shadow: inset 0 1px 3px rgba(#000, 0.1);
45 | transition-duration: 0;
46 | background: $color_f0;
47 | }
48 |
49 | &:after {
50 | display: block;
51 | position: absolute;
52 | right: -$sep_width;
53 | top: 0;
54 |
55 | content: '\e805';
56 | width: $sep_width;
57 | text-align: center;
58 |
59 | font-family: 'rango';
60 | font-size: 25px;
61 | color: $color_b;
62 | }
63 |
64 | &:last-child {
65 | margin-right: 0;
66 |
67 | &:after {
68 | content: none;
69 | }
70 | }
71 | }
72 |
73 | h1, span, .button {
74 | line-height: $line_height;
75 | }
76 |
77 | .button-group {
78 | float: right;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/admin/style/main.scss:
--------------------------------------------------------------------------------
1 | $color_bg: #FFFFFF;
2 | $color_fg: #2B2B28;
3 |
4 | $color_a: #AA8C81;
5 | $color_b: #9E2D4A;
6 | $color_c: #25ABB0;
7 |
8 | $color_b0: mix($color_fg, $color_bg, 5);
9 | $color_b1: mix($color_fg, $color_bg, 10);
10 | $color_b2: mix($color_fg, $color_bg, 20);
11 | $color_b3: mix($color_fg, $color_bg, 40);
12 |
13 | $color_f0: mix($color_bg, $color_fg, 5);
14 | $color_f1: mix($color_bg, $color_fg, 10);
15 | $color_f2: mix($color_bg, $color_fg, 20);
16 | $color_f3: mix($color_bg, $color_fg, 40);
17 |
18 | $header_height: 82px;
19 |
20 | $regular: 400;
21 | $semibold: 500;
22 | $bold: 700;
23 |
24 | // Vendor
25 | @import 'vendor/normalize';
26 | @import 'vendor/jeet/index';
27 | @import 'vendor/codemirror/index';
28 | @import 'vendor/codemirror/base16-light';
29 |
30 | // Modules
31 | @import 'modules/functions';
32 | @import 'modules/mixins';
33 | @import 'modules/extends';
34 | @import 'modules/buttons';
35 | @import 'modules/type';
36 | @import 'modules/form';
37 | @import 'modules/icon_font';
38 | @import 'modules/sortable';
39 |
40 | // App
41 | @import 'app/body';
42 | @import 'app/header';
43 | @import 'app/browser';
44 | @import 'app/editor';
45 |
--------------------------------------------------------------------------------
/admin/style/modules/_buttons.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | $color: $color_a;
3 |
4 | background: $color;
5 |
6 | border: 0;
7 | outline: 0;
8 |
9 | color: $color_bg;
10 | font-weight: $semibold;
11 | font-size: 14px;
12 |
13 | line-height: 40px;
14 | padding: 0 40px;
15 | border-radius: 40px;
16 |
17 | transition: background 0.15s;
18 |
19 | &:hover {
20 | background: lighten($color, 5);
21 | }
22 |
23 | &:active {
24 | color: darken($color, 20);
25 | background: $color;
26 | box-shadow: inset 0 1px 3px rgba(#000, 0.3);
27 | transition-duration: 0;
28 | }
29 | }
30 |
31 | .button-primary {
32 | $color: $color_b;
33 |
34 | background: $color;
35 |
36 | &:hover {
37 | background: lighten($color, 5);
38 | }
39 |
40 | &:active {
41 | color: darken($color, 20);
42 | background: $color;
43 | }
44 | }
45 |
46 | .button-group {
47 | .button {
48 | margin-left: 10px;
49 |
50 | &:first-child {
51 | margin-left: 0;
52 | }
53 | }
54 | }
55 |
56 | .button[disabled] {
57 | background: transparent;
58 | color: rgba(#fff, 0.2);
59 | box-shadow: 0 0 0 2px rgba(#fff, 0.1);
60 | }
61 |
--------------------------------------------------------------------------------
/admin/style/modules/_extends.scss:
--------------------------------------------------------------------------------
1 | #group:after {
2 | content: "";
3 | display: table;
4 | clear: both;
5 | }
6 |
7 | #scroll {
8 | overflow-y: scroll;
9 | overflow-x: hidden;
10 | -webkit-overflow-scrolling: touch;
11 |
12 | &::-webkit-scrollbar {
13 | width: 10px;
14 | }
15 |
16 | &::-webkit-scrollbar-track {
17 | background-color: transparent !important;
18 | }
19 |
20 | &::-webkit-scrollbar-track-piece {
21 | background-color: transparent !important;
22 | }
23 |
24 | &::-webkit-scrollbar-thumb {
25 | background-color: rgba(#000, 0.5);
26 | border: 0;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/admin/style/modules/_form.scss:
--------------------------------------------------------------------------------
1 | .input {
2 | margin-bottom: 10px;
3 |
4 | label {
5 | display: block;
6 | color: $color_b3;
7 | font-weight: $semibold;
8 | font-size: 12px;
9 | line-height: 21px;
10 | text-transform: uppercase;
11 | }
12 | }
13 |
14 | .input-text {
15 | input {
16 | width: 100%;
17 | }
18 | }
19 |
20 | .input-checkbox {
21 | @include cf();
22 |
23 | input {
24 | float: left;
25 | margin-right: 10px;
26 | }
27 | label {
28 | float: left;
29 | line-height: 16px;
30 | }
31 | }
32 |
33 | input[type=text],
34 | input[type=password],
35 | input[type=date],
36 | input[type=email] {
37 | display: block;
38 | border: 2px solid $color_b2;
39 | height: 38px;
40 | line-height: 19px;
41 |
42 | padding: 0 10px;
43 | font-size: 14px;
44 | outline: 0;
45 | box-sizing: border-box;
46 |
47 | &:focus {
48 | border-color: $color_c;
49 | }
50 | }
51 |
52 | input[type=checkbox] {
53 | -webkit-appearance: none;
54 | width: 16px;
55 | height: 16px;
56 | background: $color_b2;
57 | border: 0;
58 | outline: 0;
59 | position: relative;
60 |
61 | &:checked {
62 | background: $color_c;
63 |
64 | &:after {
65 | content: '✓';
66 | position: absolute;
67 | top: 0;
68 | left: 0;
69 | font-size: 25px;
70 | color: $color_bg;
71 | font-family: 'rango';
72 | line-height: 16px;
73 | width: 16px;
74 | text-align: center;
75 | }
76 | }
77 | }
78 |
79 | .textarea {
80 | textarea {
81 | width: 100%;
82 |
83 | padding: 20px;
84 | box-sizing: border-box;
85 | border: 2px solid $color_b2;
86 | font-family: 'source code pro', monaco, monospace;
87 | font-size: 14px;
88 | outline: 0;
89 |
90 | &:focus {
91 | border-color: $color_b2;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/admin/style/modules/_functions.scss:
--------------------------------------------------------------------------------
1 | @function readable ($bg, $fg_a, $fg_b) {
2 |
3 | @if(lightness($fg_a) < lightness($fg_b)) {
4 | $dark: $fg_a;
5 | $light: $fg_b;
6 | } @else {
7 | $dark: $fg_b;
8 | $light: $fg_a;
9 | }
10 |
11 | @if (lightness($bg) < 50) {
12 | @return $light;
13 | } @else {
14 | @return $dark;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/admin/style/modules/_icon_font.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'rango';
3 | src: url('font/rango.eot?10872707');
4 | src: url('font/rango.eot?10872707#iefix') format('embedded-opentype'),
5 | url('font/rango.woff?10872707') format('woff'),
6 | url('font/rango.ttf?10872707') format('truetype'),
7 | url('font/rango.svg?10872707#rango') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
12 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
13 | /*
14 | @media screen and (-webkit-min-device-pixel-ratio:0) {
15 | @font-face {
16 | font-family: 'rango';
17 | src: url('../font/rango.svg?10872707#rango') format('svg');
18 | }
19 | }
20 | */
21 |
22 | [class^="icon-"]:before, [class*=" icon-"]:before {
23 | font-family: "rango";
24 | font-style: normal;
25 | font-weight: normal;
26 | speak: none;
27 |
28 | display: inline-block;
29 | text-decoration: inherit;
30 | width: 1em;
31 | margin-right: .2em;
32 | text-align: center;
33 | /* opacity: .8; */
34 |
35 | /* For safety - reset parent styles, that can break glyph codes*/
36 | font-variant: normal;
37 | text-transform: none;
38 |
39 | /* fix buttons height, for twitter bootstrap */
40 | line-height: 1em;
41 |
42 | /* Animation center compensation - margins should be symmetric */
43 | /* remove if not needed */
44 | margin-left: .2em;
45 |
46 | /* you can be more comfortable with increased icons size */
47 | /* font-size: 120%; */
48 |
49 | /* Uncomment for 3D effect */
50 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
51 | }
52 |
53 | .icon-doc-text:before { content: '\e800'; } /* '' */
54 | .icon-folder:before { content: '\e803'; } /* '' */
55 | .icon-right:before { content: '\e805'; } /* '' */
56 | .icon-left:before { content: '\e806'; } /* '' */
57 | .icon-up:before { content: '\e807'; } /* '' */
58 | .icon-down:before { content: '\e808'; } /* '' */
59 |
--------------------------------------------------------------------------------
/admin/style/modules/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin pos($top:auto, $right:auto, $bottom:auto, $left:auto) {
2 | position: absolute;
3 | top: $top;
4 | right: $right;
5 | bottom: $bottom;
6 | left: $left;
7 | }
8 |
--------------------------------------------------------------------------------
/admin/style/modules/_sortable.scss:
--------------------------------------------------------------------------------
1 | /* pre-built style */
2 | .ui-sortable {
3 | position: relative;
4 | display: block;
5 | overflow: visible;
6 | -webkit-user-select: none;
7 | -moz-user-select: none;
8 | user-select: none;
9 | }
10 |
11 | .ui-sortable:before,
12 | .ui-sortable:after{
13 | content: " ";
14 | display: table;
15 | }
16 |
17 | .ui-sortable:after{
18 | clear: both;
19 | }
20 |
21 | .ui-sortable .ui-sortable-item {
22 | float: left;
23 | cursor: move;
24 | }
25 |
26 | .ui-sortable .ui-sortable-item.ui-sortable-dragging {
27 | position: absolute;
28 | z-index: 1688;
29 | }
30 |
31 | .ui-sortable .ui-sortable-placeholder {
32 | display: none;
33 | }
34 |
35 | .ui-sortable .ui-sortable-placeholder.visible {
36 | display: block;
37 | z-index: -1;
38 | }
39 |
40 | /* custom style */
41 | .ui-sortable .ui-sortable-placeholder.visible {
42 | opacity: .4;
43 | border: 2px #ccc dashed;
44 | box-sizing: border-box;
45 | }
46 |
--------------------------------------------------------------------------------
/admin/style/modules/_type.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'firasans', 'helvetica neue', helvetica, sans-serif;
3 | color: $color_fg;
4 | }
5 |
6 | h1 {
7 | font-size: 24px;
8 | line-height: 36px;
9 | }
10 |
11 | h2 {
12 | font-size: 16px;
13 | line-height: 24px;
14 | font-weight: 600;
15 | margin: 0 0 20px;
16 | text-transform: uppercase;
17 | }
18 |
19 | hr {
20 | background: $color_b1;
21 | height: 2px;
22 | border: 0;
23 | outline: 0;
24 | }
25 |
--------------------------------------------------------------------------------
/admin/style/vendor/_normalize.scss:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.1 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
29 | * Correct `block` display not defined for `main` in IE 11.
30 | */
31 |
32 | article,
33 | aside,
34 | details,
35 | figcaption,
36 | figure,
37 | footer,
38 | header,
39 | hgroup,
40 | main,
41 | nav,
42 | section,
43 | summary {
44 | display: block;
45 | }
46 |
47 | /**
48 | * 1. Correct `inline-block` display not defined in IE 8/9.
49 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
50 | */
51 |
52 | audio,
53 | canvas,
54 | progress,
55 | video {
56 | display: inline-block; /* 1 */
57 | vertical-align: baseline; /* 2 */
58 | }
59 |
60 | /**
61 | * Prevent modern browsers from displaying `audio` without controls.
62 | * Remove excess height in iOS 5 devices.
63 | */
64 |
65 | audio:not([controls]) {
66 | display: none;
67 | height: 0;
68 | }
69 |
70 | /**
71 | * Address `[hidden]` styling not present in IE 8/9/10.
72 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
73 | */
74 |
75 | [hidden],
76 | template {
77 | display: none;
78 | }
79 |
80 | /* Links
81 | ========================================================================== */
82 |
83 | /**
84 | * Remove the gray background color from active links in IE 10.
85 | */
86 |
87 | a {
88 | background: transparent;
89 | }
90 |
91 | /**
92 | * Improve readability when focused and also mouse hovered in all browsers.
93 | */
94 |
95 | a:active,
96 | a:hover {
97 | outline: 0;
98 | }
99 |
100 | /* Text-level semantics
101 | ========================================================================== */
102 |
103 | /**
104 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
105 | */
106 |
107 | abbr[title] {
108 | border-bottom: 1px dotted;
109 | }
110 |
111 | /**
112 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
113 | */
114 |
115 | b,
116 | strong {
117 | font-weight: bold;
118 | }
119 |
120 | /**
121 | * Address styling not present in Safari and Chrome.
122 | */
123 |
124 | dfn {
125 | font-style: italic;
126 | }
127 |
128 | /**
129 | * Address variable `h1` font-size and margin within `section` and `article`
130 | * contexts in Firefox 4+, Safari, and Chrome.
131 | */
132 |
133 | h1 {
134 | font-size: 2em;
135 | margin: 0.67em 0;
136 | }
137 |
138 | /**
139 | * Address styling not present in IE 8/9.
140 | */
141 |
142 | mark {
143 | background: #ff0;
144 | color: #000;
145 | }
146 |
147 | /**
148 | * Address inconsistent and variable font size in all browsers.
149 | */
150 |
151 | small {
152 | font-size: 80%;
153 | }
154 |
155 | /**
156 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
157 | */
158 |
159 | sub,
160 | sup {
161 | font-size: 75%;
162 | line-height: 0;
163 | position: relative;
164 | vertical-align: baseline;
165 | }
166 |
167 | sup {
168 | top: -0.5em;
169 | }
170 |
171 | sub {
172 | bottom: -0.25em;
173 | }
174 |
175 | /* Embedded content
176 | ========================================================================== */
177 |
178 | /**
179 | * Remove border when inside `a` element in IE 8/9/10.
180 | */
181 |
182 | img {
183 | border: 0;
184 | }
185 |
186 | /**
187 | * Correct overflow not hidden in IE 9/10/11.
188 | */
189 |
190 | svg:not(:root) {
191 | overflow: hidden;
192 | }
193 |
194 | /* Grouping content
195 | ========================================================================== */
196 |
197 | /**
198 | * Address margin not present in IE 8/9 and Safari.
199 | */
200 |
201 | figure {
202 | margin: 1em 40px;
203 | }
204 |
205 | /**
206 | * Address differences between Firefox and other browsers.
207 | */
208 |
209 | hr {
210 | -moz-box-sizing: content-box;
211 | box-sizing: content-box;
212 | height: 0;
213 | }
214 |
215 | /**
216 | * Contain overflow in all browsers.
217 | */
218 |
219 | pre {
220 | overflow: auto;
221 | }
222 |
223 | /**
224 | * Address odd `em`-unit font size rendering in all browsers.
225 | */
226 |
227 | code,
228 | kbd,
229 | pre,
230 | samp {
231 | font-family: monospace, monospace;
232 | font-size: 1em;
233 | }
234 |
235 | /* Forms
236 | ========================================================================== */
237 |
238 | /**
239 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
240 | * styling of `select`, unless a `border` property is set.
241 | */
242 |
243 | /**
244 | * 1. Correct color not being inherited.
245 | * Known issue: affects color of disabled elements.
246 | * 2. Correct font properties not being inherited.
247 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
248 | */
249 |
250 | button,
251 | input,
252 | optgroup,
253 | select,
254 | textarea {
255 | color: inherit; /* 1 */
256 | font: inherit; /* 2 */
257 | margin: 0; /* 3 */
258 | }
259 |
260 | /**
261 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
262 | */
263 |
264 | button {
265 | overflow: visible;
266 | }
267 |
268 | /**
269 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
270 | * All other form control elements do not inherit `text-transform` values.
271 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
272 | * Correct `select` style inheritance in Firefox.
273 | */
274 |
275 | button,
276 | select {
277 | text-transform: none;
278 | }
279 |
280 | /**
281 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
282 | * and `video` controls.
283 | * 2. Correct inability to style clickable `input` types in iOS.
284 | * 3. Improve usability and consistency of cursor style between image-type
285 | * `input` and others.
286 | */
287 |
288 | button,
289 | html input[type="button"], /* 1 */
290 | input[type="reset"],
291 | input[type="submit"] {
292 | -webkit-appearance: button; /* 2 */
293 | cursor: pointer; /* 3 */
294 | }
295 |
296 | /**
297 | * Re-set default cursor for disabled elements.
298 | */
299 |
300 | button[disabled],
301 | html input[disabled] {
302 | cursor: default;
303 | }
304 |
305 | /**
306 | * Remove inner padding and border in Firefox 4+.
307 | */
308 |
309 | button::-moz-focus-inner,
310 | input::-moz-focus-inner {
311 | border: 0;
312 | padding: 0;
313 | }
314 |
315 | /**
316 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
317 | * the UA stylesheet.
318 | */
319 |
320 | input {
321 | line-height: normal;
322 | }
323 |
324 | /**
325 | * It's recommended that you don't attempt to style these elements.
326 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
327 | *
328 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
329 | * 2. Remove excess padding in IE 8/9/10.
330 | */
331 |
332 | input[type="checkbox"],
333 | input[type="radio"] {
334 | box-sizing: border-box; /* 1 */
335 | padding: 0; /* 2 */
336 | }
337 |
338 | /**
339 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
340 | * `font-size` values of the `input`, it causes the cursor style of the
341 | * decrement button to change from `default` to `text`.
342 | */
343 |
344 | input[type="number"]::-webkit-inner-spin-button,
345 | input[type="number"]::-webkit-outer-spin-button {
346 | height: auto;
347 | }
348 |
349 | /**
350 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
351 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
352 | * (include `-moz` to future-proof).
353 | */
354 |
355 | input[type="search"] {
356 | -webkit-appearance: textfield; /* 1 */
357 | -moz-box-sizing: content-box;
358 | -webkit-box-sizing: content-box; /* 2 */
359 | box-sizing: content-box;
360 | }
361 |
362 | /**
363 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
364 | * Safari (but not Chrome) clips the cancel button when the search input has
365 | * padding (and `textfield` appearance).
366 | */
367 |
368 | input[type="search"]::-webkit-search-cancel-button,
369 | input[type="search"]::-webkit-search-decoration {
370 | -webkit-appearance: none;
371 | }
372 |
373 | /**
374 | * Define consistent border, margin, and padding.
375 | */
376 |
377 | fieldset {
378 | border: 1px solid #c0c0c0;
379 | margin: 0 2px;
380 | padding: 0.35em 0.625em 0.75em;
381 | }
382 |
383 | /**
384 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
385 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
386 | */
387 |
388 | legend {
389 | border: 0; /* 1 */
390 | padding: 0; /* 2 */
391 | }
392 |
393 | /**
394 | * Remove default vertical scrollbar in IE 8/9/10/11.
395 | */
396 |
397 | textarea {
398 | overflow: auto;
399 | }
400 |
401 | /**
402 | * Don't inherit the `font-weight` (applied by a rule above).
403 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
404 | */
405 |
406 | optgroup {
407 | font-weight: bold;
408 | }
409 |
410 | /* Tables
411 | ========================================================================== */
412 |
413 | /**
414 | * Remove most spacing between table cells.
415 | */
416 |
417 | table {
418 | border-collapse: collapse;
419 | border-spacing: 0;
420 | }
421 |
422 | td,
423 | th {
424 | padding: 0;
425 | }
426 |
--------------------------------------------------------------------------------
/admin/style/vendor/codemirror/_base16-light.scss:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Name: Base16 Default Light
4 | Author: Chris Kempson (http://chriskempson.com)
5 |
6 | CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-chrome-devtools)
7 | Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
8 |
9 | */
10 |
11 | .cm-s-base16-light.CodeMirror {background: #f5f5f5; color: #202020;}
12 | .cm-s-base16-light div.CodeMirror-selected {background: #e0e0e0 !important;}
13 | .cm-s-base16-light .CodeMirror-gutters {background: #f5f5f5; border-right: 0px;}
14 | .cm-s-base16-light .CodeMirror-guttermarker { color: #ac4142; }
15 | .cm-s-base16-light .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
16 | .cm-s-base16-light .CodeMirror-linenumber {color: #b0b0b0;}
17 | .cm-s-base16-light .CodeMirror-cursor {border-left: 1px solid #505050 !important;}
18 |
19 | .cm-s-base16-light span.cm-comment {color: #8f5536;}
20 | .cm-s-base16-light span.cm-atom {color: #aa759f;}
21 | .cm-s-base16-light span.cm-number {color: #aa759f;}
22 |
23 | .cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute {color: #90a959;}
24 | .cm-s-base16-light span.cm-keyword {color: #ac4142;}
25 | .cm-s-base16-light span.cm-string {color: #f4bf75;}
26 |
27 | .cm-s-base16-light span.cm-variable {color: #90a959;}
28 | .cm-s-base16-light span.cm-variable-2 {color: #6a9fb5;}
29 | .cm-s-base16-light span.cm-def {color: #d28445;}
30 | .cm-s-base16-light span.cm-bracket {color: #202020;}
31 | .cm-s-base16-light span.cm-tag {color: #ac4142;}
32 | .cm-s-base16-light span.cm-link {color: #aa759f;}
33 | .cm-s-base16-light span.cm-error {background: #ac4142; color: #505050;}
34 |
35 | .cm-s-base16-light .CodeMirror-activeline-background {background: #DDDCDC !important;}
36 | .cm-s-base16-light .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;}
37 |
--------------------------------------------------------------------------------
/admin/style/vendor/codemirror/_index.scss:
--------------------------------------------------------------------------------
1 | /* BASICS */
2 |
3 | .CodeMirror {
4 | /* Set height, width, borders, and global font properties here */
5 | font-family: monospace;
6 | height: 300px;
7 | }
8 | .CodeMirror-scroll {
9 | /* Set scrolling behaviour here */
10 | overflow: auto;
11 | }
12 |
13 | /* PADDING */
14 |
15 | .CodeMirror-lines {
16 | padding: 4px 0; /* Vertical padding around content */
17 | }
18 | .CodeMirror pre {
19 | padding: 0 4px; /* Horizontal padding of content */
20 | }
21 |
22 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
23 | background-color: white; /* The little square between H and V scrollbars */
24 | }
25 |
26 | /* GUTTER */
27 |
28 | .CodeMirror-gutters {
29 | border-right: 1px solid #ddd;
30 | background-color: #f7f7f7;
31 | white-space: nowrap;
32 | }
33 | .CodeMirror-linenumbers {}
34 | .CodeMirror-linenumber {
35 | padding: 0 3px 0 5px;
36 | min-width: 20px;
37 | text-align: right;
38 | color: #999;
39 | -moz-box-sizing: content-box;
40 | box-sizing: content-box;
41 | }
42 |
43 | .CodeMirror-guttermarker { color: black; }
44 | .CodeMirror-guttermarker-subtle { color: #999; }
45 |
46 | /* CURSOR */
47 |
48 | .CodeMirror div.CodeMirror-cursor {
49 | border-left: 1px solid black;
50 | }
51 | /* Shown when moving in bi-directional text */
52 | .CodeMirror div.CodeMirror-secondarycursor {
53 | border-left: 1px solid silver;
54 | }
55 | .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
56 | width: auto;
57 | border: 0;
58 | background: #7e7;
59 | }
60 | .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursors {
61 | z-index: 1;
62 | }
63 |
64 | .cm-animate-fat-cursor {
65 | width: auto;
66 | border: 0;
67 | -webkit-animation: blink 1.06s steps(1) infinite;
68 | -moz-animation: blink 1.06s steps(1) infinite;
69 | animation: blink 1.06s steps(1) infinite;
70 | }
71 | @-moz-keyframes blink {
72 | 0% { background: #7e7; }
73 | 50% { background: none; }
74 | 100% { background: #7e7; }
75 | }
76 | @-webkit-keyframes blink {
77 | 0% { background: #7e7; }
78 | 50% { background: none; }
79 | 100% { background: #7e7; }
80 | }
81 | @keyframes blink {
82 | 0% { background: #7e7; }
83 | 50% { background: none; }
84 | 100% { background: #7e7; }
85 | }
86 |
87 | /* Can style cursor different in overwrite (non-insert) mode */
88 | div.CodeMirror-overwrite div.CodeMirror-cursor {}
89 |
90 | .cm-tab { display: inline-block; text-decoration: inherit; }
91 |
92 | .CodeMirror-ruler {
93 | border-left: 1px solid #ccc;
94 | position: absolute;
95 | }
96 |
97 | /* DEFAULT THEME */
98 |
99 | .cm-s-default .cm-keyword {color: #708;}
100 | .cm-s-default .cm-atom {color: #219;}
101 | .cm-s-default .cm-number {color: #164;}
102 | .cm-s-default .cm-def {color: #00f;}
103 | .cm-s-default .cm-variable,
104 | .cm-s-default .cm-punctuation,
105 | .cm-s-default .cm-property,
106 | .cm-s-default .cm-operator {}
107 | .cm-s-default .cm-variable-2 {color: #05a;}
108 | .cm-s-default .cm-variable-3 {color: #085;}
109 | .cm-s-default .cm-comment {color: #a50;}
110 | .cm-s-default .cm-string {color: #a11;}
111 | .cm-s-default .cm-string-2 {color: #f50;}
112 | .cm-s-default .cm-meta {color: #555;}
113 | .cm-s-default .cm-qualifier {color: #555;}
114 | .cm-s-default .cm-builtin {color: #30a;}
115 | .cm-s-default .cm-bracket {color: #997;}
116 | .cm-s-default .cm-tag {color: #170;}
117 | .cm-s-default .cm-attribute {color: #00c;}
118 | .cm-s-default .cm-header {color: blue;}
119 | .cm-s-default .cm-quote {color: #090;}
120 | .cm-s-default .cm-hr {color: #999;}
121 | .cm-s-default .cm-link {color: #00c;}
122 |
123 | .cm-negative {color: #d44;}
124 | .cm-positive {color: #292;}
125 | .cm-header, .cm-strong {font-weight: bold;}
126 | .cm-em {font-style: italic;}
127 | .cm-link {text-decoration: underline;}
128 |
129 | .cm-s-default .cm-error {color: #f00;}
130 | .cm-invalidchar {color: #f00;}
131 |
132 | /* Default styles for common addons */
133 |
134 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
135 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
136 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
137 | .CodeMirror-activeline-background {background: #e8f2ff;}
138 |
139 | /* STOP */
140 |
141 | /* The rest of this file contains styles related to the mechanics of
142 | the editor. You probably shouldn't touch them. */
143 |
144 | .CodeMirror {
145 | line-height: 1;
146 | position: relative;
147 | overflow: hidden;
148 | background: white;
149 | color: black;
150 | }
151 |
152 | .CodeMirror-scroll {
153 | /* 30px is the magic margin used to hide the element's real scrollbars */
154 | /* See overflow: hidden in .CodeMirror */
155 | margin-bottom: -30px; margin-right: -30px;
156 | padding-bottom: 30px;
157 | height: 100%;
158 | outline: none; /* Prevent dragging from highlighting the element */
159 | position: relative;
160 | -moz-box-sizing: content-box;
161 | box-sizing: content-box;
162 | }
163 | .CodeMirror-sizer {
164 | position: relative;
165 | border-right: 30px solid transparent;
166 | -moz-box-sizing: content-box;
167 | box-sizing: content-box;
168 | }
169 |
170 | /* The fake, visible scrollbars. Used to force redraw during scrolling
171 | before actuall scrolling happens, thus preventing shaking and
172 | flickering artifacts. */
173 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
174 | position: absolute;
175 | z-index: 6;
176 | display: none;
177 | }
178 | .CodeMirror-vscrollbar {
179 | right: 0; top: 0;
180 | overflow-x: hidden;
181 | overflow-y: scroll;
182 | }
183 | .CodeMirror-hscrollbar {
184 | bottom: 0; left: 0;
185 | overflow-y: hidden;
186 | overflow-x: scroll;
187 | }
188 | .CodeMirror-scrollbar-filler {
189 | right: 0; bottom: 0;
190 | }
191 | .CodeMirror-gutter-filler {
192 | left: 0; bottom: 0;
193 | }
194 |
195 | .CodeMirror-gutters {
196 | position: absolute; left: 0; top: 0;
197 | padding-bottom: 30px;
198 | z-index: 3;
199 | }
200 | .CodeMirror-gutter {
201 | white-space: normal;
202 | height: 100%;
203 | -moz-box-sizing: content-box;
204 | box-sizing: content-box;
205 | padding-bottom: 30px;
206 | margin-bottom: -32px;
207 | display: inline-block;
208 | /* Hack to make IE7 behave */
209 | *zoom:1;
210 | *display:inline;
211 | }
212 | .CodeMirror-gutter-elt {
213 | position: absolute;
214 | cursor: default;
215 | z-index: 4;
216 | }
217 |
218 | .CodeMirror-lines {
219 | cursor: text;
220 | min-height: 1px; /* prevents collapsing before first draw */
221 | }
222 | .CodeMirror pre {
223 | /* Reset some styles that the rest of the page might have set */
224 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
225 | border-width: 0;
226 | background: transparent;
227 | font-family: inherit;
228 | font-size: inherit;
229 | margin: 0;
230 | white-space: pre;
231 | word-wrap: normal;
232 | line-height: inherit;
233 | color: inherit;
234 | z-index: 2;
235 | position: relative;
236 | overflow: visible;
237 | }
238 | .CodeMirror-wrap pre {
239 | word-wrap: break-word;
240 | white-space: pre-wrap;
241 | word-break: normal;
242 | }
243 |
244 | .CodeMirror-linebackground {
245 | position: absolute;
246 | left: 0; right: 0; top: 0; bottom: 0;
247 | z-index: 0;
248 | }
249 |
250 | .CodeMirror-linewidget {
251 | position: relative;
252 | z-index: 2;
253 | overflow: auto;
254 | }
255 |
256 | .CodeMirror-widget {}
257 |
258 | .CodeMirror-wrap .CodeMirror-scroll {
259 | overflow-x: hidden;
260 | }
261 |
262 | .CodeMirror-measure {
263 | position: absolute;
264 | width: 100%;
265 | height: 0;
266 | overflow: hidden;
267 | visibility: hidden;
268 | }
269 | .CodeMirror-measure pre { position: static; }
270 |
271 | .CodeMirror div.CodeMirror-cursor {
272 | position: absolute;
273 | border-right: none;
274 | width: 0;
275 | }
276 |
277 | div.CodeMirror-cursors {
278 | visibility: hidden;
279 | position: relative;
280 | z-index: 3;
281 | }
282 | .CodeMirror-focused div.CodeMirror-cursors {
283 | visibility: visible;
284 | }
285 |
286 | .CodeMirror-selected { background: #d9d9d9; }
287 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
288 | .CodeMirror-crosshair { cursor: crosshair; }
289 |
290 | .cm-searching {
291 | background: #ffa;
292 | background: rgba(255, 255, 0, .4);
293 | }
294 |
295 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */
296 | .CodeMirror span { *vertical-align: text-bottom; }
297 |
298 | /* Used to force a border model for a node */
299 | .cm-force-border { padding-right: .1px; }
300 |
301 | @media print {
302 | /* Hide the cursor when printing */
303 | .CodeMirror div.CodeMirror-cursors {
304 | visibility: hidden;
305 | }
306 | }
307 |
308 | /* Help users use markselection to safely style text background */
309 | span.CodeMirror-selectedtext { background: none; }
310 |
--------------------------------------------------------------------------------
/admin/style/vendor/codemirror/_solarized.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Solarized theme for code-mirror
3 | http://ethanschoonover.com/solarized
4 | */
5 |
6 | /*
7 | Solarized color pallet
8 | http://ethanschoonover.com/solarized/img/solarized-palette.png
9 | */
10 |
11 | .solarized.base03 { color: #002b36; }
12 | .solarized.base02 { color: #073642; }
13 | .solarized.base01 { color: #586e75; }
14 | .solarized.base00 { color: #657b83; }
15 | .solarized.base0 { color: #839496; }
16 | .solarized.base1 { color: #93a1a1; }
17 | .solarized.base2 { color: #eee8d5; }
18 | .solarized.base3 { color: #fdf6e3; }
19 | .solarized.solar-yellow { color: #b58900; }
20 | .solarized.solar-orange { color: #cb4b16; }
21 | .solarized.solar-red { color: #dc322f; }
22 | .solarized.solar-magenta { color: #d33682; }
23 | .solarized.solar-violet { color: #6c71c4; }
24 | .solarized.solar-blue { color: #268bd2; }
25 | .solarized.solar-cyan { color: #2aa198; }
26 | .solarized.solar-green { color: #859900; }
27 |
28 | /* Color scheme for code-mirror */
29 |
30 | .cm-s-solarized {
31 | line-height: 1.45em;
32 | color-profile: sRGB;
33 | rendering-intent: auto;
34 | }
35 | .cm-s-solarized.cm-s-dark {
36 | color: #839496;
37 | background-color: #002b36;
38 | text-shadow: #002b36 0 1px;
39 | }
40 | .cm-s-solarized.cm-s-light {
41 | background-color: #fdf6e3;
42 | color: #657b83;
43 | text-shadow: #eee8d5 0 1px;
44 | }
45 |
46 | .cm-s-solarized .CodeMirror-widget {
47 | text-shadow: none;
48 | }
49 |
50 |
51 | .cm-s-solarized .cm-keyword { color: #cb4b16 }
52 | .cm-s-solarized .cm-atom { color: #d33682; }
53 | .cm-s-solarized .cm-number { color: #d33682; }
54 | .cm-s-solarized .cm-def { color: #2aa198; }
55 |
56 | .cm-s-solarized .cm-variable { color: #268bd2; }
57 | .cm-s-solarized .cm-variable-2 { color: #b58900; }
58 | .cm-s-solarized .cm-variable-3 { color: #6c71c4; }
59 |
60 | .cm-s-solarized .cm-property { color: #2aa198; }
61 | .cm-s-solarized .cm-operator {color: #6c71c4;}
62 |
63 | .cm-s-solarized .cm-comment { color: #586e75; font-style:italic; }
64 |
65 | .cm-s-solarized .cm-string { color: #859900; }
66 | .cm-s-solarized .cm-string-2 { color: #b58900; }
67 |
68 | .cm-s-solarized .cm-meta { color: #859900; }
69 | .cm-s-solarized .cm-qualifier { color: #b58900; }
70 | .cm-s-solarized .cm-builtin { color: #d33682; }
71 | .cm-s-solarized .cm-bracket { color: #cb4b16; }
72 | .cm-s-solarized .CodeMirror-matchingbracket { color: #859900; }
73 | .cm-s-solarized .CodeMirror-nonmatchingbracket { color: #dc322f; }
74 | .cm-s-solarized .cm-tag { color: #93a1a1 }
75 | .cm-s-solarized .cm-attribute { color: #2aa198; }
76 | .cm-s-solarized .cm-header { color: #586e75; }
77 | .cm-s-solarized .cm-quote { color: #93a1a1; }
78 | .cm-s-solarized .cm-hr {
79 | color: transparent;
80 | border-top: 1px solid #586e75;
81 | display: block;
82 | }
83 | .cm-s-solarized .cm-link { color: #93a1a1; cursor: pointer; }
84 | .cm-s-solarized .cm-special { color: #6c71c4; }
85 | .cm-s-solarized .cm-em {
86 | color: #999;
87 | text-decoration: underline;
88 | text-decoration-style: dotted;
89 | }
90 | .cm-s-solarized .cm-strong { color: #eee; }
91 | .cm-s-solarized .cm-tab:before {
92 | content: "➤"; /*visualize tab character*/
93 | color: #586e75;
94 | position:absolute;
95 | }
96 | .cm-s-solarized .cm-error,
97 | .cm-s-solarized .cm-invalidchar {
98 | color: #586e75;
99 | border-bottom: 1px dotted #dc322f;
100 | }
101 |
102 | .cm-s-solarized.cm-s-dark .CodeMirror-selected {
103 | background: #073642;
104 | }
105 |
106 | .cm-s-solarized.cm-s-light .CodeMirror-selected {
107 | background: #eee8d5;
108 | }
109 |
110 | /* Editor styling */
111 |
112 |
113 |
114 | /* Little shadow on the view-port of the buffer view */
115 | .cm-s-solarized.CodeMirror {
116 | -moz-box-shadow: inset 7px 0 12px -6px #000;
117 | -webkit-box-shadow: inset 7px 0 12px -6px #000;
118 | box-shadow: inset 7px 0 12px -6px #000;
119 | }
120 |
121 | /* Gutter border and some shadow from it */
122 | .cm-s-solarized .CodeMirror-gutters {
123 | border-right: 1px solid;
124 | }
125 |
126 | /* Gutter colors and line number styling based of color scheme (dark / light) */
127 |
128 | /* Dark */
129 | .cm-s-solarized.cm-s-dark .CodeMirror-gutters {
130 | background-color: #002b36;
131 | border-color: #00232c;
132 | }
133 |
134 | .cm-s-solarized.cm-s-dark .CodeMirror-linenumber {
135 | text-shadow: #021014 0 -1px;
136 | }
137 |
138 | /* Light */
139 | .cm-s-solarized.cm-s-light .CodeMirror-gutters {
140 | background-color: #fdf6e3;
141 | border-color: #eee8d5;
142 | }
143 |
144 | /* Common */
145 | .cm-s-solarized .CodeMirror-linenumber {
146 | color: #586e75;
147 | padding: 0 5px;
148 | }
149 | .cm-s-solarized .CodeMirror-guttermarker-subtle { color: #586e75; }
150 | .cm-s-solarized.cm-s-dark .CodeMirror-guttermarker { color: #ddd; }
151 | .cm-s-solarized.cm-s-light .CodeMirror-guttermarker { color: #cb4b16; }
152 |
153 | .cm-s-solarized .CodeMirror-gutter .CodeMirror-gutter-text {
154 | color: #586e75;
155 | }
156 |
157 | .cm-s-solarized .CodeMirror-lines .CodeMirror-cursor {
158 | border-left: 1px solid #819090;
159 | }
160 |
161 | /*
162 | Active line. Negative margin compensates left padding of the text in the
163 | view-port
164 | */
165 | .cm-s-solarized.cm-s-dark .CodeMirror-activeline-background {
166 | background: rgba(255, 255, 255, 0.10);
167 | }
168 | .cm-s-solarized.cm-s-light .CodeMirror-activeline-background {
169 | background: rgba(0, 0, 0, 0.10);
170 | }
171 |
--------------------------------------------------------------------------------
/admin/style/vendor/jeet/_functions.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * List functions courtesy of the wonderful folks at Team Sass.
3 | * Check out their awesome grid: Singularity.
4 | */
5 |
6 | /**
7 | * Get percentage from a given ratio.
8 | * @param {number} [$ratio=1] - The column ratio of the element.
9 | * @returns {number} - The percentage value.
10 | */
11 | @function jeet-get-span($ratio: 1) {
12 | @return $ratio * 100;
13 | }
14 |
15 | /**
16 | * Work out the column widths based on the ratio and gutter sizes.
17 | * @param {number} [$ratios=1] - The column ratio of the element.
18 | * @param {number} [$gutter=$jeet-gutter] - The gutter for the column.
19 | * @returns {list} $width $gutter - A list containing the with and gutter for the element.
20 | */
21 | @function jeet-get-column($ratios: 1, $gutter: $jeet-gutter) {
22 | $ratios: if(not $jeet-parent-first, jeet-reverse($ratios), $ratios);
23 | $width: 100;
24 |
25 | @each $ratio in $ratios {
26 | $gutter: $gutter / $width * 100;
27 | $width: 100 * $ratio - $gutter + $ratio * $gutter;
28 | }
29 |
30 | @return $width $gutter;
31 | }
32 |
33 | /**
34 | * Get the set layout direction for the project.
35 | * @returns {string} $direction - The layout direction.
36 | */
37 | @function jeet-get-layout-direction() {
38 | $direction: if($jeet-layout-direction == "RTL", right, left);
39 |
40 | @return $direction;
41 | }
42 |
43 | /**
44 | * Replace a specified list value with a new value (uses built in set-nth() if available)
45 | * @param {list} $list - The list of values you want to alter.
46 | * @param {number} $index - The index of the list item you want to replace.
47 | * @param {*} $value - The value you want to replace $index with.
48 | * @returns {list} $list - The list with the value replaced or removed.
49 | * @warn if an invalid index is supplied.
50 | */
51 | @function jeet-replace-nth($list, $index, $value) {
52 | // Fallback for Sass 3.2
53 | @if function-exists("set-nth") != true {
54 | $result: ();
55 | $index: if($index < 0, length($list) + $index + 1, $index);
56 |
57 | @for $i from 1 through length($list) {
58 | $result: append($result, if($i == $index, $value, nth($list, $i)));
59 | }
60 |
61 | @return $result;
62 | }
63 |
64 | // Sass 3.3
65 | $result: set-nth($list, $index, $value);
66 |
67 | @return $result;
68 | }
69 |
70 | /**
71 | * Reverse a list (progressively enhanced for Sass 3.3)
72 | * @param {list} $list - The list of values you want to reverse.
73 | * @returns {list} $result - The reversed list.
74 | */
75 | @function jeet-reverse($list) {
76 | // Sass 3.2
77 | @if function-exists("set-nth") != true {
78 | $result: ();
79 |
80 | @for $i from length($list) * -1 through -1 {
81 | $item: nth($list, abs($i));
82 |
83 | @if length($item) > 1 and $recursive {
84 | $result: append($result, jeet-reverse($item, $recursive));
85 | }
86 | @else {
87 | $result: append($result, $item);
88 | }
89 | }
90 |
91 | @return $result;
92 | }
93 |
94 | // Sass 3.3+
95 | @for $i from 1 through ceil(length($list)/2) {
96 | $tmp: nth($list, $i);
97 | $tmp: if(length($tmp) > 1 and $recursive, reverse($tmp, $recursive), $tmp);
98 |
99 | $list: set-nth($list, $i, nth($list, -$i));
100 | $list: set-nth($list, -$i, $tmp);
101 | }
102 |
103 | @return $list;
104 | }
105 |
106 | /**
107 | * Get the opposite direction to a given value.
108 | * @param {string} $dir - The direction you want the opposite of.
109 | * @returns {string} - The opposite direction to $dir.
110 | * @warn if an incorrect string is provided.
111 | */
112 | @function jeet-opposite-direction($direction) {
113 | @if $direction == "left" {
114 | @return right;
115 | } @else if $direction == "right" {
116 | @return left;
117 | } @else if $direction == "top" {
118 | @return bottom;
119 | } @else if $direction == "bottom" {
120 | @return top;
121 | } @else if index("ltr" "LTR", $direction) {
122 | @return rtl;
123 | } @else if index("rtl" "RTL", $direction) {
124 | @return ltr;
125 | } @else {
126 | @warn "`#{$direction}` is not a direction; please make sure your direction is all lowercase.";
127 | @return false;
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/admin/style/vendor/jeet/_grid.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Style an element as a column with a gutter.
3 | * @param {number} [$ratios=1] - A width relative to its container as a fraction.
4 | * @param {number} [$offset=0] - A offset specified as a fraction (see $ratios).
5 | * @param {number} [$cycle=0] - Easily create an nth column grid where $cycle equals the number of columns.
6 | * @param {number} [$uncycle=0] - Undo a previous cycle value to allow for a new one.
7 | * @param {number} [$gutter=$jeet-gutter] - Specify the gutter width as a percentage of the containers width.
8 | */
9 | @mixin column($ratios: 1, $offset: 0, $cycle: 0, $uncycle: 0, $gutter: $jeet-gutter) {
10 | $side: jeet-get-layout-direction();
11 | $opposite-side: jeet-opposite-direction($side);
12 | $column-widths: jeet-get-column($ratios, $gutter);
13 | $margin-last: 0;
14 | $margin-l: $margin-last;
15 | $margin-r: nth($column-widths, 2);
16 |
17 | @if $offset != 0 {
18 | @if $offset < 0 {
19 | $offset: $offset * -1;
20 | $offset: nth(jeet-get-column($offset, nth($column-widths, 2)), 1);
21 | $margin-last: $offset + nth($column-widths, 2) * 2;
22 | $margin-r: $margin-last;
23 | } @else {
24 | $offset: nth(jeet-get-column($offset, nth($column-widths, 2)), 1);
25 | $margin-l: $offset + nth($column-widths, 2);
26 | }
27 | }
28 |
29 | @include cf;
30 | float: $side;
31 | clear: none;
32 | text-align: inherit;
33 | width: nth($column-widths, 1) * 1%;
34 | margin: {
35 | #{$side}: $margin-l * 1%;
36 | #{$opposite-side}: $margin-r * 1%;
37 | };
38 |
39 | @if $uncycle != 0 {
40 | &:nth-child(#{$uncycle}n) {
41 | margin-#{jeet-opposite-direction($side)}: $margin-r * 1%;
42 | float: $side;
43 | }
44 | &:nth-child(#{$uncycle}n + 1) {
45 | clear: none;
46 | }
47 | }
48 |
49 | @if $cycle != 0 {
50 | &:nth-child(#{$cycle}n) {
51 | margin-#{jeet-opposite-direction($side)}: $margin-last * 1%;
52 | float: jeet-opposite-direction($side);
53 | }
54 | &:nth-child(#{$cycle}n + 1) {
55 | clear: both;
56 | }
57 | } @else {
58 | &:last-child {
59 | margin-#{jeet-opposite-direction($side)}: $margin-last * 1%;
60 | }
61 | }
62 | }
63 |
64 | /**
65 | * An alias for the column mixin.
66 | * @param [$args...] - All arguments get passed through to column().
67 | */
68 | @mixin col($args...) {
69 | @include column($args...);
70 | }
71 |
72 | /**
73 | * Get the width of a column and nothing else.
74 | * @param {number} [$ratios=1] - A width relative to its container as a fraction.
75 | * @param {number} [$gutter=$jeet-gutter] - Specify the gutter width as a percentage of the containers width.
76 | */
77 | @function column-width($ratios: 1, $gutter: $jeet-gutter) {
78 | @return unquote(nth(jeet-get-column($ratios, $gutter), 1) + '%');
79 | }
80 |
81 | /**
82 | * Get the gutter size of a column and nothing else.
83 | * @param {number} [ratios=1] - A width relative to its container as a fraction.
84 | * @param {number} [gutter=jeet.gutter] - Specify the gutter width as a percentage of the containers width.
85 | */
86 | @function column-gutter($ratios: 1, $gutter: $jeet-gutter) {
87 | @return unquote(nth(jeet-get-column($ratios, $gutter), 2) + '%');
88 | }
89 |
90 | /**
91 | * An alias for the column-width function.
92 | * @param [$args...] - All arguments get passed through to column().
93 | */
94 | @function cw($args...) {
95 | @return column-width($args...);
96 | }
97 |
98 | /**
99 | * An alias for the column-gutter function.
100 | * @param [$args...] - All arguments get passed through to column().
101 | */
102 | @function cg($args...) {
103 | @return column-gutter($args...);
104 | }
105 |
106 | /**
107 | * Style an element as a column without any gutters for a seamless row.
108 | * @param {number} [$ratios=1] - A width relative to its container as a fraction.
109 | * @param {number} [$offset=0] - A offset specified as a fraction (see $ratios).
110 | * @param {number} [cycle=0] - Easily create an nth column grid where cycle equals the number of columns.
111 | * @param {number} [uncycle=0] - Undo a previous cycle value to allow for a new one.
112 | */
113 | @mixin span($ratio: 1, $offset: 0, $cycle: 0, $uncycle: 0) {
114 | $side: jeet-get-layout-direction();
115 | $opposite-side: jeet-opposite-direction($side);
116 | $span-width: jeet-get-span($ratio);
117 | $margin-r: 0;
118 | $margin-l: $margin-r;
119 | @if $offset != 0 {
120 | @if $offset < 0 {
121 | $offset: $offset * -1;
122 | $margin-r: jeet-get-span($offset);
123 | } @else {
124 | $margin-l: jeet-get-span($offset);
125 | }
126 | }
127 |
128 | @include cf;
129 | float: $side;
130 | clear: none;
131 | text-align: inherit;
132 | width: $span-width * 1%;
133 | margin: {
134 | #{$side}: $margin-l * 1%;
135 | #{$opposite-side}: $margin-r * 1%;
136 | };
137 |
138 | @if $cycle != 0 {
139 | &:nth-child(#{$cycle}n) {
140 | float: $opposite-side;
141 | }
142 | &:nth-child(#{$cycle}n + 1) {
143 | clear: both;
144 | }
145 | }
146 |
147 | @if $uncycle != 0 {
148 | &:nth-child(#{$uncycle}n) {
149 | float: $side;
150 | }
151 | &:nth-child(#{$uncycle}n + 1) {
152 | clear: none;
153 | }
154 | }
155 |
156 | }
157 |
158 | /**
159 | * Reorder columns without altering the HTML.
160 | * @param {number} [$ratios=0] - Specify how far along you want the element to move.
161 | * @param {string} [$col-or-span=column] - Specify whether the element has a gutter or not.
162 | * @param {number} [$gutter=$jeet-gutter] - Specify the gutter width as a percentage of the containers width.
163 | */
164 | @mixin shift($ratios: 0, $col-or-span: column, $gutter: $jeet-gutter) {
165 | $translate: '';
166 | $side: jeet-get-layout-direction();
167 |
168 | @if $side == right {
169 | $ratios: jeet-replace-nth($ratios, 0, nth($ratios, 1) * -1);
170 | }
171 |
172 | @if index("column" "col" "c", $col-or-span) {
173 | $column-widths: jeet-get-column($ratios, $gutter);
174 | $translate: nth($column-widths, 1) + nth($column-widths, 2);
175 | } @else {
176 | $translate: jeet-get-span($ratios);
177 | }
178 |
179 | position: relative;
180 | left: $translate * 1%;
181 | }
182 |
183 | /**
184 | * Reset an element that has had shift() applied to it.
185 | */
186 | @mixin unshift() {
187 | position: static;
188 | left: 0;
189 | }
190 |
191 | /**
192 | * View the grid and its layers for easy debugging.
193 | * @param {string} [$color=black] - The background tint applied.
194 | * @param {boolean} [$important=false] - Whether to apply the style as !important.
195 | */
196 | @mixin edit($color: black, $important: false) {
197 | @if $important {
198 | * {
199 | background: rgba($color, .05) !important;
200 | }
201 | } @else {
202 | * {
203 | background: rgba($color, .05);
204 | }
205 | }
206 | }
207 |
208 | /**
209 | * Alias for edit().
210 | */
211 | @mixin debug() {
212 | @include edit;
213 | }
214 |
215 | /**
216 | * Horizontally center an element.
217 | * @param {number} [$max-width=1410px] - The max width the element can be.
218 | * @param {number} [$pad=0] - Specify the element's left and right padding.
219 | */
220 | @mixin center($max-width: $jeet-max-width, $pad: 0) {
221 | @include cf;
222 | width: auto;
223 | max-width: $max-width;
224 | float: none;
225 | display: block;
226 | margin: {
227 | right: auto;
228 | left: auto;
229 | };
230 | padding: {
231 | left: $pad;
232 | right: $pad;
233 | };
234 | }
235 |
236 | /**
237 | * Uncenter an element.
238 | */
239 | @mixin uncenter() {
240 | max-width: none;
241 | margin-right: 0;
242 | margin-left: 0;
243 | padding-left: 0;
244 | padding-right: 0;
245 | }
246 |
247 | /**
248 | * Stack an element so that nothing is either side of it.
249 | * @param {number} [$pad=0] - Specify the element's left and right padding.
250 | * @param {boolean/string} [$align=false] - Specify the text align for the element.
251 | */
252 | @mixin stack($pad: 0, $align: false) {
253 | $side: jeet-get-layout-direction();
254 | $opposite-side: jeet-opposite-direction($side);
255 |
256 | display: block;
257 | clear: both;
258 | float: none;
259 | width: 100%;
260 | margin: {
261 | left: auto;
262 | right: auto;
263 | };
264 |
265 | &:first-child {
266 | margin-#{$side}: auto;
267 | }
268 |
269 | &:last-child {
270 | margin-#{$opposite-side}: auto;
271 | }
272 |
273 | @if $pad != 0 {
274 | padding: {
275 | left: $pad;
276 | right: $pad;
277 | }
278 | }
279 |
280 | @if ($align is not false) {
281 | @if index("center" "c", $align) {
282 | text-align: center;
283 | } @else if index("left" "l", $align) {
284 | text-align: left;
285 | } @else if index("right" "r", $align) {
286 | text-align: right;
287 | }
288 | }
289 | }
290 |
291 | /**
292 | * Unstack an element.
293 | */
294 | @mixin unstack() {
295 | $side: jeet-get-layout-direction();
296 | $opposite-side: jeet-opposite-direction($side);
297 |
298 | text-align: $side;
299 | display: inline;
300 | clear: none;
301 | width: auto;
302 | margin: {
303 | left: 0;
304 | right: 0;
305 | };
306 |
307 | &:first-child {
308 | margin-#{$side}: 0;
309 | }
310 |
311 | &:last-child {
312 | margin-#{jeet-opposite-direction($side)}: 0;
313 | }
314 | }
315 |
316 | /**
317 | * Center an element on either or both axes.
318 | * @requires A parent container with relative positioning.
319 | * @param {string} [$direction=both] - Specify which axes to center the element on.
320 | */
321 | @mixin align($direction: both) {
322 | position: absolute;
323 | transform-style: preserve-3d;
324 |
325 | @if index("horizontal" "h", $direction) {
326 | left: 50%;
327 | transform: translateX(-50%);
328 | } @else if index("vertical" "v", $direction) {
329 | top: 50%;
330 | transform: translateY(-50%);
331 | } @else {
332 | top: 50%;
333 | left: 50%;
334 | transform: translate(-50%, -50%);
335 | }
336 | }
337 |
338 | /**
339 | * Apply a clearfix to an element.
340 | */
341 | @mixin cf() {
342 | *zoom: 1;
343 |
344 | &:before, &:after {
345 | content: '';
346 | display: table;
347 | }
348 |
349 | &:after {
350 | clear: both;
351 | }
352 | }
353 |
--------------------------------------------------------------------------------
/admin/style/vendor/jeet/_settings.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Grid settings.
3 | * All values are defaults and can therefore be easily overidden.
4 | */
5 |
6 | $jeet-gutter: 3 !default;
7 | $jeet-parent-first: false !default;
8 | $jeet-layout-direction: "LTR" !default;
9 | $jeet-max-width: 1410px !default;
10 |
--------------------------------------------------------------------------------
/admin/style/vendor/jeet/index.scss:
--------------------------------------------------------------------------------
1 | /* Syntax Quick Reference
2 | --------------------------
3 | column($ratios: 1, $offset: 0, $cycle: 0, $uncycle: 0, $gutter: $jeet-gutter)
4 | span($ratio: 1, $offset: 0)
5 | shift($ratios: 0, $col_or_span: column, $gutter: $jeet-gutter)
6 | unshift()
7 | edit()
8 | center($max_width: 1410px, $pad: 0)
9 | stack($pad: 0, $align: false)
10 | unstack()
11 | align($direction: both)
12 | cf()
13 | */
14 |
15 | @import '_settings';
16 | @import '_functions';
17 | @import '_grid';
18 |
--------------------------------------------------------------------------------
/config.toml:
--------------------------------------------------------------------------------
1 | ContentDir = "content"
2 |
3 | # rango
4 | AdminDir = "admin/dist"
5 | AssetsDir = "static/assets"
6 |
7 | title = "That is awesome"
8 |
9 | [rango]
10 | [rango.types]
11 | [rango.types.carousel]
12 | [rango.types.carousel.details]
13 | array = true
14 | type = "text"
15 | [rango.types.carousel.galleries]
16 | array = true
17 | dropdown = ["Current Work", "Previous Work"]
18 | type = "text"
19 | [rango.types.carousel.images]
20 | array = true
21 | type = "image"
22 | [rango.types.carousel.part]
23 | type = "text"
24 | [rango.types.contact]
25 | [rango.types.contact.images]
26 | array = true
27 | type = "image"
28 | [rango.types.default]
29 | [rango.types.default.images]
30 | array = true
31 | type = "image"
32 |
--------------------------------------------------------------------------------
/docs/screenshot_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stayradiated/rango/71c995cba6cef7f0cb387f530474dd796302f4d6/docs/screenshot_1.jpg
--------------------------------------------------------------------------------
/docs/screenshot_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stayradiated/rango/71c995cba6cef7f0cb387f530474dd796302f4d6/docs/screenshot_2.jpg
--------------------------------------------------------------------------------
/errors.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "net/http"
4 |
5 | type apiError struct {
6 | Code int `json:"code"`
7 | Status string `json:"status"`
8 | Message string `json:"message"`
9 | }
10 |
11 | func newApiError(code int, message string) *apiError {
12 | return &apiError{
13 | Code: code,
14 | Status: http.StatusText(code),
15 | Message: message,
16 | }
17 | }
18 |
19 | func wrapError(err error) *apiError {
20 | return newApiError(http.StatusInternalServerError,
21 | err.Error())
22 | }
23 |
24 | func (a *apiError) Write(w http.ResponseWriter) {
25 | w.WriteHeader(a.Code)
26 | printError(w, a)
27 | }
28 |
29 | var errInvalidDir = newApiError(http.StatusBadRequest,
30 | "Invalid Directory")
31 |
32 | var errDirNotFound = newApiError(http.StatusNotFound,
33 | "Directory does not exist")
34 |
35 | var errPageNotFound = newApiError(http.StatusNotFound,
36 | "Page does not exist")
37 |
38 | var errMalformedJson = newApiError(http.StatusBadRequest,
39 | "Could not parse request body")
40 |
41 | var errDirConflict = newApiError(http.StatusConflict,
42 | "Directory already exists")
43 |
44 | var errNoMeta = newApiError(http.StatusBadRequest,
45 | "page[meta] not sent in request")
46 |
47 | var errInvalidJson = newApiError(http.StatusBadRequest,
48 | "Malformed JSON")
49 |
50 | var errNoConfig = newApiError(http.StatusNotFound,
51 | "Could not find config file")
52 |
--------------------------------------------------------------------------------
/handlers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "os"
10 | "path"
11 | "path/filepath"
12 | "strings"
13 |
14 | "github.com/gorilla/mux"
15 | "github.com/kennygrant/sanitize"
16 | "github.com/stayradiated/rango/rangolib"
17 | )
18 |
19 | type Handlers struct {
20 | Config rangolib.ConfigManager
21 | Dir rangolib.DirManager
22 | Page rangolib.PageManager
23 |
24 | ContentDir string
25 | AssetsDir string
26 | }
27 |
28 | // ┌┬┐┬┬─┐┌─┐┌─┐┌┬┐┌─┐┬─┐┬┌─┐┌─┐
29 | // │││├┬┘├┤ │ │ │ │├┬┘│├┤ └─┐
30 | // ─┴┘┴┴└─└─┘└─┘ ┴ └─┘┴└─┴└─┘└─┘
31 |
32 | type readDirResponse struct {
33 | Data rangolib.Files `json:"data"`
34 | }
35 |
36 | type createDirResponse struct {
37 | Dir *rangolib.File `json:"dir"`
38 | }
39 |
40 | type updateDirResponse struct {
41 | Dir *rangolib.File `json:"dir"`
42 | }
43 |
44 | // readDir reads contents of a directory
45 | func (h Handlers) ReadDir(w http.ResponseWriter, r *http.Request) {
46 | fp, err := h.fixPathWithDir(mux.Vars(r)["path"], h.ContentDir)
47 | if err != nil {
48 | errInvalidDir.Write(w)
49 | return
50 | }
51 |
52 | // try and read contents of dir
53 | var contents rangolib.Files
54 | contents, err = h.Dir.Read(fp)
55 | if err != nil {
56 | errDirNotFound.Write(w)
57 | return
58 | }
59 |
60 | // trim content prefix
61 | for _, item := range contents {
62 | item.Path = strings.TrimPrefix(item.Path, h.ContentDir)
63 | }
64 |
65 | printJson(w, &readDirResponse{Data: contents})
66 | }
67 |
68 | // createDir creates a directory
69 | func (h Handlers) CreateDir(w http.ResponseWriter, r *http.Request) {
70 |
71 | // combine parent and dirname
72 | parent := mux.Vars(r)["path"]
73 | dirname := sanitize.Path(r.FormValue("dir[name]"))
74 | fp := filepath.Join(parent, dirname)
75 |
76 | // check that it is a valid path
77 | fp, err := h.fixPathWithDir(fp, h.ContentDir)
78 | if err != nil {
79 | errInvalidDir.Write(w)
80 | return
81 | }
82 |
83 | // check if dir already exists
84 | if fileExists(fp) || dirExists(fp) {
85 | errDirConflict.Write(w)
86 | return
87 | }
88 |
89 | // make directory
90 | dir, err := h.Dir.Create(fp)
91 | if err != nil {
92 | wrapError(err).Write(w)
93 | return
94 | }
95 |
96 | // trim content prefix
97 | dir.Path = strings.TrimPrefix(dir.Path, h.ContentDir)
98 |
99 | // print info
100 | printJson(w, &createDirResponse{Dir: dir})
101 | }
102 |
103 | // updateDir renames a directory
104 | func (h Handlers) UpdateDir(w http.ResponseWriter, r *http.Request) {
105 | fp, err := h.fixPathWithDir(mux.Vars(r)["path"], h.ContentDir)
106 | if err != nil {
107 | errInvalidDir.Write(w)
108 | return
109 | }
110 |
111 | // check that the specified directory is not the root content folder
112 | if fp == h.ContentDir {
113 | errInvalidDir.Write(w)
114 | return
115 | }
116 |
117 | // check that directory exists
118 | if dirExists(fp) == false {
119 | errDirNotFound.Write(w)
120 | return
121 | }
122 |
123 | // combine parent dir with dir name
124 | parent := filepath.Dir(fp)
125 | dirname := sanitize.Path(r.FormValue("dir[name]"))
126 | dest := filepath.Join(parent, dirname)
127 |
128 | // rename directory
129 | dir, err := h.Dir.Update(fp, dest)
130 | if err != nil {
131 | wrapError(err).Write(w)
132 | return
133 | }
134 |
135 | // print info
136 | printJson(w, &updateDirResponse{Dir: dir})
137 | }
138 |
139 | // destroyDir deletes a directory
140 | func (h Handlers) DestroyDir(w http.ResponseWriter, r *http.Request) {
141 | fp, err := h.fixPathWithDir(mux.Vars(r)["path"], h.ContentDir)
142 | if err != nil {
143 | errInvalidDir.Write(w)
144 | return
145 | }
146 |
147 | // check that the specified directory is not the root content folder
148 | if fp == h.ContentDir {
149 | errInvalidDir.Write(w)
150 | return
151 | }
152 |
153 | // remove directory
154 | if err = h.Dir.Destroy(fp); err != nil {
155 | errDirNotFound.Write(w)
156 | return
157 | }
158 |
159 | w.WriteHeader(http.StatusNoContent)
160 | }
161 |
162 | // ┌─┐┌─┐┌─┐┌─┐┌─┐
163 | // ├─┘├─┤│ ┬├┤ └─┐
164 | // ┴ ┴ ┴└─┘└─┘└─┘
165 |
166 | type readPageResponse struct {
167 | Page *rangolib.PageFile `json:"page"`
168 | }
169 |
170 | type createPageResponse struct {
171 | Page *rangolib.PageFile `json:"page"`
172 | }
173 |
174 | type updatePageResponse struct {
175 | Page *rangolib.PageFile `json:"page"`
176 | }
177 |
178 | // readPage reads page data
179 | func (h Handlers) ReadPage(w http.ResponseWriter, r *http.Request) {
180 | fp, err := h.fixPathWithDir(mux.Vars(r)["path"], h.ContentDir)
181 | if err != nil {
182 | errInvalidDir.Write(w)
183 | return
184 | }
185 |
186 | // read page from disk
187 | page, err := h.Page.Read(fp)
188 | if err != nil {
189 | errPageNotFound.Write(w)
190 | return
191 | }
192 |
193 | // trim content prefix from path
194 | page.Path = strings.TrimPrefix(page.Path, h.ContentDir)
195 |
196 | // print json
197 | printJson(w, &readPageResponse{Page: page})
198 | }
199 |
200 | // createPage creates a new page
201 | func (h Handlers) CreatePage(w http.ResponseWriter, r *http.Request) {
202 | fp, err := h.fixPathWithDir(mux.Vars(r)["path"], h.ContentDir)
203 | if err != nil {
204 | fmt.Fprint(w, err)
205 | return
206 | }
207 |
208 | // check that parent dir exists
209 | if fileExists(fp) || dirExists(fp) == false {
210 | errDirNotFound.Write(w)
211 | return
212 | }
213 |
214 | metastring := r.FormValue("page[meta]")
215 | if len(metastring) == 0 {
216 | errNoMeta.Write(w)
217 | }
218 |
219 | metadata := rangolib.Frontmatter{}
220 | err = json.Unmarshal([]byte(metastring), &metadata)
221 | if err != nil {
222 | errInvalidJson.Write(w)
223 | return
224 | }
225 |
226 | content := []byte(r.FormValue("page[content]"))
227 |
228 | page, err := h.Page.Create(fp, metadata, content)
229 | if err != nil {
230 | wrapError(err).Write(w)
231 | return
232 | }
233 |
234 | // trim content prefix from path
235 | page.Path = strings.TrimPrefix(page.Path, h.ContentDir)
236 |
237 | printJson(w, &createPageResponse{Page: page})
238 | }
239 |
240 | // updatePage writes page data to a file
241 | func (h Handlers) UpdatePage(w http.ResponseWriter, r *http.Request) {
242 | fp, err := h.fixPathWithDir(mux.Vars(r)["path"], h.ContentDir)
243 | if err != nil {
244 | fmt.Fprint(w, err)
245 | return
246 | }
247 |
248 | // check that existing page exists
249 | if dirExists(fp) || fileExists(fp) == false {
250 | errPageNotFound.Write(w)
251 | return
252 | }
253 |
254 | metastring := r.FormValue("page[meta]")
255 | if len(metastring) == 0 {
256 | errNoMeta.Write(w)
257 | }
258 |
259 | metadata := rangolib.Frontmatter{}
260 | err = json.Unmarshal([]byte(metastring), &metadata)
261 | if err != nil {
262 | fmt.Fprint(w, err)
263 | return
264 | }
265 |
266 | content := []byte(r.FormValue("page[content]"))
267 |
268 | page, err := h.Page.Update(fp, metadata, content)
269 | if err != nil {
270 | wrapError(err).Write(w)
271 | return
272 | }
273 |
274 | // trim content prefix from path
275 | page.Path = strings.TrimPrefix(page.Path, h.ContentDir)
276 |
277 | printJson(w, &updatePageResponse{Page: page})
278 | }
279 |
280 | // destroyPage deletes a page
281 | func (h Handlers) DestroyPage(w http.ResponseWriter, r *http.Request) {
282 | fp, err := h.fixPathWithDir(mux.Vars(r)["path"], h.ContentDir)
283 | if err != nil {
284 | fmt.Fprint(w, err)
285 | return
286 | }
287 |
288 | // delete page
289 | if err = h.Page.Destroy(fp); err != nil {
290 | errPageNotFound.Write(w)
291 | return
292 | }
293 |
294 | // don't need to send anything back
295 | w.WriteHeader(http.StatusNoContent)
296 | }
297 |
298 | // ┌─┐┌─┐┌┐┌┌─┐┬┌─┐
299 | // │ │ ││││├┤ ││ ┬
300 | // └─┘└─┘┘└┘└ ┴└─┘
301 |
302 | // readConfig reads data from a config
303 | func (h Handlers) ReadConfig(w http.ResponseWriter, r *http.Request) {
304 | config, err := h.Config.Parse()
305 | if err != nil {
306 | errNoConfig.Write(w)
307 | return
308 | }
309 |
310 | printJson(w, config)
311 | }
312 |
313 | // updateConfig writes json data to a config file
314 | func (h Handlers) UpdateConfig(w http.ResponseWriter, r *http.Request) {
315 |
316 | // parse the config
317 | config := &rangolib.ConfigMap{}
318 | err := json.Unmarshal([]byte(r.FormValue("config")), config)
319 | if err != nil {
320 | errInvalidJson.Write(w)
321 | return
322 | }
323 |
324 | // save config
325 | if err := h.Config.Save(config); err != nil {
326 | wrapError(err).Write(w)
327 | return
328 | }
329 |
330 | // don't need to send anything back
331 | w.WriteHeader(http.StatusNoContent)
332 | }
333 |
334 | // ┌─┐┌─┐┌─┐┌─┐┌┬┐┌─┐
335 | // ├─┤└─┐└─┐├┤ │ └─┐
336 | // ┴ ┴└─┘└─┘└─┘ ┴ └─┘
337 |
338 | type createAssetResponse struct {
339 | Asset *rangolib.Asset `json:"asset"`
340 | }
341 |
342 | // CreateAsset uploads a file into the assets directory
343 | func (h Handlers) CreateAsset(w http.ResponseWriter, r *http.Request) {
344 |
345 | // get path to store file in
346 | dir, err := h.fixPathWithDir(mux.Vars(r)["path"], h.AssetsDir)
347 | if err != nil {
348 | fmt.Fprint(w, err)
349 | return
350 | }
351 |
352 | // Check page exists [optional]
353 |
354 | // Remove .md extension
355 | ext := path.Ext(dir)
356 | dir = dir[0 : len(dir)-len(ext)]
357 |
358 | // Create folder structure in assets folder
359 | os.MkdirAll(dir, 0755)
360 |
361 | // Get file form request
362 | file, header, err := r.FormFile("file")
363 | if err != nil {
364 | fmt.Fprintln(w, err)
365 | return
366 | }
367 | defer file.Close()
368 |
369 | // Sanitize file name
370 | filename := sanitize.Path(header.Filename)
371 | fp := path.Join(dir, filename)
372 |
373 | // Check file name doesn't already exist
374 |
375 | // TODO: save to path based on page name and sanitized file name
376 | out, err := os.Create(fp)
377 | if err != nil {
378 | fmt.Fprintf(w, "Unable to create the file for writing.")
379 | return
380 | }
381 | defer out.Close()
382 |
383 | // write the content from POST to the file
384 | _, err = io.Copy(out, file)
385 | if err != nil {
386 | fmt.Fprintln(w, err)
387 | }
388 |
389 | asset := &rangolib.Asset{
390 | Name: filename,
391 | Path: dir,
392 | }
393 |
394 | asset.Resample()
395 |
396 | asset.Path = strings.TrimPrefix(asset.Path, h.AssetsDir)
397 |
398 | // TODO: print out proper status message
399 | printJson(w, &createAssetResponse{Asset: asset})
400 |
401 | // Write filename into page [optional]
402 | }
403 |
404 | // ┬ ┬┬ ┬┌─┐┌─┐
405 | // ├─┤│ ││ ┬│ │
406 | // ┴ ┴└─┘└─┘└─┘
407 |
408 | func (h Handlers) PublishSite(w http.ResponseWriter, r *http.Request) {
409 | output, err := rangolib.RunHugo()
410 | if err != nil {
411 | wrapError(err).Write(w)
412 | }
413 |
414 | printJson(w, struct {
415 | Output string `json:"output"`
416 | }{
417 | Output: string(output),
418 | })
419 | }
420 |
421 | func (h Handlers) fixPathWithDir(p string, dir string) (string, error) {
422 | err := errors.New("invalid path")
423 |
424 | // join path with content folder
425 | fp := path.Join(dir, p)
426 |
427 | fmt.Println(fp)
428 |
429 | // check that path still starts with content dir
430 | if !strings.HasPrefix(fp, dir) {
431 | return fp, err
432 | }
433 |
434 | // check that path doesn't contain any ..
435 | if strings.Contains(fp, "..") {
436 | return fp, err
437 | }
438 |
439 | return fp, nil
440 | }
441 |
--------------------------------------------------------------------------------
/handlers_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "net/http/httptest"
7 | "os"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/stayradiated/rango/rangolib"
12 | "github.com/stretchr/testify/suite"
13 | )
14 |
15 | var (
16 | server *httptest.Server
17 | contentDir string = "__tmp__"
18 | )
19 |
20 | type HandlersTestSuite struct {
21 | suite.Suite
22 | }
23 |
24 | func (assert *HandlersTestSuite) SetupTest() {
25 | os.Mkdir(contentDir, 0755)
26 |
27 | server = httptest.NewServer(NewRouter(&RouterConfig{
28 | Handlers: &Handlers{
29 | Config: rangolib.NewConfig("config.toml"),
30 | Dir: rangolib.NewDir(),
31 | Page: rangolib.NewPage(),
32 | ContentDir: contentDir,
33 | },
34 | AdminDir: "./admin/dist",
35 | }))
36 | }
37 |
38 | func (assert *HandlersTestSuite) TearDownTest() {
39 | os.RemoveAll(contentDir)
40 | server.Close()
41 | }
42 |
43 | func (assert *HandlersTestSuite) TestReadDir() {
44 | url := server.URL + "/api/dir/"
45 | reader := strings.NewReader("")
46 |
47 | req, _ := http.NewRequest("GET", url, reader)
48 | res, err := http.DefaultClient.Do(req)
49 | assert.Nil(err)
50 |
51 | assert.Equal(res.StatusCode, http.StatusOK)
52 |
53 | var body readDirResponse
54 | err = json.NewDecoder(res.Body).Decode(&body)
55 | assert.Nil(err)
56 | }
57 |
58 | func TestHandlers(t *testing.T) {
59 | suite.Run(t, new(HandlersTestSuite))
60 | }
61 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | func Logger(inner http.Handler, name string) http.Handler {
10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
11 | start := time.Now()
12 |
13 | inner.ServeHTTP(w, r)
14 |
15 | log.Printf(
16 | "%12s\t%6s\t%12s\t%s",
17 | time.Since(start),
18 | r.Method,
19 | name,
20 | r.RequestURI,
21 | )
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/spf13/viper"
10 | "github.com/stayradiated/rango/rangolib"
11 | )
12 |
13 | func main() {
14 |
15 | // setup config file
16 | viper.SetConfigName("config")
17 | viper.ReadInConfig()
18 |
19 | // set config defaults
20 | viper.SetDefault("ContentDir", "content")
21 | viper.SetDefault("AdminDir", "admin")
22 | viper.SetDefault("AssetsDir", "static/assets")
23 |
24 | // make sure content dir exists
25 | contentDir := viper.GetString("ContentDir")
26 | _, err := os.Stat(contentDir)
27 | if err != nil && os.IsNotExist(err) {
28 | os.Mkdir(contentDir, 0755)
29 | }
30 |
31 | // make sure assets dir exists
32 | assetsDir := viper.GetString("AssetsDir")
33 | _, err = os.Stat(assetsDir)
34 | if err != nil && os.IsNotExist(err) {
35 | os.Mkdir(assetsDir, 0755)
36 | }
37 |
38 | // create router
39 | router := NewRouter(&RouterConfig{
40 | Handlers: &Handlers{
41 | Config: rangolib.NewConfig("config.toml"),
42 | Dir: rangolib.NewDir(),
43 | Page: rangolib.NewPage(),
44 | ContentDir: contentDir,
45 | AssetsDir: assetsDir,
46 | },
47 | AdminDir: viper.GetString("AdminDir"),
48 | })
49 |
50 | // start http server
51 | fmt.Println("Starting server on :8080")
52 | log.Fatal(http.ListenAndServe(":8080", router))
53 | }
54 |
--------------------------------------------------------------------------------
/rangolib/asset.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import (
4 | "fmt"
5 | "image/jpeg"
6 | "io"
7 | "log"
8 | "os"
9 | "path"
10 |
11 | "github.com/nfnt/resize"
12 | )
13 |
14 | type Asset struct {
15 | Name string `json:"name"`
16 | Path string `json:"path"`
17 | }
18 |
19 | func NewAsset(path, filename string, file io.Reader) (*Asset, error) {
20 | return nil, nil
21 | }
22 |
23 | func (a Asset) Resample() {
24 | fp := path.Join(a.Path, a.Name)
25 |
26 | fmt.Println("Resampling", fp)
27 |
28 | file, err := os.Open(fp)
29 | if err != nil {
30 | log.Println(err)
31 | return
32 | }
33 |
34 | img, err := jpeg.Decode(file)
35 | if err != nil {
36 | log.Println(err)
37 | return
38 | }
39 |
40 | file.Close()
41 |
42 | out := resize.Resize(300, 0, img, resize.Bilinear)
43 |
44 | resampledDir := path.Join(a.Path, "_resampled")
45 | os.MkdirAll(resampledDir, 0755)
46 |
47 | resampledFp := path.Join(resampledDir, a.Name)
48 | file, err = os.Create(resampledFp)
49 | if err != nil {
50 | log.Println(err)
51 | return
52 | }
53 | defer file.Close()
54 |
55 | jpeg.Encode(file, out, nil)
56 | }
57 |
--------------------------------------------------------------------------------
/rangolib/config.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import (
4 | "bytes"
5 | "os"
6 |
7 | "github.com/BurntSushi/toml"
8 | )
9 |
10 | type ConfigManager interface {
11 | Parse() (*ConfigMap, error)
12 | Save(config *ConfigMap) error
13 | }
14 | type ConfigMap map[string]interface{}
15 |
16 | type Config struct {
17 | path string
18 | }
19 |
20 | func NewConfig(path string) *Config {
21 | return &Config{
22 | path: path,
23 | }
24 | }
25 |
26 | // Open returns a fd to the config file
27 | func (c Config) Open() (*os.File, error) {
28 | return os.Open(c.path)
29 | }
30 |
31 | // Create empties the config file and returns the fd to it
32 | func (c Config) Create() (*os.File, error) {
33 | return os.Create(c.path)
34 | }
35 |
36 | // Parse converts the config file into a readable map
37 | func (c Config) Parse() (*ConfigMap, error) {
38 | config := &ConfigMap{}
39 | file, err := c.Open()
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | _, err = toml.DecodeReader(file, config)
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | return config, nil
50 | }
51 |
52 | // Save saves the config to disk
53 | func (c Config) Save(config *ConfigMap) error {
54 |
55 | // convert config into a string
56 | buf := new(bytes.Buffer)
57 | if err := toml.NewEncoder(buf).Encode(config); err != nil {
58 | return err
59 | }
60 |
61 | // write config to disk
62 | file, err := c.Create()
63 | if err != nil {
64 | return err
65 | }
66 |
67 | _, err = buf.WriteTo(file)
68 | return err
69 | }
70 |
--------------------------------------------------------------------------------
/rangolib/config_test.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/suite"
9 | )
10 |
11 | const SIMPLE_CONFIG = `title = "Something Neat"
12 | `
13 |
14 | var simpleConfigData = &ConfigMap{
15 | "title": "Something Neat",
16 | }
17 |
18 | type ConfigTestSuite struct {
19 | suite.Suite
20 | Config Config
21 | }
22 |
23 | func (t *ConfigTestSuite) SetupTest() {
24 | t.Config = Config{
25 | path: "./content/config.toml",
26 | }
27 |
28 | os.Mkdir("content", 0755)
29 | }
30 |
31 | func (t *ConfigTestSuite) TearDownTest() {
32 | os.RemoveAll("content")
33 | }
34 |
35 | // test ReadConfig on a simple config
36 | func (t *ConfigTestSuite) TestReadConfig() {
37 | file, _ := t.Config.Create()
38 | file.Write([]byte(SIMPLE_CONFIG))
39 |
40 | config, err := t.Config.Parse()
41 | t.Nil(err)
42 | t.Equal(config, simpleConfigData)
43 | }
44 |
45 | // test SaveConfig on a simple config
46 | func (t *ConfigTestSuite) TestSaveConfig() {
47 | err := t.Config.Save(simpleConfigData)
48 | t.Nil(err)
49 |
50 | file, _ := t.Config.Open()
51 | data, err := ioutil.ReadAll(file)
52 | t.Nil(err)
53 | t.Equal(string(data), SIMPLE_CONFIG)
54 | }
55 |
56 | // run config tests
57 | func TestConfigTestSuite(t *testing.T) {
58 | suite.Run(t, new(ConfigTestSuite))
59 | }
60 |
--------------------------------------------------------------------------------
/rangolib/dir.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | )
9 |
10 | type DirManager interface {
11 | Read(string) (Files, error)
12 | Create(string) (*File, error)
13 | Update(string, string) (*File, error)
14 | Destroy(string) error
15 | }
16 |
17 | type Dir struct{}
18 |
19 | func NewDir() *Dir {
20 | return &Dir{}
21 | }
22 |
23 | // Read lists the contents of a directory
24 | func (d Dir) Read(dirname string) (Files, error) {
25 | contents, err := ioutil.ReadDir(dirname)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | // make a new slice of File's to hold the dir contents
31 | files := make(Files, len(contents))
32 |
33 | // convert os.FileInfo into Files
34 | for i, info := range contents {
35 | files[i] = NewFile(filepath.Join(dirname, info.Name()), info)
36 | }
37 |
38 | return files, nil
39 | }
40 |
41 | // Create creates a new directory
42 | func (d Dir) Create(dirname string) (*File, error) {
43 |
44 | // make directory
45 | if err := os.Mkdir(dirname, 0755); err != nil {
46 | return nil, err
47 | }
48 |
49 | // check that directory was created
50 | info, err := os.Stat(dirname)
51 | if err != nil {
52 | return nil, err
53 | }
54 |
55 | // convert fileinfo into something we can print
56 | return NewFile(dirname, info), nil
57 | }
58 |
59 | // Update renames an existing directory
60 | func (d Dir) Update(src string, dest string) (*File, error) {
61 |
62 | // check that destination doesn't exist
63 | info, err := os.Stat(dest)
64 | if info != nil {
65 | return nil, errors.New("Cannot overwrite destination")
66 | }
67 |
68 | // move directory including it's contents
69 | if err := moveDir(src, dest); err != nil {
70 | return nil, err
71 | }
72 |
73 | // check that directory was created
74 | info, err = os.Stat(dest)
75 | if err != nil {
76 | return nil, err
77 | }
78 |
79 | // convert fileinfo into something we can print
80 | return NewFile(dest, info), nil
81 | }
82 |
83 | // Destroy will delete a directory and it's contents
84 | func (d Dir) Destroy(dirname string) error {
85 |
86 | // check that directory exists
87 | dir, err := os.Stat(dirname)
88 | if err != nil {
89 | return err
90 | }
91 |
92 | // check that directory is a directory
93 | if dir.IsDir() == false {
94 | return errors.New("DeleteDir can only delete directories")
95 | }
96 |
97 | // remove the directory
98 | return os.RemoveAll(dirname)
99 | }
100 |
--------------------------------------------------------------------------------
/rangolib/dir_test.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/suite"
9 | )
10 |
11 | type DirTestSuite struct {
12 | suite.Suite
13 | Dir Dir
14 | }
15 |
16 | func (t *DirTestSuite) SetupTest() {
17 | t.Dir = Dir{}
18 | os.Mkdir("content", 0755)
19 | }
20 |
21 | func (t *DirTestSuite) TearDownTest() {
22 | os.RemoveAll("content")
23 | }
24 |
25 | // test readdir on an empty directory
26 | func (t *DirTestSuite) TestReadDirWithEmptyDir() {
27 | files, err := t.Dir.Read("content/")
28 | t.Nil(err)
29 | t.Equal(len(files), 0)
30 | }
31 |
32 | // test readdir on a single directory
33 | func (t *DirTestSuite) TestReadDirWithSingleDir() {
34 | os.Mkdir("content/foo", 0755)
35 | dirInfo, _ := os.Stat("content/foo")
36 |
37 | files, err := t.Dir.Read("content/")
38 | t.Nil(err)
39 | t.Equal(len(files), 1)
40 |
41 | t.Equal(files[0], &File{
42 | Name: "foo",
43 | Path: "content/foo",
44 | IsDir: true,
45 | Size: dirInfo.Size(),
46 | ModTime: dirInfo.ModTime().Unix(),
47 | })
48 | }
49 |
50 | // test readdir on a single file
51 | func (t *DirTestSuite) TestReadDirWithSingleFile() {
52 | ioutil.WriteFile("content/bar.md", []byte(SIMPLE_PAGE), 0644)
53 | fileInfo, err := os.Stat("content/bar.md")
54 |
55 | files, err := t.Dir.Read("content/")
56 | t.Nil(err)
57 | t.Equal(len(files), 1)
58 |
59 | t.Equal(files[0], &File{
60 | Name: "bar.md",
61 | Path: "content/bar.md",
62 | IsDir: false,
63 | Size: fileInfo.Size(),
64 | ModTime: fileInfo.ModTime().Unix(),
65 | })
66 | }
67 |
68 | // test readdir on a non-existant directory
69 | func (t *DirTestSuite) TestReadDirWithMissingDir() {
70 | files, err := t.Dir.Read("does_not_exist/")
71 | t.Nil(files)
72 | t.NotNil(err)
73 | }
74 |
75 | // test createdir
76 | func (t *DirTestSuite) TestCreateDir() {
77 | dir, err := t.Dir.Create("content/foo")
78 | t.Nil(err)
79 |
80 | dirInfo, _ := os.Stat("content/foo")
81 |
82 | t.Equal(dir, &File{
83 | Name: "foo",
84 | Path: "content/foo",
85 | IsDir: true,
86 | Size: dirInfo.Size(),
87 | ModTime: dirInfo.ModTime().Unix(),
88 | })
89 | }
90 |
91 | // test createdir on existing directory
92 | func (t *DirTestSuite) TestCreateDirWithExistingDir() {
93 | os.Mkdir("content/foo", 0755)
94 |
95 | dir, err := t.Dir.Create("content/foo")
96 | t.NotNil(err)
97 | t.Nil(dir)
98 | }
99 |
100 | // test updatedir on an existing directory with contents
101 | func (t *DirTestSuite) TestUpdateDirWithExistingDir() {
102 | os.Mkdir("content/foo", 0755)
103 | ioutil.WriteFile("content/foo/bar.md", []byte(SIMPLE_PAGE), 0644)
104 |
105 | dir, err := t.Dir.Update("content/foo", "content/bar")
106 | t.Nil(err)
107 |
108 | _, err = os.Stat("content/foo")
109 | t.NotNil(err)
110 |
111 | dirInfo, err := os.Stat("content/bar")
112 | t.Nil(err)
113 |
114 | _, err = os.Stat("content/bar/bar.md")
115 | t.Nil(err)
116 |
117 | bytes, err := ioutil.ReadFile("content/bar/bar.md")
118 | t.Nil(err)
119 | t.Equal(string(bytes), SIMPLE_PAGE)
120 |
121 | t.Equal(dir, &File{
122 | Name: "bar",
123 | Path: "content/bar",
124 | IsDir: true,
125 | Size: dirInfo.Size(),
126 | ModTime: dirInfo.ModTime().Unix(),
127 | })
128 | }
129 |
130 | // test updatedir on a non-existant directory
131 | func (t *DirTestSuite) TestUpdateDirOnMissingDir() {
132 | dir, err := t.Dir.Update("content/foo", "content/bar")
133 | t.NotNil(err)
134 | t.Nil(dir)
135 | }
136 |
137 | // test updatedir on a conflicting directory
138 | func (t *DirTestSuite) TestUpdateDirOnConflictingDir() {
139 | os.Mkdir("content/foo", 0755)
140 | os.Mkdir("content/bar", 0755)
141 |
142 | dir, err := t.Dir.Update("content/foo", "content/bar")
143 | t.NotNil(err)
144 | t.Nil(dir)
145 | }
146 |
147 | // test deletedir on an existing directory
148 | func (t *DirTestSuite) TestDestroyDirWithExistingDir() {
149 | os.Mkdir("content/foo", 0755)
150 |
151 | err := t.Dir.Destroy("content/foo")
152 | t.Nil(err)
153 |
154 | _, err = os.Stat("content/foo")
155 | t.NotNil(err)
156 | }
157 |
158 | // test deletedir on a non-directory
159 | func (t *DirTestSuite) TestDestroyDirWithMissingDir() {
160 | err := t.Dir.Destroy("content/foo")
161 | t.NotNil(err)
162 |
163 | ioutil.WriteFile("content/bar.md", []byte(SIMPLE_PAGE), 0644)
164 | err = t.Dir.Destroy("content/bar.md")
165 | t.NotNil(err)
166 | }
167 |
168 | func TestDirTestSuite(t *testing.T) {
169 | suite.Run(t, new(DirTestSuite))
170 | }
171 |
--------------------------------------------------------------------------------
/rangolib/file.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import "os"
4 |
5 | type File struct {
6 | Name string `json:"name"`
7 | Path string `json:"path"`
8 | IsDir bool `json:"isDir"`
9 | Size int64 `json:"size"`
10 | ModTime int64 `json:"modTime"`
11 | }
12 |
13 | type Files []*File
14 |
15 | // NewFile constructs a new File based on a path and file info
16 | func NewFile(path string, info os.FileInfo) *File {
17 | file := &File{Path: path}
18 | file.Load(info)
19 | return file
20 | }
21 | func (f *File) Load(info os.FileInfo) {
22 | f.Name = info.Name()
23 | f.IsDir = info.IsDir()
24 | f.Size = info.Size()
25 | f.ModTime = info.ModTime().Unix()
26 | }
27 |
--------------------------------------------------------------------------------
/rangolib/hugo.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import "os/exec"
4 |
5 | func RunHugo() ([]byte, error) {
6 | hugo := exec.Command("hugo")
7 |
8 | output, err := hugo.Output()
9 | if err != nil {
10 | return nil, err
11 | }
12 |
13 | return output, nil
14 | }
15 |
--------------------------------------------------------------------------------
/rangolib/page.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 | "strconv"
8 |
9 | "github.com/kennygrant/sanitize"
10 | "github.com/spf13/cast"
11 | "github.com/spf13/hugo/hugolib"
12 | "github.com/spf13/hugo/parser"
13 | )
14 |
15 | // ┌┬┐┬ ┬┌─┐┌─┐┌─┐
16 | // │ └┬┘├─┘├┤ └─┐
17 | // ┴ ┴ ┴ └─┘└─┘
18 |
19 | const TOML = '+'
20 | const YAML = '-'
21 |
22 | // Frontmatter stores encodeable data
23 | type Frontmatter map[string]interface{}
24 |
25 | // Page represents a markdown file
26 | type PageFile struct {
27 | Path string `json:"path"`
28 | Metadata Frontmatter `json:"metadata"`
29 | Content string `json:"content"`
30 | }
31 |
32 | func (p *PageFile) Save() error {
33 | // create new hugo page
34 | page, err := hugolib.NewPage(p.Path)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | // set attributes
40 | page.SetSourceMetaData(p.Metadata, TOML)
41 | page.SetSourceContent([]byte(p.Content))
42 |
43 | // save page
44 | return page.SafeSaveSourceAs(p.Path)
45 | }
46 |
47 | // ┌─┐┬ ┬┌┐┌┌─┐┌┬┐┬┌─┐┌┐┌┌─┐
48 | // ├┤ │ │││││ │ ││ ││││└─┐
49 | // └ └─┘┘└┘└─┘ ┴ ┴└─┘┘└┘└─┘
50 |
51 | type PageManager interface {
52 | Read(fp string) (*PageFile, error)
53 | Create(fp string, fm Frontmatter, content []byte) (*PageFile, error)
54 | Update(fp string, fm Frontmatter, content []byte) (*PageFile, error)
55 | Destroy(fp string) error
56 | }
57 |
58 | type Page struct{}
59 |
60 | func NewPage() *Page {
61 | return &Page{}
62 | }
63 |
64 | // ReadPage reads a page from disk
65 | func (p Page) Read(fp string) (*PageFile, error) {
66 | // open the file for reading
67 | file, err := os.Open(fp)
68 | if err != nil {
69 | return nil, err
70 | }
71 | defer file.Close()
72 |
73 | // use the Hugo parser lib to read the contents
74 | parser, err := parser.ReadFrom(file)
75 | if err != nil {
76 | return nil, err
77 | }
78 |
79 | // get the metadata
80 | rawdata, err := parser.Metadata()
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | // convert the interface{} into map[string]interface{}
86 | metadata, err := cast.ToStringMapE(rawdata)
87 | if err != nil {
88 | return nil, err
89 | }
90 |
91 | // assemble a new Page instance
92 | return &PageFile{
93 | Path: fp,
94 | Metadata: metadata,
95 | Content: string(parser.Content()),
96 | }, nil
97 | }
98 |
99 | // CreatePage creates a new file and saves page content to it
100 | func (p Page) Create(dirname string, fm Frontmatter, content []byte) (*PageFile, error) {
101 |
102 | // get title from metadata
103 | title, err := getTitle(fm)
104 | if err != nil {
105 | return nil, err
106 | }
107 |
108 | // the filepath for the page
109 | fp := generateFilePath(dirname, title)
110 |
111 | // create a new page
112 | page := &PageFile{
113 | Path: fp,
114 | Metadata: fm,
115 | Content: string(content),
116 | }
117 |
118 | // save page to disk
119 | err = page.Save()
120 | if err != nil {
121 | return nil, err
122 | }
123 |
124 | return page, nil
125 | }
126 |
127 | // UpdatePage changes the content of an existing page
128 | func (p Page) Update(fp string, fm Frontmatter, content []byte) (*PageFile, error) {
129 |
130 | // get title from metadata
131 | title, err := getTitle(fm)
132 | if err != nil {
133 | return nil, err
134 | }
135 |
136 | // delete existing page
137 | err = p.Destroy(fp)
138 | if err != nil {
139 | return nil, err
140 | }
141 |
142 | // the filepath for the page
143 | dirname := filepath.Dir(fp)
144 | fp = generateFilePath(dirname, title)
145 |
146 | // create a new page
147 | page := &PageFile{
148 | Path: fp,
149 | Metadata: fm,
150 | Content: string(content),
151 | }
152 |
153 | // save page to disk
154 | err = page.Save()
155 | if err != nil {
156 | return nil, err
157 | }
158 |
159 | return page, nil
160 | }
161 |
162 | // Destroy deletes a page
163 | func (p Page) Destroy(fp string) error {
164 |
165 | // check that file exists
166 | info, err := os.Stat(fp)
167 | if err != nil {
168 | return err
169 | }
170 |
171 | // that file is a directory
172 | if info.IsDir() {
173 | return errors.New("DeletePage cannot delete directories")
174 | }
175 |
176 | // remove the directory
177 | return os.Remove(fp)
178 | }
179 |
180 | // ┬ ┬┌─┐┬ ┌─┐┌─┐┬─┐┌─┐
181 | // ├─┤├┤ │ ├─┘├┤ ├┬┘└─┐
182 | // ┴ ┴└─┘┴─┘┴ └─┘┴└─└─┘
183 |
184 | // generateFilePath generates a filepath based on a page title
185 | // if the filename already exists, add a number on the end
186 | // if that exists, increment the number by one until we find a filename
187 | // that doesn't exist
188 | func generateFilePath(dirname, title string) (fp string) {
189 | count := 0
190 |
191 | for {
192 |
193 | // combine title with count
194 | name := title
195 | if count != 0 {
196 | name += " " + strconv.Itoa(count)
197 | }
198 |
199 | // join filename with dirname
200 | filename := sanitize.Path(name + ".md")
201 | fp = filepath.Join(dirname, filename)
202 |
203 | // only stop looping when file doesn't already exist
204 | if _, err := os.Stat(fp); err != nil {
205 | break
206 | }
207 |
208 | // try again with a different number
209 | count += 1
210 | }
211 |
212 | return fp
213 | }
214 |
215 | func getTitle(fm Frontmatter) (string, error) {
216 |
217 | // check that title has been specified
218 | t, ok := fm["title"]
219 | if ok == false {
220 | return "", errors.New("page[meta].title must be specified")
221 | }
222 |
223 | // check that title is a string
224 | title, ok := t.(string)
225 | if ok == false {
226 | return "", errors.New("page[meta].title must be a string")
227 | }
228 |
229 | return title, nil
230 | }
231 |
--------------------------------------------------------------------------------
/rangolib/page_test.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/suite"
9 | )
10 |
11 | const SIMPLE_PAGE = `+++
12 | draft = true
13 | keywords = ["test", "awesome"]
14 | title = "Simple Page"
15 |
16 | +++
17 |
18 | Hello World
19 | ===========
20 |
21 | How are you today?
22 | `
23 |
24 | var simplePageFile = &PageFile{
25 | Path: "content/simple-page.md",
26 | Metadata: Frontmatter{
27 | "title": "Simple Page",
28 | "draft": true,
29 | "keywords": []interface{}{
30 | "test",
31 | "awesome",
32 | },
33 | },
34 | Content: "Hello World\n===========\n\nHow are you today?\n",
35 | }
36 |
37 | type PageTestSuite struct {
38 | suite.Suite
39 | Page Page
40 | }
41 |
42 | func (t *PageTestSuite) SetupTest() {
43 | t.Page = Page{}
44 | os.Mkdir("content", 0755)
45 | }
46 |
47 | func (t *PageTestSuite) TearDownTest() {
48 | os.RemoveAll("content")
49 | }
50 |
51 | // test ReadPage on a simple page
52 | func (t *PageTestSuite) TestReadPageOnSimplePage() {
53 | ioutil.WriteFile("content/simple-page.md", []byte(SIMPLE_PAGE), 0644)
54 |
55 | page, err := t.Page.Read("content/simple-page.md")
56 | t.Nil(err)
57 | t.Equal(page, simplePageFile)
58 | }
59 |
60 | // test ReadPage on a missing page
61 | func (t *PageTestSuite) TestReadPageOnMissingPage() {
62 | page, err := t.Page.Read("content/simple-page.md")
63 | t.NotNil(err)
64 | t.Nil(page)
65 | }
66 |
67 | // test CreatePage on a simple page
68 | func (t *PageTestSuite) TestCreatePageOnSimplePage() {
69 | metadata := simplePageFile.Metadata
70 | content := []byte(simplePageFile.Content)
71 |
72 | page, err := t.Page.Create("content/", metadata, content)
73 | t.Nil(err)
74 | t.Equal(page, simplePageFile)
75 |
76 | data, err := ioutil.ReadFile("content/simple-page.md")
77 | t.Nil(err)
78 | t.Equal(string(data), SIMPLE_PAGE)
79 | }
80 |
81 | // test UpdatePage
82 | func (t *PageTestSuite) TestUpdatePage() {
83 | os.Create("content/old-page.md")
84 | metadata := simplePageFile.Metadata
85 | content := []byte(simplePageFile.Content)
86 |
87 | page, err := t.Page.Update("content/old-page.md", metadata, content)
88 | t.Nil(err)
89 | t.Equal(page, simplePageFile)
90 |
91 | data, err := ioutil.ReadFile("content/simple-page.md")
92 | t.Nil(err)
93 | t.Equal(string(data), SIMPLE_PAGE)
94 | }
95 |
96 | // test DestroyPage on a simple page
97 | func (t *PageTestSuite) TestDestroyPageOnSimplePage() {
98 | os.Create("content/stuff.md")
99 | err := t.Page.Destroy("content/stuff.md")
100 | t.Nil(err)
101 | }
102 |
103 | // test DestroyPage on a missing page
104 | func (t *PageTestSuite) TestDestroyPageOnMissingPage() {
105 | err := t.Page.Destroy("content/stuff.md")
106 | t.NotNil(err)
107 | }
108 |
109 | // test generateFilePath against multiple cases
110 | func (t *PageTestSuite) TestGenerateFilePath() {
111 | var path string
112 |
113 | path = generateFilePath("content/", "Super Simple")
114 | t.Equal(path, "content/super-simple.md")
115 |
116 | os.Create("content/super-simple.md")
117 | path = generateFilePath("content/", "Super Simple")
118 | t.Equal(path, "content/super-simple-1.md")
119 |
120 | os.Create("content/super-simple-1.md")
121 | path = generateFilePath("content/", "Super Simple")
122 | t.Equal(path, "content/super-simple-2.md")
123 | }
124 |
125 | // Run tests
126 | func TestPageTestSuite(t *testing.T) {
127 | suite.Run(t, new(PageTestSuite))
128 | }
129 |
--------------------------------------------------------------------------------
/rangolib/treecopier.go:
--------------------------------------------------------------------------------
1 | package rangolib
2 |
3 | import (
4 | "io"
5 | "os"
6 | "path/filepath"
7 | )
8 |
9 | type treeCopier struct {
10 | srcRoot, destRoot string
11 | }
12 |
13 | func (c *treeCopier) convertPath(src string) string {
14 | return filepath.Join(c.destRoot, src[len(c.srcRoot):])
15 | }
16 |
17 | func (c *treeCopier) visitDir(src string, info os.FileInfo) error {
18 | return os.Mkdir(c.convertPath(src), info.Mode())
19 | }
20 |
21 | // based on https://gist.github.com/elazarl/5507969
22 | func (c *treeCopier) visitFile(src string, info os.FileInfo) error {
23 | dest := c.convertPath(src)
24 |
25 | sf, err := os.Open(src)
26 | if err != nil {
27 | return err
28 | }
29 | // no need to check errors on read only file, we already got everything
30 | // we need from the filesystem, so nothing can go wrong now.
31 | defer sf.Close()
32 |
33 | df, err := os.Create(dest)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | if _, err := io.Copy(df, sf); err != nil {
39 | df.Close()
40 | return err
41 | }
42 |
43 | return df.Close()
44 | }
45 |
46 | func (c *treeCopier) Walk(path string, info os.FileInfo, err error) error {
47 | if err != nil {
48 | return nil
49 | }
50 |
51 | if info.IsDir() {
52 | err = c.visitDir(path, info)
53 | if err != nil {
54 | return filepath.SkipDir
55 | }
56 | } else {
57 | c.visitFile(path, info)
58 | }
59 |
60 | return nil
61 | }
62 |
63 | // copyDir copies a directory to another location (including sub-directories)
64 | func copyDir(srcRoot, destRoot string) error {
65 | c := &treeCopier{srcRoot: srcRoot, destRoot: destRoot}
66 | return filepath.Walk(srcRoot, c.Walk)
67 | }
68 |
69 | // moveDir moves a directory to another location (include sub-directories)
70 | func moveDir(srcRoot, destRoot string) error {
71 | err := copyDir(srcRoot, destRoot)
72 | if err != nil {
73 | return err
74 | }
75 | return os.RemoveAll(srcRoot)
76 | }
77 |
--------------------------------------------------------------------------------
/router.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gorilla/mux"
7 | )
8 |
9 | type RouterConfig struct {
10 | Handlers *Handlers
11 | AdminDir string
12 | }
13 |
14 | func NewRouter(config *RouterConfig) *mux.Router {
15 | router := mux.NewRouter()
16 | apiRouter := router.PathPrefix("/api").Subrouter()
17 |
18 | // load routes
19 | for _, route := range GetRoutes(config.Handlers) {
20 | var handler http.Handler
21 |
22 | handler = route.HandlerFunc
23 | handler = Logger(handler, route.Name)
24 |
25 | apiRouter.
26 | Methods(route.Method).
27 | Path(route.Pattern).
28 | Name(route.Name).
29 | Handler(handler)
30 | }
31 |
32 | // serve static assets (user images, etc)
33 | assetsFs := http.FileServer(http.Dir(config.Handlers.AssetsDir))
34 | router.PathPrefix("/assets").Handler(http.StripPrefix("/assets/", assetsFs))
35 |
36 | // serve admin client files (html, css, etc)
37 | adminFs := http.FileServer(http.Dir(config.AdminDir))
38 | router.PathPrefix("/").Handler(adminFs)
39 |
40 | return router
41 | }
42 |
--------------------------------------------------------------------------------
/routes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "net/http"
4 |
5 | type Route struct {
6 | Name string
7 | Method string
8 | Pattern string
9 | HandlerFunc http.HandlerFunc
10 | }
11 |
12 | type Routes []Route
13 |
14 | func GetRoutes(h *Handlers) Routes {
15 | return Routes{
16 | // directories
17 | Route{
18 | "ReadDir",
19 | "GET", "/dir/{path:.*}", h.ReadDir,
20 | },
21 | Route{
22 | "CreateDir",
23 | "POST", "/dir/{path:.*}", h.CreateDir,
24 | },
25 | Route{
26 | "UpdateDir",
27 | "PUT", "/dir/{path:.*}", h.UpdateDir,
28 | },
29 | Route{
30 | "DestroyDir",
31 | "DELETE", "/dir/{path:.*}", h.DestroyDir,
32 | },
33 |
34 | // pages
35 | Route{
36 | "ReadPage",
37 | "GET", "/page/{path:.*}", h.ReadPage,
38 | },
39 | Route{
40 | "CreatePage",
41 | "POST", "/page/{path:.*}", h.CreatePage,
42 | },
43 | Route{
44 | "UpdatePage",
45 | "PUT", "/page/{path:.*}", h.UpdatePage,
46 | },
47 | Route{
48 | "DestroyPage",
49 | "DELETE", "/page/{path:.*}", h.DestroyPage,
50 | },
51 |
52 | // config
53 | Route{
54 | "ReadConfig",
55 | "GET", "/config", h.ReadConfig,
56 | },
57 | Route{
58 | "UpdateConfig",
59 | "PUT", "/config", h.UpdateConfig,
60 | },
61 |
62 | // assets
63 | Route{
64 | "CreateAsset",
65 | "POST", "/asset/{path:.*}", h.CreateAsset,
66 | },
67 |
68 | // misc
69 | Route{
70 | "PublishSite",
71 | "POST", "/site/publish", h.PublishSite,
72 | },
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "os"
7 | )
8 |
9 | func fileExists(fp string) bool {
10 | info, err := os.Stat(fp)
11 | if err != nil {
12 | return false
13 | }
14 | return info.IsDir() == false
15 | }
16 |
17 | func dirExists(fp string) bool {
18 | info, err := os.Stat(fp)
19 | if err != nil {
20 | return false
21 | }
22 | return info.IsDir()
23 | }
24 |
25 | func printError(w http.ResponseWriter, err interface{}) {
26 | printJson(w, err)
27 | }
28 |
29 | func printJson(w http.ResponseWriter, obj interface{}) {
30 | w.Header().Set("Content-Type", "application/json; charset=UTF-8")
31 | json.NewEncoder(w).Encode(obj)
32 | }
33 |
--------------------------------------------------------------------------------