diff --git a/webmaker/.gitignore b/webmaker/.gitignore new file mode 100644 index 0000000..3fb8754 --- /dev/null +++ b/webmaker/.gitignore @@ -0,0 +1,6 @@ +node_modules +/build +/*.log +*.lock + +package-lock.json \ No newline at end of file diff --git a/webmaker/README.md b/webmaker/README.md new file mode 100644 index 0000000..d245064 --- /dev/null +++ b/webmaker/README.md @@ -0,0 +1,22 @@ +# webmaker + +## CLI Commands + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:8080 +npm run dev + +# build for production with minification +npm run build + +# test the production build locally +npm run serve + +# run tests with jest and preact-render-spy +npm run test +``` + +For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md). diff --git a/webmaker/package.json b/webmaker/package.json new file mode 100644 index 0000000..5102a6b --- /dev/null +++ b/webmaker/package.json @@ -0,0 +1,59 @@ +{ + "private": true, + "name": "webmaker", + "version": "0.0.0", + "license": "MIT", + "scripts": { + "start": "if-env NODE_ENV=production && npm run -s serve || npm run -s dev", + "build": "preact build", + "serve": "preact build && preact serve", + "dev": "preact watch", + "lint": "eslint src", + "test": "jest ./tests" + }, + "eslintConfig": { + "extends": "eslint-config-synacor" + }, + "eslintIgnore": [ + "build/*" + ], + "devDependencies": { + "eslint": "^4.9.0", + "eslint-config-synacor": "^2.0.2", + "identity-obj-proxy": "^3.0.0", + "if-env": "^1.0.0", + "jest": "^21.2.1", + "preact-cli": "^2.1.0", + "preact-render-spy": "^1.2.1" + }, + "dependencies": { + "codemirror": "^5.37.0", + "preact": "^8.2.6", + "preact-compat": "^3.17.0", + "preact-router": "^2.5.7" + }, + "jest": { + "verbose": true, + "setupFiles": [ + "/src/tests/__mocks__/browserMocks.js" + ], + "testURL": "http://localhost:8080", + "moduleFileExtensions": [ + "js", + "jsx" + ], + "moduleDirectories": [ + "node_modules" + ], + "moduleNameMapper": { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/tests/__mocks__/fileMock.js", + "\\.(css|less|scss)$": "identity-obj-proxy", + "^./style$": "identity-obj-proxy", + "^preact$": "/node_modules/preact/dist/preact.min.js", + "^react$": "preact-compat", + "^react-dom$": "preact-compat", + "^create-react-class$": "preact-compat/lib/create-react-class", + "^react-addons-css-transition-group$": "preact-css-transition-group" + } + } +} diff --git a/webmaker/src/.babelrc b/webmaker/src/.babelrc new file mode 100644 index 0000000..ba8581b --- /dev/null +++ b/webmaker/src/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + ["preact-cli/babel", { "modules": "commonjs" }] + ] +} \ No newline at end of file diff --git a/webmaker/src/assets/favicon.ico b/webmaker/src/assets/favicon.ico new file mode 100644 index 0000000..0741914 Binary files /dev/null and b/webmaker/src/assets/favicon.ico differ diff --git a/webmaker/src/assets/icons/android-chrome-192x192.png b/webmaker/src/assets/icons/android-chrome-192x192.png new file mode 100644 index 0000000..93ebe2e Binary files /dev/null and b/webmaker/src/assets/icons/android-chrome-192x192.png differ diff --git a/webmaker/src/assets/icons/android-chrome-512x512.png b/webmaker/src/assets/icons/android-chrome-512x512.png new file mode 100644 index 0000000..52d1623 Binary files /dev/null and b/webmaker/src/assets/icons/android-chrome-512x512.png differ diff --git a/webmaker/src/assets/icons/apple-touch-icon.png b/webmaker/src/assets/icons/apple-touch-icon.png new file mode 100644 index 0000000..254e4bb Binary files /dev/null and b/webmaker/src/assets/icons/apple-touch-icon.png differ diff --git a/webmaker/src/assets/icons/favicon-16x16.png b/webmaker/src/assets/icons/favicon-16x16.png new file mode 100644 index 0000000..e81177d Binary files /dev/null and b/webmaker/src/assets/icons/favicon-16x16.png differ diff --git a/webmaker/src/assets/icons/favicon-32x32.png b/webmaker/src/assets/icons/favicon-32x32.png new file mode 100644 index 0000000..40e9b5b Binary files /dev/null and b/webmaker/src/assets/icons/favicon-32x32.png differ diff --git a/webmaker/src/assets/icons/mstile-150x150.png b/webmaker/src/assets/icons/mstile-150x150.png new file mode 100644 index 0000000..9cfb889 Binary files /dev/null and b/webmaker/src/assets/icons/mstile-150x150.png differ diff --git a/webmaker/src/codeModes.js b/webmaker/src/codeModes.js new file mode 100644 index 0000000..08cfba1 --- /dev/null +++ b/webmaker/src/codeModes.js @@ -0,0 +1,19 @@ +export const HtmlModes = { + HTML: 'html', + MARKDOWN: 'markdown', + JADE: 'jade' // unsafe eval is put in manifest for this file +}; +export const CssModes = { + CSS: 'css', + SCSS: 'scss', + SASS: 'sass', + LESS: 'less', + STYLUS: 'stylus', + ACSS: 'acss' +}; +export const JsModes = { + JS: 'js', + ES6: 'es6', + COFFEESCRIPT: 'coffee', + TS: 'typescript' +}; diff --git a/webmaker/src/components/ContentWrap.jsx b/webmaker/src/components/ContentWrap.jsx new file mode 100644 index 0000000..626ef33 --- /dev/null +++ b/webmaker/src/components/ContentWrap.jsx @@ -0,0 +1,485 @@ +import { h, Component } from 'preact'; +import UserCodeMirror from './UserCodeMirror.jsx'; +import { computeHtml, computeCss, computeJs } from '../computes'; +import { HtmlModes, CssModes, JsModes } from '../codeModes'; + +const BASE_PATH = chrome.extension || window.DEBUG ? '/' : '/app'; + +export default class ContentWrap extends Component { + constructor(props) { + super(props); + this.updateTimer = null; + this.updateDelay = 500; + this.htmlMode = HtmlModes.HTML; + this.jsMode = HtmlModes.HTML; + this.cssMode = CssModes.CSS; + this.jsMode = JsModes.JS; + this.prefs = {}; + this.codeInPreview = { html: null, css: null, js: null }; + this.cmCodes = { html: '', css: '', js: '' }; + } + getInitialState() { + return {}; + } + + onHtmlCodeChange(editor, change) { + this.cmCodes.html = editor.getValue(); + this.onCodeChange(editor, change); + } + onCssCodeChange(editor, change) { + this.cmCodes.css = editor.getValue(); + this.onCodeChange(editor, change); + } + onJsCodeChange(editor, change) { + this.cmCodes.js = editor.getValue(); + this.onCodeChange(editor, change); + } + onCodeChange(editor, change) { + clearTimeout(this.updateTimer); + + this.updateTimer = setTimeout(() => { + // This is done so that multiple simultaneous setValue don't trigger too many preview refreshes + // and in turn too many file writes on a single file (eg. preview.html). + if (change.origin !== 'setValue') { + // Specifically checking for false so that the condition doesn't get true even + // on absent key - possible when the setting key hasn't been fetched yet. + if (this.prefs.autoPreview !== false) { + this.setPreviewContent(); + } + + /* saveBtn.classList.add('is-marked'); + this.unsavedEditCount += 1; + if ( + this.unsavedEditCount % this.unsavedEditWarningCount === 0 && + this.unsavedEditCount >= this.unsavedEditWarningCount + ) { + saveBtn.classList.add('animated'); + saveBtn.classList.add('wobble'); + saveBtn.addEventListener('animationend', () => { + saveBtn.classList.remove('animated'); + saveBtn.classList.remove('wobble'); + }); + } */ + + // Track when people actually are working. + // trackEvent.previewCount = (trackEvent.previewCount || 0) + 1; + // if (trackEvent.previewCount === 4) { + // trackEvent('fn', 'usingPreview'); + // } + } + }, this.updateDelay); + } + clearConsole() {} + + /* eslint max-params: ["error", 4] */ + getCompleteHtml(html, css, js, isForExport) { + /* var externalJs = externalJsTextarea.value + .split('\n') + .reduce(function(scripts, url) { + return scripts + (url ? '\n' : ''); + }, ''); + var externalCss = externalCssTextarea.value + .split('\n') + .reduce(function(links, url) { + return ( + links + + (url ? '\n' : '') + ); + }, ''); */ + var contents = + '\n' + + '\n\n' + + '\n' + + // externalCss + + '\n' + + '\n' + + '\n' + + '\n' + + html + + '\n' + + // externalJs + + '\n'; + + if (!isForExport) { + contents += + ''; + } + + if (this.jsMode === JsModes.ES6) { + contents += + ''; + } + + if (typeof js === 'string') { + contents += '\n\n'; + + return contents; + } + + writeFile(name, blob, cb) { + var fileWritten = false; + function getErrorHandler(type) { + return function() { + utils.log(arguments); + trackEvent('fn', 'error', type); + // When there are too many write errors, show a message. + writeFile.errorCount = (writeFile.errorCount || 0) + 1; + if (writeFile.errorCount === 4) { + setTimeout(function() { + alert( + "Oops! Seems like your preview isn't updating. It's recommended to switch to the web app: https://webmakerapp.com/app/.\n\n If you still want to get the extension working, please try the following steps until it fixes:\n - Refresh Web Maker\n - Restart browser\n - Update browser\n - Reinstall Web Maker (don't forget to export all your creations from saved items pane (click the OPEN button) before reinstalling)\n\nIf nothing works, please tweet out to @webmakerApp." + ); + trackEvent('ui', 'writeFileMessageSeen'); + }, 1000); + } + }; + } + + // utils.log('writing file ', name); + window.webkitRequestFileSystem( + window.TEMPORARY, + 1024 * 1024 * 5, + function(fs) { + fs.root.getFile( + name, + { create: true }, + function(fileEntry) { + fileEntry.createWriter(fileWriter => { + function onWriteComplete() { + if (fileWritten) { + // utils.log('file written ', name); + return cb(); + } + fileWritten = true; + // Set the write pointer to starting of file + fileWriter.seek(0); + fileWriter.write(blob); + return false; + } + fileWriter.onwriteend = onWriteComplete; + // Empty the file contents + fileWriter.truncate(0); + // utils.log('truncating file ', name); + }, getErrorHandler('createWriterFail')); + }, + getErrorHandler('getFileFail') + ); + }, + getErrorHandler('webkitRequestFileSystemFail') + ); + } + + createPreviewFile(html, css, js) { + console.log(999); + const shouldInlineJs = + !window.webkitRequestFileSystem || !window.IS_EXTENSION; + var contents = this.getCompleteHtml(html, css, shouldInlineJs ? js : null); + var blob = new Blob([contents], { type: 'text/plain;charset=UTF-8' }); + var blobjs = new Blob([js], { type: 'text/plain;charset=UTF-8' }); + + // Track if people have written code. + // if (!trackEvent.hasTrackedCode && (html || css || js)) { + // trackEvent('fn', 'hasCode'); + // trackEvent.hasTrackedCode = true; + // } + + if (shouldInlineJs) { + if (this.detachedWindow) { + utils.log('✉️ Sending message to detached window'); + scope.detachedWindow.postMessage({ contents }, '*'); + } else { + this.frame.src = this.frame.src; + setTimeout(() => { + this.frame.contentDocument.open(); + this.frame.contentDocument.write(contents); + this.frame.contentDocument.close(); + }, 10); + } + } else { + // we need to store user script in external JS file to prevent inline-script + // CSP from affecting it. + writeFile('script.js', blobjs, function() { + writeFile('preview.html', blob, function() { + var origin = chrome.i18n.getMessage() + ? `chrome-extension://${chrome.i18n.getMessage('@@extension_id')}` + : `${location.origin}`; + var src = `filesystem:${origin}/temporary/preview.html`; + if (scope.detachedWindow) { + scope.detachedWindow.postMessage(src, '*'); + } else { + frame.src = src; + } + }); + }); + } + } + + /** + * Generates the preview from the current code. + * @param {boolean} isForced Should refresh everything without any check or not + * @param {boolean} isManual Is this a manual preview request from user? + */ + setPreviewContent(isForced, isManual) { + if (!this.prefs.autoPreview && !isManual) { + // return; + } + + if (!this.prefs.preserveConsoleLogs) { + this.clearConsole(); + } + + var currentCode = { + html: this.cmCodes.html, + css: this.cmCodes.css, + js: this.cmCodes.js + }; + // utils.log('🔎 setPreviewContent', isForced); + const targetFrame = this.detachedWindow + ? this.detachedWindow.document.querySelector('iframe') + : this.frame; + + // If just CSS was changed (and everything shudn't be empty), + // change the styles inside the iframe. + if ( + !isForced && + currentCode.html === this.codeInPreview.html && + currentCode.js === this.codeInPreview.js + ) { + computeCss(currentCode.css, this.cssMode).then(function(css) { + if (targetFrame.contentDocument.querySelector('#webmakerstyle')) { + targetFrame.contentDocument.querySelector( + '#webmakerstyle' + ).textContent = css; + } + }); + } else { + var htmlPromise = computeHtml(currentCode.html, this.htmlMode); + var cssPromise = computeCss(currentCode.css, this.cssMode); + var jsPromise = computeJs(currentCode.js, this.jsMode); + Promise.all([htmlPromise, cssPromise, jsPromise]).then(result => { + this.createPreviewFile(result[0], result[1], result[2]); + }); + } + + this.codeInPreview.html = currentCode.html; + this.codeInPreview.css = currentCode.css; + this.codeInPreview.js = currentCode.js; + } + + render() { + return ( +
+ +
+