├── .DS_Store
├── demoit.zip
├── _assets
├── .DS_Store
├── demoit.png
├── demoit.psd
├── layouts.psd
├── demoit.app.psd
├── demoit_32x32.png
├── demoit_64x64.png
├── demoit_dark.png
├── demoit_light.png
├── demoit_100x100.png
└── demoit_200x200.png
├── dist
├── img
│ └── demoit_64x64.png
├── resources
│ ├── state-example2.json
│ ├── state-example.json
│ └── FileSaver.min.js
└── index.html
├── src
├── img
│ └── demoit_64x64.png
├── js
│ ├── utils
│ │ ├── setTheme.js
│ │ ├── transpile.js
│ │ ├── localStorage.js
│ │ ├── executeCSS.js
│ │ ├── commitDiff.js
│ │ ├── codeMirrorCommands.js
│ │ ├── svg.js
│ │ ├── cleanUpMarkdown.js
│ │ ├── index.js
│ │ ├── element.js
│ │ └── icons.js
│ ├── story
│ │ ├── getTitleFromCommitMessage.js
│ │ ├── setAnnotationLink.js
│ │ ├── codeMirror.js
│ │ ├── renderGraph.js
│ │ ├── renderDiffs.js
│ │ ├── renderCommits.js
│ │ └── index.js
│ ├── constants.js
│ ├── popups
│ │ ├── confirmPopUp.js
│ │ ├── newFilePopUp.js
│ │ ├── editFilePopUp.js
│ │ ├── editNamePopUp.js
│ │ ├── popup.js
│ │ └── settingsPopUp.js
│ ├── settings.js
│ ├── annotate.js
│ ├── storyPreview.js
│ ├── providers
│ │ └── api.js
│ ├── dependencies.js
│ ├── download.js
│ ├── output.js
│ ├── execute.js
│ ├── console.js
│ ├── __tests__
│ │ └── commitDiff.spec.js
│ ├── index.js
│ ├── storyReadOnly.js
│ ├── layout.js
│ ├── editor.js
│ ├── statusBar.js
│ └── state.js
├── resources
│ ├── state-example2.json
│ ├── state-example.json
│ └── FileSaver.min.js
├── js-vendor
│ ├── colorize.js
│ ├── runmode.js
│ ├── overlay.js
│ ├── mark-selection.js
│ ├── split.js
│ ├── gfm.js
│ ├── jsx.js
│ ├── htmlmixed.js
│ ├── match-highlighter.js
│ ├── matchbrackets.js
│ └── closebrackets.js
├── css
│ ├── la.css
│ ├── light_theme.css
│ └── dark_theme.css
└── index.html
├── .npmignore
├── scripts
└── zipit.js
├── webpack.config.prod.js
├── webpack.config.js
├── samples
├── HTML+CSS.json
├── Vue.json
├── React.json
├── index.html
└── Canvas.json
├── LICENSE
├── .gitignore
├── package.json
├── README.md
└── .eslintrc
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/.DS_Store
--------------------------------------------------------------------------------
/demoit.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/demoit.zip
--------------------------------------------------------------------------------
/_assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/.DS_Store
--------------------------------------------------------------------------------
/_assets/demoit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit.png
--------------------------------------------------------------------------------
/_assets/demoit.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit.psd
--------------------------------------------------------------------------------
/_assets/layouts.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/layouts.psd
--------------------------------------------------------------------------------
/_assets/demoit.app.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit.app.psd
--------------------------------------------------------------------------------
/_assets/demoit_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_32x32.png
--------------------------------------------------------------------------------
/_assets/demoit_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_64x64.png
--------------------------------------------------------------------------------
/_assets/demoit_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_dark.png
--------------------------------------------------------------------------------
/_assets/demoit_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_light.png
--------------------------------------------------------------------------------
/dist/img/demoit_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/dist/img/demoit_64x64.png
--------------------------------------------------------------------------------
/src/img/demoit_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/src/img/demoit_64x64.png
--------------------------------------------------------------------------------
/_assets/demoit_100x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_100x100.png
--------------------------------------------------------------------------------
/_assets/demoit_200x200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_200x200.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | demoit.png
2 | src
3 | webpack.config.js
4 | webpack.config.prod.js
5 | *.log
6 | yarn.lock
7 | scripts
8 | NOTES
9 | _assets
10 | .tmp
--------------------------------------------------------------------------------
/src/js/utils/setTheme.js:
--------------------------------------------------------------------------------
1 | import el from './element';
2 |
3 | export default function setTheme(theme) {
4 | el.withRelaxedCleanup('.app').attr('class', 'app ' + theme);
5 | }
--------------------------------------------------------------------------------
/src/js/story/getTitleFromCommitMessage.js:
--------------------------------------------------------------------------------
1 | import { truncate } from '../utils';
2 | import cleanUpMarkdown from '../utils/cleanUpMarkdown';
3 |
4 | export default function getTitleFromCommitMessage(text) {
5 | return cleanUpMarkdown(truncate(text.split('\n').shift(), 36));
6 | };
7 |
--------------------------------------------------------------------------------
/scripts/zipit.js:
--------------------------------------------------------------------------------
1 | var zipFolder = require('zip-folder');
2 |
3 | zipFolder(__dirname + '/../dist', __dirname + '/../demoit.zip', function (err) {
4 | if (err) {
5 | console.log('Zipping extension failed.', err);
6 | } else {
7 | console.log('Zipping extension successful.');
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/src/js/utils/transpile.js:
--------------------------------------------------------------------------------
1 | const OPTIONS = {
2 | presets: [
3 | 'react',
4 | ['es2015',
5 | { 'modules': false }
6 | ],
7 | 'es2016',
8 | 'es2017',
9 | 'stage-0',
10 | 'stage-1',
11 | 'stage-2',
12 | 'stage-3'
13 | ],
14 | plugins: [
15 | 'transform-es2015-modules-commonjs'
16 | ]
17 | };
18 |
19 | export default function preprocess(str) {
20 | const { code } = Babel.transform(str, OPTIONS);
21 |
22 | return code;
23 | };
24 |
--------------------------------------------------------------------------------
/src/js/constants.js:
--------------------------------------------------------------------------------
1 | function matchAgainstURL(pattern) {
2 | return window.location.href.match(pattern);
3 | }
4 |
5 | export const DEBUG = false;
6 |
7 | export const IS_PROD = matchAgainstURL(/^https:\/\/poet.krasimir.now.sh/) || matchAgainstURL(/^http:\/\/localhost:8004/);
8 | export const DEV = matchAgainstURL(/^http:\/\/localhost:8004/);
9 |
10 | export const SAVE_DEMO_URL = !DEV ? 'https://poet.krasimir.now.sh/api/demo' : 'http://localhost:8004/api/demo';
11 | export const GET_DEMOS_URL = !DEV ? 'https://poet.krasimir.now.sh/api/profile' : 'http://localhost:8004/api/profile';
12 |
--------------------------------------------------------------------------------
/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: [
5 | './src/js/index.js'
6 | ],
7 | output: {
8 | path: path.resolve(__dirname, '.tmp'),
9 | filename: 'demoit.js'
10 | },
11 | mode: 'production',
12 | module: {
13 | rules: [
14 | {
15 | test: /\.m?js$/,
16 | exclude: /(node_modules|bower_components)/,
17 | use: {
18 | loader: 'babel-loader',
19 | options: {
20 | presets: ['@babel/preset-env'],
21 | plugins: ['@babel/plugin-transform-runtime']
22 | }
23 | }
24 | }
25 | ]
26 | }
27 | };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: [
5 | './src/js/index.js'
6 | ],
7 | output: {
8 | path: path.resolve(__dirname, '.tmp'),
9 | filename: 'demoit.js'
10 | },
11 | mode: 'development',
12 | watch: true,
13 | module: {
14 | rules: [
15 | {
16 | test: /\.m?js$/,
17 | exclude: /(node_modules|bower_components)/,
18 | use: {
19 | loader: 'babel-loader',
20 | options: {
21 | presets: ['@babel/preset-env'],
22 | plugins: ['@babel/plugin-transform-runtime']
23 | }
24 | }
25 | }
26 | ]
27 | }
28 | };
--------------------------------------------------------------------------------
/src/js/popups/confirmPopUp.js:
--------------------------------------------------------------------------------
1 | import { CHECK_ICON, CLOSE_ICON } from '../utils/icons';
2 | import createPopup from './popup';
3 |
4 | export default function confirmPopUp(title, message, onChange) {
5 | createPopup({
6 | title,
7 | content: `
8 |
${ message }
9 |
10 |
11 | `,
12 | onRender({ yesButton, noButton, closePopup }) {
13 | const save = (value) => {
14 | onChange(value);
15 | closePopup();
16 | };
17 |
18 | yesButton.onClick(() => save(true));
19 | noButton.onClick(() => save(false));
20 | }
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/src/js/utils/localStorage.js:
--------------------------------------------------------------------------------
1 | const AVAILABLE = (function () {
2 | const test = 'test';
3 |
4 | try {
5 | localStorage.setItem(test, test);
6 | localStorage.removeItem(test);
7 | return true;
8 | } catch(e) {
9 | return false;
10 | }
11 | })();
12 |
13 | const LS = function (key, value) {
14 | if (!AVAILABLE) return null;
15 |
16 | // setting
17 | if (typeof value !== 'undefined') {
18 | localStorage.setItem(key, JSON.stringify(value));
19 | }
20 |
21 | // reading
22 | const data = localStorage.getItem(key);
23 |
24 | try {
25 | if (data) return JSON.parse(data);
26 | } catch(error) {
27 | console.error(`There is some data in the local storage under the ${ key } key. However, it is not a valid JSON.`);
28 | }
29 | return null
30 | }
31 |
32 | export default LS;
--------------------------------------------------------------------------------
/src/js/settings.js:
--------------------------------------------------------------------------------
1 | import settingsPopUp from './popups/settingsPopUp';
2 | import download from './download';
3 |
4 | const filterDeps = deps => deps.filter(dep => (dep !== '' && dep !== '\n'));
5 |
6 | export default function settings(state, render, executeCurrentFile) {
7 | const showSettingsPopUp = tab => settingsPopUp(
8 | download(state),
9 | state.getEditorSettings(),
10 | filterDeps(state.getDependencies()).join('\n'),
11 | function onDepsUpdated(newDeps) {
12 | if (newDeps) {
13 | state.setDependencies(newDeps);
14 | executeCurrentFile();
15 | }
16 | },
17 | function onGeneralUpdate(newTheme, newLayout) {
18 | state.updateThemeAndLayout(newLayout, newTheme);
19 | render();
20 | },
21 | tab,
22 | state.version()
23 | );
24 |
25 | return showSettingsPopUp;
26 | }
27 |
--------------------------------------------------------------------------------
/src/js/story/setAnnotationLink.js:
--------------------------------------------------------------------------------
1 | export default function setAnnotationLink(editor, code, list, activeFile) {
2 | let thingToInsert = '';
3 |
4 | try {
5 | let { line, ch } = editor.getCursor();
6 | const currentLine = editor.getValue().split('\n')[line];
7 |
8 | if (
9 | currentLine.charAt(ch) === ')' &&
10 | currentLine.charAt(ch - 1) === '(' &&
11 | currentLine.charAt(ch - 2) === ']'
12 | ) {
13 | let { anchor, head } = list.shift();
14 |
15 | thingToInsert = ['a', activeFile, anchor.line, anchor.ch, head.line, head.ch ].join(':');
16 | } else if (
17 | currentLine.charAt(ch - 1) === '' &&
18 | currentLine.charAt(ch + 1) === ''
19 | ) {
20 | thingToInsert = code;
21 | }
22 | } catch (error) {
23 | console.log('Error while setting annotation.');
24 | }
25 |
26 | editor.replaceSelection(thingToInsert);
27 | }
28 |
--------------------------------------------------------------------------------
/src/js/utils/executeCSS.js:
--------------------------------------------------------------------------------
1 | const STYLES_CACHE = {};
2 | const guaranteeValidIdName = filename => filename.replace(/\./g, '_');
3 |
4 | export const injectCSS = function (css, id) {
5 | const node = document.querySelector('#' + id);
6 |
7 | if (node) {
8 | node.innerHTML = css;
9 | } else {
10 | const node = document.createElement('style');
11 |
12 | id && node.setAttribute('id', id);
13 | node.innerHTML = css;
14 | document.body.appendChild(node);
15 | }
16 | };
17 |
18 | export const executeCSS = function (filename, content) {
19 | if (!STYLES_CACHE[filename]) {
20 | const node = document.createElement('style');
21 |
22 | node.setAttribute('id', guaranteeValidIdName(filename));
23 | node.innerHTML = content;
24 | setTimeout(function () {
25 | document.body.appendChild(node);
26 | }, 1);
27 | STYLES_CACHE[filename] = node;
28 | } else {
29 | STYLES_CACHE[filename].innerHTML = content;
30 | }
31 | };
32 |
33 |
--------------------------------------------------------------------------------
/src/js/popups/newFilePopUp.js:
--------------------------------------------------------------------------------
1 | import createPopup from './popup';
2 | import { CHECK_ICON } from '../utils/icons';
3 |
4 | const ENTER_KEY = 13;
5 |
6 | export default function newFilePopUp() {
7 | return new Promise(done => createPopup({
8 | title: 'New file',
9 | content: `
10 |
11 |
12 | `,
13 | cleanUp() {
14 | done();
15 | },
16 | onRender({ filenameInput, saveButton, closePopup }) {
17 | const save = () => {
18 | filenameInput.e.value !== '' && done(filenameInput.e.value);
19 | closePopup();
20 | };
21 |
22 | filenameInput.e.focus();
23 | filenameInput.onKeyUp(e => {
24 | if (e.keyCode === ENTER_KEY) {
25 | save();
26 | }
27 | });
28 | saveButton.onClick(save);
29 | }
30 | }));
31 | };
32 |
--------------------------------------------------------------------------------
/samples/HTML+CSS.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "theme": "light",
4 | "statusBar": true,
5 | "layout": {
6 | "name": "default",
7 | "direction": "horizontal",
8 | "sizes": [
9 | 30,
10 | 70
11 | ],
12 | "elements": [
13 | {
14 | "direction": "vertical",
15 | "sizes": [
16 | 50,
17 | 50
18 | ],
19 | "elements": [
20 | "output",
21 | "log"
22 | ]
23 | },
24 | "editor"
25 | ]
26 | }
27 | },
28 | "dependencies": [],
29 | "files": [
30 | {
31 | "content": "import 'styles.css';\nimport 'markup.html';",
32 | "filename": "app.js",
33 | "editing": false,
34 | "entryPoint": true
35 | },
36 | {
37 | "content": "h1 {\n color: purple;\n}",
38 | "filename": "styles.css",
39 | "editing": false
40 | },
41 | {
42 | "content": "Hello app
",
43 | "filename": "markup.html",
44 | "editing": false
45 | }
46 | ]
47 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Krasimir Tsonev
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 |
--------------------------------------------------------------------------------
/dist/resources/state-example2.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "theme": "light",
4 | "statusBar": false,
5 | "layout": {
6 | "elements": [
7 | {
8 | "name": "story",
9 | "elements": []
10 | },
11 | {
12 | "name": "story-preview",
13 | "elements": []
14 | }
15 | ],
16 | "direction": "horizontal"
17 | }
18 | },
19 | "dependencies": [],
20 | "files": {
21 | "working": [
22 | [
23 | "code.js",
24 | {
25 | "c": "document.querySelector('#output').innerHTML = 'Hello world';\n\nconsole.log('Hello world');"
26 | }
27 | ]
28 | ],
29 | "head": "_2",
30 | "i": 2,
31 | "stage": [],
32 | "commits": {
33 | "_1": {
34 | "message": "first commit",
35 | "parent": null,
36 | "files": "[[\"code.js\",{\"c\":\"document.querySelector('#output').innerHTML = 'Hello world';\\n\\nconsole.log('Hello world');\"}]]"
37 | },
38 | "_2": {
39 | "message": "second commit",
40 | "parent": "_1",
41 | "files": ""
42 | }
43 | }
44 | },
45 | "v": "7.1.0"
46 | }
--------------------------------------------------------------------------------
/src/resources/state-example2.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "theme": "light",
4 | "statusBar": false,
5 | "layout": {
6 | "elements": [
7 | {
8 | "name": "story",
9 | "elements": []
10 | },
11 | {
12 | "name": "story-preview",
13 | "elements": []
14 | }
15 | ],
16 | "direction": "horizontal"
17 | }
18 | },
19 | "dependencies": [],
20 | "files": {
21 | "working": [
22 | [
23 | "code.js",
24 | {
25 | "c": "document.querySelector('#output').innerHTML = 'Hello world';\n\nconsole.log('Hello world');"
26 | }
27 | ]
28 | ],
29 | "head": "_2",
30 | "i": 2,
31 | "stage": [],
32 | "commits": {
33 | "_1": {
34 | "message": "first commit",
35 | "parent": null,
36 | "files": "[[\"code.js\",{\"c\":\"document.querySelector('#output').innerHTML = 'Hello world';\\n\\nconsole.log('Hello world');\"}]]"
37 | },
38 | "_2": {
39 | "message": "second commit",
40 | "parent": "_1",
41 | "files": ""
42 | }
43 | }
44 | },
45 | "v": "7.1.0"
46 | }
--------------------------------------------------------------------------------
/src/js/utils/commitDiff.js:
--------------------------------------------------------------------------------
1 | const gitfred = require('gitfred');
2 |
3 | const git = gitfred();
4 | const toDict = arr => arr.reduce((dict, file) => {
5 | dict[file[0]] = file[1].c;
6 | return dict;
7 | }, {});
8 |
9 | module.exports = function commitDiff(oldFiles, newFiles) {
10 | const diffs = [];
11 | const dictA = toDict(oldFiles);
12 | const dictB = toDict(newFiles);
13 |
14 | Object.keys(dictA).forEach(filename => {
15 | if (dictB.hasOwnProperty(filename)) {
16 | const strDiff = git.calcStrDiff(dictA[filename], dictB[filename]);
17 |
18 | if (strDiff !== null) {
19 | diffs.push(['E', filename, strDiff]);
20 | }
21 | } else {
22 | diffs.push(['D', filename, dictA[filename]]);
23 | Object.keys(dictB).forEach(filenameInB => {
24 | if (dictA[filename] === dictB[filenameInB]) {
25 | diffs.push(['R', filename, filenameInB]);
26 | delete dictB[filenameInB];
27 | }
28 | });
29 | }
30 | });
31 | Object.keys(dictB).forEach(filename => {
32 | if (!dictA.hasOwnProperty(filename)) {
33 | diffs.push(['N', filename, dictB[filename]]);
34 | }
35 | });
36 |
37 | return diffs;
38 | };
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | NOTES
64 |
65 | *.log
66 |
67 | .vscode
68 | .tmp
--------------------------------------------------------------------------------
/src/js/story/codeMirror.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-sequences */
2 |
3 | import defineCodeMirrorCommands from '../utils/codeMirrorCommands';
4 |
5 | export default function codeMirror(container, editorSettings, value, onSave, onChange, onCancel) {
6 | defineCodeMirrorCommands(CodeMirror);
7 |
8 | const editor = CodeMirror(container.e, {
9 | value: value || '',
10 | mode: 'gfm',
11 | tabSize: 2,
12 | lineNumbers: false,
13 | autofocus: true,
14 | foldGutter: false,
15 | gutters: [],
16 | styleSelectedText: true,
17 | lineWrapping: true,
18 | highlightFormatting: true,
19 | ...editorSettings
20 | });
21 | const save = () => onSave(editor.getValue());
22 | const change = () => onChange(editor.getValue());
23 |
24 | editor.on('change', change);
25 | editor.setOption('extraKeys', {
26 | 'Ctrl-S': save,
27 | 'Cmd-S': save,
28 | Esc: () => onCancel(editor.getValue()),
29 | 'Ctrl-Enter': () => (save(), onCancel(editor.getValue())),
30 | 'Cmd-Enter': () => (save(), onCancel(editor.getValue()))
31 | });
32 | CodeMirror.normalizeKeyMap();
33 | setTimeout(() => {
34 | editor.focus();
35 | console.log(editor.defaultTextHeight());
36 | }, 50);
37 |
38 | return editor;
39 | }
40 |
--------------------------------------------------------------------------------
/samples/Vue.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "theme": "light",
4 | "statusBar": true,
5 | "layout": {
6 | "name": "default",
7 | "direction": "horizontal",
8 | "sizes": [
9 | 30,
10 | 70
11 | ],
12 | "elements": [
13 | {
14 | "direction": "vertical",
15 | "sizes": [
16 | 50,
17 | 50
18 | ],
19 | "elements": [
20 | "output",
21 | "log"
22 | ]
23 | },
24 | "editor"
25 | ]
26 | }
27 | },
28 | "dependencies": [
29 | "https://unpkg.com/vue@2.5.21/dist/vue.min.js",
30 | "./resources/marked@0.3.6.js",
31 | "./resources/lodash@4.16.0.js"
32 | ],
33 | "files": [
34 | {
35 | "content": "document.querySelector('.output').innerHTML = `\n \n`;\n\nnew Vue({\n el: '#editor',\n data: {\n input: '# hello'\n },\n computed: {\n compiledMarkdown: function () {\n return marked(this.input, { sanitize: true })\n }\n },\n methods: {\n update: _.debounce(function (e) {\n this.input = e.target.value\n }, 300)\n }\n});",
36 | "filename": "Vue.js",
37 | "editing": false
38 | }
39 | ]
40 | }
--------------------------------------------------------------------------------
/src/js/story/renderGraph.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define, max-len */
2 |
3 | import { connectCommits, empty as emptySVGGraph } from '../utils/svg';
4 | import el from '../utils/element';
5 | import { DEBUG } from '../constants';
6 |
7 | const SVG_X = 4;
8 | const SVG_INITIAL_Y = 25;
9 |
10 | export default function renderGraph(sortedCommits, tree) {
11 | setTimeout(() => {
12 | emptySVGGraph();
13 |
14 | DEBUG && console.log(JSON.stringify(sortedCommits.map(({ hash, position }) => ({ hash, position })), null, 2));
15 |
16 | const { connections, commitsYs } = renderCommitGraphs(getYValueOfCommitElement(sortedCommits[0].hash), tree);
17 |
18 | connections.forEach(([ hashA, hashB ]) => connectCommits(SVG_X, SVG_INITIAL_Y + commitsYs[hashA], SVG_INITIAL_Y + commitsYs[hashB]));
19 | }, 30);
20 | }
21 | function renderCommitGraphs(rootY, { parent, hash, derivatives }, result = { commitsYs: {}, connections: [] }) {
22 | result.commitsYs[hash] = getYValueOfCommitElement(hash) - rootY;
23 | if (parent !== null) {
24 | result.connections.push([ parent, hash ]);
25 | }
26 | if (derivatives.length > 0) {
27 | derivatives.forEach(commit => renderCommitGraphs(rootY, commit, result));
28 | }
29 | return result;
30 | }
31 | function getYValueOfCommitElement(hash) {
32 | if (el.exists('#c' + hash)) {
33 | return el('#c' + hash).e.getBoundingClientRect().top + 0.3;
34 | }
35 | return 0;
36 | }
37 |
--------------------------------------------------------------------------------
/src/js/annotate.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import el from './utils/element';
3 |
4 | export default function (state) {
5 | if (!el.exists('#annotate')) return;
6 |
7 | const git = state.git();
8 |
9 | window.addEventListener('message', function (e) {
10 | if (e.data.checkoutTo) {
11 | git.checkout(e.data.checkoutTo);
12 | }
13 | });
14 |
15 | const container = el.withFallback('#annotate');
16 | const demoId = state.getDemoId();
17 | const preview = (input, hash, form) => {
18 | if (git.head() !== null) {
19 | input.prop('value', JSON.stringify(git.export()));
20 | hash.prop('value', git.head());
21 | form.e.submit();
22 | }
23 | };
24 |
25 | container.content(`
26 |
30 |
31 | `);
32 |
33 | const { form, input, hash } = container.namedExports();
34 |
35 | state.listen(event => {
36 | preview(input, hash, form);
37 | });
38 |
39 | preview(input, hash, form);
40 | };
41 |
--------------------------------------------------------------------------------
/src/js/storyPreview.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import el from './utils/element';
3 |
4 | export default function (state) {
5 | if (!el.exists('#preview')) return;
6 |
7 | const git = state.git();
8 |
9 | window.addEventListener('message', function (e) {
10 | if (e.data.checkoutTo) {
11 | git.checkout(e.data.checkoutTo);
12 | }
13 | });
14 |
15 | const container = el.withFallback('#preview');
16 | const demoId = state.getDemoId();
17 | const preview = (input, hash, form) => {
18 | if (git.head() !== null) {
19 | input.prop('value', JSON.stringify(git.export()));
20 | hash.prop('value', git.head());
21 | form.e.submit();
22 | }
23 | };
24 |
25 | container.content(`
26 |
30 |
31 | `);
32 |
33 | const { form, input, hash } = container.namedExports();
34 |
35 | state.listen(event => {
36 | preview(input, hash, form);
37 | });
38 |
39 | preview(input, hash, form);
40 | };
41 |
--------------------------------------------------------------------------------
/src/js/utils/codeMirrorCommands.js:
--------------------------------------------------------------------------------
1 | function isSelectedRange(ranges, from, to) {
2 | for (var i = 0; i < ranges.length; i++)
3 | if (ranges[i].from() == from && ranges[i].to() == to) return true
4 | return false
5 | }
6 |
7 | export default function (CodeMirror) {
8 | const cmds = CodeMirror.commands;
9 | const Pos = CodeMirror.Pos;
10 |
11 | cmds.selectNextOccurrence = function(cm) {
12 | var from = cm.getCursor("from"), to = cm.getCursor("to");
13 | var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel;
14 | if (CodeMirror.cmpPos(from, to) == 0) {
15 | var word = wordAt(cm, from);
16 | if (!word.word) return;
17 | cm.setSelection(word.from, word.to);
18 | fullWord = true;
19 | } else {
20 | var text = cm.getRange(from, to);
21 | var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
22 | var cur = cm.getSearchCursor(query, to);
23 | var found = cur.findNext();
24 | if (!found) {
25 | cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
26 | found = cur.findNext();
27 | }
28 | if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to()))
29 | return CodeMirror.Pass
30 | cm.addSelection(cur.from(), cur.to());
31 | }
32 | if (fullWord)
33 | cm.state.sublimeFindFullWord = cm.doc.sel;
34 | };
35 |
36 | cmds.toggleCommentIndented = function(cm) {
37 | cm.toggleComment({ indent: true });
38 | }
39 | }
--------------------------------------------------------------------------------
/src/js/utils/svg.js:
--------------------------------------------------------------------------------
1 | function createSVG() {
2 | return document.getElementById('svg-canvas');
3 | }
4 | function drawCircle(x, y, radius, color) {
5 | const svg = createSVG();
6 | const shape = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
7 |
8 | shape.setAttributeNS(null, 'cx', x);
9 | shape.setAttributeNS(null, 'cy', y);
10 | shape.setAttributeNS(null, 'r', radius);
11 | shape.setAttributeNS(null, 'fill', color);
12 | svg.appendChild(shape);
13 | }
14 | function drawCurvedLine(x1, y1, x2, y2, color) {
15 | const svg = createSVG();
16 | const shape = document.createElementNS('http://www.w3.org/2000/svg', 'path');
17 | const hx1 = 40;
18 | const hy1 = y1 + 15;
19 | const hx2 = x2 + 15;
20 | const hy2 = y2;
21 |
22 | const path = 'M ' + x1 + ' ' + y1 + ' C ' + hx1 + ' ' + hy1 + ' ' + hx2 + ' ' + hy2 + ' ' + x2 + ' ' + y2;
23 |
24 | shape.setAttributeNS(null, 'd', path);
25 | shape.setAttributeNS(null, 'fill', 'none');
26 | shape.setAttributeNS(null, 'stroke', color);
27 | svg.appendChild(shape);
28 | }
29 | function connectCommits(x, y1, y2) {
30 | const color = '#999';
31 |
32 | drawCircle(x, y1, 3, color);
33 | drawCircle(x, y2, 3, color);
34 | drawCurvedLine(x, y1, x, y2, color);
35 | };
36 | function empty() {
37 | const e = createSVG();
38 |
39 | while (e.firstChild) {
40 | e.removeChild(e.firstChild);
41 | }
42 | }
43 |
44 | module.exports = {
45 | connectCommits: connectCommits,
46 | empty: empty
47 | };
48 |
--------------------------------------------------------------------------------
/src/js-vendor/colorize.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
3 | // Distributed under an MIT license: https://codemirror.net/LICENSE
4 |
5 | (function(mod) {
6 | if (typeof exports == "object" && typeof module == "object") // CommonJS
7 | mod(require("../../lib/codemirror"), require("./runmode"));
8 | else if (typeof define == "function" && define.amd) // AMD
9 | define(["../../lib/codemirror", "./runmode"], mod);
10 | else // Plain browser env
11 | mod(CodeMirror);
12 | })(function(CodeMirror) {
13 | "use strict";
14 |
15 | var isBlock = /^(p|li|div|h\\d|pre|blockquote|td)$/;
16 |
17 | function textContent(node, out) {
18 | if (node.nodeType == 3) return out.push(node.nodeValue);
19 | for (var ch = node.firstChild; ch; ch = ch.nextSibling) {
20 | textContent(ch, out);
21 | if (isBlock.test(node.nodeType)) out.push("\n");
22 | }
23 | }
24 |
25 | CodeMirror.colorize = function(collection, defaultMode, themeClass) {
26 | if (!collection) collection = document.body.getElementsByTagName("pre");
27 |
28 | for (var i = 0; i < collection.length; ++i) {
29 | var node = collection[i];
30 | var mode = node.getAttribute("data-lang") || defaultMode;
31 | if (!mode) continue;
32 |
33 | var text = [];
34 | textContent(node, text);
35 | node.innerHTML = "";
36 | CodeMirror.runMode(text.join(""), mode, node);
37 |
38 | node.className += themeClass ? themeClass : " cm-s-default";
39 | }
40 | };
41 | });
--------------------------------------------------------------------------------
/samples/React.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "theme": "light",
4 | "statusBar": true,
5 | "layout": {
6 | "name": "layoutBottom",
7 | "direction": "vertical",
8 | "sizes": [
9 | 70,
10 | 30
11 | ],
12 | "elements": [
13 | "editor",
14 | {
15 | "direction": "horizontal",
16 | "sizes": [
17 | 50,
18 | 50
19 | ],
20 | "elements": [
21 | "output",
22 | "log"
23 | ]
24 | }
25 | ]
26 | }
27 | },
28 | "dependencies": [
29 | "./resources/react-16.7.0-alpha.0.js",
30 | "./resources/react-dom.16.7.0-alpha.0.js"
31 | ],
32 | "files": [
33 | {
34 | "filename": "ReactHooks.js",
35 | "content": "import Title from 'Title.js';\nimport 'styles.css';\n\nconst useState = React.useState;\n\nconst App = function () {\n const [ count, change ] = useState(0);\n console.log(`count is: ${ count }`);\n return (\n \t\n \n \n \n )\n}\n\nReactDOM.render(, document.querySelector('.output'));\n",
36 | "editing": false,
37 | "entryPoint": true
38 | },
39 | {
40 | "content": "export default function Title({ count }) {\n return Counter: { count }
;\n}",
41 | "filename": "Title.js",
42 | "editing": false
43 | },
44 | {
45 | "content": "h1 {\n\tfont-family: Tahoma; \n}",
46 | "filename": "styles.css",
47 | "editing": false
48 | }
49 | ]
50 | }
--------------------------------------------------------------------------------
/src/js/popups/editFilePopUp.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-sequences */
2 | import createPopup from './popup';
3 | import { TRASH_ICON, CHECK_ICON, DOT_CIRCLE } from '../utils/icons';
4 |
5 | const ENTER_KEY = 13;
6 |
7 | export default function editFilePopUp(filename, totalNumOfFiles, deleteFile, rename, setAsEntryPoint) {
8 | createPopup({
9 | title: 'Edit',
10 | content: `
11 |
12 |
13 |
14 |
15 | `,
16 | onRender({ filenameInput, saveButton, closePopup, deleteButton, setAsEntryPointButton }) {
17 | const save = () => {
18 | filenameInput.e.value !== '' && rename(filenameInput.e.value);
19 | closePopup();
20 | };
21 |
22 | filenameInput.e.focus();
23 | filenameInput.e.setSelectionRange(0, filename.lastIndexOf('.'));
24 | filenameInput.onKeyUp(e => {
25 | if (e.keyCode === ENTER_KEY) {
26 | save();
27 | }
28 | });
29 | saveButton.onClick(save);
30 |
31 | totalNumOfFiles > 1 ? deleteButton.css('display', 'block') : deleteButton.css('display', 'none');
32 | deleteButton.onClick(() => (deleteFile(), closePopup()));
33 | setAsEntryPointButton.onClick(() => (setAsEntryPoint(), closePopup()));
34 | }
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/src/js/providers/api.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable consistent-return, no-use-before-define */
2 | import { SAVE_DEMO_URL, GET_DEMOS_URL } from '../constants';
3 |
4 | let requestInFlight = false;
5 | let queue = [];
6 |
7 | const emptyQueue = function () {
8 | if (queue.length > 0) {
9 | const { state, token, diff } = queue.shift();
10 |
11 | saveDemo(state, token, diff);
12 | }
13 | };
14 |
15 | const saveDemo = async function (state, token, diff) {
16 | if (requestInFlight) {
17 | queue.push({ state, token, diff });
18 | return;
19 | }
20 |
21 | try {
22 | requestInFlight = true;
23 | const response = await fetch(SAVE_DEMO_URL, {
24 | method: 'POST',
25 | body: JSON.stringify({ state, a: diff }),
26 | headers: { token }
27 | });
28 |
29 | const result = await response.json();
30 |
31 | requestInFlight = false;
32 | emptyQueue();
33 |
34 | if (result.error) {
35 | console.error(result.error);
36 | } else if (result.demoId) {
37 | return result.demoId;
38 | }
39 | console.log(result);
40 | } catch (error) {
41 | emptyQueue();
42 | console.error(error);
43 | }
44 | };
45 |
46 | const getDemos = async function (id, token) {
47 | try {
48 | const response = await fetch(GET_DEMOS_URL + '/' + id, { headers: { token } });
49 | const result = await response.json();
50 |
51 | if (result.error) {
52 | console.error(result.error);
53 | } else if (result) {
54 | return result;
55 | } else {
56 | console.log(result);
57 | }
58 | } catch (error) {
59 | console.error(error);
60 | }
61 | };
62 |
63 | export default {
64 | saveDemo,
65 | getDemos
66 | };
67 |
--------------------------------------------------------------------------------
/src/js/dependencies.js:
--------------------------------------------------------------------------------
1 | const LOADED_FILES_CACHE = {};
2 |
3 | const addJSFile = function (path, done) {
4 | if (LOADED_FILES_CACHE[path]) return done();
5 | LOADED_FILES_CACHE[path] = false;
6 |
7 | const node = document.createElement('script');
8 |
9 | node.src = path;
10 | node.addEventListener('load', () => {
11 | LOADED_FILES_CACHE[path] = true;
12 | done();
13 | });
14 | document.body.appendChild(node);
15 | return true;
16 | };
17 | const addCSSFile = function (path, done) {
18 | if (LOADED_FILES_CACHE[path]) return done();
19 | LOADED_FILES_CACHE[path] = false;
20 |
21 | const node = document.createElement('link');
22 |
23 | node.setAttribute('rel', 'stylesheet');
24 | node.setAttribute('type', 'text/css');
25 | node.setAttribute('href', path);
26 | node.addEventListener('load', () => {
27 | LOADED_FILES_CACHE[path] = true;
28 | done();
29 | });
30 | document.body.appendChild(node);
31 | return true;
32 | };
33 |
34 | export const load = async function (dependencies, onProgress = () => {}) {
35 | return new Promise(done => {
36 | (function load(index) {
37 | if (index === dependencies.length) {
38 | done();
39 | return;
40 | }
41 | onProgress(
42 | Math.ceil(100 * (index / dependencies.length)),
43 | dependencies[index].split(/\//).pop()
44 | );
45 |
46 | const resource = dependencies[index];
47 | const extension = resource.split('.').pop().toLowerCase();
48 |
49 | if (extension === 'js') {
50 | addJSFile(resource, () => load(index + 1));
51 | } else if (extension === 'css') {
52 | addCSSFile(resource, () => load(index + 1));
53 | } else {
54 | load(index + 1);
55 | }
56 | })(0);
57 | });
58 | };
59 | export const cache = function () {
60 | return LOADED_FILES_CACHE;
61 | };
62 | export default async function dependencies(onProgress) {
63 | var dependencies = ['./resources/editor.js'];
64 |
65 | await load(dependencies, onProgress);
66 | }
67 |
--------------------------------------------------------------------------------
/src/js/download.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import { load as loadDependencies } from './dependencies';
3 | import { clone } from './utils';
4 |
5 | const ZIP_FILE = '/poet.krasimir.now.sh.zip';
6 |
7 | async function fetchRawFile(url, blob = false) {
8 | return {
9 | content: await (await fetch(url))[ blob ? 'blob' : 'text'](),
10 | url
11 | };
12 | }
13 | async function fetchRemoteDependencies(depsToLoad) {
14 | return await Promise.all(
15 | depsToLoad
16 | .filter(url => url.match(/^(http|https)/))
17 | .map(fetchRawFile)
18 | );
19 | }
20 |
21 | export default function download(state) {
22 | return async button => {
23 | button
24 | .prop('disabled', 'disabled')
25 | .prop('innerHTML', 'Please wait. Preparing the zip file.');
26 |
27 | try {
28 | const remoteDeps = await fetchRemoteDependencies(state.getDependencies());
29 |
30 | await loadDependencies(['./resources/jszip.min.js', './resources/FileSaver.min.js']);
31 |
32 | const zip = await JSZip.loadAsync((await fetchRawFile(ZIP_FILE, true)).content);
33 |
34 | button
35 | .prop('disabled', false)
36 | .prop('innerHTML', 'Download poet.krasimir.now.sh.zip');
37 | button.onClick(async () => {
38 | const indexFile = await zip.file('index.html').async('string');
39 | const newState = clone(state.dump());
40 |
41 | newState.dependencies = remoteDeps.map(({ content, url }) => {
42 | const fileName = './resources/' + url.split('/').pop();
43 |
44 | zip.file(fileName, content);
45 | return fileName;
46 | });
47 | zip.file('index.html', indexFile.replace('var state = null;', `var state = ${ JSON.stringify(newState, null, 2) };`));
48 | saveAs(await zip.generateAsync({ type: 'blob' }), 'poet.krasimir.now.sh.zip');
49 | });
50 |
51 | } catch (error) {
52 | console.error(error);
53 | button.prop('innerHTML', 'There is an error creating the zip file.');
54 | }
55 | };
56 | };
57 |
--------------------------------------------------------------------------------
/src/js/output.js:
--------------------------------------------------------------------------------
1 | import el from './utils/element';
2 | import { DEBUG } from './constants';
3 |
4 | var m = 0;
5 | const markers = {};
6 | const DEFAULT_HINT = '<div id="output" />
';
7 | const createMessageSender = (iframe, addToConsole, clearConsole) => {
8 | window.addEventListener('message', function (e) {
9 | if (e.data.marker) {
10 | DEBUG && console.log('<-- ' + e.data.marker);
11 | if (markers[e.data.marker]) {
12 | markers[e.data.marker].done();
13 | delete markers[e.data.marker];
14 | }
15 | } else if (e.data.log) {
16 | addToConsole(e.data.log);
17 | } else if (e.data.op) {
18 | DEBUG && console.log('<-- ' + e.data.op);
19 | clearConsole();
20 | }
21 | });
22 | return (op, value, customMarker = null) => {
23 | const markerId = ++m;
24 |
25 | DEBUG && console.log('Demoit -> op=' + op + ' markerId=' + markerId);
26 | return new Promise(done => {
27 | markers[customMarker ? customMarker : markerId] = { done, op, value };
28 | if (iframe.e.contentWindow) {
29 | iframe.e.contentWindow.postMessage({
30 | op,
31 | value,
32 | marker: markerId
33 | }, '*');
34 | }
35 | });
36 | };
37 | };
38 |
39 | export default async function output(state, addToConsole, clearConsole) {
40 | const output = el.withFallback('.output');
41 | const iframe = output.find('#sandbox');
42 | const sendMessage = createMessageSender(iframe, addToConsole, clearConsole);
43 |
44 | return {
45 | setOutputHTML: async function clearOutput(hintValue = DEFAULT_HINT) {
46 | return sendMessage('html', hintValue);
47 | },
48 | resetOutput: async function () {
49 | return await sendMessage('reload', null, 'loaded');
50 | },
51 | loadDependenciesInOutput: async function () {
52 | return sendMessage('dependencies', state.getDependencies());
53 | },
54 | executeInOut: async function (value) {
55 | return sendMessage('code', value);
56 | }
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/src/js/execute.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-new-func */
2 | import transpile from './utils/transpile';
3 |
4 | const getExt = file => file.split(/\./).pop().toLowerCase();
5 | const prepareExecution = (filename, content) => {
6 | const ext = getExt(filename || '');
7 |
8 | if (ext === 'css' || ext === 'scss') {
9 | content = `window.executeCSS("${ filename }", ${ JSON.stringify(content) });`;
10 | } else if (ext === 'html') {
11 | content = `window.executeHTML("${ filename }", ${ JSON.stringify(content) });`;
12 | } else if (ext === 'md') {
13 | content = `window.executeMarkdown(${ JSON.stringify(content) });`;
14 | }
15 |
16 | return { filename, content };
17 | };
18 | const formatModule = ({ filename, content }) => `
19 | {
20 | filename: "${ filename }",
21 | func: function (require, exports) {
22 | ${ transpile(content) }
23 | },
24 | exports: {}
25 | }
26 | `;
27 |
28 | export default function execute(activeFile, files) {
29 | const formattedFiles = [];
30 | let index = 0;
31 |
32 | try {
33 | let entryPoint = files.findIndex(([ filename ]) => filename === activeFile);
34 |
35 | files.forEach(([filename, file ]) => {
36 | formattedFiles.push(formatModule(prepareExecution(filename, file.c)));
37 | if (file.en === true) entryPoint = index;
38 | index += 1;
39 | });
40 |
41 | const code = `
42 | const imported = [];
43 | const modules = [${ formattedFiles.join(',') }];
44 | const require = function(file) {
45 | const module = modules.find(({ filename }) => filename === file);
46 |
47 | if (!module) {
48 | throw new Error('Can not find "' + file + '" file.');
49 | }
50 | imported.push(file);
51 | module.func(require, module.exports);
52 | return module.exports;
53 | };
54 |
55 | modules[index].func(require, modules[index].exports);
56 | `;
57 |
58 | return {
59 | code: transpile(code),
60 | entryPoint
61 | };
62 | } catch (error) {
63 | console.error(error);
64 | return null;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/js/story/renderDiffs.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | export default function renderDiffs(git, diffs) {
4 | if (diffs.length === 0) {
5 | return `
6 |
13 | `;
14 | }
15 | return `
16 |
17 |
18 | ${diffs.map(renderDiff).join('')}
19 |
20 |
35 |
36 | `;
37 | };
38 | function renderDiff(diff) {
39 | let diffChangeLabel = '';
40 | let diffAdditionalInfo = '';
41 |
42 | switch (diff[0]) {
43 | case 'E':
44 | diffChangeLabel = 'edit';
45 | diffAdditionalInfo = diff[2].html;
46 | break;
47 | case 'N':
48 | diffChangeLabel = 'new';
49 | break;
50 | case 'D':
51 | diffChangeLabel = 'deleted';
52 | break;
53 | case 'R':
54 | diffChangeLabel = 'renamed';
55 | diffAdditionalInfo = diff[2];
56 | break;
57 | }
58 | return `
59 |
60 |
${diffChangeLabel}
61 |
${diff[1]}
62 |
${diffAdditionalInfo}
63 |
64 | `;
65 | }
66 |
--------------------------------------------------------------------------------
/src/js/popups/editNamePopUp.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import createPopup from './popup';
3 | import { CHECK_ICON } from '../utils/icons';
4 |
5 | const ENTER_KEY = 13;
6 |
7 | export default function editNamePopUp({ name, id, description, published, storyWithCode, comments }, onChange) {
8 | name = name || '';
9 |
10 | createPopup({
11 | title: 'Edit story name',
12 | content: `
13 |
14 |
15 |
18 |
21 |
24 |
25 | `,
26 | onRender({ nameInput, descriptionInput, publishCheckbox, saveButton, closePopup, withCode, withComments }) {
27 | const save = () => {
28 | if (nameInput.e.value !== '') {
29 | onChange({
30 | name: nameInput.e.value,
31 | description: descriptionInput.e.value,
32 | published: publishCheckbox.e.checked,
33 | storyWithCode: withCode.e.checked,
34 | comments: withComments.e.checked
35 | });
36 | }
37 | closePopup();
38 | };
39 |
40 | nameInput.e.focus();
41 | nameInput.e.setSelectionRange(0, name.lastIndexOf('.'));
42 | nameInput.onKeyUp(e => {
43 | if (e.keyCode === ENTER_KEY) {
44 | save();
45 | }
46 | });
47 | saveButton.onClick(save);
48 | }
49 | });
50 | };
51 |
--------------------------------------------------------------------------------
/src/js/console.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 | import el from './utils/element';
3 |
4 | function htmlEncode(str) {
5 | if (typeof str !== 'string') return str;
6 | return str.replace(/[&<>"']/g, function ($0) {
7 | return "&" + {"&": "amp", "<": "lt", ">": "gt", '"': "quot", "'": "#39"}[$0] + ";";
8 | });
9 | }
10 |
11 | export default function logger() {
12 | const element = el.withFallback('.console');
13 |
14 | let empty = true;
15 | const add = something => {
16 | const node = document.createElement('div');
17 | let text = String(something);
18 | let parsed;
19 |
20 | try {
21 | parsed = String(JSON.parse(text));
22 | } catch (error) {
23 | parsed = text;
24 | }
25 |
26 | node.innerHTML = '' + htmlEncode(parsed).replace(/\\n/, '
') + '
';
27 | if (empty) {
28 | element.empty();
29 | empty = false;
30 | }
31 | element.appendChild(node);
32 | element.scrollToBottom();
33 | };
34 |
35 | element.css('opacity', 1);
36 |
37 | (function () {
38 | const originalError = console.error;
39 | const originalLog = console.log;
40 | const originalWarning = console.warn;
41 | const originalInfo = console.info;
42 | const originalClear = console.clear;
43 |
44 | console.error = function (error) {
45 | add(error.stack);
46 | originalError.apply(console, arguments);
47 | };
48 | console.log = function (...args) {
49 | args.forEach(add);
50 | originalLog.apply(console, args);
51 | };
52 | console.warn = function (...args) {
53 | args.forEach(add);
54 | originalWarning.apply(console, args);
55 | };
56 | console.info = function (...args) {
57 | args.forEach(add);
58 | originalInfo.apply(console, args);
59 | };
60 | console.clear = function (...args) {
61 | element.content('');
62 | originalClear.apply(console, args);
63 | };
64 | })();
65 |
66 | return {
67 | clearConsole: function clearConsole(hintValue = '') {
68 | empty = true;
69 | element.empty().content(hintValue);
70 | },
71 | addToConsole: add
72 | };
73 | }
74 |
--------------------------------------------------------------------------------
/src/css/la.css:
--------------------------------------------------------------------------------
1 | .la {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | }
6 | .la-block {
7 | box-sizing: border-box;
8 | height: 100%;
9 | max-width: 100%;
10 | max-height: 100%;
11 | overflow: hidden;
12 | text-align: center;
13 | position: relative;
14 | }
15 | .la-block a.la-remove {
16 | position: absolute;
17 | top: 1em;
18 | right: 1em;
19 | display: none;
20 | border: solid 1px rgba(0, 0, 0, 0);
21 | text-decoration: none;
22 | color: #000;
23 | border-radius: 6px;
24 | width: 20px;
25 | text-align: center;
26 | }
27 | .la-block a.la-remove svg {
28 | fill: #999;
29 | pointer-events: none;
30 | }
31 | .la-block a.la-remove:hover {
32 | border: solid 1px #999;
33 | }
34 | .la-block:hover a.la-remove {
35 | display: block;
36 | }
37 | .la-block a.la-left,
38 | .la-block a.la-right,
39 | .la-block a.la-top,
40 | .la-block a.la-bottom {
41 | opacity: 0.3;
42 | position: absolute;
43 | box-sizing: border-box;
44 | border: solid 1px #999;
45 | }
46 | .la-block a.la-left:hover,
47 | .la-block a.la-right:hover,
48 | .la-block a.la-top:hover,
49 | .la-block a.la-bottom:hover {
50 | opacity: 1;
51 | background: #000;
52 | }
53 | .la-block a.la-left {
54 | top: 0;
55 | left: 0;
56 | height: 100%;
57 | width: 10px;
58 | }
59 | .la-block a.la-right {
60 | top: 0;
61 | right: 0;
62 | height: 100%;
63 | width: 10px;
64 | }
65 | .la-block a.la-top {
66 | top: 0;
67 | left: 0;
68 | width: 100%;
69 | height: 10px;
70 | }
71 | .la-block a.la-bottom {
72 | bottom: 0;
73 | left: 0;
74 | width: 100%;
75 | height: 10px;
76 | }
77 | .la-children {
78 | display: grid;
79 | height: 100%;
80 | padding: 1em;
81 | }
82 | .la-name {
83 | position: absolute;
84 | bottom: 1.5em;
85 | left: 1.5em;
86 | font-size: 0.8em;
87 | }
88 | .la-selector {
89 | box-sizing: border-box;
90 | }
91 | .la-selector a {
92 | box-sizing: border-box;
93 | text-decoration: none;
94 | color: #000;
95 | display: block;
96 | width: 100%;
97 | padding: 0.4em;
98 | }
99 | .la-selector a:hover {
100 | background-color: #e2e2e2;
101 | }
--------------------------------------------------------------------------------
/dist/resources/state-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "theme": "light",
4 | "statusBar": true,
5 | "layout": {
6 | "elements": [
7 | {
8 | "name": "editor",
9 | "elements": []
10 | },
11 | {
12 | "elements": [
13 | {
14 | "name": "HTML",
15 | "elements": []
16 | },
17 | {
18 | "name": "console",
19 | "elements": []
20 | }
21 | ],
22 | "direction": "horizontal",
23 | "sizes": [
24 | 50,
25 | 50
26 | ]
27 | }
28 | ],
29 | "direction": "vertical",
30 | "sizes": [
31 | 49.1499227202473,
32 | 50.8500772797527
33 | ]
34 | }
35 | },
36 | "dependencies": [
37 | "https://unpkg.com/marked@0.6.0/lib/marked.js"
38 | ],
39 | "files": {
40 | "working": [
41 | [
42 | "code.js",
43 | {
44 | "c": "import 'styles.css';\nimport 'markup.html';\n\ndocument.querySelector('#output').innerHTML += marked('# Hello world');\n\nconsole.log('Hello world');\n\nconst api = {};\n\nconsole.log('a');\nconsole.log(100);\nconsole.log([1, 2]);\nconsole.log({ foo: 'bar', f: function() { var a=10;}});\nconsole.log([{ a: 10, b: 20}, { a: 10, b: 30}])\nconsole.log(null);\nconsole.log(undefined);\nconsole.log(NaN);\n\nconst a = [{}];\na[0].a = a;\na.push(a);\n\nconsole.log(a);",
45 | "en": true
46 | }
47 | ],
48 | [
49 | "markup.html",
50 | {
51 | "c": "I am red!
",
52 | "en": false
53 | }
54 | ],
55 | [
56 | "styles.css",
57 | {
58 | "c": "body {\n font-size: 24px;\n padding: 0;\n margin: 0;\n}",
59 | "en": false
60 | }
61 | ],
62 | [
63 | "text.md",
64 | {
65 | "c": "# Markdown text here\n\nThis is text that is using [GitHub](https://github.com)'s **markdown** styling.",
66 | "en": false
67 | }
68 | ]
69 | ],
70 | "head": null,
71 | "i": 0,
72 | "stage": [],
73 | "commits": {}
74 | },
75 | "v": "6.5.1"
76 | }
--------------------------------------------------------------------------------
/src/resources/state-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "theme": "light",
4 | "statusBar": true,
5 | "layout": {
6 | "elements": [
7 | {
8 | "name": "editor",
9 | "elements": []
10 | },
11 | {
12 | "elements": [
13 | {
14 | "name": "HTML",
15 | "elements": []
16 | },
17 | {
18 | "name": "console",
19 | "elements": []
20 | }
21 | ],
22 | "direction": "horizontal",
23 | "sizes": [
24 | 50,
25 | 50
26 | ]
27 | }
28 | ],
29 | "direction": "vertical",
30 | "sizes": [
31 | 49.1499227202473,
32 | 50.8500772797527
33 | ]
34 | }
35 | },
36 | "dependencies": [
37 | "https://unpkg.com/marked@0.6.0/lib/marked.js"
38 | ],
39 | "files": {
40 | "working": [
41 | [
42 | "code.js",
43 | {
44 | "c": "import 'styles.css';\nimport 'markup.html';\n\ndocument.querySelector('#output').innerHTML += marked('# Hello world');\n\nconsole.log('Hello world');\n\nconst api = {};\n\nconsole.log('a');\nconsole.log(100);\nconsole.log([1, 2]);\nconsole.log({ foo: 'bar', f: function() { var a=10;}});\nconsole.log([{ a: 10, b: 20}, { a: 10, b: 30}])\nconsole.log(null);\nconsole.log(undefined);\nconsole.log(NaN);\n\nconst a = [{}];\na[0].a = a;\na.push(a);\n\nconsole.log(a);",
45 | "en": true
46 | }
47 | ],
48 | [
49 | "markup.html",
50 | {
51 | "c": "I am red!
",
52 | "en": false
53 | }
54 | ],
55 | [
56 | "styles.css",
57 | {
58 | "c": "body {\n font-size: 24px;\n padding: 0;\n margin: 0;\n}",
59 | "en": false
60 | }
61 | ],
62 | [
63 | "text.md",
64 | {
65 | "c": "# Markdown text here\n\nThis is text that is using [GitHub](https://github.com)'s **markdown** styling.",
66 | "en": false
67 | }
68 | ]
69 | ],
70 | "head": null,
71 | "i": 0,
72 | "stage": [],
73 | "commits": {}
74 | },
75 | "v": "6.5.1"
76 | }
--------------------------------------------------------------------------------
/src/js/__tests__/commitDiff.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 | const commitDiff = require('../utils/commitDiff');
3 |
4 | describe('Given the Story commit diff helper', () => {
5 | describe('when giving the files of two commits', () => {
6 | [
7 | {
8 | A: [
9 | [ "code.js", { "c": "h" } ]
10 | ],
11 | B: [
12 | [ "code.js", { "c": "hi" } ]
13 | ],
14 | result: [
15 | [
16 | "E",
17 | "code.js",
18 | {
19 | "text": "@@ -1 +1,2 @@\n h\n+i\n",
20 | "html": "hi"
21 | }
22 | ]
23 | ]
24 | },
25 |
26 | {
27 | A: [
28 | [ "code.js", { "c": "h" } ]
29 | ],
30 | B: [
31 | [ "code.js", { "c": "h" } ]
32 | ],
33 | result: []
34 | },
35 |
36 | {
37 | A: [
38 | [ "code.js", { "c": "h" } ],
39 | [ "a.js", { "c": "b" } ]
40 | ],
41 | B: [
42 | [ "code.js", { "c": "h" } ],
43 | [ "b.js", { "c": "d" } ]
44 | ],
45 | result: [
46 | [
47 | "D", "a.js", "b"
48 | ],
49 | [
50 | "N", "b.js", "d"
51 | ]
52 | ]
53 | },
54 |
55 | {
56 | A: [
57 | [ "code.js", { "c": "h" } ],
58 | [ "a.js", { "c": "b" } ]
59 | ],
60 | B: [
61 | [ "code.js", { "c": "h" } ],
62 | [ "b.js", { "c": "b" } ]
63 | ],
64 | result: [
65 | [
66 | "D", "a.js", "b"
67 | ],
68 | [
69 | "R", "a.js", "b.js"
70 | ]
71 | ]
72 | },
73 |
74 | {
75 | A: [
76 | [ "code.js", { "c": "" } ]
77 | ],
78 | B: [
79 | [ "code.js", { "c": "" } ]
80 | ],
81 | result: []
82 | }
83 | ].forEach((testCase, i) => {
84 | it('should calculate the diff #' + (i + 1), () => {
85 | // if (i == 3) console.log(JSON.stringify(commitDiff(testCase.A, testCase.B), null, 2));
86 | expect(commitDiff(testCase.A, testCase.B)).toStrictEqual(testCase.result);
87 | });
88 | });
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Demoit
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
21 |
22 |
28 |
29 |
32 |
33 |
38 |
39 |
44 |
45 |
51 |
52 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Demoit
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
21 |
22 |
28 |
29 |
32 |
33 |
38 |
39 |
44 |
45 |
51 |
52 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/dist/resources/FileSaver.min.js:
--------------------------------------------------------------------------------
1 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Depricated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(b,c,d){var e=new XMLHttpRequest;e.open("GET",b),e.responseType="blob",e.onload=function(){a(e.response,c,d)},e.onerror=function(){console.error("could not download file")},e.send()}function d(a){var b=new XMLHttpRequest;return b.open("HEAD",a,!1),b.send(),200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.saveAs||"object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(a,b,d,e){if(e=e||open("","_blank"),e&&(e.document.title=e.document.body.innerText="downloading..."),"string"==typeof a)return c(a,b,d);var g="application/octet-stream"===a.type,h=/constructor/i.test(f.HTMLElement)||f.safari,i=/CriOS\/[\d]+/.test(navigator.userAgent);if((i||g&&h)&&"object"==typeof FileReader){var j=new FileReader;j.onloadend=function(){var a=j.result;a=i?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),e?e.location.href=a:location=a,e=null},j.readAsDataURL(a)}else{var k=f.URL||f.webkitURL,l=k.createObjectURL(a);e?e.location=l:location.href=l,e=null,setTimeout(function(){k.revokeObjectURL(l)},4E4)}};f.saveAs=a.saveAs=a,"undefined"!=typeof module&&(module.exports=a)});
--------------------------------------------------------------------------------
/src/resources/FileSaver.min.js:
--------------------------------------------------------------------------------
1 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Depricated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(b,c,d){var e=new XMLHttpRequest;e.open("GET",b),e.responseType="blob",e.onload=function(){a(e.response,c,d)},e.onerror=function(){console.error("could not download file")},e.send()}function d(a){var b=new XMLHttpRequest;return b.open("HEAD",a,!1),b.send(),200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.saveAs||"object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(a,b,d,e){if(e=e||open("","_blank"),e&&(e.document.title=e.document.body.innerText="downloading..."),"string"==typeof a)return c(a,b,d);var g="application/octet-stream"===a.type,h=/constructor/i.test(f.HTMLElement)||f.safari,i=/CriOS\/[\d]+/.test(navigator.userAgent);if((i||g&&h)&&"object"==typeof FileReader){var j=new FileReader;j.onloadend=function(){var a=j.result;a=i?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),e?e.location.href=a:location=a,e=null},j.readAsDataURL(a)}else{var k=f.URL||f.webkitURL,l=k.createObjectURL(a);e?e.location=l:location.href=l,e=null,setTimeout(function(){k.revokeObjectURL(l)},4E4)}};f.saveAs=a.saveAs=a,"undefined"!=typeof module&&(module.exports=a)});
--------------------------------------------------------------------------------
/src/js/popups/popup.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-use-before-define */
2 | import el from '../utils/element';
3 | import { CLOSE_ICON } from '../utils/icons';
4 |
5 | const ESC_KEY = 27;
6 |
7 | const DEFAULT_MARKUP = ({ title, content }) => `
8 | ${ title }
9 |
10 | ${ content }
11 | `;
12 | const MULTIPLE_PAGES_MARKUP = ({ buttons, content }, index) => `
13 |
14 | ${ buttons.map((label, i) => `- ${ label }
`).join('') }
15 |
16 |
17 | ${ content[index] }
18 | `;
19 |
20 | export default function popup(config) {
21 | const container = el.fromString('');
22 | const body = el.withRelaxedCleanup('body');
23 | const layout = el.withRelaxedCleanup('.layout');
24 | const escHandler = e => (e.keyCode === ESC_KEY && close());
25 | const removeKeyUpListener = body.onKeyUp(escHandler);
26 | const close = () => {
27 | removeKeyUpListener();
28 | container.css('opacity', 0);
29 | config.cleanUp && config.cleanUp();
30 | setTimeout(() => container.destroy(), 200);
31 | layout.css('filter', 'none');
32 | };
33 | const render = (markup) => {
34 | container.content(markup).forEach(button => {
35 | const dataExport = button.attr('data-export');
36 |
37 | if (dataExport === 'close') {
38 | button.onClick(close);
39 | } else if (dataExport.match(/^page/)) {
40 | button.onClick(() => render(MULTIPLE_PAGES_MARKUP(config, Number(dataExport.split(':').pop()))))
41 | }
42 | });
43 | config.onRender && config.onRender({
44 | closePopup: close,
45 | ...container.namedExports()
46 | });
47 | };
48 |
49 | layout.css('filter', 'blur(2px)');
50 | container.appendTo(body);
51 | render('buttons' in config ? MULTIPLE_PAGES_MARKUP(config, config.defaultTab) : DEFAULT_MARKUP(config));
52 | setTimeout(() => container.css('opacity', 1), 1);
53 | };
54 |
--------------------------------------------------------------------------------
/samples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Demoit samples
8 |
32 |
33 |
34 |
35 |
Empty
36 |
37 |
38 |
39 |
40 |
41 |
42 |
React
43 |
44 |
45 |
46 |
47 |
48 |
49 |
Vue
50 |
51 |
52 |
53 |
54 |
55 |
56 |
HTML+CSS
57 |
58 |
59 |
60 |
61 |
62 |
63 |
Canvas
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/js-vendor/runmode.js:
--------------------------------------------------------------------------------
1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 | // Distributed under an MIT license: https://codemirror.net/LICENSE
3 |
4 | (function(mod) {
5 | if (typeof exports == "object" && typeof module == "object") // CommonJS
6 | mod(require("../../lib/codemirror"));
7 | else if (typeof define == "function" && define.amd) // AMD
8 | define(["../../lib/codemirror"], mod);
9 | else // Plain browser env
10 | mod(CodeMirror);
11 | })(function(CodeMirror) {
12 | "use strict";
13 |
14 | CodeMirror.runMode = function(string, modespec, callback, options) {
15 | var mode = CodeMirror.getMode(CodeMirror.defaults, modespec);
16 | var ie = /MSIE \d/.test(navigator.userAgent);
17 | var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9);
18 |
19 | if (callback.appendChild) {
20 | var tabSize = (options && options.tabSize) || CodeMirror.defaults.tabSize;
21 | var node = callback, col = 0;
22 | node.innerHTML = "";
23 | callback = function(text, style) {
24 | if (text == "\n") {
25 | // Emitting LF or CRLF on IE8 or earlier results in an incorrect display.
26 | // Emitting a carriage return makes everything ok.
27 | node.appendChild(document.createTextNode(ie_lt9 ? '\r' : text));
28 | col = 0;
29 | return;
30 | }
31 | var content = "";
32 | // replace tabs
33 | for (var pos = 0;;) {
34 | var idx = text.indexOf("\t", pos);
35 | if (idx == -1) {
36 | content += text.slice(pos);
37 | col += text.length - pos;
38 | break;
39 | } else {
40 | col += idx - pos;
41 | content += text.slice(pos, idx);
42 | var size = tabSize - col % tabSize;
43 | col += size;
44 | for (var i = 0; i < size; ++i) content += " ";
45 | pos = idx + 1;
46 | }
47 | }
48 |
49 | if (style) {
50 | var sp = node.appendChild(document.createElement("span"));
51 | sp.className = "cm-" + style.replace(/ +/g, " cm-");
52 | sp.appendChild(document.createTextNode(content));
53 | } else {
54 | node.appendChild(document.createTextNode(content));
55 | }
56 | };
57 | }
58 |
59 | var lines = CodeMirror.splitLines(string), state = (options && options.state) || CodeMirror.startState(mode);
60 | for (var i = 0, e = lines.length; i < e; ++i) {
61 | if (i) callback("\n");
62 | var stream = new CodeMirror.StringStream(lines[i]);
63 | if (!stream.string && mode.blankLine) mode.blankLine(state);
64 | while (!stream.eol()) {
65 | var style = mode.token(stream, state);
66 | callback(stream.current(), style, i, stream.start, state);
67 | stream.start = stream.pos;
68 | }
69 | }
70 | };
71 |
72 | });
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define, no-sequences */
2 | import pkg from '../../package.json';
3 | import el from './utils/element';
4 | import layout from './layout';
5 | import editor, { ON_SELECT, ON_FILE_CHANGE, ON_FILE_SAVE } from './editor';
6 | import createState from './state';
7 | import newFilePopUp from './popups/newFilePopUp';
8 | import editFilePopUp from './popups/editFilePopUp';
9 | import editNamePopUp from './popups/editNamePopUp';
10 | import settings from './settings';
11 | import statusBar from './statusBar';
12 | import story from './story';
13 | import storyReadOnly from './storyReadOnly';
14 | import storyPreview from './storyPreview';
15 | import annotate from './annotate';
16 | import { DEBUG } from './constants';
17 |
18 | createState(pkg.version).then(state => {
19 | async function render() {
20 | layout(state);
21 |
22 | let setFilePendingStatus = () => {};
23 | const addToStory = story(state, () => executeCurrentFile());
24 | const { loadFileInEditor: executeCurrentFile, save: saveCurrentFile } = await editor(
25 | state,
26 | (event, data, editor) => {
27 | DEBUG && console.log('editor event=' + event);
28 | switch (event) {
29 | case ON_SELECT:
30 | addToStory(data, editor);
31 | break;
32 | case ON_FILE_CHANGE:
33 | setFilePendingStatus(true);
34 | break;
35 | case ON_FILE_SAVE:
36 | setFilePendingStatus(false);
37 | break;
38 | }
39 | }
40 | );
41 |
42 | storyReadOnly(state, () => executeCurrentFile());
43 | storyPreview(state);
44 | annotate(state);
45 | executeCurrentFile();
46 |
47 | const showSettings = settings(
48 | state,
49 | () => {
50 | state.removeListeners(),
51 | el.destroy();
52 | render();
53 | },
54 | () => executeCurrentFile()
55 | );
56 |
57 | setFilePendingStatus = statusBar(
58 | state,
59 | function showFile(filename) {
60 | state.setActiveFile(filename);
61 | executeCurrentFile();
62 | },
63 | async function newFile() {
64 | const newFilename = await newFilePopUp();
65 |
66 | if (newFilename) {
67 | state.addNewFile(newFilename);
68 | executeCurrentFile();
69 | }
70 | },
71 | async function editFile(filename) {
72 | editFilePopUp(
73 | filename,
74 | state.getNumOfFiles(),
75 | function onDelete() {
76 | state.deleteFile(filename);
77 | executeCurrentFile();
78 | },
79 | function onRename(newName) {
80 | state.renameFile(filename, newName);
81 | executeCurrentFile();
82 | },
83 | function onSetAsEntryPoint() {
84 | state.setEntryPoint(filename);
85 | executeCurrentFile();
86 | }
87 | );
88 | },
89 | showSettings,
90 | saveCurrentFile,
91 | function editName() {
92 | editNamePopUp(state.meta(), meta => state.meta(meta));
93 | }
94 | );
95 | };
96 | render();
97 | });
98 |
--------------------------------------------------------------------------------
/src/css/light_theme.css:
--------------------------------------------------------------------------------
1 | .cm-s-light.CodeMirror {
2 | background: #fff;
3 | color: #24292e; }
4 |
5 | .cm-s-light .CodeMirror-gutters {
6 | background: #fff;
7 | border-right-width: 0; }
8 |
9 | .cm-s-light .CodeMirror-guttermarker {
10 | color: white; }
11 |
12 | .cm-s-light .CodeMirror-guttermarker-subtle {
13 | color: #d0d0d0; }
14 |
15 | .cm-s-light .CodeMirror-linenumber {
16 | color: #959da5;
17 | padding: 0 16px 0 16px; }
18 |
19 | .cm-s-light .CodeMirror-cursor {
20 | border-left: 1px solid #24292e; }
21 |
22 | .cm-s-light div.CodeMirror-selected,
23 | .cm-s-light .CodeMirror-line::selection,
24 | .cm-s-light .CodeMirror-line > span::selection,
25 | .cm-s-light .CodeMirror-line > span > span::selection,
26 | .cm-s-light .CodeMirror-line::-moz-selection,
27 | .cm-s-light .CodeMirror-line > span::-moz-selection,
28 | .cm-s-light .CodeMirror-line > span > span::-moz-selection {
29 | background: #c8c8fa; }
30 |
31 | .cm-s-light .CodeMirror-activeline-background {
32 | background: #fafbfc; }
33 |
34 | .cm-s-light .CodeMirror-lines {
35 | font-family: inherit;
36 | font-size: inherit;
37 | background: #fff;
38 | line-height: 1.5; }
39 |
40 | .cm-s-light .cm-comment {
41 | color: #6a737d; }
42 |
43 | .cm-s-light .cm-constant {
44 | color: #005cc5; }
45 |
46 | .cm-s-light .cm-entity {
47 | font-weight: normal;
48 | font-style: normal;
49 | text-decoration: none;
50 | color: #6f42c1; }
51 |
52 | .cm-s-light .cm-keyword, .cm-s-light .cm-qualifier {
53 | font-weight: normal;
54 | font-style: normal;
55 | text-decoration: none;
56 | color: #d73a49; }
57 |
58 | .cm-s-light .cm-storage {
59 | color: #d73a49; }
60 |
61 | .cm-s-light .cm-string {
62 | font-weight: normal;
63 | font-style: normal;
64 | text-decoration: none;
65 | color: #03941e; }
66 |
67 | .cm-s-light .cm-support {
68 | font-weight: normal;
69 | font-style: normal;
70 | text-decoration: none;
71 | color: #005cc5; }
72 |
73 | .cm-s-light .cm-property {
74 | font-weight: normal;
75 | font-style: normal;
76 | text-decoration: none;
77 | color: #245082;
78 | }
79 |
80 | .cm-s-light .cm-variable {
81 | font-weight: normal;
82 | font-style: normal;
83 | text-decoration: none;
84 | color: #e36209; }
85 |
86 | .cm-s-light .cm-tag {
87 | font-weight: normal;
88 | font-style: normal;
89 | text-decoration: none;
90 | color: #0d6eab; }
91 |
92 | .cm-s-light .cm-string-2 {
93 | font-weight: normal;
94 | font-style: normal;
95 | text-decoration: none;
96 | color: #03941e; }
97 |
98 | .CodeMirror-selectedtext {
99 | color: #000 !important;
100 | background: #e0e0e0 !important;
101 | }
102 | .CodeMirror-selected {
103 | background: #e0e0e0 !important;
104 | }
105 | .CodeMirror-markedText {
106 | background: #fff;
107 | box-shadow: 11px 6px 21px 0 rgba(111, 111, 111, 0.75);
108 | -webkit-box-shadow: 11px 6px 21px 0 rgba(111, 111, 111, 0.75);
109 | }
110 | .cm-s-light .cm-error {color: #f00;}
111 | .cm-s-light .cm-number {color: #164;}
112 | .cm-s-light .cm-atom {color: #219;}
113 | .cm-s-light .cm-string {color: #a11;}
114 |
115 | .cm-s-light .cm-matchhighlight {
116 | background: #f9f9f9;
117 | }
--------------------------------------------------------------------------------
/src/css/dark_theme.css:
--------------------------------------------------------------------------------
1 | .cm-s-dark.CodeMirror {
2 | background: #20303a;
3 | color: #f3f3f3; }
4 |
5 | .cm-s-dark .CodeMirror-gutters {
6 | background: #20303a;
7 | border-right-width: 0; }
8 |
9 | .cm-s-dark .CodeMirror-guttermarker {
10 | color: black; }
11 |
12 | .cm-s-dark .CodeMirror-guttermarker-subtle {
13 | color: #d0d0d0; }
14 |
15 | .cm-s-dark .CodeMirror-linenumber {
16 | color: #959da5;
17 | padding: 0 16px 0 16px; }
18 |
19 | .cm-s-dark .CodeMirror-cursor {
20 | border-left: 1px solid #f3f3f3; }
21 |
22 | .cm-s-dark div.CodeMirror-selected,
23 | .cm-s-dark .CodeMirror-line::selection,
24 | .cm-s-dark .CodeMirror-line > span::selection,
25 | .cm-s-dark .CodeMirror-line > span > span::selection,
26 | .cm-s-dark .CodeMirror-line::-moz-selection,
27 | .cm-s-dark .CodeMirror-line > span::-moz-selection,
28 | .cm-s-dark .CodeMirror-line > span > span::-moz-selection {
29 | background: #3a3a3a; }
30 |
31 | .cm-s-dark .CodeMirror-activeline-background {
32 | background: #3a3a3a; }
33 |
34 | .cm-s-dark .CodeMirror-lines {
35 | font-family: inherit;
36 | font-size: inherit;
37 | background: #20303a;
38 | line-height: 1.5; }
39 |
40 | .cm-s-dark .cm-comment {
41 | color: #6a737d; }
42 |
43 | .cm-s-dark .cm-constant {
44 | color: #005cc5; }
45 |
46 | .cm-s-dark .cm-entity {
47 | font-weight: normal;
48 | font-style: normal;
49 | text-decoration: none;
50 | color: #6f42c1; }
51 |
52 | .cm-s-dark .cm-keyword, .cm-s-dark .cm-qualifier {
53 | font-weight: normal;
54 | font-style: normal;
55 | text-decoration: none;
56 | color: #b2c2e2; }
57 |
58 | .cm-s-dark .cm-storage {
59 | color: #b2c2e2; }
60 |
61 | .cm-s-dark .cm-string {
62 | font-weight: normal;
63 | font-style: normal;
64 | text-decoration: none;
65 | color: #8eaf86; }
66 |
67 | .cm-s-dark .cm-support {
68 | font-weight: normal;
69 | font-style: normal;
70 | text-decoration: none;
71 | color: #005cc5; }
72 |
73 | .cm-s-light .cm-property {
74 | font-weight: normal;
75 | font-style: normal;
76 | text-decoration: none;
77 | color: #245082;
78 | }
79 |
80 | .cm-s-dark .cm-variable {
81 | font-weight: normal;
82 | font-style: normal;
83 | text-decoration: none;
84 | color: #deb79c; }
85 |
86 | .cm-s-dark .cm-def {
87 | font-weight: normal;
88 | font-style: normal;
89 | text-decoration: none;
90 | color: #deb79c; }
91 |
92 | .cm-s-dark .cm-tag {
93 | font-weight: normal;
94 | font-style: normal;
95 | text-decoration: none;
96 | color: #afbfe6; }
97 |
98 | .cm-s-dark .cm-string-2 {
99 | font-weight: normal;
100 | font-style: normal;
101 | text-decoration: none;
102 | color: #8eaf86; }
103 |
104 | .cm-s-dark .CodeMirror-selectedtext {
105 | color: #000 !important;
106 | background: #6b95af !important;
107 | }
108 | .cm-s-dark .CodeMirror-selected {
109 | background: #6b95af !important;
110 | }
111 | .cm-s-dark .CodeMirror-markedText {
112 | background: #386480;
113 | box-shadow: 11px 6px 21px 0 rgba(0, 0, 0, 0.75);
114 | -webkit-box-shadow: 11px 6px 21px 0 rgba(0, 0, 0, 0.75);
115 | }
116 | .cm-s-dark .cm-error {color: #f00;}
117 | .cm-s-dark .cm-number {color: #a6d4c1;}
118 | .cm-s-dark .cm-atom {color: #9a91d6;}
119 | .cm-s-dark .cm-string {color: #d46a6a;}
120 |
121 | .cm-s-dark .cm-matchhighlight {
122 | background: #18242b;
123 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demoit",
3 | "version": "7.10.0",
4 | "description": "A live coding tool",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/krasimir/demoit.git"
9 | },
10 | "scripts": {
11 | "clean-demoit": "shx rm -rf ./dist/*",
12 | "copy-static": "shx cp ./src/index.html ./dist/index.html && shx cp ./src/sandbox.html ./dist/sandbox.html && shx cp -r ./src/resources ./dist && shx cp -r ./src/img ./dist",
13 | "produce-minified-js": "uglifyjs ./src/js-vendor/split.js ./.tmp/demoit.js -c -m -o ./dist/demoit.js",
14 | "produce-js": "shx cat ./src/js-vendor/split.js ./.tmp/demoit.js > ./dist/demoit.js",
15 | "produce-css": "shx cat ./src/css/codemirror.css ./src/css/la.css ./src/css/styles.css ./src/css/light_theme.css ./src/css/dark_theme.css | uglifycss > ./dist/styles.css",
16 | "produce-editor-js": "uglifyjs ./src/js-vendor/codemirror.js ./src/js-vendor/javascript.js ./src/js-vendor/xml.js ./src/js-vendor/jsx.js ./src/js-vendor/mark-selection.js ./src/js-vendor/matchbrackets.js ./src/js-vendor/comment.js ./src/js-vendor/search_cursor.js ./src/js-vendor/overlay.js ./src/js-vendor/markdown.js ./src/js-vendor/gfm.js ./src/js-vendor/runmode.js ./src/js-vendor/colorize.js ./src/js-vendor/closebrackets.js ./src/js-vendor/match-highlighter.js ./src/js-vendor/css.js ./src/js-vendor/htmlmixed.js ./src/js-vendor/deep-diff.js ./src/js-vendor/babel-6.26.0.min.js ./src/js-vendor/babel-polyfill@6.26.0.js ./src/js-vendor/babel-plugin-transform-es2015-modules-commonjs@6.26.2.js -c -m -o ./dist/resources/editor.js",
17 | "dev": "yarn build && concurrently \"webpack\" \"onchange ./src/css/*.css -- yarn produce-css\" \"onchange ./.tmp/*.js -- yarn produce-js\" \"cpx ./src/index.html ./dist/ -w\" \"cpx ./src/sandbox.html ./dist/ -w\"",
18 | "build": "yarn clean-demoit && yarn copy-static && yarn produce-css && yarn produce-editor-js && webpack --config ./webpack.config.prod.js && yarn produce-minified-js",
19 | "zip": "node ./scripts/zipit.js",
20 | "release": "yarn test && yarn build && yarn zip",
21 | "test": "jest",
22 | "test-watch": "jest --watch --verbose false",
23 | "lint": "./node_modules/.bin/eslint --ext .js src/js"
24 | },
25 | "keywords": [
26 | "demo",
27 | "code",
28 | "live",
29 | "coding"
30 | ],
31 | "author": "Krasimir Tsonev",
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/krasimir/demoit/issues"
35 | },
36 | "homepage": "https://github.com/krasimir/demoit#readme",
37 | "devDependencies": {
38 | "@babel/core": "7.1.5",
39 | "@babel/plugin-transform-runtime": "7.1.0",
40 | "@babel/preset-env": "7.1.5",
41 | "@babel/runtime": "7.1.5",
42 | "babel-core": "^7.0.0-bridge.0",
43 | "babel-eslint": "8.0.3",
44 | "babel-jest": "23.6.0",
45 | "babel-loader": "8.0.4",
46 | "clean-css-cli": "4.2.1",
47 | "concurrently": "4.0.1",
48 | "cpx": "1.5.0",
49 | "eslint": "4.12.1",
50 | "jest": "23.6.0",
51 | "onchange": "5.1.3",
52 | "regenerator-runtime": "0.13.1",
53 | "shx": "^0.3.2",
54 | "uglify-js": "3.4.9",
55 | "uglifycss": "0.0.29",
56 | "webpack": "4.25.1",
57 | "webpack-cli": "3.1.2",
58 | "zip-folder": "1.0.0"
59 | },
60 | "dependencies": {
61 | "gitfred": "7.2.4",
62 | "hashids": "1.2.2",
63 | "jszip": "3.1.5",
64 | "layout-architect": "3.0.0"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |
4 | # **Demoit** is the front-end app behind [Poet](https://poet.krasimir.now.sh)
5 |
6 | * No installation.
7 | * No server needed. It works offline.
8 | * No building process needed. Built-in Babel support. It translates your code at runtime.
9 | * Supports external libraries and styles. Like React for example.
10 | * Export your work to an external file
11 | * Supports `import` statement (between the files of app)
12 | * Supports `import`ing of CSS and HTML files
13 | * Supports dependencies via HTTP (everything from [unpkg](https://unpkg.com/#/) or [cdnjs](https://cdnjs.com))
14 |
15 | ## Demo :rocket:
16 |
17 | [https://poet.krasimir.now.sh/new](https://poet.krasimir.now.sh/new)
18 |
19 | ---
20 |
21 | ## Usage
22 |
23 | * Online at [poet.krasimir.now.sh](https://poet.krasimir.now.sh)
24 | * Offline by downloading [Demoit.zip](https://github.com/krasimir/demoit/raw/master/demoit.zip)
25 |
26 | ## Configuration
27 |
28 | When you open the app and start writing code you progress gets saved to an internal state. You can grab it by opening the bar at the top and clicking on the gear icon (check the "Export" section). The JSON there contains all the configuration that Demoit needs. You can save this configuration to an external file and let Demoit knows the path to it via the `state` GET parameter (for example `http://localhost/demoit?state=./mycode.json`).
29 |
30 | ## GET Params
31 |
32 | * `?state=` - relative path to a JSON file
33 | * `?mode=preview` - it loads the editor just in a preview mode. The code is visible but not compiled, not editable and not executed. This significantly reduces the file size and it is useful for showing your code in a blog post for example.
34 | * `?mode=readonly` - it loads the editor in a readonly mode. It means that the code is transpiled and executed but you can't make changes. This also reduces the page's size because it is not loading Babel and CodeMirror (which is roughly 1.5MB)
35 |
36 | ## Continuing your work offline
37 |
38 | * You have to download [Demoit.zip](https://github.com/krasimir/demoit/raw/master/demoit.zip)
39 | * You need to transfer your progress to a JSON file and pass it to the app via `state` GET param
40 | * If you use external dependencies make sure that they are also saved locally and the path to the files is properly set (check the gear icon in the status bar at the top of the app)
41 |
42 | ## Keyboard shortcuts when the focus is on the editor
43 |
44 | * `Ctrl + S` / `Cmd + S` - essential for seeing the result of your code. This keys combination triggers transpilation and execution.
45 | * `Ctrl + <0-9>` / `Cmd + <0-9>` - switch between files
46 |
47 | ## Editing filenames and deleting files
48 |
49 | Right mouse click on the file's tab.
50 |
51 | ## Troubleshooting
52 |
53 | ### Error `URL scheme must be "http" or "https" for CORS request.`
54 |
55 | It means that the browser doesn't load the files that the tool needs because the protocol is `file://`. That's a problem in Chrome at the moment. Everything works fine in Firefox. To fix the problem in Chrome you have to run it like so:
56 |
57 | ```
58 | open /Applications/Google\ Chrome.app/ --args --disable-web-security
59 | ```
60 | or under Windows:
61 | ```
62 | chrome.exe --disable-web-security
63 | ```
64 |
65 | Of course Demoit works just fine if you open `index.html` via `http` protocol but to do that you need a server.
66 |
--------------------------------------------------------------------------------
/src/js/storyReadOnly.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-sequences */
2 | import el from './utils/element';
3 | import commitDiff from './utils/commitDiff';
4 | import confirmPopUp from './popups/confirmPopUp';
5 | import { DEBUG } from './constants';
6 | import getTitleFromCommitMessage from './story/getTitleFromCommitMessage';
7 |
8 | function renderCommits(git, commits) {
9 | function process(commit) {
10 | const { hash, message, position } = commit;
11 | const currentPosition = position && position > 0 ? `${position}` : '';
12 | const messageFirstLine = getTitleFromCommitMessage(message);
13 | const isCurrent = git.head() === hash;
14 | let html = '';
15 |
16 | html += `';
23 | return html;
24 | }
25 |
26 | if (commits.length === 0) {
27 | return '';
28 | }
29 | return commits.map(process).join('');
30 | };
31 |
32 | export default function story(state, onChange) {
33 | const container = el.withFallback('.read-only');
34 | const git = state.git();
35 |
36 | if (!container.found()) return;
37 |
38 | const render = () => {
39 | DEBUG && console.log('story:render');
40 | const allCommits = git.log();
41 | const commits = Object.keys(allCommits).map(hash => ({
42 | hash,
43 | message: allCommits[hash].message,
44 | position: allCommits[hash].meta ? parseInt(allCommits[hash].meta.position, 10) || null : null
45 | })).sort((a, b) => {
46 | if (a.position !== null && b.position !== null) {
47 | return a.position - b.position;
48 | } else if (a.position !== null && b.position === null) {
49 | return -1;
50 | } else if (a.position === null && b.position !== null) {
51 | return 1;
52 | }
53 | return a.hash - b.hash;
54 | });
55 | const numOfCommits = commits.length;
56 | const diffs = commitDiff(numOfCommits > 0 ? git.show().files : [], git.getAll());
57 | const renderedCommits = renderCommits(git, commits);
58 |
59 | container.attr('class', 'editor-section story');
60 | container.content(`
61 | ${ renderedCommits !== '' ? '' + renderedCommits + '
' : '' }
62 | `).forEach(el => {
63 | if (el.attr('data-export') === 'checkoutLink') {
64 | el.onClick(() => {
65 | const hashToCheckout = el.attr('data-hash');
66 |
67 | if (diffs.length > 0) {
68 | confirmPopUp(
69 | 'Checkout',
70 | 'You are about to checkout another commit. You have an unstaged changes. Are you sure?',
71 | decision => {
72 | if (decision && allCommits[hashToCheckout]) {
73 | git.checkout(hashToCheckout, true);
74 | onChange();
75 | render();
76 | }
77 | }
78 | );
79 | } else {
80 | if (allCommits[hashToCheckout]) {
81 | git.checkout(hashToCheckout);
82 | onChange();
83 | render();
84 | }
85 | }
86 | });
87 | }
88 | });
89 | };
90 |
91 | state.listen(event => {
92 | render();
93 | });
94 |
95 | render();
96 | }
97 |
--------------------------------------------------------------------------------
/src/js-vendor/overlay.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
3 | // Distributed under an MIT license: https://codemirror.net/LICENSE
4 |
5 | // Utility function that allows modes to be combined. The mode given
6 | // as the base argument takes care of most of the normal mode
7 | // functionality, but a second (typically simple) mode is used, which
8 | // can override the style of text. Both modes get to parse all of the
9 | // text, but when both assign a non-null style to a piece of code, the
10 | // overlay wins, unless the combine argument was true and not overridden,
11 | // or state.overlay.combineTokens was true, in which case the styles are
12 | // combined.
13 |
14 | (function(mod) {
15 | if (typeof exports == "object" && typeof module == "object") // CommonJS
16 | mod(require("../../lib/codemirror"));
17 | else if (typeof define == "function" && define.amd) // AMD
18 | define(["../../lib/codemirror"], mod);
19 | else // Plain browser env
20 | mod(CodeMirror);
21 | })(function(CodeMirror) {
22 | "use strict";
23 |
24 | CodeMirror.overlayMode = function(base, overlay, combine) {
25 | return {
26 | startState: function() {
27 | return {
28 | base: CodeMirror.startState(base),
29 | overlay: CodeMirror.startState(overlay),
30 | basePos: 0, baseCur: null,
31 | overlayPos: 0, overlayCur: null,
32 | streamSeen: null
33 | };
34 | },
35 | copyState: function(state) {
36 | return {
37 | base: CodeMirror.copyState(base, state.base),
38 | overlay: CodeMirror.copyState(overlay, state.overlay),
39 | basePos: state.basePos, baseCur: null,
40 | overlayPos: state.overlayPos, overlayCur: null
41 | };
42 | },
43 |
44 | token: function(stream, state) {
45 | if (stream != state.streamSeen ||
46 | Math.min(state.basePos, state.overlayPos) < stream.start) {
47 | state.streamSeen = stream;
48 | state.basePos = state.overlayPos = stream.start;
49 | }
50 |
51 | if (stream.start == state.basePos) {
52 | state.baseCur = base.token(stream, state.base);
53 | state.basePos = stream.pos;
54 | }
55 | if (stream.start == state.overlayPos) {
56 | stream.pos = stream.start;
57 | state.overlayCur = overlay.token(stream, state.overlay);
58 | state.overlayPos = stream.pos;
59 | }
60 | stream.pos = Math.min(state.basePos, state.overlayPos);
61 |
62 | // state.overlay.combineTokens always takes precedence over combine,
63 | // unless set to null
64 | if (state.overlayCur == null) return state.baseCur;
65 | else if (state.baseCur != null &&
66 | state.overlay.combineTokens ||
67 | combine && state.overlay.combineTokens == null)
68 | return state.baseCur + " " + state.overlayCur;
69 | else return state.overlayCur;
70 | },
71 |
72 | indent: base.indent && function(state, textAfter, line) {
73 | return base.indent(state.base, textAfter, line);
74 | },
75 | electricChars: base.electricChars,
76 |
77 | innerMode: function(state) { return {state: state.base, mode: base}; },
78 |
79 | blankLine: function(state) {
80 | var baseToken, overlayToken;
81 | if (base.blankLine) baseToken = base.blankLine(state.base);
82 | if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay);
83 |
84 | return overlayToken == null ?
85 | baseToken :
86 | (combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken);
87 | }
88 | };
89 | };
90 |
91 | });
--------------------------------------------------------------------------------
/src/js/story/renderCommits.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-use-before-define */
2 | import { CHECK_ICON, CLOSE_ICON, TRASH_ICON, DOT_CIRCLE, BOOK } from '../utils/icons';
3 | import getTitleFromCommitMessage from './getTitleFromCommitMessage';
4 |
5 | export default function renderCommits(git, commits, editMode, currentlyEditingHash) {
6 | function process(commit) {
7 | const { hash, message, position } = commit;
8 | const isEditing = currentlyEditingHash === hash && editMode;
9 | const currentPosition = position && position > 0 ? `${position}` : '';
10 | const messageFirstLine = getTitleFromCommitMessage(message);
11 | const isCurrent = git.head() === hash;
12 | let html = '';
13 |
14 | html += `';
46 |
47 | if (isEditing) {
48 | html += `
49 |
52 | `;
53 | }
54 | return html;
55 | }
56 |
57 | if (commits.length === 0) {
58 | return '';
59 | }
60 | return commits.map(process).join('');
61 | };
62 |
63 | function getPublishOptions(git, currentHash) {
64 | const allCommits = git.log();
65 | const { meta } = allCommits[currentHash];
66 | const currentPosition = meta ? parseInt(meta.position, 10) : 0;
67 | let options = [];
68 |
69 | options.push(``);
70 | for (let i = 1; i < Object.keys(allCommits).length + 1; i++) {
71 | options.push(``);
72 | }
73 |
74 | return options.join('');
75 | }
76 | function getFileInjectionOptions(git, currentHash) {
77 | const files = git.show(currentHash).files;
78 |
79 | return files.map(file => ``);
80 | }
81 |
--------------------------------------------------------------------------------
/samples/Canvas.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "theme": "dark",
4 | "statusBar": true,
5 | "layout": {
6 | "name": "layoutEO",
7 | "direction": "horizontal",
8 | "sizes": [
9 | 69.0625,
10 | 30.9375
11 | ],
12 | "elements": [
13 | "editor",
14 | "output"
15 | ]
16 | }
17 | },
18 | "dependencies": [],
19 | "files": [
20 | {
21 | "content": "import 'layout.html';\nimport 'styles.css';\n\n// JS Port of PlusParticle\n// k3lab ( http://wonderfl.net/user/k3lab )\n// http://wa.zozuar.org/code.php?c=hEiW\n\"use strict\";\nconst canvas = document.querySelector(\"canvas\");\nconst ctx = canvas.getContext(\"2d\", { lowLatency: true, alpha: false });\nconst w = (canvas.width = canvas.offsetWidth / 2);\nconst h = (canvas.height = canvas.offsetHeight / 2);\n///////////////////////////////////////\nconst createDot = () => {\n\tconst dot = document.createElement('canvas');\n\tdot.width = 20;\n\tdot.height = 20;\n\tconst ctx = dot.getContext(\"2d\");\n\tctx.globalCompositeOperation = \"xor\";\n\tctx.fillStyle = \"#fff\";\n\tctx.fillRect(0, 10 - 2, 20, 2);\n\tctx.fillRect(10 - 2, 0, 2, 20);\n\treturn dot;\n}\n///////////////////////////////////////\nconst Particle = class {\n\tconstructor(x, y, z) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tthis.z = z;\n\t}\n};\n///////////////////////////////////////\nconst count = 4000;\nconst degree = 2 * Math.PI / count;\nconst particles = [];\nconst dot = createDot();\n///////////////////////////////////////\nconst run = time => {\n\trequestAnimationFrame(run);\n\tctx.fillRect(0, 0, w, h);\n\tconst horizontal = Math.sin(time / 2000) * 2;\n\tconst vertical = Math.sin(time / 30000);\n\tconst cosY = Math.cos(horizontal);\n\tconst sinY = Math.sin(horizontal);\n\tconst cosX = Math.cos(vertical);\n\tconst sinX = Math.sin(vertical);\n\tconst s = 2500 + 1500 * Math.sin(time / 1000);\n\tconst a = s * 2 / count;\n\tconst round = degree * 2 * Math.sin(time / 10000);\n\tfor (let i = 0; i < count; i++) {\n\t\tconst p = particles[i];\n\t\tconst size = s * Math.sin(time / 10000 + i / 10);\n\t\tconst radius = size * Math.sin(degree / 2 * i);\n\t\tp.x = radius * Math.sin(round * i + time / 1000);\n\t\tp.y = radius * Math.cos(round * i + time / 1000);\n\t\tp.z = -s + i * a;\n\t\tconst z1 = p.z * cosY + p.x * sinY;\n\t\tconst z2 = z1 * cosX + p.y * sinX;\n\t\tif (z2 > -600) {\n\t\t\tconst perspective = 300 / (600 + z2);\n\t\t\tconst px = w / 2 + (p.x * cosY - p.z * sinY) * perspective;\n\t\t\tconst py = h / 2 + (p.y * cosX - z1 * sinX) * perspective;\n\t\t\tif (px > -30 && px < w && py > -30 && py < h) {\n\t\t\t\tlet wi = (z2 - 200) * -1 / 200;\n\t\t\t\tif (wi < 0.1) wi = 0.1;\n\t\t\t\tctx.save();\n\t\t\t\tctx.translate(px, py);\n\t\t\t\tctx.rotate(i / 100);\n\t\t\t\tctx.scale(wi, wi);\n\t\t\t\tctx.drawImage(dot, -10, -10);\n\t\t\t\tctx.restore();\n\t\t\t}\n\t\t}\n\t}\n};\n///////////////////////////////////////\nfor (let i = 0; i < count; i++) {\n\tparticles.push(\n\t\tnew Particle(\n\t\t\t200 * Math.sin(degree * i),\n\t\t\t200 * Math.cos(degree * i),\n\t\t\t-100 + i / 100\n\t\t)\n\t);\n}\nrun();\n",
22 | "filename": "app.js",
23 | "entryPoint": true,
24 | "editing": false
25 | },
26 | {
27 | "content": "",
28 | "filename": "layout.html",
29 | "editing": false
30 | },
31 | {
32 | "content": "#output {\n\toverflow: hidden;\n\ttouch-action: none;\n\tcontent-zooming: none;\n\tmargin: 0;\n\tpadding: 0;\n\twidth: 100%;\n\theight: 100%;\n\tbackground: #111;\n}\n#output canvas {\n\tposition:absolute;\n width: 100%;\n height: 100%;\n\tbackground: #000;\n}",
33 | "filename": "styles.css",
34 | "editing": false
35 | }
36 | ]
37 | }
--------------------------------------------------------------------------------
/src/js/popups/settingsPopUp.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import Layout from 'layout-architect';
3 | import createPopup from './popup';
4 | import { LAYOUT_BLOCKS, DEFAULT_LAYOUT } from '../layout';
5 | import { IS_PROD } from '../constants';
6 |
7 | const generateIframe = url => ``;
8 |
9 | export default function settingsPopUp(
10 | enableDownload,
11 | { layout, theme },
12 | dependenciesStr,
13 | onDepsUpdated,
14 | onGeneralUpdate,
15 | defaultTab,
16 | version
17 | ) {
18 | return new Promise(done => createPopup({
19 | defaultTab: defaultTab || 0,
20 | buttons: [
21 | 'General',
22 | 'Dependencies',
23 | 'Export/Share',
24 | 'About'
25 | ],
26 | content: [
27 | `
28 |
29 | Theme:
30 |
34 |
35 | Layout:
36 |
37 |
38 | `,
39 | `
40 |
41 | (Separate your dependencies by a new line)
42 |
43 | `,
44 | `
45 | Embed
46 |
47 | ${ IS_PROD ? `
48 | Download/Offline mode
49 | The archive contains all the files that you need to run the app locally. Including your dependencies.
50 | ` : '' }
51 | `,
52 | `
53 |
54 | v${ version }
55 | On the web: poet.krasimir.now.sh
56 | GitHub repo: github.com/krasimir/poet.krasimir.now.sh.feedback
57 |
58 | `
59 | ],
60 | cleanUp() {
61 | done();
62 | },
63 | onRender({
64 | closePopup,
65 | saveGeneral,
66 | dependenciesTextarea,
67 | saveDependenciesButton,
68 | themePicker,
69 | iframeTextarea,
70 | layoutArchitectContainer,
71 | downloadButton
72 | }) {
73 | // general settings
74 | if (layoutArchitectContainer && themePicker) {
75 | const la = Layout(layoutArchitectContainer.e, LAYOUT_BLOCKS, layout);
76 |
77 | themePicker.e.value = theme || 'light';
78 | saveGeneral.onClick(() => {
79 | onGeneralUpdate(themePicker.e.value, la.get() || DEFAULT_LAYOUT);
80 | closePopup();
81 | });
82 | }
83 | // share
84 | if (iframeTextarea) {
85 | iframeTextarea.selectOnClick();
86 | }
87 | if (downloadButton) {
88 | enableDownload(downloadButton);
89 | }
90 | // managing dependencies
91 | if (dependenciesTextarea && saveDependenciesButton) {
92 | dependenciesTextarea.prop('value', dependenciesStr);
93 | saveDependenciesButton.onClick(() => {
94 | onDepsUpdated(dependenciesTextarea.prop('value').split(/\r?\n/));
95 | closePopup();
96 | });
97 | }
98 | }
99 | }));
100 | };
101 |
--------------------------------------------------------------------------------
/src/js/utils/cleanUpMarkdown.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Source: https://github.com/stiang/remove-markdown
4 | Author: https://github.com/stiang
5 |
6 | The MIT License (MIT)
7 |
8 | Copyright (c) 2015 Stian Grytøyr
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a copy
11 | of this software and associated documentation files (the "Software"), to deal
12 | in the Software without restriction, including without limitation the rights
13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | copies of the Software, and to permit persons to whom the Software is
15 | furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in all
18 | copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | SOFTWARE.
27 |
28 | */
29 |
30 | export default function cleanUpMarkdown(md, options) {
31 | options = options || {};
32 | options.listUnicodeChar = options.hasOwnProperty('listUnicodeChar') ? options.listUnicodeChar : false;
33 | options.stripListLeaders = options.hasOwnProperty('stripListLeaders') ? options.stripListLeaders : true;
34 | options.gfm = options.hasOwnProperty('gfm') ? options.gfm : true;
35 | options.useImgAltText = options.hasOwnProperty('useImgAltText') ? options.useImgAltText : true;
36 |
37 | let output = md || '';
38 |
39 | // Remove horizontal rules (stripListHeaders conflict with this rule, which is why it has been moved to the top)
40 | output = output.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, '');
41 |
42 | try {
43 | if (options.stripListLeaders) {
44 | if (options.listUnicodeChar) {
45 | output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, options.listUnicodeChar + ' $1');
46 | } else {
47 | output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, '$1');
48 | }
49 | }
50 | if (options.gfm) {
51 | output = output
52 | // Header
53 | .replace(/\n={2,}/g, '\n')
54 | // Fenced codeblocks
55 | .replace(/~{3}.*\n/g, '')
56 | // Strikethrough
57 | .replace(/~~/g, '')
58 | // Fenced codeblocks
59 | .replace(/`{3}.*\n/g, '');
60 | }
61 | output = output
62 | // Remove HTML tags
63 | .replace(/<[^>]*>/g, '')
64 | // Remove setext-style headers
65 | .replace(/^[=\-]{2,}\s*$/g, '')
66 | // Remove footnotes?
67 | .replace(/\[\^.+?\](\: .*?$)?/g, '')
68 | .replace(/\s{0,2}\[.*?\]: .*?$/g, '')
69 | // Remove images
70 | .replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? '$1' : '')
71 | // Remove inline links
72 | .replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
73 | // Remove blockquotes
74 | .replace(/^\s{0,3}>\s?/g, '')
75 | // Remove reference-style links?
76 | .replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
77 | // Remove atx-style headers
78 | .replace(/^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm, '$1$2$3')
79 | // Remove emphasis (repeat the line to remove double emphasis)
80 | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, '$2')
81 | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, '$2')
82 | // Remove code blocks
83 | .replace(/(`{3,})(.*?)\1/gm, '$2')
84 | // Remove inline code
85 | .replace(/`(.+?)`/g, '$1')
86 | // Replace two or more newlines with exactly two? Not entirely sure this belongs here...
87 | .replace(/\n{2,}/g, '\n\n');
88 | } catch (e) {
89 | console.error(e);
90 | return md;
91 | }
92 | return output;
93 | };
94 |
--------------------------------------------------------------------------------
/src/js/utils/index.js:
--------------------------------------------------------------------------------
1 | export const debounce = function (func, wait, immediate) {
2 | var timeout;
3 |
4 | return function () {
5 | var context = this, args = arguments;
6 | var later = function () {
7 | timeout = null;
8 | if (!immediate) func.apply(context, args);
9 | };
10 | let callNow = immediate && !timeout;
11 |
12 | clearTimeout(timeout);
13 | timeout = setTimeout(later, wait);
14 | if (callNow) func.apply(context, args);
15 | };
16 | };
17 |
18 | export const delay = async (amount = 1) => new Promise(done => setTimeout(done, amount));
19 | export const once = callback => {
20 | let called = false;
21 |
22 | return (...args) => {
23 | if (called) return;
24 | called = true;
25 | callback(...args);
26 | };
27 | };
28 | export const getParam = (parameterName, defaultValue) => {
29 | var result = defaultValue, tmp = [];
30 |
31 | location.search
32 | .substr(1)
33 | .split('&')
34 | .forEach(function (item) {
35 | tmp = item.split('=');
36 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
37 | });
38 | return result;
39 | };
40 |
41 | export const readFromJSONFile = async function (file) {
42 | const res = await fetch(file);
43 |
44 | return await res.json();
45 | };
46 |
47 | export const removeParam = function (key, sourceURL) {
48 | const urlWithoutParams = sourceURL.split('?')[0];
49 | const hash = sourceURL.split('#')[1];
50 | const queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : '';
51 | let params = [];
52 | let param;
53 |
54 | if (queryString !== '') {
55 | params = queryString.split('&');
56 | for (let i = params.length - 1; i >= 0; i -= 1) {
57 | param = params[i].split('=')[0];
58 | if (param === key) {
59 | params.splice(i, 1);
60 | }
61 | }
62 | params = params.join('&');
63 |
64 | return [
65 | urlWithoutParams,
66 | params !== '' ? '?' + params : '',
67 | hash ? '#' + hash : ''
68 | ].join('');
69 | }
70 | return urlWithoutParams;
71 | };
72 |
73 | export const ensureDemoIdInPageURL = demoId => {
74 | const currentURL = window.location.href;
75 | const hash = currentURL.split('#')[1];
76 |
77 | history.pushState(null, null, `/e/${ demoId }${ hash ? '#' + hash : '' }`);
78 | };
79 |
80 | export const ensureUniqueFileName = (filename) => {
81 | const tmp = filename.split('.');
82 |
83 | if (tmp.length === 1) {
84 | return tmp[0] + '.1';
85 | } else if (tmp.length === 2) {
86 | return `${ tmp[0] }.1.${ tmp[1] }`;
87 | }
88 | const ext = tmp.pop();
89 | const num = tmp.pop();
90 |
91 | if (isNaN(parseInt(num, 10))) {
92 | return `${ tmp.join('.') }.${ num }.1.${ ext }`;
93 | }
94 | return `${ tmp.join('.') }.${ (parseInt(num, 10) + 1) }.${ ext }`;
95 | };
96 |
97 | export const truncate = function (str, len) {
98 | if (str.length > len) {
99 | return str.substr(0, len) + '...';
100 | }
101 | return str;
102 | };
103 |
104 | export const escapeHTML = function (html) {
105 | const tagsToReplace = {
106 | '&': '&',
107 | '<': '<',
108 | '>': '>'
109 | };
110 | const replaceTag = (tag) => {
111 | return tagsToReplace[tag] || tag;
112 | };
113 |
114 | return html.replace(/[&<>]/g, replaceTag);
115 | };
116 |
117 | export function jsEncode(s) {
118 | let enc = '';
119 |
120 | s = s.toString();
121 | for (let i = 0; i < s.length; i++) {
122 | let a = s.charCodeAt(i);
123 | let b = a ^ 3;
124 |
125 | enc = enc + String.fromCharCode(b);
126 | }
127 | return enc;
128 | };
129 |
130 | export const clone = function (data) {
131 | return JSON.parse(JSON.stringify(data));
132 | };
133 |
134 | export const isEmbedded = function () {
135 | try {
136 | return window.self !== window.top;
137 | } catch (e) {
138 | return true;
139 | }
140 | };
141 | export const formatDate = function (date = new Date()) {
142 | const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
143 | const day = date.getDate(),
144 | monthIndex = date.getMonth(),
145 | year = date
146 | .getFullYear()
147 | .toString()
148 | .substr(-2);
149 |
150 | return day + ' ' + monthNames[monthIndex] + ' ' + year + ' ' + date.getHours() + ':' + date.getMinutes();
151 | }
152 |
--------------------------------------------------------------------------------
/src/js/layout.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import el from './utils/element';
3 | import setTheme from './utils/setTheme';
4 | import { IS_PROD } from './constants';
5 |
6 | export const LAYOUT_BLOCKS = ['editor', 'HTML', 'console', 'story'];
7 |
8 | if (IS_PROD) {
9 | LAYOUT_BLOCKS.push('story-preview');
10 | LAYOUT_BLOCKS.push('story-read-only');
11 | LAYOUT_BLOCKS.push('annotate');
12 | }
13 |
14 | export const DEFAULT_LAYOUT = {
15 | elements: [
16 | {
17 | name: 'editor',
18 | elements: []
19 | },
20 | {
21 | elements: [
22 | {
23 | name: 'HTML',
24 | elements: []
25 | },
26 | {
27 | name: 'console',
28 | elements: []
29 | }
30 | ],
31 | direction: 'horizontal'
32 | }
33 | ],
34 | direction: 'vertical'
35 | };
36 |
37 | function validateLayout(item) {
38 | if (typeof item === 'string') {
39 | if (item === 'output') item = 'HTML';
40 | if (item === 'log') item = 'console';
41 | return {
42 | name: item,
43 | elements: []
44 | };
45 | }
46 | if (item.elements.length > 0) {
47 | item.elements.forEach((i, index) => (item.elements[index] = validateLayout(i)));
48 | }
49 | return item;
50 | }
51 | function generateSizes(elements) {
52 | return elements.map(() => 100 / elements.length);
53 | }
54 |
55 | export default state => {
56 | const container = el.withRelaxedCleanup('.app .layout');
57 | const body = el.withRelaxedCleanup('body');
58 |
59 | setTheme(state.getEditorSettings().theme);
60 |
61 | const layout = validateLayout(state.getEditorSettings().layout || DEFAULT_LAYOUT);
62 | const HTML = el.fromTemplate('#template-html');
63 | const consoleE = el.fromTemplate('#template-console');
64 | const editor = el.fromTemplate('#template-editor');
65 | const story = el.fromTemplate('#template-story');
66 | const storyPreview = el.fromTemplate('#template-story-preview');
67 | const storyReadOnly = el.fromTemplate('#template-story-read-only');
68 | const annotate = el.fromTemplate('#template-annotate');
69 | const empty = el.withFallback('.does-not-exists');
70 | const elementsMap = {
71 | HTML,
72 | console: consoleE,
73 | editor,
74 | story,
75 | 'story-preview': storyPreview,
76 | 'story-read-only': storyReadOnly,
77 | annotate
78 | };
79 | const usedBlocks = [];
80 |
81 | const splitFuncs = [];
82 | let splits;
83 | const build = block => {
84 | let { direction, elements, sizes } = block;
85 | const normalizedElements = elements.map(item => {
86 | if (item.elements.length > 0) {
87 | const wrapper = el.wrap(build(item));
88 |
89 | wrapper.attr('class', 'editor-section');
90 | return wrapper;
91 | }
92 | usedBlocks.push(item.name);
93 | return elementsMap[item.name] ? elementsMap[item.name] : empty;
94 | });
95 |
96 | if (sizes && sizes.length !== elements.length) {
97 | sizes = elements.map(() => (100 / elements.length));
98 | }
99 |
100 | splitFuncs.push(() => ({
101 | b: block,
102 | split: Split(normalizedElements.map(({ e }) => e), {
103 | sizes: sizes || generateSizes(normalizedElements),
104 | gutterSize: 2,
105 | direction,
106 | onDragEnd: () => {
107 | splits.forEach(({ b, split }) => {
108 | b.sizes = split.getSizes();
109 | });
110 | state.updateThemeAndLayout(layout);
111 | }
112 | })
113 | }));
114 |
115 | if (direction === 'horizontal') {
116 | normalizedElements.map(el => el.css('float', 'left'));
117 | }
118 |
119 | return normalizedElements;
120 | };
121 |
122 | container.empty().appendChildren(build({ elements: [layout] }));
123 |
124 | if (usedBlocks.indexOf('HTML') === -1) {
125 | HTML.css('position', 'absolute');
126 | HTML.css('width', '10px');
127 | HTML.css('height', '10px');
128 | HTML.css('overflow', 'hidden');
129 | HTML.css('top', '-100px');
130 | HTML.css('left', '-100px');
131 | HTML.css('visibility', 'hidden');
132 | HTML.css('display', 'none');
133 | HTML.appendTo(body);
134 | }
135 |
136 | setTimeout(() => (splits = splitFuncs.map(f => f())), 1);
137 | };
138 |
--------------------------------------------------------------------------------
/src/js-vendor/mark-selection.js:
--------------------------------------------------------------------------------
1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 | // Distributed under an MIT license: https://codemirror.net/LICENSE
3 |
4 | // Because sometimes you need to mark the selected *text*.
5 | //
6 | // Adds an option 'styleSelectedText' which, when enabled, gives
7 | // selected text the CSS class given as option value, or
8 | // "CodeMirror-selectedtext" when the value is not a string.
9 |
10 | (function(mod) {
11 | if (typeof exports == "object" && typeof module == "object") // CommonJS
12 | mod(require("../../lib/codemirror"));
13 | else if (typeof define == "function" && define.amd) // AMD
14 | define(["../../lib/codemirror"], mod);
15 | else // Plain browser env
16 | mod(CodeMirror);
17 | })(function(CodeMirror) {
18 | "use strict";
19 |
20 | CodeMirror.defineOption("styleSelectedText", false, function(cm, val, old) {
21 | var prev = old && old != CodeMirror.Init;
22 | if (val && !prev) {
23 | cm.state.markedSelection = [];
24 | cm.state.markedSelectionStyle = typeof val == "string" ? val : "CodeMirror-selectedtext";
25 | reset(cm);
26 | cm.on("cursorActivity", onCursorActivity);
27 | cm.on("change", onChange);
28 | } else if (!val && prev) {
29 | cm.off("cursorActivity", onCursorActivity);
30 | cm.off("change", onChange);
31 | clear(cm);
32 | cm.state.markedSelection = cm.state.markedSelectionStyle = null;
33 | }
34 | });
35 |
36 | function onCursorActivity(cm) {
37 | if (cm.state.markedSelection)
38 | cm.operation(function() { update(cm); });
39 | }
40 |
41 | function onChange(cm) {
42 | if (cm.state.markedSelection && cm.state.markedSelection.length)
43 | cm.operation(function() { clear(cm); });
44 | }
45 |
46 | var CHUNK_SIZE = 8;
47 | var Pos = CodeMirror.Pos;
48 | var cmp = CodeMirror.cmpPos;
49 |
50 | function coverRange(cm, from, to, addAt) {
51 | if (cmp(from, to) == 0) return;
52 | var array = cm.state.markedSelection;
53 | var cls = cm.state.markedSelectionStyle;
54 | for (var line = from.line;;) {
55 | var start = line == from.line ? from : Pos(line, 0);
56 | var endLine = line + CHUNK_SIZE, atEnd = endLine >= to.line;
57 | var end = atEnd ? to : Pos(endLine, 0);
58 | var mark = cm.markText(start, end, {className: cls});
59 | if (addAt == null) array.push(mark);
60 | else array.splice(addAt++, 0, mark);
61 | if (atEnd) break;
62 | line = endLine;
63 | }
64 | }
65 |
66 | function clear(cm) {
67 | var array = cm.state.markedSelection;
68 | for (var i = 0; i < array.length; ++i) array[i].clear();
69 | array.length = 0;
70 | }
71 |
72 | function reset(cm) {
73 | clear(cm);
74 | var ranges = cm.listSelections();
75 | for (var i = 0; i < ranges.length; i++)
76 | coverRange(cm, ranges[i].from(), ranges[i].to());
77 | }
78 |
79 | function update(cm) {
80 | if (!cm.somethingSelected()) return clear(cm);
81 | if (cm.listSelections().length > 1) return reset(cm);
82 |
83 | var from = cm.getCursor("start"), to = cm.getCursor("end");
84 |
85 | var array = cm.state.markedSelection;
86 | if (!array.length) return coverRange(cm, from, to);
87 |
88 | var coverStart = array[0].find(), coverEnd = array[array.length - 1].find();
89 | if (!coverStart || !coverEnd || to.line - from.line <= CHUNK_SIZE ||
90 | cmp(from, coverEnd.to) >= 0 || cmp(to, coverStart.from) <= 0)
91 | return reset(cm);
92 |
93 | while (cmp(from, coverStart.from) > 0) {
94 | array.shift().clear();
95 | coverStart = array[0].find();
96 | }
97 | if (cmp(from, coverStart.from) < 0) {
98 | if (coverStart.to.line - from.line < CHUNK_SIZE) {
99 | array.shift().clear();
100 | coverRange(cm, from, coverStart.to, 0);
101 | } else {
102 | coverRange(cm, from, coverStart.from, 0);
103 | }
104 | }
105 |
106 | while (cmp(to, coverEnd.to) < 0) {
107 | array.pop().clear();
108 | coverEnd = array[array.length - 1].find();
109 | }
110 | if (cmp(to, coverEnd.to) > 0) {
111 | if (to.line - coverEnd.from.line < CHUNK_SIZE) {
112 | array.pop().clear();
113 | coverRange(cm, coverEnd.from, to);
114 | } else {
115 | coverRange(cm, coverEnd.to, to);
116 | }
117 | }
118 | }
119 | });
--------------------------------------------------------------------------------
/src/js/editor.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 | import el from './utils/element';
3 | import executeCode from './execute';
4 | import createConsole from './console';
5 | import output from './output';
6 | import loadAppDeps from './dependencies';
7 | import defineCodeMirrorCommands from './utils/codeMirrorCommands';
8 | import { isEmbedded } from './utils';
9 |
10 | export const ON_SELECT = 'e_ON_SELECT';
11 | export const ON_FILE_CHANGE = 'e_ON_FILE_CHANGE';
12 | export const ON_FILE_SAVE = 'e_ON_FILE_SAVE';
13 |
14 | export default async function editor(state, listener) {
15 | const { clearConsole, addToConsole } = createConsole();
16 | const { resetOutput, loadDependenciesInOutput, executeInOut } = await output(state, addToConsole, clearConsole);
17 | const execute = async () => {
18 | await resetOutput();
19 | await loadDependenciesInOutput();
20 | clearConsole();
21 | await executeInOut(executeCode(state.getActiveFile(), state.getFiles()));
22 | };
23 | const onSave = async (code) => {
24 | clearConsole();
25 | state.editFile(state.getActiveFile(), { c: code });
26 | listener(ON_FILE_SAVE, code, codeMirrorEditor);
27 | execute();
28 | };
29 | const container = el.withFallback('.js-code-editor');
30 |
31 | clearConsole('');
32 | await loadAppDeps();
33 |
34 | // Initializing CodeMirror
35 | const codeMirrorEditor = codeMirror(
36 | container.empty(),
37 | state.getEditorSettings(),
38 | state.getActiveFileContent(),
39 | onSave,
40 | function onChange() {
41 | listener(ON_FILE_CHANGE);
42 | },
43 | function showFile(index) {
44 | state.setActiveFileByIndex(index);
45 | loadFileInEditor();
46 | },
47 | function onSelection(code, list) {
48 | listener(ON_SELECT, { code, list }, codeMirrorEditor);
49 | }
50 | );
51 |
52 | // The function that we call to execute a file
53 | async function loadFileInEditor() {
54 | clearConsole();
55 | codeMirrorEditor.setValue(state.getActiveFileContent());
56 | if (!isEmbedded()) { codeMirrorEditor.focus(); }
57 | switch (state.getActiveFile().split('.').pop().toLowerCase()) {
58 | case 'css': codeMirrorEditor.setOption('mode', 'css'); break;
59 | case 'scss': codeMirrorEditor.setOption('mode', 'css'); break;
60 | case 'html': codeMirrorEditor.setOption('mode', 'htmlmixed'); break;
61 | case 'md':
62 | codeMirrorEditor.setOption('mode', {
63 | name: 'gfm',
64 | highlightFormatting: true,
65 | emoji: true,
66 | xml: true
67 | });
68 | break;
69 | default: codeMirrorEditor.setOption('mode', 'jsx'); break;
70 | }
71 | execute();
72 | }
73 |
74 | return { loadFileInEditor, save: () => {
75 | onSave(codeMirrorEditor.getValue());
76 | codeMirrorEditor.focus();
77 | }};
78 | }
79 |
80 | function codeMirror(container, editorSettings, value, onSave, onChange, showFile, onSelection) {
81 | defineCodeMirrorCommands(CodeMirror);
82 |
83 | const editor = CodeMirror(container.e, {
84 | value: value || '',
85 | mode: 'jsx',
86 | tabSize: 2,
87 | lineNumbers: false,
88 | autofocus: false,
89 | foldGutter: false,
90 | gutters: [],
91 | styleSelectedText: true,
92 | matchBrackets: true,
93 | autoCloseBrackets: true,
94 | lineWrapping: true,
95 | theme: editorSettings.theme,
96 | highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: true }
97 | });
98 | const save = () => onSave(editor.getValue());
99 | const change = (instance, { origin }) => {
100 | if (origin !== 'setValue') {
101 | onChange(editor.getValue());
102 | }
103 | };
104 |
105 | editor.on('change', change);
106 | editor.setOption('extraKeys', {
107 | 'Ctrl-S': save,
108 | 'Cmd-S': save,
109 | 'Cmd-1': () => showFile(0),
110 | 'Cmd-2': () => showFile(1),
111 | 'Cmd-3': () => showFile(2),
112 | 'Cmd-4': () => showFile(3),
113 | 'Cmd-5': () => showFile(4),
114 | 'Cmd-6': () => showFile(5),
115 | 'Cmd-7': () => showFile(6),
116 | 'Cmd-8': () => showFile(7),
117 | 'Cmd-9': () => showFile(8),
118 | 'Ctrl-1': () => showFile(0),
119 | 'Ctrl-2': () => showFile(1),
120 | 'Ctrl-3': () => showFile(2),
121 | 'Ctrl-4': () => showFile(3),
122 | 'Ctrl-5': () => showFile(4),
123 | 'Ctrl-6': () => showFile(5),
124 | 'Ctrl-7': () => showFile(6),
125 | 'Ctrl-8': () => showFile(7),
126 | 'Ctrl-9': () => showFile(8),
127 | 'Cmd-D': 'selectNextOccurrence',
128 | 'Ctrl-D': 'selectNextOccurrence',
129 | 'Cmd-/': 'toggleCommentIndented',
130 | 'Ctrl-/': 'toggleCommentIndented'
131 | });
132 | CodeMirror.normalizeKeyMap();
133 |
134 | container.onMouseUp(() => {
135 | const selection = editor.getSelection();
136 | const list = editor.listSelections();
137 |
138 | selection !== '' && onSelection(selection, list);
139 | });
140 |
141 | return editor;
142 | };
143 |
--------------------------------------------------------------------------------
/src/js-vendor/split.js:
--------------------------------------------------------------------------------
1 | /*! Split.js - v1.5.7 */
2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Split=t()}(this,function(){"use strict";var B=window,L=B.document,T="addEventListener",N="removeEventListener",R="getBoundingClientRect",q="horizontal",H=function(){return!1},I=B.attachEvent&&!B[T],i=["","-webkit-","-moz-","-o-"].filter(function(e){var t=L.createElement("div");return t.style.cssText="width:"+e+"calc(9px)",!!t.style.length}).shift()+"calc",s=function(e){return"string"==typeof e||e instanceof String},W=function(e){if(s(e)){var t=L.querySelector(e);if(!t)throw new Error("Selector "+e+" did not match a DOM element");return t}return e},X=function(e,t,n){var r=e[t];return void 0!==r?r:n},Y=function(e,t,n,r){if(t){if("end"===r)return 0;if("center"===r)return e/2}else if(n){if("start"===r)return 0;if("center"===r)return e/2}return e},G=function(e,t){var n=L.createElement("div");return n.className="gutter gutter-"+t,n},J=function(e,t,n){var r={};return s(t)?r[e]=t:r[e]=I?t+"%":i+"("+t+"% - "+n+"px)",r},K=function(e,t){var n;return(n={})[e]=t+"px",n};return function(e,i){void 0===i&&(i={});var u,t,s,o,r,a,l=e;Array.from&&(l=Array.from(l));var c=W(l[0]).parentNode,f=getComputedStyle?getComputedStyle(c).flexDirection:null,m=X(i,"sizes")||l.map(function(){return 100/l.length}),n=X(i,"minSize",100),h=Array.isArray(n)?n:l.map(function(){return n}),d=X(i,"expandToMin",!1),g=X(i,"gutterSize",10),v=X(i,"gutterAlign","center"),p=X(i,"snapOffset",30),y=X(i,"dragInterval",1),z=X(i,"direction",q),S=X(i,"cursor",z===q?"col-resize":"row-resize"),b=X(i,"gutter",G),_=X(i,"elementStyle",J),E=X(i,"gutterStyle",K);function w(t,e,n,r){var i=_(u,e,n,r);Object.keys(i).forEach(function(e){t.style[e]=i[e]})}function k(){return a.map(function(e){return e.size})}function x(e){return"touches"in e?e.touches[0][t]:e[t]}function M(e){var t=a[this.a],n=a[this.b],r=t.size+n.size;t.size=e/this.size*r,n.size=r-e/this.size*r,w(t.element,t.size,this._b,t.i),w(n.element,n.size,this._c,n.i)}function U(){var e=a[this.a].element,t=a[this.b].element,n=e[R](),r=t[R]();this.size=n[u]+r[u]+this._b+this._c,this.start=n[s],this.end=n[o]}function O(s){var o=function(e){if(!getComputedStyle)return null;var t=getComputedStyle(e),n=e[r];return n-=z===q?parseFloat(t.paddingLeft)+parseFloat(t.paddingRight):parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)}(c);if(null===o)return s;var a=0,u=[],e=s.map(function(e,t){var n=o*e/100,r=Y(g,0===t,t===s.length-1,v),i=h[t]+r;return n=this.size-(r.minSize+p+this._c)&&(t=this.size-(r.minSize+this._c)),M.call(this,t),X(i,"onDrag",H)())}.bind(t),t.stop=function(){var e=this,t=a[e.a].element,n=a[e.b].element;e.dragging&&X(i,"onDragEnd",H)(k()),e.dragging=!1,B[N]("mouseup",e.stop),B[N]("touchend",e.stop),B[N]("touchcancel",e.stop),B[N]("mousemove",e.move),B[N]("touchmove",e.move),e.stop=null,e.move=null,t[N]("selectstart",H),t[N]("dragstart",H),n[N]("selectstart",H),n[N]("dragstart",H),t.style.userSelect="",t.style.webkitUserSelect="",t.style.MozUserSelect="",t.style.pointerEvents="",n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",e.gutter.style.cursor="",e.parent.style.cursor="",L.body.style.cursor=""}.bind(t),B[T]("mouseup",t.stop),B[T]("touchend",t.stop),B[T]("touchcancel",t.stop),B[T]("mousemove",t.move),B[T]("touchmove",t.move),n[T]("selectstart",H),n[T]("dragstart",H),r[T]("selectstart",H),r[T]("dragstart",H),n.style.userSelect="none",n.style.webkitUserSelect="none",n.style.MozUserSelect="none",n.style.pointerEvents="none",r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",t.gutter.style.cursor=S,t.parent.style.cursor=S,L.body.style.cursor=S,U.call(t),t.dragOffset=x(e)-t.end}}z===q?(u="width",t="clientX",s="left",o="right",r="clientWidth"):"vertical"===z&&(u="height",t="clientY",s="top",o="bottom",r="clientHeight"),m=O(m);var D=[];function A(e){var t=e.i===D.length,n=t?D[e.i-1]:D[e.i];U.call(n);var r=t?n.size-e.minSize-n._c:e.minSize+n._b;M.call(n,r)}function j(e){var s=O(e);s.forEach(function(e,t){if(0]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i
16 |
17 | CodeMirror.defineMode("gfm", function(config, modeConfig) {
18 | var codeDepth = 0;
19 | function blankLine(state) {
20 | state.code = false;
21 | return null;
22 | }
23 | var gfmOverlay = {
24 | startState: function() {
25 | return {
26 | code: false,
27 | codeBlock: false,
28 | ateSpace: false
29 | };
30 | },
31 | copyState: function(s) {
32 | return {
33 | code: s.code,
34 | codeBlock: s.codeBlock,
35 | ateSpace: s.ateSpace
36 | };
37 | },
38 | token: function(stream, state) {
39 | state.combineTokens = null;
40 |
41 | // Hack to prevent formatting override inside code blocks (block and inline)
42 | if (state.codeBlock) {
43 | if (stream.match(/^```+/)) {
44 | state.codeBlock = false;
45 | return null;
46 | }
47 | stream.skipToEnd();
48 | return null;
49 | }
50 | if (stream.sol()) {
51 | state.code = false;
52 | }
53 | if (stream.sol() && stream.match(/^```+/)) {
54 | stream.skipToEnd();
55 | state.codeBlock = true;
56 | return null;
57 | }
58 | // If this block is changed, it may need to be updated in Markdown mode
59 | if (stream.peek() === '`') {
60 | stream.next();
61 | var before = stream.pos;
62 | stream.eatWhile('`');
63 | var difference = 1 + stream.pos - before;
64 | if (!state.code) {
65 | codeDepth = difference;
66 | state.code = true;
67 | } else {
68 | if (difference === codeDepth) { // Must be exact
69 | state.code = false;
70 | }
71 | }
72 | return null;
73 | } else if (state.code) {
74 | stream.next();
75 | return null;
76 | }
77 | // Check if space. If so, links can be formatted later on
78 | if (stream.eatSpace()) {
79 | state.ateSpace = true;
80 | return null;
81 | }
82 | if (stream.sol() || state.ateSpace) {
83 | state.ateSpace = false;
84 | if (modeConfig.gitHubSpice !== false) {
85 | if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?=.{0,6}\d)(?:[a-f0-9]{7,40}\b)/)) {
86 | // User/Project@SHA
87 | // User@SHA
88 | // SHA
89 | state.combineTokens = true;
90 | return "link";
91 | } else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) {
92 | // User/Project#Num
93 | // User#Num
94 | // #Num
95 | state.combineTokens = true;
96 | return "link";
97 | }
98 | }
99 | }
100 | if (stream.match(urlRE) &&
101 | stream.string.slice(stream.start - 2, stream.start) != "](" &&
102 | (stream.start == 0 || /\W/.test(stream.string.charAt(stream.start - 1)))) {
103 | // URLs
104 | // Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
105 | // And then (issue #1160) simplified to make it not crash the Chrome Regexp engine
106 | // And then limited url schemes to the CommonMark list, so foo:bar isn't matched as a URL
107 | state.combineTokens = true;
108 | return "link";
109 | }
110 | stream.next();
111 | return null;
112 | },
113 | blankLine: blankLine
114 | };
115 |
116 | var markdownConfig = {
117 | taskLists: true,
118 | strikethrough: true,
119 | emoji: true
120 | };
121 | for (var attr in modeConfig) {
122 | markdownConfig[attr] = modeConfig[attr];
123 | }
124 | markdownConfig.name = "markdown";
125 | return CodeMirror.overlayMode(CodeMirror.getMode(config, markdownConfig), gfmOverlay);
126 |
127 | }, "markdown");
128 |
129 | CodeMirror.defineMIME("text/x-gfm", "gfm");
130 | });
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 |
8 | "globals": {
9 | "document": false,
10 | "escape": false,
11 | "navigator": false,
12 | "unescape": false,
13 | "window": false,
14 | "describe": true,
15 | "before": true,
16 | "it": true,
17 | "expect": true,
18 | "sinon": true,
19 | "chrome": true,
20 | "browser": true,
21 | "Mousetrap": true,
22 | "io": true,
23 | "jest": true,
24 | "beforeEach": true,
25 | "afterEach": true,
26 | "CodeMirror": true
27 | },
28 |
29 | "parser": "babel-eslint",
30 |
31 | "plugins": [
32 |
33 | ],
34 |
35 | "extends": [
36 |
37 | ],
38 |
39 | "rules": {
40 | "block-scoped-var": 2,
41 | "brace-style": [2, "1tbs", { "allowSingleLine": true }],
42 | "camelcase": [2, { "properties": "always" }],
43 | "comma-dangle": [2, "never"],
44 | "comma-spacing": [2, { "before": false, "after": true }],
45 | "comma-style": [2, "last"],
46 | "complexity": 0,
47 | "consistent-return": 2,
48 | "consistent-this": 0,
49 | "curly": [2, "multi-line"],
50 | "default-case": 0,
51 | "dot-location": [2, "property"],
52 | "dot-notation": 0,
53 | "eol-last": 2,
54 | "eqeqeq": [2, "allow-null"],
55 | "func-names": 0,
56 | "func-style": 0,
57 | "generator-star-spacing": [2, "both"],
58 | "guard-for-in": 0,
59 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ],
60 | "indent": "off",
61 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }],
62 | "keyword-spacing": [2, {"before": true, "after": true}],
63 | "linebreak-style": 0,
64 | "max-depth": 0,
65 | "max-len": [2, 120, 4],
66 | "max-nested-callbacks": 0,
67 | "max-params": 0,
68 | "max-statements": 0,
69 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }],
70 | "newline-after-var": [2, "always"],
71 | "new-parens": 2,
72 | "no-alert": 0,
73 | "no-array-constructor": 2,
74 | "no-bitwise": 0,
75 | "no-caller": 2,
76 | "no-catch-shadow": 0,
77 | "no-cond-assign": 2,
78 | "no-console": 0,
79 | "no-constant-condition": 0,
80 | "no-continue": 0,
81 | "no-control-regex": 2,
82 | "no-debugger": 2,
83 | "no-delete-var": 2,
84 | "no-div-regex": 0,
85 | "no-dupe-args": 2,
86 | "no-dupe-keys": 2,
87 | "no-duplicate-case": 2,
88 | "no-else-return": 2,
89 | "no-empty": 0,
90 | "no-empty-character-class": 2,
91 | "no-eq-null": 0,
92 | "no-eval": 2,
93 | "no-ex-assign": 2,
94 | "no-extend-native": 2,
95 | "no-extra-bind": 2,
96 | "no-extra-boolean-cast": 2,
97 | "no-extra-parens": 0,
98 | "no-extra-semi": 0,
99 | "no-extra-strict": 0,
100 | "no-fallthrough": 2,
101 | "no-floating-decimal": 2,
102 | "no-func-assign": 2,
103 | "no-implied-eval": 2,
104 | "no-inline-comments": 0,
105 | "no-inner-declarations": [2, "functions"],
106 | "no-invalid-regexp": 2,
107 | "no-irregular-whitespace": 2,
108 | "no-iterator": 2,
109 | "no-label-var": 2,
110 | "no-labels": 2,
111 | "no-lone-blocks": 0,
112 | "no-lonely-if": 0,
113 | "no-loop-func": 0,
114 | "no-mixed-requires": 0,
115 | "no-mixed-spaces-and-tabs": [2, false],
116 | "no-multi-spaces": 2,
117 | "no-multi-str": 2,
118 | "no-multiple-empty-lines": [2, { "max": 1 }],
119 | "no-native-reassign": 2,
120 | "no-negated-in-lhs": 2,
121 | "no-nested-ternary": 0,
122 | "no-new": 2,
123 | "no-new-func": 2,
124 | "no-new-object": 2,
125 | "no-new-require": 2,
126 | "no-new-wrappers": 2,
127 | "no-obj-calls": 2,
128 | "no-octal": 2,
129 | "no-octal-escape": 2,
130 | "no-path-concat": 0,
131 | "no-plusplus": 0,
132 | "no-process-env": 0,
133 | "no-process-exit": 0,
134 | "no-proto": 2,
135 | "no-redeclare": 2,
136 | "no-regex-spaces": 2,
137 | "no-reserved-keys": 0,
138 | "no-restricted-modules": 0,
139 | "no-return-assign": 2,
140 | "no-script-url": 0,
141 | "no-self-compare": 2,
142 | "no-sequences": 2,
143 | "no-shadow": 0,
144 | "no-shadow-restricted-names": 2,
145 | "no-spaced-func": 2,
146 | "no-sparse-arrays": 2,
147 | "no-sync": 0,
148 | "no-ternary": 0,
149 | "no-throw-literal": 2,
150 | "no-trailing-spaces": 2,
151 | "no-undef": 2,
152 | "no-undef-init": 2,
153 | "no-undefined": 0,
154 | "no-underscore-dangle": 0,
155 | "no-unneeded-ternary": 2,
156 | "no-unreachable": 2,
157 | "no-unused-expressions": 0,
158 | "no-unused-vars": [2, { "vars": "all", "args": "none" }],
159 | "no-use-before-define": 2,
160 | "no-var": 0,
161 | "no-void": 0,
162 | "no-warning-comments": 0,
163 | "no-with": 2,
164 | "one-var": 0,
165 | "operator-assignment": 0,
166 | "operator-linebreak": [2, "after"],
167 | "padded-blocks": 0,
168 | "quote-props": 0,
169 | "quotes": [2, "single", "avoid-escape"],
170 | "radix": 2,
171 | "semi": [2, "always"],
172 | "semi-spacing": 0,
173 | "sort-vars": 0,
174 | "space-before-blocks": [2, "always"],
175 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
176 | "space-in-brackets": 0,
177 | "space-in-parens": [2, "never"],
178 | "space-infix-ops": 2,
179 | "space-unary-ops": [2, { "words": true, "nonwords": false }],
180 | "spaced-comment": [2, "always"],
181 | "strict": 0,
182 | "use-isnan": 2,
183 | "valid-jsdoc": 0,
184 | "valid-typeof": 2,
185 | "vars-on-top": 2,
186 | "wrap-iife": [2, "any"],
187 | "wrap-regex": 0,
188 | "yoda": [2, "never"]
189 | }
190 | }
--------------------------------------------------------------------------------
/src/js/utils/element.js:
--------------------------------------------------------------------------------
1 | var createdElements = [];
2 |
3 | export default function el(selector, parent = document, fallbackToEmpty = false, relaxedCleanup = false) {
4 | const removeListenersCallbacks = [];
5 | var e = typeof selector === 'string' ? parent.querySelector(selector) : selector;
6 | var found = true;
7 |
8 | if (!e) {
9 | found = false;
10 | if (!fallbackToEmpty) {
11 | throw new Error(`Ops! There is no DOM element matching "${ selector }" selector.`);
12 | } else {
13 | e = document.createElement('div');
14 | }
15 | }
16 |
17 | const registerEventListener = (type, callback) => {
18 | e.addEventListener(type, callback);
19 |
20 | const removeListener = () => e.removeEventListener(type, callback);
21 |
22 | removeListenersCallbacks.push(removeListener);
23 | return removeListener;
24 | };
25 |
26 | const api = {
27 | e,
28 | found() {
29 | return found;
30 | },
31 | content(str) {
32 | if (!str) {
33 | return e.innerHTML;
34 | }
35 | removeListenersCallbacks.forEach(c => c());
36 | e.innerHTML = str;
37 | return this.exports();
38 | },
39 | text(str) {
40 | if (!str) {
41 | return e.innerText;
42 | }
43 | e.innerText = str;
44 | return str;
45 | },
46 | appendChild(child) {
47 | e.appendChild(child);
48 | return this;
49 | },
50 | appendChildren(children) {
51 | children.forEach(c => e.appendChild(c.e));
52 | return this;
53 | },
54 | css(prop, value) {
55 | if (typeof value !== 'undefined') {
56 | e.style[prop] = value;
57 | return this;
58 | }
59 | return e.style[prop];
60 | },
61 | clearCSS() {
62 | e.style = {};
63 | return this;
64 | },
65 | prop(name, value) {
66 | if (typeof value !== 'undefined') {
67 | e[name] = value;
68 | return this;
69 | }
70 | return e[name];
71 | },
72 | attr(attr, value) {
73 | if (typeof value !== 'undefined') {
74 | e.setAttribute(attr, value);
75 | return this;
76 | }
77 | return e.getAttribute(attr);
78 | },
79 | onClick(callback) {
80 | return registerEventListener('click', callback);
81 | },
82 | onKeyUp(callback) {
83 | return registerEventListener('keyup', callback);
84 | },
85 | onKeyDown(callback) {
86 | return registerEventListener('keydown', callback);
87 | },
88 | onMouseOver(callback) {
89 | return registerEventListener('mouseover', callback);
90 | },
91 | onMouseOut(callback) {
92 | return registerEventListener('mouseout', callback);
93 | },
94 | onMouseUp(callback) {
95 | return registerEventListener('mouseup', callback);
96 | },
97 | onRightClick(callback) {
98 | const handler = event => {
99 | event.preventDefault();
100 | callback();
101 | };
102 |
103 | e.addEventListener('contextmenu', handler);
104 |
105 | const removeListener = () => e.removeEventListener('oncontextmenu', handler);
106 |
107 | removeListenersCallbacks.push(removeListener);
108 | return removeListener;
109 | },
110 | onChange(callback) {
111 | e.addEventListener('change', () => callback(e.value));
112 |
113 | const removeListener = () => e.removeEventListener('change', callback);
114 |
115 | removeListenersCallbacks.push(removeListener);
116 | return removeListener;
117 | },
118 | find(selector) {
119 | return el(selector, e);
120 | },
121 | appendTo(parent) {
122 | parent.e.appendChild(e);
123 | },
124 | exports() {
125 | return Array
126 | .prototype.slice.call(e.querySelectorAll('[data-export]'))
127 | .map(element => el(element, e));
128 | },
129 | namedExports() {
130 | return this.exports().reduce((result, exportedElement) => {
131 | result[exportedElement.attr('data-export')] = exportedElement;
132 | return result;
133 | }, {});
134 | },
135 | detach() {
136 | if (e.parentNode && e.parentNode.contains(e)) {
137 | e.parentNode.removeChild(e);
138 | }
139 | },
140 | empty() {
141 | while (e.firstChild) {
142 | e.removeChild(e.firstChild);
143 | }
144 | return this;
145 | },
146 | destroy() {
147 | removeListenersCallbacks.forEach(c => c());
148 | if (!relaxedCleanup) {
149 | this.empty();
150 | this.detach();
151 | }
152 | },
153 | scrollToBottom() {
154 | e.scrollTop = e.scrollHeight;
155 | },
156 | selectOnClick() {
157 | const removeListener = this.onClick(() => {
158 | e.select();
159 | removeListener();
160 | });
161 | }
162 | };
163 |
164 | createdElements.push(api);
165 |
166 | return api;
167 | }
168 |
169 | el.fromString = str => {
170 | const node = document.createElement('div');
171 |
172 | node.innerHTML = str;
173 |
174 | const filteredNodes = Array.prototype.slice.call(node.childNodes).filter(node => node.nodeType === 1);
175 |
176 | if (filteredNodes.length > 0) {
177 | return el(filteredNodes[0]);
178 | }
179 | throw new Error('fromString accepts HTMl with a single parent.');
180 | };
181 | el.wrap = elements => el(document.createElement('div')).appendChildren(elements);
182 | el.fromTemplate = selector => el.fromString(document.querySelector(selector).innerHTML);
183 | el.withFallback = selector => el(selector, document, true);
184 | el.withRelaxedCleanup = selector => el(selector, document, false, true);
185 | el.destroy = () => {
186 | createdElements.forEach(elInstance => elInstance.destroy());
187 | createdElements = [];
188 | };
189 | el.exists = (selector) => {
190 | return !!document.querySelector(selector);
191 | };
192 |
--------------------------------------------------------------------------------
/src/js-vendor/jsx.js:
--------------------------------------------------------------------------------
1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 | // Distributed under an MIT license: https://codemirror.net/LICENSE
3 |
4 | (function(mod) {
5 | if (typeof exports == "object" && typeof module == "object") // CommonJS
6 | mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"))
7 | else if (typeof define == "function" && define.amd) // AMD
8 | define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript"], mod)
9 | else // Plain browser env
10 | mod(CodeMirror)
11 | })(function(CodeMirror) {
12 | "use strict"
13 |
14 | // Depth means the amount of open braces in JS context, in XML
15 | // context 0 means not in tag, 1 means in tag, and 2 means in tag
16 | // and js block comment.
17 | function Context(state, mode, depth, prev) {
18 | this.state = state; this.mode = mode; this.depth = depth; this.prev = prev
19 | }
20 |
21 | function copyContext(context) {
22 | return new Context(CodeMirror.copyState(context.mode, context.state),
23 | context.mode,
24 | context.depth,
25 | context.prev && copyContext(context.prev))
26 | }
27 |
28 | CodeMirror.defineMode("jsx", function(config, modeConfig) {
29 | var xmlMode = CodeMirror.getMode(config, {name: "xml", allowMissing: true, multilineTagIndentPastTag: false, allowMissingTagName: true})
30 | var jsMode = CodeMirror.getMode(config, modeConfig && modeConfig.base || "javascript")
31 |
32 | function flatXMLIndent(state) {
33 | var tagName = state.tagName
34 | state.tagName = null
35 | var result = xmlMode.indent(state, "")
36 | state.tagName = tagName
37 | return result
38 | }
39 |
40 | function token(stream, state) {
41 | if (state.context.mode == xmlMode)
42 | return xmlToken(stream, state, state.context)
43 | else
44 | return jsToken(stream, state, state.context)
45 | }
46 |
47 | function xmlToken(stream, state, cx) {
48 | if (cx.depth == 2) { // Inside a JS /* */ comment
49 | if (stream.match(/^.*?\*\//)) cx.depth = 1
50 | else stream.skipToEnd()
51 | return "comment"
52 | }
53 |
54 | if (stream.peek() == "{") {
55 | xmlMode.skipAttribute(cx.state)
56 |
57 | var indent = flatXMLIndent(cx.state), xmlContext = cx.state.context
58 | // If JS starts on same line as tag
59 | if (xmlContext && stream.match(/^[^>]*>\s*$/, false)) {
60 | while (xmlContext.prev && !xmlContext.startOfLine)
61 | xmlContext = xmlContext.prev
62 | // If tag starts the line, use XML indentation level
63 | if (xmlContext.startOfLine) indent -= config.indentUnit
64 | // Else use JS indentation level
65 | else if (cx.prev.state.lexical) indent = cx.prev.state.lexical.indented
66 | // Else if inside of tag
67 | } else if (cx.depth == 1) {
68 | indent += config.indentUnit
69 | }
70 |
71 | state.context = new Context(CodeMirror.startState(jsMode, indent),
72 | jsMode, 0, state.context)
73 | return null
74 | }
75 |
76 | if (cx.depth == 1) { // Inside of tag
77 | if (stream.peek() == "<") { // Tag inside of tag
78 | xmlMode.skipAttribute(cx.state)
79 | state.context = new Context(CodeMirror.startState(xmlMode, flatXMLIndent(cx.state)),
80 | xmlMode, 0, state.context)
81 | return null
82 | } else if (stream.match("//")) {
83 | stream.skipToEnd()
84 | return "comment"
85 | } else if (stream.match("/*")) {
86 | cx.depth = 2
87 | return token(stream, state)
88 | }
89 | }
90 |
91 | var style = xmlMode.token(stream, cx.state), cur = stream.current(), stop
92 | if (/\btag\b/.test(style)) {
93 | if (/>$/.test(cur)) {
94 | if (cx.state.context) cx.depth = 0
95 | else state.context = state.context.prev
96 | } else if (/^ -1) {
100 | stream.backUp(cur.length - stop)
101 | }
102 | return style
103 | }
104 |
105 | function jsToken(stream, state, cx) {
106 | if (stream.peek() == "<" && jsMode.expressionAllowed(stream, cx.state)) {
107 | jsMode.skipExpression(cx.state)
108 | state.context = new Context(CodeMirror.startState(xmlMode, jsMode.indent(cx.state, "")),
109 | xmlMode, 0, state.context)
110 | return null
111 | }
112 |
113 | var style = jsMode.token(stream, cx.state)
114 | if (!style && cx.depth != null) {
115 | var cur = stream.current()
116 | if (cur == "{") {
117 | cx.depth++
118 | } else if (cur == "}") {
119 | if (--cx.depth == 0) state.context = state.context.prev
120 | }
121 | }
122 | return style
123 | }
124 |
125 | return {
126 | startState: function() {
127 | return {context: new Context(CodeMirror.startState(jsMode), jsMode)}
128 | },
129 |
130 | copyState: function(state) {
131 | return {context: copyContext(state.context)}
132 | },
133 |
134 | token: token,
135 |
136 | indent: function(state, textAfter, fullLine) {
137 | return state.context.mode.indent(state.context.state, textAfter, fullLine)
138 | },
139 |
140 | innerMode: function(state) {
141 | return state.context
142 | }
143 | }
144 | }, "xml", "javascript")
145 |
146 | CodeMirror.defineMIME("text/jsx", "jsx")
147 | CodeMirror.defineMIME("text/typescript-jsx", {name: "jsx", base: {name: "javascript", typescript: true}})
148 | });
149 |
--------------------------------------------------------------------------------
/src/js/statusBar.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-sequences */
2 | import el from './utils/element';
3 | import { CLOSE_ICON, PLUS_ICON, SETTINGS_ICON, NO_USER, FORK, SHARE, BARS } from './utils/icons';
4 | import { IS_PROD } from './constants';
5 |
6 | const STATUS_BAR_HIDDEN_HEIGHT = '6px';
7 | const STATUS_BAR_VISIBLE_HEIGHT = '36px';
8 |
9 | const showProfilePicAndName = profile => {
10 | return `
`;
11 | };
12 | const createLink = (exportKey, label, className = '', href = 'javascript:void(0)') => {
13 | return `${ label }`;
14 | };
15 | const createStr = (str, n) => Array(n).join(str);
16 |
17 | export default function statusBar(state, showFile, newFile, editFile, showSettings, saveCurrentFile, editName) {
18 | const bar = el.withRelaxedCleanup('.status-bar');
19 | const layout = el.withRelaxedCleanup('.app .layout');
20 | const menu = el.withRelaxedCleanup('.status-bar-menu');
21 | let visibility = !!state.getEditorSettings().statusBar;
22 | let visibilityMenu = false;
23 | let pending = false;
24 |
25 | const toggleMenu = () => {
26 | menu.css('display', (visibilityMenu = !visibilityMenu) ? 'block' : 'none');
27 | };
28 |
29 | const render = () => {
30 | const items = [];
31 | const menuItems = [];
32 | const files = state.getFiles();
33 |
34 | items.push('');
35 | files.forEach(([ filename, file ]) => {
36 | const isCurrentFile = state.isCurrentFile(filename);
37 |
38 | items.push(createLink(
39 | 'file:' + filename,
40 | `
${ filename }${ isCurrentFile && pending ? '*' : ''}`,
41 | `file${ isCurrentFile ? ' active' : '' }${ file.en ? ' entry' : ''}`
42 | ));
43 | });
44 | items.push(createLink('newFileButton', PLUS_ICON(14), 'new-file'));
45 | items.push(`
46 |
52 | `);
53 | items.push(createLink('menuButton', BARS(14)));
54 | items.push(createLink('closeButton', CLOSE_ICON(14)));
55 | items.push('
');
56 |
57 | // `/login?did=${ demoId }`;
58 | // '/u/' + state.getProfile().id;
59 | IS_PROD && menuItems.push(createLink(
60 | 'profileButton',
61 | state.loggedIn() ?
62 | showProfilePicAndName(state.getProfile()) + ' Profile' :
63 | NO_USER() + ' Log in',
64 | 'profile',
65 | state.loggedIn() ?
66 | '/u/' + state.getProfile().id :
67 | `/login?did=${ state.getDemoId() }`
68 | ));
69 | IS_PROD && menuItems.push(createLink('', PLUS_ICON(14) + ' New story', '', '/new'));
70 | state.isForkable() && menuItems.push(createLink('forkButton', FORK(14) + ' Fork'));
71 | IS_PROD && menuItems.push(createLink('shareButton', SHARE(14) + ' Share/Embed'));
72 | state.isDemoOwner() && menuItems.push(createLink('nameButton', SETTINGS_ICON(14) + ' Story'));
73 | menuItems.push(createLink('settingsButton', SETTINGS_ICON(14) + ' Editor'));
74 | state.isForkable() && menuItems.push(createLink('', NO_USER() + ' Log out', '', `/logout?r=e/${ state.getDemoId() }`));
75 |
76 | bar.content(items.join('')).forEach(button => {
77 | if (button.attr('data-export').indexOf('file') === 0) {
78 | const filename = button.attr('data-export').split(':').pop();
79 |
80 | button.onClick(() => {
81 | if (!state.isCurrentFile(filename)) {
82 | showFile(filename);
83 | } else if (pending) {
84 | saveCurrentFile();
85 | }
86 | });
87 | button.onRightClick(() => editFile(filename));
88 | }
89 | });
90 | menu.content(menuItems.join(''));
91 |
92 | const { newFileButton, closeButton, menuButton } = bar.namedExports();
93 | const { forkButton, shareButton, nameButton, settingsButton } = menu.namedExports();
94 |
95 | const manageVisibility = () => {
96 | const { buttons } = bar.namedExports();
97 |
98 | buttons.css('display', visibility ? 'grid' : 'none');
99 | buttons.css('gridTemplateColumns', [
100 | createStr('minmax(auto, 135px) ', state.getNumOfFiles() + 1),
101 | '30px',
102 | '1fr',
103 | '30px',
104 | '30px'
105 | ].filter(value => value).join(' '));
106 | bar.css('height', visibility ? STATUS_BAR_VISIBLE_HEIGHT : STATUS_BAR_HIDDEN_HEIGHT);
107 | layout.css('height', visibility ? `calc(100% - ${ STATUS_BAR_VISIBLE_HEIGHT })` : `calc(100% - ${ STATUS_BAR_HIDDEN_HEIGHT })`);
108 | state.updateStatusBarVisibility(visibility);
109 | };
110 |
111 | newFileButton && newFileButton.onClick(newFile);
112 | shareButton && shareButton.onClick(() => (showSettings(2), toggleMenu()));
113 | settingsButton && settingsButton.onClick(() => (showSettings(), toggleMenu()));
114 | state.isDemoOwner() && nameButton && nameButton.onClick(() => (editName(), toggleMenu()));
115 | forkButton && forkButton.onClick(() => (state.fork(), toggleMenu()));
116 | menuButton && menuButton.onClick(toggleMenu);
117 | closeButton.onClick(e => {
118 | e.stopPropagation();
119 | visibility = false;
120 | manageVisibility();
121 | });
122 | bar.onClick(() => {
123 | if (!visibility) {
124 | visibility = true;
125 | manageVisibility();
126 | }
127 | });
128 |
129 | manageVisibility();
130 | };
131 |
132 | render();
133 |
134 | state.listen(render);
135 |
136 | return (value) => {
137 | pending = value;
138 | render();
139 | };
140 | }
141 |
--------------------------------------------------------------------------------
/src/js-vendor/htmlmixed.js:
--------------------------------------------------------------------------------
1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 | // Distributed under an MIT license: https://codemirror.net/LICENSE
3 |
4 | (function(mod) {
5 | if (typeof exports == "object" && typeof module == "object") // CommonJS
6 | mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"), require("../css/css"));
7 | else if (typeof define == "function" && define.amd) // AMD
8 | define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod);
9 | else // Plain browser env
10 | mod(CodeMirror);
11 | })(function(CodeMirror) {
12 | "use strict";
13 |
14 | var defaultTags = {
15 | script: [
16 | ["lang", /(javascript|babel)/i, "javascript"],
17 | ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i, "javascript"],
18 | ["type", /./, "text/plain"],
19 | [null, null, "javascript"]
20 | ],
21 | style: [
22 | ["lang", /^css$/i, "css"],
23 | ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"],
24 | ["type", /./, "text/plain"],
25 | [null, null, "css"]
26 | ]
27 | };
28 |
29 | function maybeBackup(stream, pat, style) {
30 | var cur = stream.current(), close = cur.search(pat);
31 | if (close > -1) {
32 | stream.backUp(cur.length - close);
33 | } else if (cur.match(/<\/?$/)) {
34 | stream.backUp(cur.length);
35 | if (!stream.match(pat, false)) stream.match(cur);
36 | }
37 | return style;
38 | }
39 |
40 | var attrRegexpCache = {};
41 | function getAttrRegexp(attr) {
42 | var regexp = attrRegexpCache[attr];
43 | if (regexp) return regexp;
44 | return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*");
45 | }
46 |
47 | function getAttrValue(text, attr) {
48 | var match = text.match(getAttrRegexp(attr))
49 | return match ? /^\s*(.*?)\s*$/.exec(match[2])[1] : ""
50 | }
51 |
52 | function getTagRegexp(tagName, anchored) {
53 | return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i");
54 | }
55 |
56 | function addTags(from, to) {
57 | for (var tag in from) {
58 | var dest = to[tag] || (to[tag] = []);
59 | var source = from[tag];
60 | for (var i = source.length - 1; i >= 0; i--)
61 | dest.unshift(source[i])
62 | }
63 | }
64 |
65 | function findMatchingMode(tagInfo, tagText) {
66 | for (var i = 0; i < tagInfo.length; i++) {
67 | var spec = tagInfo[i];
68 | if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2];
69 | }
70 | }
71 |
72 | CodeMirror.defineMode("htmlmixed", function (config, parserConfig) {
73 | var htmlMode = CodeMirror.getMode(config, {
74 | name: "xml",
75 | htmlMode: true,
76 | multilineTagIndentFactor: parserConfig.multilineTagIndentFactor,
77 | multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag
78 | });
79 |
80 | var tags = {};
81 | var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes;
82 | addTags(defaultTags, tags);
83 | if (configTags) addTags(configTags, tags);
84 | if (configScript) for (var i = configScript.length - 1; i >= 0; i--)
85 | tags.script.unshift(["type", configScript[i].matches, configScript[i].mode])
86 |
87 | function html(stream, state) {
88 | var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName
89 | if (tag && !/[<>\s\/]/.test(stream.current()) &&
90 | (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) &&
91 | tags.hasOwnProperty(tagName)) {
92 | state.inTag = tagName + " "
93 | } else if (state.inTag && tag && />$/.test(stream.current())) {
94 | var inTag = /^([\S]+) (.*)/.exec(state.inTag)
95 | state.inTag = null
96 | var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2])
97 | var mode = CodeMirror.getMode(config, modeSpec)
98 | var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false);
99 | state.token = function (stream, state) {
100 | if (stream.match(endTagA, false)) {
101 | state.token = html;
102 | state.localState = state.localMode = null;
103 | return null;
104 | }
105 | return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState));
106 | };
107 | state.localMode = mode;
108 | state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, ""));
109 | } else if (state.inTag) {
110 | state.inTag += stream.current()
111 | if (stream.eol()) state.inTag += " "
112 | }
113 | return style;
114 | };
115 |
116 | return {
117 | startState: function () {
118 | var state = CodeMirror.startState(htmlMode);
119 | return {token: html, inTag: null, localMode: null, localState: null, htmlState: state};
120 | },
121 |
122 | copyState: function (state) {
123 | var local;
124 | if (state.localState) {
125 | local = CodeMirror.copyState(state.localMode, state.localState);
126 | }
127 | return {token: state.token, inTag: state.inTag,
128 | localMode: state.localMode, localState: local,
129 | htmlState: CodeMirror.copyState(htmlMode, state.htmlState)};
130 | },
131 |
132 | token: function (stream, state) {
133 | return state.token(stream, state);
134 | },
135 |
136 | indent: function (state, textAfter, line) {
137 | if (!state.localMode || /^\s*<\//.test(textAfter))
138 | return htmlMode.indent(state.htmlState, textAfter);
139 | else if (state.localMode.indent)
140 | return state.localMode.indent(state.localState, textAfter, line);
141 | else
142 | return CodeMirror.Pass;
143 | },
144 |
145 | innerMode: function (state) {
146 | return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
147 | }
148 | };
149 | }, "xml", "javascript", "css");
150 |
151 | CodeMirror.defineMIME("text/html", "htmlmixed");
152 | });
--------------------------------------------------------------------------------
/src/js-vendor/match-highlighter.js:
--------------------------------------------------------------------------------
1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 | // Distributed under an MIT license: https://codemirror.net/LICENSE
3 |
4 | // Highlighting text that matches the selection
5 | //
6 | // Defines an option highlightSelectionMatches, which, when enabled,
7 | // will style strings that match the selection throughout the
8 | // document.
9 | //
10 | // The option can be set to true to simply enable it, or to a
11 | // {minChars, style, wordsOnly, showToken, delay} object to explicitly
12 | // configure it. minChars is the minimum amount of characters that should be
13 | // selected for the behavior to occur, and style is the token style to
14 | // apply to the matches. This will be prefixed by "cm-" to create an
15 | // actual CSS class name. If wordsOnly is enabled, the matches will be
16 | // highlighted only if the selected text is a word. showToken, when enabled,
17 | // will cause the current token to be highlighted when nothing is selected.
18 | // delay is used to specify how much time to wait, in milliseconds, before
19 | // highlighting the matches. If annotateScrollbar is enabled, the occurences
20 | // will be highlighted on the scrollbar via the matchesonscrollbar addon.
21 |
22 | (function(mod) {
23 | if (typeof exports == "object" && typeof module == "object") // CommonJS
24 | mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
25 | else if (typeof define == "function" && define.amd) // AMD
26 | define(["../../lib/codemirror", "./matchesonscrollbar"], mod);
27 | else // Plain browser env
28 | mod(CodeMirror);
29 | })(function(CodeMirror) {
30 | "use strict";
31 |
32 | var defaults = {
33 | style: "matchhighlight",
34 | minChars: 2,
35 | delay: 100,
36 | wordsOnly: false,
37 | annotateScrollbar: false,
38 | showToken: false,
39 | trim: true
40 | }
41 |
42 | function State(options) {
43 | this.options = {}
44 | for (var name in defaults)
45 | this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name]
46 | this.overlay = this.timeout = null;
47 | this.matchesonscroll = null;
48 | this.active = false;
49 | }
50 |
51 | CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
52 | if (old && old != CodeMirror.Init) {
53 | removeOverlay(cm);
54 | clearTimeout(cm.state.matchHighlighter.timeout);
55 | cm.state.matchHighlighter = null;
56 | cm.off("cursorActivity", cursorActivity);
57 | cm.off("focus", onFocus)
58 | }
59 | if (val) {
60 | var state = cm.state.matchHighlighter = new State(val);
61 | if (cm.hasFocus()) {
62 | state.active = true
63 | highlightMatches(cm)
64 | } else {
65 | cm.on("focus", onFocus)
66 | }
67 | cm.on("cursorActivity", cursorActivity);
68 | }
69 | });
70 |
71 | function cursorActivity(cm) {
72 | var state = cm.state.matchHighlighter;
73 | if (state.active || cm.hasFocus()) scheduleHighlight(cm, state)
74 | }
75 |
76 | function onFocus(cm) {
77 | var state = cm.state.matchHighlighter
78 | if (!state.active) {
79 | state.active = true
80 | scheduleHighlight(cm, state)
81 | }
82 | }
83 |
84 | function scheduleHighlight(cm, state) {
85 | clearTimeout(state.timeout);
86 | state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay);
87 | }
88 |
89 | function addOverlay(cm, query, hasBoundary, style) {
90 | var state = cm.state.matchHighlighter;
91 | cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
92 | if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
93 | var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + "\\b") : query;
94 | state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
95 | {className: "CodeMirror-selection-highlight-scrollbar"});
96 | }
97 | }
98 |
99 | function removeOverlay(cm) {
100 | var state = cm.state.matchHighlighter;
101 | if (state.overlay) {
102 | cm.removeOverlay(state.overlay);
103 | state.overlay = null;
104 | if (state.matchesonscroll) {
105 | state.matchesonscroll.clear();
106 | state.matchesonscroll = null;
107 | }
108 | }
109 | }
110 |
111 | function highlightMatches(cm) {
112 | cm.operation(function() {
113 | var state = cm.state.matchHighlighter;
114 | removeOverlay(cm);
115 | if (!cm.somethingSelected() && state.options.showToken) {
116 | var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken;
117 | var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
118 | while (start && re.test(line.charAt(start - 1))) --start;
119 | while (end < line.length && re.test(line.charAt(end))) ++end;
120 | if (start < end)
121 | addOverlay(cm, line.slice(start, end), re, state.options.style);
122 | return;
123 | }
124 | var from = cm.getCursor("from"), to = cm.getCursor("to");
125 | if (from.line != to.line) return;
126 | if (state.options.wordsOnly && !isWord(cm, from, to)) return;
127 | var selection = cm.getRange(from, to)
128 | if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "")
129 | if (selection.length >= state.options.minChars)
130 | addOverlay(cm, selection, false, state.options.style);
131 | });
132 | }
133 |
134 | function isWord(cm, from, to) {
135 | var str = cm.getRange(from, to);
136 | if (str.match(/^\w+$/) !== null) {
137 | if (from.ch > 0) {
138 | var pos = {line: from.line, ch: from.ch - 1};
139 | var chr = cm.getRange(pos, from);
140 | if (chr.match(/\W/) === null) return false;
141 | }
142 | if (to.ch < cm.getLine(from.line).length) {
143 | var pos = {line: to.line, ch: to.ch + 1};
144 | var chr = cm.getRange(to, pos);
145 | if (chr.match(/\W/) === null) return false;
146 | }
147 | return true;
148 | } else return false;
149 | }
150 |
151 | function boundariesAround(stream, re) {
152 | return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
153 | (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
154 | }
155 |
156 | function makeOverlay(query, hasBoundary, style) {
157 | return {token: function(stream) {
158 | if (stream.match(query) &&
159 | (!hasBoundary || boundariesAround(stream, hasBoundary)))
160 | return style;
161 | stream.next();
162 | stream.skipTo(query.charAt(0)) || stream.skipToEnd();
163 | }};
164 | }
165 | });
--------------------------------------------------------------------------------
/src/js/utils/icons.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 |
3 | export const TRASH_ICON = (size = 20) => ``;
4 | export const CHECK_ICON = (size = 20) => ``;
5 | export const CLOSE_ICON = (size = 24) => ``;
6 | export const PLUS_ICON = (size = 24) => ``;
7 | export const SETTINGS_ICON = (size = 24) => ``;
8 | export const DOT_CIRCLE = (size = 24) => ``;
9 | export const NO_USER = (sizeW = 16, sizeH = 14) => ``;
10 | export const FORK = (size = 14) => ``;
11 | export const SHARE = (size = 14) => ``;
12 | export const BARS = (size = 14) => ``;
13 | export const EYE = (size = 14) => ``;
14 | export const BOOK = (size = 14) => ``;
15 |
--------------------------------------------------------------------------------
/src/js-vendor/matchbrackets.js:
--------------------------------------------------------------------------------
1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 | // Distributed under an MIT license: https://codemirror.net/LICENSE
3 |
4 | (function(mod) {
5 | if (typeof exports == "object" && typeof module == "object") // CommonJS
6 | mod(require("../../lib/codemirror"));
7 | else if (typeof define == "function" && define.amd) // AMD
8 | define(["../../lib/codemirror"], mod);
9 | else // Plain browser env
10 | mod(CodeMirror);
11 | })(function(CodeMirror) {
12 | var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
13 | (document.documentMode == null || document.documentMode < 8);
14 |
15 | var Pos = CodeMirror.Pos;
16 |
17 | var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"};
18 |
19 | function bracketRegex(config) {
20 | return config && config.bracketRegex || /[(){}[\]]/
21 | }
22 |
23 | function findMatchingBracket(cm, where, config) {
24 | var line = cm.getLineHandle(where.line), pos = where.ch - 1;
25 | var afterCursor = config && config.afterCursor
26 | if (afterCursor == null)
27 | afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className)
28 | var re = bracketRegex(config)
29 |
30 | // A cursor is defined as between two characters, but in in vim command mode
31 | // (i.e. not insert mode), the cursor is visually represented as a
32 | // highlighted box on top of the 2nd character. Otherwise, we allow matches
33 | // from before or after the cursor.
34 | var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) ||
35 | re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)];
36 | if (!match) return null;
37 | var dir = match.charAt(1) == ">" ? 1 : -1;
38 | if (config && config.strict && (dir > 0) != (pos == where.ch)) return null;
39 | var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
40 |
41 | var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
42 | if (found == null) return null;
43 | return {from: Pos(where.line, pos), to: found && found.pos,
44 | match: found && found.ch == match.charAt(0), forward: dir > 0};
45 | }
46 |
47 | // bracketRegex is used to specify which type of bracket to scan
48 | // should be a regexp, e.g. /[[\]]/
49 | //
50 | // Note: If "where" is on an open bracket, then this bracket is ignored.
51 | //
52 | // Returns false when no bracket was found, null when it reached
53 | // maxScanLines and gave up
54 | function scanForBracket(cm, where, dir, style, config) {
55 | var maxScanLen = (config && config.maxScanLineLength) || 10000;
56 | var maxScanLines = (config && config.maxScanLines) || 1000;
57 |
58 | var stack = [];
59 | var re = bracketRegex(config)
60 | var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
61 | : Math.max(cm.firstLine() - 1, where.line - maxScanLines);
62 | for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
63 | var line = cm.getLine(lineNo);
64 | if (!line) continue;
65 | var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
66 | if (line.length > maxScanLen) continue;
67 | if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
68 | for (; pos != end; pos += dir) {
69 | var ch = line.charAt(pos);
70 | if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
71 | var match = matching[ch];
72 | if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
73 | else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
74 | else stack.pop();
75 | }
76 | }
77 | }
78 | return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
79 | }
80 |
81 | function matchBrackets(cm, autoclear, config) {
82 | // Disable brace matching in long lines, since it'll cause hugely slow updates
83 | var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
84 | var marks = [], ranges = cm.listSelections();
85 | for (var i = 0; i < ranges.length; i++) {
86 | var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config);
87 | if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
88 | var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
89 | marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
90 | if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
91 | marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
92 | }
93 | }
94 |
95 | if (marks.length) {
96 | // Kludge to work around the IE bug from issue #1193, where text
97 | // input stops going to the textare whever this fires.
98 | if (ie_lt8 && cm.state.focused) cm.focus();
99 |
100 | var clear = function() {
101 | cm.operation(function() {
102 | for (var i = 0; i < marks.length; i++) marks[i].clear();
103 | });
104 | };
105 | if (autoclear) setTimeout(clear, 800);
106 | else return clear;
107 | }
108 | }
109 |
110 | function doMatchBrackets(cm) {
111 | cm.operation(function() {
112 | if (cm.state.matchBrackets.currentlyHighlighted) {
113 | cm.state.matchBrackets.currentlyHighlighted();
114 | cm.state.matchBrackets.currentlyHighlighted = null;
115 | }
116 | cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
117 | });
118 | }
119 |
120 | CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
121 | if (old && old != CodeMirror.Init) {
122 | cm.off("cursorActivity", doMatchBrackets);
123 | if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
124 | cm.state.matchBrackets.currentlyHighlighted();
125 | cm.state.matchBrackets.currentlyHighlighted = null;
126 | }
127 | }
128 | if (val) {
129 | cm.state.matchBrackets = typeof val == "object" ? val : {};
130 | cm.on("cursorActivity", doMatchBrackets);
131 | }
132 | });
133 |
134 | CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
135 | CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){
136 | // Backwards-compatibility kludge
137 | if (oldConfig || typeof config == "boolean") {
138 | if (!oldConfig) {
139 | config = config ? {strict: true} : null
140 | } else {
141 | oldConfig.strict = config
142 | config = oldConfig
143 | }
144 | }
145 | return findMatchingBracket(this, pos, config)
146 | });
147 | CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
148 | return scanForBracket(this, pos, dir, style, config);
149 | });
150 | });
--------------------------------------------------------------------------------
/src/js-vendor/closebrackets.js:
--------------------------------------------------------------------------------
1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 | // Distributed under an MIT license: https://codemirror.net/LICENSE
3 |
4 | (function(mod) {
5 | if (typeof exports == "object" && typeof module == "object") // CommonJS
6 | mod(require("../../lib/codemirror"));
7 | else if (typeof define == "function" && define.amd) // AMD
8 | define(["../../lib/codemirror"], mod);
9 | else // Plain browser env
10 | mod(CodeMirror);
11 | })(function(CodeMirror) {
12 | var defaults = {
13 | pairs: "()[]{}''\"\"",
14 | triples: "",
15 | explode: "[]{}"
16 | };
17 |
18 | var Pos = CodeMirror.Pos;
19 |
20 | CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
21 | if (old && old != CodeMirror.Init) {
22 | cm.removeKeyMap(keyMap);
23 | cm.state.closeBrackets = null;
24 | }
25 | if (val) {
26 | ensureBound(getOption(val, "pairs"))
27 | cm.state.closeBrackets = val;
28 | cm.addKeyMap(keyMap);
29 | }
30 | });
31 |
32 | function getOption(conf, name) {
33 | if (name == "pairs" && typeof conf == "string") return conf;
34 | if (typeof conf == "object" && conf[name] != null) return conf[name];
35 | return defaults[name];
36 | }
37 |
38 | var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
39 | function ensureBound(chars) {
40 | for (var i = 0; i < chars.length; i++) {
41 | var ch = chars.charAt(i), key = "'" + ch + "'"
42 | if (!keyMap[key]) keyMap[key] = handler(ch)
43 | }
44 | }
45 | ensureBound(defaults.pairs + "`")
46 |
47 | function handler(ch) {
48 | return function(cm) { return handleChar(cm, ch); };
49 | }
50 |
51 | function getConfig(cm) {
52 | var deflt = cm.state.closeBrackets;
53 | if (!deflt || deflt.override) return deflt;
54 | var mode = cm.getModeAt(cm.getCursor());
55 | return mode.closeBrackets || deflt;
56 | }
57 |
58 | function handleBackspace(cm) {
59 | var conf = getConfig(cm);
60 | if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
61 |
62 | var pairs = getOption(conf, "pairs");
63 | var ranges = cm.listSelections();
64 | for (var i = 0; i < ranges.length; i++) {
65 | if (!ranges[i].empty()) return CodeMirror.Pass;
66 | var around = charsAround(cm, ranges[i].head);
67 | if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
68 | }
69 | for (var i = ranges.length - 1; i >= 0; i--) {
70 | var cur = ranges[i].head;
71 | cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
72 | }
73 | }
74 |
75 | function handleEnter(cm) {
76 | var conf = getConfig(cm);
77 | var explode = conf && getOption(conf, "explode");
78 | if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
79 |
80 | var ranges = cm.listSelections();
81 | for (var i = 0; i < ranges.length; i++) {
82 | if (!ranges[i].empty()) return CodeMirror.Pass;
83 | var around = charsAround(cm, ranges[i].head);
84 | if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
85 | }
86 | cm.operation(function() {
87 | var linesep = cm.lineSeparator() || "\n";
88 | cm.replaceSelection(linesep + linesep, null);
89 | cm.execCommand("goCharLeft");
90 | ranges = cm.listSelections();
91 | for (var i = 0; i < ranges.length; i++) {
92 | var line = ranges[i].head.line;
93 | cm.indentLine(line, null, true);
94 | cm.indentLine(line + 1, null, true);
95 | }
96 | });
97 | }
98 |
99 | function contractSelection(sel) {
100 | var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
101 | return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
102 | head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
103 | }
104 |
105 | function handleChar(cm, ch) {
106 | var conf = getConfig(cm);
107 | if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
108 |
109 | var pairs = getOption(conf, "pairs");
110 | var pos = pairs.indexOf(ch);
111 | if (pos == -1) return CodeMirror.Pass;
112 | var triples = getOption(conf, "triples");
113 |
114 | var identical = pairs.charAt(pos + 1) == ch;
115 | var ranges = cm.listSelections();
116 | var opening = pos % 2 == 0;
117 |
118 | var type;
119 | for (var i = 0; i < ranges.length; i++) {
120 | var range = ranges[i], cur = range.head, curType;
121 | var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
122 | if (opening && !range.empty()) {
123 | curType = "surround";
124 | } else if ((identical || !opening) && next == ch) {
125 | if (identical && stringStartsAfter(cm, cur))
126 | curType = "both";
127 | else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
128 | curType = "skipThree";
129 | else
130 | curType = "skip";
131 | } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
132 | cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) {
133 | if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
134 | curType = "addFour";
135 | } else if (identical) {
136 | var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
137 | if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
138 | else return CodeMirror.Pass;
139 | } else if (opening) {
140 | curType = "both";
141 | } else {
142 | return CodeMirror.Pass;
143 | }
144 | if (!type) type = curType;
145 | else if (type != curType) return CodeMirror.Pass;
146 | }
147 |
148 | var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
149 | var right = pos % 2 ? ch : pairs.charAt(pos + 1);
150 | cm.operation(function() {
151 | if (type == "skip") {
152 | cm.execCommand("goCharRight");
153 | } else if (type == "skipThree") {
154 | for (var i = 0; i < 3; i++)
155 | cm.execCommand("goCharRight");
156 | } else if (type == "surround") {
157 | var sels = cm.getSelections();
158 | for (var i = 0; i < sels.length; i++)
159 | sels[i] = left + sels[i] + right;
160 | cm.replaceSelections(sels, "around");
161 | sels = cm.listSelections().slice();
162 | for (var i = 0; i < sels.length; i++)
163 | sels[i] = contractSelection(sels[i]);
164 | cm.setSelections(sels);
165 | } else if (type == "both") {
166 | cm.replaceSelection(left + right, null);
167 | cm.triggerElectric(left + right);
168 | cm.execCommand("goCharLeft");
169 | } else if (type == "addFour") {
170 | cm.replaceSelection(left + left + left + left, "before");
171 | cm.execCommand("goCharRight");
172 | }
173 | });
174 | }
175 |
176 | function charsAround(cm, pos) {
177 | var str = cm.getRange(Pos(pos.line, pos.ch - 1),
178 | Pos(pos.line, pos.ch + 1));
179 | return str.length == 2 ? str : null;
180 | }
181 |
182 | function stringStartsAfter(cm, pos) {
183 | var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
184 | return /\bstring/.test(token.type) && token.start == pos.ch &&
185 | (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
186 | }
187 | });
--------------------------------------------------------------------------------
/src/js/story/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-use-before-define, no-sequences, no-undef */
2 | import el from '../utils/element';
3 | import commitDiff from '../utils/commitDiff';
4 | import { formatDate } from '../utils';
5 | import confirmPopUp from '../popups/confirmPopUp';
6 | import { DEBUG } from '../constants';
7 | import setAnnotationLink from './setAnnotationLink';
8 | import renderCommits from './renderCommits';
9 | import getTitleFromCommitMessage from './getTitleFromCommitMessage';
10 | import renderDiffs from './renderDiffs';
11 | import codeMirror from './codeMirror';
12 | import renderGraph from './renderGraph';
13 |
14 | export default function story(state, onChange) {
15 | const container = el.withFallback('.story');
16 | const git = state.git();
17 | let editor = null,
18 | editMode = false,
19 | currentlyEditingHash;
20 | const onSave = message => git.amend(currentlyEditingHash, { message });
21 | const onCancel = message => {
22 | editMode = false;
23 | editor = null;
24 | if (message === '') {
25 | git.amend(currentlyEditingHash, {
26 | message: formatDate()
27 | });
28 | }
29 | render();
30 | };
31 |
32 | if (!container.found()) return () => {};
33 |
34 | const render = () => {
35 | DEBUG && console.log('story:render');
36 | const allCommits = git.log();
37 | const commits = Object.keys(allCommits).map(hash => ({
38 | hash,
39 | message: allCommits[hash].message,
40 | position: allCommits[hash].meta ? parseInt(allCommits[hash].meta.position, 10) || null : null
41 | })).sort((a, b) => {
42 | if (a.position !== null && b.position !== null) {
43 | return a.position - b.position;
44 | } else if (a.position !== null && b.position === null) {
45 | return -1;
46 | } else if (a.position === null && b.position !== null) {
47 | return 1;
48 | }
49 | return a.hash - b.hash;
50 | });
51 | const numOfCommits = commits.length;
52 | const diffs = commitDiff(numOfCommits > 0 ? git.show().files : [], git.getAll());
53 | const renderedCommits = renderCommits(git, commits, editMode, currentlyEditingHash);
54 |
55 | container.attr('class', numOfCommits <= 1 || editMode ? 'editor-section story no-graph' : 'editor-section story');
56 | container.content(`
57 | ${ renderedCommits !== '' ? '' + renderedCommits + '
' : '' }
58 | ${ editMode ? '' : renderDiffs(git, diffs) }
59 |
60 | `).forEach(el => {
61 | if (el.attr('data-export') === 'checkoutLink') {
62 | el.onClick(() => {
63 | const hashToCheckout = el.attr('data-hash');
64 |
65 | if (diffs.length > 0) {
66 | confirmPopUp(
67 | 'Checkout',
68 | 'You are about to checkout another commit. You have an unstaged changes. Are you sure?',
69 | decision => {
70 | if (decision && allCommits[hashToCheckout]) {
71 | git.checkout(hashToCheckout, true);
72 | onChange();
73 | render();
74 | }
75 | }
76 | );
77 | } else {
78 | if (allCommits[hashToCheckout]) {
79 | git.checkout(hashToCheckout);
80 | onChange();
81 | render();
82 | }
83 | }
84 | });
85 | }
86 | if (el.attr('data-export') === 'editMessage') {
87 | el.onClick(() => {
88 | const hash = el.attr('data-hash');
89 |
90 | if (editMode && currentlyEditingHash === hash) {
91 | editMode = false;
92 | onCancel();
93 | } else {
94 | editMode = true;
95 | currentlyEditingHash = el.attr('data-hash');
96 | render();
97 | }
98 | });
99 | }
100 | if (el.attr('data-export') === 'deleteCommit') {
101 | el.onClick(() => {
102 | confirmPopUp(
103 | 'Deleting a commit',
104 | `Deleting "${el.attr('data-commit-message')}" commit. Are you sure?`,
105 | decision => {
106 | if (decision) {
107 | editMode = false;
108 | git.adios(el.attr('data-hash'));
109 | onChange();
110 | }
111 | }
112 | );
113 | });
114 | }
115 | if (el.attr('data-export') === 'publishStatus') {
116 | el.onChange(position => {
117 | const hash = el.attr('data-hash');
118 |
119 | git.amend(hash, {
120 | message: git.show(hash).message,
121 | meta: { position }
122 | });
123 | render();
124 | });
125 | }
126 | });
127 |
128 | const {
129 | editButton,
130 | addButton,
131 | discardButton,
132 | messageArea,
133 | confirmButton,
134 | injector
135 | } = container.namedExports();
136 |
137 | editButton &&
138 | editButton.onClick(() => {
139 | git.amend();
140 | render();
141 | });
142 |
143 | addButton &&
144 | addButton.onClick(() => {
145 | editMode = true;
146 | git.add();
147 | currentlyEditingHash = git.commit('');
148 | render();
149 | });
150 |
151 | discardButton &&
152 | discardButton.onClick(() => {
153 | confirmPopUp('Discard changes', 'You are about to discard your current changes. Are you sure?', decision => {
154 | if (decision) {
155 | git.discard();
156 | onChange();
157 | }
158 | });
159 | });
160 |
161 | if (messageArea) {
162 | editor = codeMirror(
163 | messageArea,
164 | state.getEditorSettings(),
165 | git.show(currentlyEditingHash).message,
166 | function onSaveInEditor(message) {
167 | confirmButton.css('opacity', '0.3');
168 | onSave(message);
169 | el.withFallback(`[data-hash="${ currentlyEditingHash }"] > .commit-message-text`)
170 | .text(getTitleFromCommitMessage(message));
171 | },
172 | function onChange() {
173 | confirmButton.css('opacity', '1');
174 | numOfCommits > 1 && renderGraph(commits, git.logAsTree());
175 | },
176 | onCancel
177 | );
178 | confirmButton.css('opacity', '0.3');
179 | confirmButton.onClick(() => {
180 | onSave(editor.getValue());
181 | editMode = false;
182 | editor = null;
183 | render();
184 | });
185 | injector.onChange(str => {
186 | setTimeout(() => {
187 | editor.focus();
188 | editor.refresh();
189 | editor.replaceSelection(str);
190 | injector.e.value = '';
191 | }, 1);
192 | });
193 | }
194 |
195 | numOfCommits > 1 && renderGraph(commits, git.logAsTree());
196 | };
197 |
198 | state.listen(event => {
199 | if (!editMode) render();
200 | });
201 |
202 | render();
203 |
204 | return function addToStory({ code, list }, otherEditor) {
205 | if (editMode && editor) {
206 | otherEditor.setCursor({ line: 0, ch: 0 });
207 | setTimeout(() => {
208 | editor.focus();
209 | editor.refresh();
210 |
211 | setAnnotationLink(editor, code, list, state.getActiveFile());
212 | }, 1);
213 | }
214 | };
215 | }
216 |
--------------------------------------------------------------------------------
/src/js/state.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define, no-undef */
2 | import gitfred from 'gitfred';
3 | import {
4 | getParam,
5 | readFromJSONFile,
6 | ensureDemoIdInPageURL,
7 | ensureUniqueFileName,
8 | jsEncode,
9 | clone
10 | } from './utils';
11 | import { IS_PROD, DEBUG } from './constants';
12 | import { DEFAULT_LAYOUT } from './layout';
13 | import API from './providers/api';
14 | import LS from './utils/localStorage';
15 |
16 | const git = gitfred();
17 | const LS_PROFILE_KEY = 'DEMOIT_PROFILE';
18 | const DEFAULT_STATE = {
19 | 'editor': {
20 | 'theme': 'light',
21 | 'statusBar': true,
22 | 'layout': DEFAULT_LAYOUT
23 | },
24 | 'dependencies': [],
25 | 'files': {
26 | 'working': [
27 | [
28 | 'code.js',
29 | {
30 | 'c': `document.querySelector('#output').innerHTML = 'Hello world';
31 |
32 | console.log('Hello world');`
33 | }
34 | ]
35 | ],
36 | 'head': null,
37 | 'i': 0,
38 | 'stage': [],
39 | 'commits': {}
40 | }
41 | };
42 |
43 | const getFirstFile = function () {
44 | const allFiles = git.getAll();
45 |
46 | if (allFiles.length === 0) {
47 | return 'untitled.js';
48 | }
49 | return git.getAll()[0][0];
50 | };
51 | const resolveActiveFile = function () {
52 | const hash = location.hash.replace(/^#/, '');
53 |
54 | if (hash !== '' && git.get(hash)) return hash;
55 | return getFirstFile();
56 | };
57 |
58 | export const FILE_CHANGED = 'FILE_CHANGED';
59 |
60 | export default async function createState(version) {
61 | let onChangeListeners = [];
62 | const onChange = event => {
63 | DEBUG && console.log('state:onChange event=' + event);
64 | onChangeListeners.forEach(c => c(event));
65 | };
66 | let profile = LS(LS_PROFILE_KEY);
67 |
68 | var state = window.state;
69 | var initialState;
70 |
71 | if (!state) {
72 | const stateFromURL = getParam('state');
73 |
74 | if (stateFromURL) {
75 | try {
76 | state = await readFromJSONFile(stateFromURL);
77 | } catch (error) {
78 | console.error(`Error reading ${ stateFromURL }`);
79 | }
80 | } else {
81 | state = DEFAULT_STATE;
82 | }
83 | }
84 |
85 | state.v = version;
86 | initialState = clone(state);
87 |
88 | git.import(state.files);
89 | git.listen(event => {
90 | if (event === git.ON_COMMIT) {
91 | DEBUG && console.log('state:git:commit event=' + event);
92 | persist('git.listen');
93 | DEBUG && console.log('state:git:checkout event=' + event);
94 | } else if (event === git.ON_CHECKOUT) {
95 | api.setActiveFileByIndex(0);
96 | persist('git.listen');
97 | }
98 | onChange(event);
99 | });
100 |
101 | let activeFile = resolveActiveFile();
102 |
103 | const persist = (reason, fork = false, done = () => {}) => {
104 | DEBUG && console.log('state:persist reason=' + reason);
105 | if (api.isForkable()) {
106 | if (!fork && !api.isDemoOwner()) { return; }
107 | let diff = DeepDiff.diff(initialState, state);
108 | let stateData;
109 |
110 | initialState = clone(state);
111 | if (fork) {
112 | diff = '';
113 | delete state.owner;
114 | stateData = state;
115 | } else if (typeof diff === 'undefined' || !diff) {
116 | // no diff and no forking so doesn't make sense to call the API
117 | return;
118 | } else {
119 | diff = jsEncode(JSON.stringify(diff));
120 | stateData = { demoId: state.demoId, owner: state.owner };
121 | }
122 | API.saveDemo(stateData, profile.token, diff).then(demoId => {
123 | if (demoId && demoId !== state.demoId) {
124 | state.demoId = demoId;
125 | state.owner = profile.id;
126 | ensureDemoIdInPageURL(demoId);
127 | }
128 | done();
129 | });
130 | }
131 | };
132 |
133 | const api = {
134 | getDemoId() {
135 | return state.demoId;
136 | },
137 | getActiveFile() {
138 | return activeFile;
139 | },
140 | getActiveFileContent() {
141 | return git.get(activeFile).c;
142 | },
143 | setActiveFile(filename) {
144 | activeFile = filename;
145 | location.hash = filename;
146 | onChange('setActiveFile');
147 | return filename;
148 | },
149 | setActiveFileByIndex(index) {
150 | const filename = git.getAll()[index][0];
151 |
152 | if (filename) {
153 | this.setActiveFile(filename);
154 | onChange(FILE_CHANGED);
155 | }
156 | },
157 | isCurrentFile(filename) {
158 | return activeFile === filename;
159 | },
160 | isDemoOwner() {
161 | return state.owner && profile && state.owner === profile.id;
162 | },
163 | getFiles() {
164 | return git.getAll();
165 | },
166 | getNumOfFiles() {
167 | return git.getAll().length;
168 | },
169 | meta(meta) {
170 | if (meta) {
171 | const { name, description, published, storyWithCode, comments } = meta;
172 |
173 | state.name = name;
174 | state.desc = description;
175 | state.published = !!published;
176 | state.storyWithCode = !!storyWithCode;
177 | state.comments = !!comments;
178 | onChange('meta');
179 | persist('meta');
180 | return null;
181 | }
182 |
183 | const m = {
184 | name: state.name,
185 | description: state.desc,
186 | published: !!state.published,
187 | storyWithCode: !!state.storyWithCode,
188 | comments: !!state.comments
189 | };
190 |
191 | if (state.demoId) m.id = state.demoId;
192 |
193 | return m;
194 | },
195 | getDependencies() {
196 | return state.dependencies;
197 | },
198 | setDependencies(dependencies) {
199 | state.dependencies = dependencies;
200 | persist('setDependencies');
201 | },
202 | getEditorSettings() {
203 | return state.editor;
204 | },
205 | editFile(filename, updates) {
206 | git.save(filename, updates);
207 | persist('editFile');
208 | },
209 | renameFile(filename, newName) {
210 | if (activeFile === filename) {
211 | this.setActiveFile(newName);
212 | }
213 | git.rename(filename, newName);
214 | persist('renameFile');
215 | },
216 | addNewFile(filename = 'untitled.js') {
217 | filename = git.get(filename) ? ensureUniqueFileName(filename) : filename;
218 | git.save(filename, { c: '' });
219 | this.setActiveFile(filename);
220 | persist('addNewFile');
221 | },
222 | deleteFile(filename) {
223 | git.del(filename);
224 | if (filename === activeFile) {
225 | this.setActiveFile(getFirstFile());
226 | }
227 | persist('deleteFile');
228 | },
229 | listen(callback) {
230 | onChangeListeners.push(callback);
231 | },
232 | removeListeners() {
233 | onChangeListeners = [];
234 | },
235 | updateThemeAndLayout(newLayout, newTheme) {
236 | if (newLayout) {
237 | state.editor.layout = newLayout;
238 | }
239 | if (newTheme) {
240 | state.editor.theme = newTheme;
241 | };
242 | persist('updateThemeAndLayout');
243 | },
244 | updateStatusBarVisibility(value) {
245 | if (state.editor.statusBar !== value) {
246 | state.editor.statusBar = value;
247 | persist('updateStatusBarVisibility');
248 | }
249 | },
250 | setEntryPoint(filename) {
251 | const newValue = !git.get(filename).en;
252 |
253 | git.saveAll({ en: false });
254 | git.save(filename, { en: newValue });
255 | persist();
256 | },
257 | dump() {
258 | return state;
259 | },
260 | // forking
261 | isForkable() {
262 | return IS_PROD && api.loggedIn();
263 | },
264 | fork() {
265 | persist('fork', true, () => onChange('fork'));
266 | },
267 | // profile methods
268 | loggedIn() {
269 | return profile !== null;
270 | },
271 | getProfile() {
272 | return profile;
273 | },
274 | getDemos() {
275 | return API.getDemos(profile.id, profile.token);
276 | },
277 | // misc
278 | version() {
279 | return state.v;
280 | },
281 | git() {
282 | return git;
283 | },
284 | export() {
285 | return state;
286 | },
287 | getStoryURL() {
288 | const meta = this.meta();
289 | let slug = 'story';
290 |
291 | if (meta && meta.name) {
292 | slug = meta.name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
293 | }
294 | return `/s/${ this.getDemoId() }/${ slug }`;
295 | }
296 | };
297 |
298 | window.__state = api;
299 |
300 | return api;
301 | }
302 |
--------------------------------------------------------------------------------