mirror of
https://github.com/chinchang/web-maker.git
synced 2025-06-11 20:20:50 +02:00
664 lines
16 KiB
JavaScript
664 lines
16 KiB
JavaScript
import { trackEvent } from './analytics';
|
|
import { computeHtml, computeCss, computeJs } from './computes';
|
|
import { modes, HtmlModes, CssModes, JsModes } from './codeModes';
|
|
import { deferred } from './deferred';
|
|
import { getExtensionFromFileName } from './fileUtils';
|
|
import confetti from 'canvas-confetti';
|
|
const esprima = require('esprima');
|
|
|
|
window.DEBUG = document.cookie.indexOf('wmdebug') > -1;
|
|
window.$ = document.querySelector.bind(document);
|
|
|
|
window.chrome = window.chrome || {};
|
|
window.chrome.i18n = {
|
|
getMessage: () => {}
|
|
};
|
|
|
|
window.$all = selector => [...document.querySelectorAll(selector)];
|
|
window.IS_EXTENSION = !!window.chrome.extension;
|
|
|
|
/* eslint-disable no-process-env */
|
|
export const BASE_PATH =
|
|
window.chrome.extension ||
|
|
window.DEBUG ||
|
|
process.env.NODE_ENV === 'development'
|
|
? '/'
|
|
: '/create';
|
|
/* eslint-enable no-process-env */
|
|
|
|
var alphaNum = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
|
|
/**
|
|
* The following 2 functions are supposed to find the next/previous sibling until the
|
|
* passed `selector` is matched. But for now it actually finds the next/previous
|
|
* element of `this` element in the list of `selector` matched element inside `this`'s
|
|
* parent.
|
|
* @param Selector that should match for next siblings
|
|
* @return element Next element that mathes `selector`
|
|
*/
|
|
Node.prototype.nextUntil = function (selector) {
|
|
const siblings = Array.from(this.parentNode.querySelectorAll(selector));
|
|
const index = siblings.indexOf(this);
|
|
return siblings[index + 1];
|
|
};
|
|
|
|
/*
|
|
* @param Selector that should match for next siblings
|
|
* @return element Next element that mathes `selector`
|
|
*/
|
|
Node.prototype.previousUntil = function (selector) {
|
|
const siblings = Array.from(this.parentNode.querySelectorAll(selector));
|
|
const index = siblings.indexOf(this);
|
|
return siblings[index - 1];
|
|
};
|
|
|
|
// Safari doesn't have this!
|
|
window.requestIdleCallback =
|
|
window.requestIdleCallback ||
|
|
function (fn) {
|
|
setTimeout(fn, 10);
|
|
};
|
|
|
|
// https://github.com/substack/semver-compare/blob/master/index.js
|
|
export function semverCompare(a, b) {
|
|
var pa = a.split('.');
|
|
var pb = b.split('.');
|
|
for (var i = 0; i < 3; i++) {
|
|
var na = Number(pa[i]);
|
|
var nb = Number(pb[i]);
|
|
if (na > nb) {
|
|
return 1;
|
|
}
|
|
if (nb > na) {
|
|
return -1;
|
|
}
|
|
if (!isNaN(na) && isNaN(nb)) {
|
|
return 1;
|
|
}
|
|
if (isNaN(na) && !isNaN(nb)) {
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
export function generateRandomId(len) {
|
|
var length = len || 10;
|
|
var id = '';
|
|
for (var i = length; i--; ) {
|
|
id += alphaNum[~~(Math.random() * alphaNum.length)];
|
|
}
|
|
return id;
|
|
}
|
|
|
|
export function onButtonClick(btn, listener) {
|
|
btn.addEventListener('click', function buttonClickListener(e) {
|
|
listener(e);
|
|
return false;
|
|
});
|
|
}
|
|
|
|
export function log() {
|
|
if (window.DEBUG) {
|
|
const err = new Error();
|
|
console.log(
|
|
parseInt(Date.now().toString().substr(4), 10),
|
|
...arguments,
|
|
err.stack.split('\n')[2].replace(/\(.*\)/, '')
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds timed limit on the loops found in the passed code.
|
|
* Contributed by Ariya Hidayat!
|
|
* @param code {string} Code to be protected from infinite loops.
|
|
*/
|
|
export function addInfiniteLoopProtection(code, { timeout }) {
|
|
var loopId = 1;
|
|
var patches = [];
|
|
var varPrefix = '_wmloopvar';
|
|
var varStr = 'var %d = Date.now();\n';
|
|
var checkStr = `\nif (Date.now() - %d > ${timeout}) { window.top.previewException(new Error("Infinite loop")); break;}\n`;
|
|
|
|
esprima.parse(
|
|
code,
|
|
{
|
|
tolerant: true,
|
|
range: true,
|
|
jsx: true
|
|
},
|
|
function (node) {
|
|
switch (node.type) {
|
|
case 'DoWhileStatement':
|
|
case 'ForStatement':
|
|
case 'ForInStatement':
|
|
case 'ForOfStatement':
|
|
case 'WhileStatement':
|
|
var start = 1 + node.body.range[0];
|
|
var end = node.body.range[1];
|
|
var prolog = checkStr.replace('%d', varPrefix + loopId);
|
|
var epilog = '';
|
|
|
|
if (node.body.type !== 'BlockStatement') {
|
|
// `while(1) doThat()` becomes `while(1) {doThat()}`
|
|
prolog = '{' + prolog;
|
|
epilog = '}';
|
|
--start;
|
|
}
|
|
|
|
patches.push({
|
|
pos: start,
|
|
str: prolog
|
|
});
|
|
patches.push({
|
|
pos: end,
|
|
str: epilog
|
|
});
|
|
patches.push({
|
|
pos: node.range[0],
|
|
str: varStr.replace('%d', varPrefix + loopId)
|
|
});
|
|
++loopId;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
);
|
|
|
|
/* eslint-disable no-param-reassign */
|
|
patches
|
|
.sort(function (a, b) {
|
|
return b.pos - a.pos;
|
|
})
|
|
.forEach(function (patch) {
|
|
code = code.slice(0, patch.pos) + patch.str + code.slice(patch.pos);
|
|
});
|
|
|
|
/* eslint-disable no-param-reassign */
|
|
return code;
|
|
}
|
|
|
|
export function getHumanDate(timestamp) {
|
|
var d = new Date(timestamp);
|
|
var retVal =
|
|
d.getDate() +
|
|
' ' +
|
|
[
|
|
'January',
|
|
'February',
|
|
'March',
|
|
'April',
|
|
'May',
|
|
'June',
|
|
'July',
|
|
'August',
|
|
'September',
|
|
'October',
|
|
'November',
|
|
'December'
|
|
][d.getMonth()] +
|
|
' ' +
|
|
d.getFullYear();
|
|
return retVal;
|
|
}
|
|
|
|
/**
|
|
* Convert any date-ish string/obj to human readable form -> Jul 02, 2021
|
|
* @param {string?object} date date to be formatted
|
|
* @returns string
|
|
*/
|
|
export function getHumanReadableDate(
|
|
date,
|
|
{ showTime = true, utc = false } = {}
|
|
) {
|
|
if (!date) return '';
|
|
let d = typeof date.toDate === 'function' ? date.toDate() : new Date(date);
|
|
if (utc) {
|
|
d = new Date(
|
|
Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours())
|
|
);
|
|
}
|
|
|
|
let options = {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
};
|
|
if (showTime) {
|
|
options = {
|
|
...options,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: true
|
|
};
|
|
}
|
|
const dateTimeString = d.toLocaleString(false, options);
|
|
return dateTimeString;
|
|
}
|
|
|
|
// create a one-time event
|
|
export function once(node, type, callback) {
|
|
// create event
|
|
node.addEventListener(type, function (e) {
|
|
// remove event
|
|
e.target.removeEventListener(type, arguments.callee);
|
|
// call handler
|
|
return callback(e);
|
|
});
|
|
}
|
|
|
|
export function downloadFile(fileName, blob) {
|
|
function downloadWithAnchor() {
|
|
var a = document.createElement('a');
|
|
a.href = window.URL.createObjectURL(blob);
|
|
a.download = fileName;
|
|
a.style.display = 'none';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
}
|
|
|
|
// HACK: because chrome.downloads isn't working on optional permissions
|
|
// anymore.
|
|
downloadWithAnchor();
|
|
|
|
/* if (false && window.IS_EXTENSION) {
|
|
chrome.downloads.download({
|
|
url: window.URL.createObjectURL(blob),
|
|
filename: fileName,
|
|
saveAs: true
|
|
},
|
|
() => {
|
|
// If there was an error, just download the file using ANCHOR method.
|
|
if (chrome.runtime.lastError) {
|
|
downloadWithAnchor();
|
|
}
|
|
}
|
|
);
|
|
} else {
|
|
downloadWithAnchor();
|
|
} */
|
|
}
|
|
|
|
export function writeFile(name, blob, cb) {
|
|
var fileWritten = false;
|
|
|
|
function getErrorHandler(type) {
|
|
return function () {
|
|
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://webmaker.app/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')
|
|
);
|
|
}
|
|
|
|
export function loadJS(src) {
|
|
var d = deferred();
|
|
var ref = window.document.getElementsByTagName('script')[0];
|
|
var script = window.document.createElement('script');
|
|
script.src = src;
|
|
script.async = true;
|
|
ref.parentNode.insertBefore(script, ref);
|
|
script.onload = function () {
|
|
d.resolve();
|
|
};
|
|
return d.promise;
|
|
}
|
|
|
|
export function loadCss({ url, id }) {
|
|
var d = deferred();
|
|
var style = window.document.createElement('link');
|
|
style.setAttribute('href', url);
|
|
style.setAttribute('rel', 'stylesheet');
|
|
if (id) {
|
|
style.setAttribute('id', id);
|
|
}
|
|
document.head.appendChild(style);
|
|
style.onload = function () {
|
|
d.resolve();
|
|
};
|
|
return d.promise;
|
|
}
|
|
|
|
/* eslint-disable max-params */
|
|
export function getCompleteHtml(html, css, js, item, isForExport) {
|
|
/* eslint-enable max-params */
|
|
|
|
if (!item) {
|
|
return '';
|
|
}
|
|
var externalJs = '',
|
|
externalCss = '';
|
|
if (item.externalLibs) {
|
|
externalJs = item.externalLibs.js.split('\n').reduce(function (
|
|
scripts,
|
|
url
|
|
) {
|
|
return scripts + (url ? '\n<script src="' + url + '"></script>' : '');
|
|
}, '');
|
|
externalCss = item.externalLibs.css.split('\n').reduce(function (
|
|
links,
|
|
url
|
|
) {
|
|
return (
|
|
links +
|
|
(url ? '\n<link rel="stylesheet" href="' + url + '"></link>' : '')
|
|
);
|
|
}, '');
|
|
}
|
|
var contents =
|
|
'<!DOCTYPE html>\n' +
|
|
'<html>\n<head>\n' +
|
|
'<meta charset="UTF-8" />\n' +
|
|
externalCss +
|
|
'\n' +
|
|
'<style id="webmakerstyle">\n' +
|
|
css +
|
|
'\n</style>\n' +
|
|
'</head>\n' +
|
|
'<body>\n' +
|
|
html +
|
|
'\n' +
|
|
externalJs +
|
|
'\n';
|
|
|
|
if (!isForExport) {
|
|
contents +=
|
|
'<script src="' +
|
|
(chrome.extension
|
|
? chrome.extension.getURL('lib/screenlog.js')
|
|
: `${location.origin}${
|
|
window.DEBUG ? '' : BASE_PATH
|
|
}/lib/screenlog.js`) +
|
|
'"></script>';
|
|
}
|
|
|
|
if (item.jsMode === JsModes.ES6) {
|
|
contents +=
|
|
'<script src="' +
|
|
(chrome.extension
|
|
? chrome.extension.getURL('lib/transpilers/babel-polyfill.min.js')
|
|
: `${location.origin}${BASE_PATH}/lib/transpilers/babel-polyfill.min.js`) +
|
|
'"></script>';
|
|
}
|
|
|
|
if (typeof js === 'string') {
|
|
contents += js ? '<script>\n' + js + '\n//# sourceURL=userscript.js' : '';
|
|
} else {
|
|
var origin = chrome.i18n.getMessage()
|
|
? `chrome-extension://${chrome.i18n.getMessage('@@extension_id')}`
|
|
: `${location.origin}`;
|
|
contents +=
|
|
'<script src="' + `filesystem:${origin}/temporary/script.js` + '">';
|
|
}
|
|
contents += '\n</script>\n</body>\n</html>';
|
|
|
|
return contents;
|
|
}
|
|
|
|
export function saveAsHtml(item) {
|
|
var htmlPromise = computeHtml(item.html, item.htmlMode);
|
|
var cssPromise = computeCss(item.css, item.cssMode);
|
|
var jsPromise = computeJs(item.js, item.jsMode, false);
|
|
Promise.all([htmlPromise, cssPromise, jsPromise]).then(result => {
|
|
var html = result[0].code,
|
|
css = result[1].code,
|
|
js = result[2].code;
|
|
|
|
var fileContent = getCompleteHtml(html, css, js, item, true);
|
|
|
|
var d = new Date();
|
|
var fileName = [
|
|
'web-maker',
|
|
d.getFullYear(),
|
|
d.getMonth() + 1,
|
|
d.getDate(),
|
|
d.getHours(),
|
|
d.getMinutes(),
|
|
d.getSeconds()
|
|
].join('-');
|
|
|
|
if (item.title) {
|
|
fileName = item.title;
|
|
}
|
|
fileName += '.html';
|
|
|
|
var blob = new Blob([fileContent], {
|
|
type: 'text/html;charset=UTF-8'
|
|
});
|
|
downloadFile(fileName, blob);
|
|
|
|
trackEvent('fn', 'saveFileComplete');
|
|
});
|
|
}
|
|
|
|
export function handleDownloadsPermission() {
|
|
var d = deferred();
|
|
if (!window.IS_EXTENSION) {
|
|
d.resolve();
|
|
return d.promise;
|
|
}
|
|
chrome.permissions.contains(
|
|
{
|
|
permissions: ['downloads']
|
|
},
|
|
function (result) {
|
|
if (result) {
|
|
d.resolve();
|
|
} else {
|
|
chrome.permissions.request(
|
|
{
|
|
permissions: ['downloads']
|
|
},
|
|
function (granted) {
|
|
if (granted) {
|
|
trackEvent('fn', 'downloadsPermGiven');
|
|
d.resolve();
|
|
} else {
|
|
d.reject();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
);
|
|
return d.promise;
|
|
}
|
|
|
|
/**
|
|
* Return the filename from a passed url.
|
|
* http://a.com/path/file.png -> file.png
|
|
*/
|
|
export function getFilenameFromUrl(url) {
|
|
if (!url) {
|
|
return '';
|
|
}
|
|
return url.match(/\/([^/]*)$/)[1];
|
|
}
|
|
|
|
export function prettify({ file, content, type }) {
|
|
const d = deferred();
|
|
if (file) {
|
|
type = getExtensionFromFileName(file.name);
|
|
content = file.content;
|
|
}
|
|
const worker = new Worker(
|
|
chrome.extension
|
|
? chrome.extension.getURL('lib/prettier-worker.js')
|
|
: `${BASE_PATH !== '/' ? BASE_PATH : ''}/lib/prettier-worker.js`
|
|
);
|
|
worker.postMessage({ content, type });
|
|
worker.addEventListener('message', e => {
|
|
d.resolve(e.data);
|
|
worker.terminate();
|
|
});
|
|
return d.promise;
|
|
}
|
|
|
|
/**
|
|
* Loaded the code comiler based on the mode selected
|
|
*/
|
|
export function handleModeRequirements(mode) {
|
|
const baseTranspilerPath = 'lib/transpilers';
|
|
// Exit if already loaded
|
|
var d = deferred();
|
|
if (modes[mode].hasLoaded) {
|
|
d.resolve();
|
|
return d.promise;
|
|
}
|
|
|
|
function setLoadedFlag() {
|
|
modes[mode].hasLoaded = true;
|
|
d.resolve();
|
|
}
|
|
|
|
if (mode === HtmlModes.JADE) {
|
|
loadJS(`${baseTranspilerPath}/jade.js`).then(setLoadedFlag);
|
|
} else if (mode === HtmlModes.MARKDOWN) {
|
|
loadJS(`${baseTranspilerPath}/marked.js`).then(setLoadedFlag);
|
|
} else if (mode === CssModes.LESS) {
|
|
loadJS(`${baseTranspilerPath}/less.min.js`).then(setLoadedFlag);
|
|
} else if (mode === CssModes.SCSS || mode === CssModes.SASS) {
|
|
loadJS(`${baseTranspilerPath}/sass.js`).then(function () {
|
|
window.sass = new Sass(`${baseTranspilerPath}/sass.worker.js`);
|
|
setLoadedFlag();
|
|
});
|
|
} else if (mode === CssModes.STYLUS) {
|
|
loadJS(`${baseTranspilerPath}/stylus.min.js`).then(setLoadedFlag);
|
|
} else if (mode === CssModes.ACSS) {
|
|
loadJS(`${baseTranspilerPath}/atomizer.browser.js`).then(setLoadedFlag);
|
|
} else if (mode === JsModes.COFFEESCRIPT) {
|
|
loadJS(`${baseTranspilerPath}/coffee-script.js`).then(setLoadedFlag);
|
|
} else if (mode === JsModes.ES6) {
|
|
loadJS(`${baseTranspilerPath}/babel.min.js`).then(setLoadedFlag);
|
|
} else if (mode === JsModes.TS) {
|
|
loadJS(`${baseTranspilerPath}/typescript.js`).then(setLoadedFlag);
|
|
} else {
|
|
d.resolve();
|
|
}
|
|
|
|
return d.promise;
|
|
}
|
|
|
|
export function sanitizeSplitSizes(sizes) {
|
|
// console.log('got', sizes);
|
|
let hasAllNumbers = !sizes.some(size => !Number(size));
|
|
if (hasAllNumbers) return sizes;
|
|
|
|
// Get just the perentage values from expressions like calc(93.34% - 3px)
|
|
const newSizes = sizes.map(size => {
|
|
if (typeof size.match !== 'function') return size;
|
|
const match = size.match(/([\d.]*)%/);
|
|
if (match) return parseInt(match[1], 10);
|
|
|
|
return size;
|
|
});
|
|
// console.log('percents ', newSizes);
|
|
|
|
// Do we still have any non-number?
|
|
hasAllNumbers = !newSizes.some(size => !Number(size));
|
|
// console.log('hasAllnumbers? ', hasAllNumbers);
|
|
|
|
// Make the sum 100
|
|
if (hasAllNumbers) {
|
|
const sum = newSizes.reduce((sum, val) => sum + val, 0);
|
|
// console.log('sum ', sum);
|
|
newSizes[newSizes.length - 1] += 100 - sum;
|
|
}
|
|
|
|
return newSizes;
|
|
}
|
|
|
|
if (window.IS_EXTENSION) {
|
|
document.body.classList.add('is-extension');
|
|
} else {
|
|
document.body.classList.add('is-app');
|
|
}
|
|
export async function copyToClipboard(text) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
} catch (err) {
|
|
console.error('Failed to copy text: ', err);
|
|
}
|
|
}
|
|
|
|
export function showConfetti(time = 4) {
|
|
var end = Date.now() + time * 1000;
|
|
|
|
(function frame() {
|
|
confetti({
|
|
particleCount: 1,
|
|
startVelocity: 0,
|
|
ticks: 100,
|
|
origin: {
|
|
x: Math.random(),
|
|
// since they fall down, start a bit higher than random
|
|
y: Math.random() - 0.2
|
|
},
|
|
colors: [
|
|
[
|
|
'#26ccff',
|
|
'#a25afd',
|
|
'#ff5e7e',
|
|
'#88ff5a',
|
|
'#fcff42',
|
|
'#ffa62d',
|
|
'#ff36ff'
|
|
][~~(Math.random() * 7)]
|
|
]
|
|
});
|
|
|
|
if (Date.now() < end) {
|
|
requestAnimationFrame(frame);
|
|
}
|
|
})();
|
|
}
|