1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-06-11 20:20:50 +02:00
Files
php-web-maker/src/utils.js
Kushagra Gour 5dfa0c791e fix base path
2024-03-26 12:10:30 +05:30

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);
}
})();
}