editor.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. (function() { // eslint-disable-line
  2. 'use strict'; // eslint-disable-line
  3. /* global monaco, require */
  4. const lessonHelperScriptRE = /<script src="[^"]+threejs-lessons-helper\.js"><\/script>/;
  5. function getQuery(s) {
  6. s = s === undefined ? window.location.search : s;
  7. if (s[0] === '?' ) {
  8. s = s.substring(1);
  9. }
  10. const query = {};
  11. s.split('&').forEach(function(pair) {
  12. const parts = pair.split('=').map(decodeURIComponent);
  13. query[parts[0]] = parts[1];
  14. });
  15. return query;
  16. }
  17. function getSearch(url) {
  18. // yea I know this is not perfect but whatever
  19. const s = url.indexOf('?');
  20. return s < 0 ? {} : getQuery(url.substring(s));
  21. }
  22. const getFQUrl = (function() {
  23. const a = document.createElement('a');
  24. return function getFQUrl(url) {
  25. a.href = url;
  26. return a.href;
  27. };
  28. }());
  29. function getHTML(url, callback) {
  30. const req = new XMLHttpRequest();
  31. req.open('GET', url, true);
  32. req.addEventListener('load', function() {
  33. const success = req.status === 200 || req.status === 0;
  34. callback(success ? null : 'could not load: ' + url, req.responseText);
  35. });
  36. req.addEventListener('timeout', function() {
  37. callback('timeout get: ' + url);
  38. });
  39. req.addEventListener('error', function() {
  40. callback('error getting: ' + url);
  41. });
  42. req.send('');
  43. }
  44. function getPrefix(url) {
  45. const u = new URL(window.location.origin + url);
  46. const prefix = u.origin + dirname(u.pathname);
  47. return prefix;
  48. }
  49. function fixSourceLinks(url, source) {
  50. const srcRE = /(src=)"(.*?)"/g;
  51. const linkRE = /(href=)"(.*?)"/g;
  52. const imageSrcRE = /((?:image|img)\.src = )"(.*?)"/g;
  53. const loaderLoadRE = /(loader\.load[a-z]*\s*\(\s*)('|")(.*?)('|")/ig;
  54. const loaderArrayLoadRE = /(loader\.load[a-z]*\(\[)([\s\S]*?)(\])/ig;
  55. const loadFileRE = /(loadFile\s*\(\s*)('|")(.*?)('|")/ig;
  56. const threejsfundamentalsUrlRE = /(.*?)('|")(.*?)('|")(.*?)(\/\*\s+threejsfundamentals:\s+url\s+\*\/)/ig;
  57. const arrayLineRE = /^(\s*["|'])([\s\S]*?)(["|']*$)/;
  58. const urlPropRE = /(url:\s*)('|")(.*?)('|")/g;
  59. const prefix = getPrefix(url);
  60. function addPrefix(url) {
  61. return url.indexOf('://') < 0 && url[0] !== '?' ? (prefix + url) : url;
  62. }
  63. function makeLinkFQed(match, p1, url) {
  64. return p1 + '"' + addPrefix(url) + '"';
  65. }
  66. function makeLinkFDedQuotes(match, fn, q1, url, q2) {
  67. return fn + q1 + addPrefix(url) + q2;
  68. }
  69. function makeTaggedFDedQuotes(match, start, q1, url, q2, suffix) {
  70. return start + q1 + addPrefix(url) + q2 + suffix;
  71. }
  72. function makeArrayLinksFDed(match, prefix, arrayStr, suffix) {
  73. const lines = arrayStr.split(',').map((line) => {
  74. const m = arrayLineRE.exec(line);
  75. return m
  76. ? `${m[1]}${addPrefix(m[2])}${m[3]}`
  77. : line;
  78. });
  79. return `${prefix}${lines.join(',')}${suffix}`;
  80. }
  81. source = source.replace(srcRE, makeLinkFQed);
  82. source = source.replace(linkRE, makeLinkFQed);
  83. source = source.replace(imageSrcRE, makeLinkFQed);
  84. source = source.replace(urlPropRE, makeLinkFDedQuotes);
  85. source = source.replace(loadFileRE, makeLinkFDedQuotes);
  86. source = source.replace(loaderLoadRE, makeLinkFDedQuotes);
  87. source = source.replace(loaderArrayLoadRE, makeArrayLinksFDed);
  88. source = source.replace(threejsfundamentalsUrlRE, makeTaggedFDedQuotes);
  89. return source;
  90. }
  91. function fixCSSLinks(url, source) {
  92. const cssUrlRE = /(url\()(.*?)(\))/g;
  93. const prefix = getPrefix(url);
  94. function addPrefix(url) {
  95. return url.indexOf('://') < 0 ? (prefix + url) : url;
  96. }
  97. function makeFQ(match, prefix, url, suffix) {
  98. return `${prefix}${addPrefix(url)}${suffix}`;
  99. }
  100. source = source.replace(cssUrlRE, makeFQ);
  101. return source;
  102. }
  103. const g = {
  104. html: '',
  105. };
  106. const htmlParts = {
  107. js: {
  108. language: 'javascript',
  109. },
  110. css: {
  111. language: 'css',
  112. },
  113. html: {
  114. language: 'html',
  115. },
  116. };
  117. function forEachHTMLPart(fn) {
  118. Object.keys(htmlParts).forEach(function(name, ndx) {
  119. const info = htmlParts[name];
  120. fn(info, ndx, name);
  121. });
  122. }
  123. function getHTMLPart(re, obj, tag) {
  124. let part = '';
  125. obj.html = obj.html.replace(re, function(p0, p1) {
  126. part = p1;
  127. return tag;
  128. });
  129. return part.replace(/\s*/, '');
  130. }
  131. function parseHTML(url, html) {
  132. html = fixSourceLinks(url, html);
  133. html = html.replace(/<div class="description">[^]*?<\/div>/, '');
  134. const styleRE = /<style>([^]*?)<\/style>/i;
  135. const titleRE = /<title>([^]*?)<\/title>/i;
  136. const bodyRE = /<body>([^]*?)<\/body>/i;
  137. const inlineScriptRE = /<script>([^]*?)<\/script>/i;
  138. const externalScriptRE = /(<!--(?:(?!-->)[\s\S])*?-->\n){0,1}<script\s*src\s*=\s*"(.*?)"\s*>\s*<\/script>/ig;
  139. const dataScriptRE = /(<!--(?:(?!-->)[\s\S])*?-->\n){0,1}<script (.*?)>([^]*?)<\/script>/ig;
  140. const cssLinkRE = /<link ([^>]+?)>/g;
  141. const isCSSLinkRE = /type="text\/css"|rel="stylesheet"/;
  142. const hrefRE = /href="([^"]+)"/;
  143. const obj = { html: html };
  144. htmlParts.css.source = fixCSSLinks(url, getHTMLPart(styleRE, obj, '<style>\n${css}</style>'));
  145. htmlParts.html.source = getHTMLPart(bodyRE, obj, '<body>${html}</body>');
  146. htmlParts.js.source = getHTMLPart(inlineScriptRE, obj, '<script>${js}</script>');
  147. html = obj.html;
  148. const tm = titleRE.exec(html);
  149. if (tm) {
  150. g.title = tm[1];
  151. }
  152. let scripts = '';
  153. html = html.replace(externalScriptRE, function(p0, p1, p2) {
  154. p1 = p1 || '';
  155. scripts += '\n' + p1 + '<script src="' + p2 + '"></script>';
  156. return '';
  157. });
  158. let dataScripts = '';
  159. html = html.replace(dataScriptRE, function(p0, p1, p2, p3) {
  160. p1 = p1 || '';
  161. dataScripts += '\n' + p1 + '<script ' + p2 + '>' + p3 + '</script>';
  162. return '';
  163. });
  164. htmlParts.html.source += dataScripts;
  165. htmlParts.html.source += scripts + '\n';
  166. // add style section if there is non
  167. if (html.indexOf('${css}') < 0) {
  168. html = html.replace('</head>', '<style>\n${css}</style>\n</head>');
  169. }
  170. // add hackedparams section.
  171. // We need a way to pass parameters to a blob. Normally they'd be passed as
  172. // query params but that only works in Firefox >:(
  173. html = html.replace('</head>', '<script id="hackedparams">window.hackedParams = ${hackedParams}\n</script>\n</head>');
  174. let links = '';
  175. html = html.replace(cssLinkRE, function(p0, p1) {
  176. if (isCSSLinkRE.test(p1)) {
  177. const m = hrefRE.exec(p1);
  178. if (m) {
  179. links += `@import url("${m[1]}");\n`;
  180. }
  181. return '';
  182. } else {
  183. return p0;
  184. }
  185. });
  186. htmlParts.css.source = links + htmlParts.css.source;
  187. g.html = html;
  188. }
  189. function cantGetHTML(e) { // eslint-disable-line
  190. console.log(e); // eslint-disable-line
  191. console.log("TODO: don't run editor if can't get HTML"); // eslint-disable-line
  192. }
  193. function main() {
  194. const query = getQuery();
  195. g.url = getFQUrl(query.url);
  196. g.query = getSearch(g.url);
  197. getHTML(query.url, function(err, html) {
  198. if (err) {
  199. console.log(err); // eslint-disable-line
  200. return;
  201. }
  202. parseHTML(query.url, html);
  203. setupEditor(query.url);
  204. if (query.startPane) {
  205. const button = document.querySelector('.button-' + query.startPane);
  206. toggleSourcePane(button);
  207. }
  208. });
  209. }
  210. let blobUrl;
  211. function getSourceBlob(htmlParts, options) {
  212. options = options || {};
  213. if (blobUrl) {
  214. URL.revokeObjectURL(blobUrl);
  215. }
  216. const prefix = dirname(g.url);
  217. let source = g.html;
  218. source = source.replace('${hackedParams}', JSON.stringify(g.query));
  219. source = source.replace('${html}', htmlParts.html);
  220. source = source.replace('${css}', htmlParts.css);
  221. source = source.replace('${js}', htmlParts.js);
  222. source = source.replace('<head>', '<head>\n<script match="false">threejsLessonSettings = ' + JSON.stringify(options) + ';</script>');
  223. source = source.replace('</head>', '<script src="' + prefix + '/resources/threejs-lessons-helper.js"></script>\n</head>');
  224. const scriptNdx = source.indexOf('<script>');
  225. g.numLinesBeforeScript = (source.substring(0, scriptNdx).match(/\n/g) || []).length;
  226. const blob = new Blob([source], {type: 'text/html'});
  227. blobUrl = URL.createObjectURL(blob);
  228. return blobUrl;
  229. }
  230. function getSourceBlobFromEditor(options) {
  231. return getSourceBlob({
  232. html: htmlParts.html.editor.getValue(),
  233. css: htmlParts.css.editor.getValue(),
  234. js: htmlParts.js.editor.getValue(),
  235. }, options);
  236. }
  237. function getSourceBlobFromOrig(options) {
  238. return getSourceBlob({
  239. html: htmlParts.html.source,
  240. css: htmlParts.css.source,
  241. js: htmlParts.js.source,
  242. }, options);
  243. }
  244. function dirname(path) {
  245. const ndx = path.lastIndexOf('/');
  246. return path.substring(0, ndx + 1);
  247. }
  248. function resize() {
  249. forEachHTMLPart(function(info) {
  250. info.editor.layout();
  251. });
  252. }
  253. function addCORSSupport(js) {
  254. // not yet needed for three.js
  255. return js;
  256. }
  257. function openInCodepen() {
  258. const comment = `// ${g.title}
  259. // from ${g.url}
  260. `;
  261. const pen = {
  262. title : g.title,
  263. description : 'from: ' + g.url,
  264. tags : ['three.js', 'threejsfundamentals.org'],
  265. editors : '101',
  266. html : htmlParts.html.editor.getValue().replace(lessonHelperScriptRE, ''),
  267. css : htmlParts.css.editor.getValue(),
  268. js : comment + addCORSSupport(htmlParts.js.editor.getValue()),
  269. };
  270. const elem = document.createElement('div');
  271. elem.innerHTML = `
  272. <form method="POST" target="_blank" action="https://codepen.io/pen/define" class="hidden">'
  273. <input type="hidden" name="data">
  274. <input type="submit" />
  275. "</form>"
  276. `;
  277. elem.querySelector('input[name=data]').value = JSON.stringify(pen);
  278. window.frameElement.ownerDocument.body.appendChild(elem);
  279. elem.querySelector('form').submit();
  280. window.frameElement.ownerDocument.body.removeChild(elem);
  281. }
  282. function openInJSFiddle() {
  283. const comment = `// ${g.title}
  284. // from ${g.url}
  285. `;
  286. // const pen = {
  287. // title : g.title,
  288. // description : "from: " + g.url,
  289. // tags : ["three.js", "threejsfundamentals.org"],
  290. // editors : "101",
  291. // html : htmlParts.html.editor.getValue(),
  292. // css : htmlParts.css.editor.getValue(),
  293. // js : comment + htmlParts.js.editor.getValue(),
  294. // };
  295. const elem = document.createElement('div');
  296. elem.innerHTML = `
  297. <form method="POST" target="_black" action="https://jsfiddle.net/api/mdn/" class="hidden">
  298. <input type="hidden" name="html" />
  299. <input type="hidden" name="css" />
  300. <input type="hidden" name="js" />
  301. <input type="hidden" name="title" />
  302. <input type="hidden" name="wrap" value="b" />
  303. <input type="submit" />
  304. </form>
  305. `;
  306. elem.querySelector('input[name=html]').value = htmlParts.html.editor.getValue().replace(lessonHelperScriptRE, '');
  307. elem.querySelector('input[name=css]').value = htmlParts.css.editor.getValue();
  308. elem.querySelector('input[name=js]').value = comment + addCORSSupport(htmlParts.js.editor.getValue());
  309. elem.querySelector('input[name=title]').value = g.title;
  310. window.frameElement.ownerDocument.body.appendChild(elem);
  311. elem.querySelector('form').submit();
  312. window.frameElement.ownerDocument.body.removeChild(elem);
  313. }
  314. function setupEditor() {
  315. forEachHTMLPart(function(info, ndx, name) {
  316. info.parent = document.querySelector('.panes>.' + name);
  317. info.editor = runEditor(info.parent, info.source, info.language);
  318. info.button = document.querySelector('.button-' + name);
  319. info.button.addEventListener('click', function() {
  320. toggleSourcePane(info.button);
  321. run();
  322. });
  323. });
  324. g.fullscreen = document.querySelector('.button-fullscreen');
  325. g.fullscreen.addEventListener('click', toggleFullscreen);
  326. g.run = document.querySelector('.button-run');
  327. g.run.addEventListener('click', run);
  328. g.iframe = document.querySelector('.result>iframe');
  329. g.other = document.querySelector('.panes .other');
  330. document.querySelector('.button-codepen').addEventListener('click', openInCodepen);
  331. document.querySelector('.button-jsfiddle').addEventListener('click', openInJSFiddle);
  332. g.result = document.querySelector('.panes .result');
  333. g.resultButton = document.querySelector('.button-result');
  334. g.resultButton.addEventListener('click', function() {
  335. toggleResultPane();
  336. run();
  337. });
  338. g.result.style.display = 'none';
  339. toggleResultPane();
  340. if (window.innerWidth > 1200) {
  341. toggleSourcePane(htmlParts.js.button);
  342. }
  343. window.addEventListener('resize', resize);
  344. showOtherIfAllPanesOff();
  345. document.querySelector('.other .loading').style.display = 'none';
  346. resize();
  347. run({glDebug: false});
  348. }
  349. function toggleFullscreen() {
  350. try {
  351. toggleIFrameFullscreen(window);
  352. resize();
  353. run();
  354. } catch (e) {
  355. console.error(e); // eslint-disable-line
  356. }
  357. }
  358. function run(options) {
  359. g.setPosition = false;
  360. const url = getSourceBlobFromEditor(options);
  361. g.iframe.src = url;
  362. }
  363. function addClass(elem, className) {
  364. const parts = elem.className.split(' ');
  365. if (parts.indexOf(className) < 0) {
  366. elem.className = elem.className + ' ' + className;
  367. }
  368. }
  369. function removeClass(elem, className) {
  370. const parts = elem.className.split(' ');
  371. const numParts = parts.length;
  372. for (;;) {
  373. const ndx = parts.indexOf(className);
  374. if (ndx < 0) {
  375. break;
  376. }
  377. parts.splice(ndx, 1);
  378. }
  379. if (parts.length !== numParts) {
  380. elem.className = parts.join(' ');
  381. return true;
  382. }
  383. return false;
  384. }
  385. function toggleClass(elem, className) {
  386. if (removeClass(elem, className)) {
  387. return false;
  388. } else {
  389. addClass(elem, className);
  390. return true;
  391. }
  392. }
  393. function toggleIFrameFullscreen(childWindow) {
  394. const frame = childWindow.frameElement;
  395. if (frame) {
  396. const isFullScreen = toggleClass(frame, 'fullscreen');
  397. frame.ownerDocument.body.style.overflow = isFullScreen ? 'hidden' : '';
  398. }
  399. }
  400. function addRemoveClass(elem, className, add) {
  401. if (add) {
  402. addClass(elem, className);
  403. } else {
  404. removeClass(elem, className);
  405. }
  406. }
  407. function toggleSourcePane(pressedButton) {
  408. forEachHTMLPart(function(info) {
  409. const pressed = pressedButton === info.button;
  410. if (pressed && !info.showing) {
  411. addClass(info.button, 'show');
  412. info.parent.style.display = 'block';
  413. info.showing = true;
  414. } else {
  415. removeClass(info.button, 'show');
  416. info.parent.style.display = 'none';
  417. info.showing = false;
  418. }
  419. });
  420. showOtherIfAllPanesOff();
  421. resize();
  422. }
  423. function showingResultPane() {
  424. return g.result.style.display !== 'none';
  425. }
  426. function toggleResultPane() {
  427. const showing = showingResultPane();
  428. g.result.style.display = showing ? 'none' : 'block';
  429. addRemoveClass(g.resultButton, 'show', !showing);
  430. showOtherIfAllPanesOff();
  431. resize();
  432. }
  433. function showOtherIfAllPanesOff() {
  434. let paneOn = showingResultPane();
  435. forEachHTMLPart(function(info) {
  436. paneOn = paneOn || info.showing;
  437. });
  438. g.other.style.display = paneOn ? 'none' : 'block';
  439. }
  440. function getActualLineNumberAndMoveTo(lineNo, colNo) {
  441. const actualLineNo = lineNo - g.numLinesBeforeScript;
  442. if (!g.setPosition) {
  443. // Only set the first position
  444. g.setPosition = true;
  445. htmlParts.js.editor.setPosition({
  446. lineNumber: actualLineNo,
  447. column: colNo,
  448. });
  449. htmlParts.js.editor.revealLineInCenterIfOutsideViewport(actualLineNo);
  450. htmlParts.js.editor.focus();
  451. }
  452. return actualLineNo;
  453. }
  454. window.getActualLineNumberAndMoveTo = getActualLineNumberAndMoveTo;
  455. function runEditor(parent, source, language) {
  456. return monaco.editor.create(parent, {
  457. value: source,
  458. language: language,
  459. //lineNumbers: false,
  460. theme: 'vs-dark',
  461. disableTranslate3d: true,
  462. // model: null,
  463. scrollBeyondLastLine: false,
  464. minimap: { enabled: false },
  465. });
  466. }
  467. function runAsBlob() {
  468. const query = getQuery();
  469. g.url = getFQUrl(query.url);
  470. g.query = getSearch(g.url);
  471. getHTML(query.url, function(err, html) {
  472. if (err) {
  473. console.log(err); // eslint-disable-line
  474. return;
  475. }
  476. parseHTML(query.url, html);
  477. window.location.href = getSourceBlobFromOrig();
  478. });
  479. }
  480. function start() {
  481. const parentQuery = getQuery(window.parent.location.search);
  482. const isSmallish = window.navigator.userAgent.match(/Android|iPhone|iPod|Windows Phone/i);
  483. const isEdge = window.navigator.userAgent.match(/Edge/i);
  484. if (isEdge || isSmallish || parentQuery.editor === 'false') {
  485. runAsBlob();
  486. // var url = query.url;
  487. // window.location.href = url;
  488. } else {
  489. require.config({ paths: { 'vs': '/monaco-editor/min/vs' }});
  490. require(['vs/editor/editor.main'], main);
  491. }
  492. }
  493. start();
  494. }());