diff --git a/editor/web/index.html b/editor/web/index.html index 5293952..e981256 100644 --- a/editor/web/index.html +++ b/editor/web/index.html @@ -396,15 +396,18 @@
- - + + +
- +
- +
@@ -430,9 +433,58 @@ - + + + + \ No newline at end of file diff --git a/editor/web/package.json b/editor/web/package.json index 8069b9f..3c031b9 100644 --- a/editor/web/package.json +++ b/editor/web/package.json @@ -10,15 +10,18 @@ "preview": "npm run build && vite preview" }, "devDependencies": { + "@types/bootstrap": "^5.2.10", + "@types/ejs": "^3.1.5", + "@types/html-minifier-terser": "^7.0.2", "@types/node": "^24.10.2", "html-minifier-terser": "^7.2.0", "prettier": "3.7.4", "typescript": "^5.9.3", - "vite": "^7.2.6" + "vite": "^7.2.6", + "vite-plugin-static-copy": "^3.1.4" }, "dependencies": { "bootstrap": "^5.3.8", - "ejs": "^3.1.10", - "vite-plugin-static-copy": "^3.1.4" + "ejs": "^3.1.10" } } diff --git a/editor/web/src/assets.d.ts b/editor/web/src/assets.d.ts new file mode 100644 index 0000000..88d404d --- /dev/null +++ b/editor/web/src/assets.d.ts @@ -0,0 +1,4 @@ +declare module '*?raw' { + const content: string; + export default content; +} diff --git a/editor/web/src/codegen/index.ts b/editor/web/src/codegen/index.ts new file mode 100644 index 0000000..0b0447f --- /dev/null +++ b/editor/web/src/codegen/index.ts @@ -0,0 +1,26 @@ +import ejs from 'ejs'; + +import jsTemplate from './js.ejs?raw'; +import jsonTemplate from './json.ejs?raw'; +import pythonTemplate from './python.ejs?raw'; + +interface CodeGen { + name: string; + generate(params: any): string; +} + +class EjsCodeGen implements CodeGen { + name: string; + private template: ejs.TemplateFunction; + constructor(name: string, templateContent: any) { + this.name = name; + this.template = ejs.compile(templateContent); + } + generate(params: any): string { + return this.template({ params }); + } +} + +export const jsCodeGen = new EjsCodeGen('NodeJS Example', jsTemplate); +export const jsonCodeGen = new EjsCodeGen('JSON', jsonTemplate); +export const pythonCodeGen = new EjsCodeGen('Python Example', pythonTemplate); diff --git a/editor/web/src/codegen/js.ejs b/editor/web/src/codegen/js.ejs new file mode 100644 index 0000000..ae0f8b3 --- /dev/null +++ b/editor/web/src/codegen/js.ejs @@ -0,0 +1,16 @@ +import express from 'express'; +import { render as render_cf_error_page } from 'cloudflare-error-page'; + +const app = express(); +const port = 3000; + +// Define a route for GET requests to the root URL +<%# TODO: format to JS-style object (key w/o parens) _%> +app.get('/', (req, res) => { + res.status(500).send(render_cf_error_page(<%=JSON.stringify(params, null, 2).replaceAll('\n', '\n ')%>)); +}); + +// Start the server and listen on the specified port +app.listen(port, () => { + console.log(`Example app listening at http://localhost:${port}`); +}); diff --git a/editor/web/src/codegen/json.ejs b/editor/web/src/codegen/json.ejs new file mode 100644 index 0000000..94a36fb --- /dev/null +++ b/editor/web/src/codegen/json.ejs @@ -0,0 +1 @@ +<%=JSON.stringify(params, null, 4)%> diff --git a/editor/web/src/codegen/python.ejs b/editor/web/src/codegen/python.ejs new file mode 100644 index 0000000..31ac4e4 --- /dev/null +++ b/editor/web/src/codegen/python.ejs @@ -0,0 +1,29 @@ +<% +// Covert the parameters to Python format object +const randomKey = Math.random() + '' +const paramsArg = JSON.stringify(params, (key, value) => { + if (typeof value === 'boolean') { + return randomKey + value.toString() + } else if (value === null) { + return randomKey + 'null' + } else { + return value + } +}, 4) + .replace(`"${randomKey}true"`, 'True') + .replace(`"${randomKey}false"`, 'False') + .replace(`"${randomKey}null"`, 'None') +_%> +from flask import Flask +from cloudflare_error_page import render as render_cf_error_page + +app = Flask(__name__) + +# Define a route for GET requests to the root URL +@app.route('/') +def index(): + # Render the error page + return render_cf_error_page(<%=paramsArg.replaceAll('\n', '\n ')%>), 500 + +if __name__ == '__main__': + app.run(debug=True, port=8080) diff --git a/editor/web/src/index.js b/editor/web/src/index.js index c87b922..6e43012 100644 --- a/editor/web/src/index.js +++ b/editor/web/src/index.js @@ -7,8 +7,11 @@ */ import ejs from 'ejs'; import templateContent from './template.ejs?raw'; + +import Modal from 'bootstrap/js/src/modal.js'; import 'bootstrap/dist/css/bootstrap.min.css'; +import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen'; let template = ejs.compile(templateContent); @@ -332,22 +335,6 @@ function createShareableLink() { $('shareLink').value = result.url; }); } -function exportJSON() { - let content = JSON.stringify(lastCfg, null, 4); - const file = new File([content], 'cloudflare-error-page-params.json', { - type: 'text/plain', - }); - const url = URL.createObjectURL(file); - - const link = document.createElement('a'); - link.href = url; - link.download = file.name; - document.body.appendChild(link); - link.click(); - - document.body.removeChild(link); - window.URL.revokeObjectURL(url); -} function resizePreviewFrame() { const iframe = $('previewFrame'); const height = iframe.contentWindow.document.body.scrollHeight + 2; @@ -387,22 +374,14 @@ $('presetSelect').addEventListener('change', (e) => { // Render / Open button handlers // $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); }); $('btnOpen').addEventListener('click', (e) => { - e.preventDefault(); openInNewTab(); }); $('btnShare').addEventListener('click', (e) => { - e.preventDefault(); createShareableLink(); }); -$('btnExport').addEventListener('click', (e) => { - e.preventDefault(); - exportJSON(); -}); - $('btnCopyLink').addEventListener('click', () => { const field = $('shareLink'); field.select(); - field.setSelectionRange(0, field.value.length); navigator.clipboard.writeText(field.value).then(() => { // No notification required unless you want one }); @@ -413,8 +392,7 @@ const inputs = document.querySelectorAll('#editorForm input, #editorForm textare inputs.forEach((inp) => { inp.addEventListener('input', () => render()); // for radio change events (error_source) - if (inp.type === 'radio') - inp.addEventListener('change', () => render()); + if (inp.type === 'radio') inp.addEventListener('change', () => render()); }); // Automatically update frame height @@ -423,3 +401,84 @@ const iframe = $('previewFrame'); observer.observe(iframe.contentWindow.document.body); // resizePreviewFrame() setInterval(resizePreviewFrame, 1000); // TODO... + +function saveFile(content, saveName) { + const file = new File([content], saveName, { + type: 'text/plain', + }); + const url = URL.createObjectURL(file); + + const link = document.createElement('a'); + link.href = url; + link.download = file.name; + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + window.URL.revokeObjectURL(url); +} +let saveAsType; + +function updateSaveAsDialog(e) { + if (e) { + const target = e.target; + saveAsType = target.dataset.type; + } else { + saveAsType = 'json'; + } + let codegen; + switch (saveAsType) { + case 'js': + codegen = jsCodeGen; + break; + case 'json': + codegen = jsonCodeGen; + break; + case 'python': + codegen = pythonCodeGen; + break; + } + const params = { ...lastCfg }; + delete params.time; + document.getElementById('saveAsDialogCode').innerHTML = codegen.generate(params); + + document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => { + const isCurrent = element.dataset.type == saveAsType; + if (isCurrent) { + element.classList.add('active'); + } else { + element.classList.remove('active'); + } + element.ariaCurrent = isCurrent; + }); +} +$('saveAsDialog').addEventListener('show.bs.modal', (e) => { + updateSaveAsDialog(); +}); +document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => { + element.addEventListener('click', updateSaveAsDialog); +}); + +$('saveAsDialogCopyBtn').addEventListener('click', (e) => { + const field = $('saveAsDialogCode'); + field.select(); + // field.setSelectionRange(0, field.value.length); + navigator.clipboard.writeText(field.value).then(() => { + // No notification required unless you want one + }); +}); +$('saveAsDialogSaveBtn').addEventListener('click', (e) => { + let saveName = ''; + switch (saveAsType) { + case 'js': + saveName = 'cf-error-page-example.js'; + break; + case 'json': + saveName = 'cf-error-page-params.json'; + break; + case 'python': + saveName = 'cf_error_page_example.py'; + break; + } + saveFile($('saveAsDialogCode').innerHTML, saveName); +}); diff --git a/editor/web/yarn.lock b/editor/web/yarn.lock index 122fcaf..4bc8f27 100644 --- a/editor/web/yarn.lock +++ b/editor/web/yarn.lock @@ -166,6 +166,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@popperjs/core@^2.9.2": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@rollup/rollup-android-arm-eabi@4.53.3": version "4.53.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb" @@ -276,11 +281,28 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe" integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ== +"@types/bootstrap@^5.2.10": + version "5.2.10" + resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.10.tgz#58506463bccc6602bc051487ad8d3a6458f94c6c" + integrity sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g== + dependencies: + "@popperjs/core" "^2.9.2" + +"@types/ejs@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.5.tgz#49d738257cc73bafe45c13cb8ff240683b4d5117" + integrity sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg== + "@types/estree@1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== +"@types/html-minifier-terser@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-7.0.2.tgz#2290fa13e6e49b6cc0ab0afa2d6cf6a66feedb48" + integrity sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA== + "@types/node@^24.10.2": version "24.10.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26"