Basic editing capability when Javascript is disabled

This commit is contained in:
David Evans 2021-10-23 17:45:17 +01:00
parent 57cfe939f7
commit 4dc77897e9
9 changed files with 260 additions and 11 deletions

View File

@ -0,0 +1,60 @@
const {RequestHandler} = require('../server/RequestHandler');
const {VirtualSequenceDiagram} = require('../../lib/sequence-diagram');
const UNSAFE_HTML = /[^a-zA-Z0-9 :;.,]/g;
const escChar = (c) => `&#x${c.charCodeAt(0).toString(16).padStart(4, '0')};`;
const escapeHTML = (v) => v.replaceAll(UNSAFE_HTML, escChar);
class PreviewRequestHandler extends RequestHandler {
constructor(baseUrlPattern) {
super('GET', new RegExp(`^${baseUrlPattern}/?(.*)$`, 'i'));
this.info = `Rendering preview at ${baseUrlPattern}/`;
}
handle(req, res, {match, pickEncoding, writeEncoded}) {
this.applyCommonHeaders(req, res);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
const encoding = pickEncoding();
const params = new URLSearchParams(match[1]);
const code = params.get('c');
const encoded = code
.replaceAll(/[\r\n]+/g, '\n')
.split('\n')
.filter((ln) => ln !== '')
.map(encodeURIComponent)
.join('/');
let content = '';
try {
new VirtualSequenceDiagram().process(code);
content = [
'<!DOCTYPE html>',
'<html lang="en">',
'<head>',
'<link rel="stylesheet" href="web/styles/preview.css">',
'</head>',
'<body>',
`<img src="/render/uri/${encoded}.svg" download="diagram.svg">`,
'</body>',
'</html>',
].join('');
} catch(e) {
content = [
'<!DOCTYPE html>',
'<html lang="en">',
'<head>',
'<link rel="stylesheet" href="web/styles/preview.css">',
'</head>',
'<body>',
`<div class="error">${escapeHTML(String(e))}</div>`,
'</body>',
'</html>',
].join('');
}
writeEncoded(encoding, content);
}
}
module.exports = {PreviewRequestHandler};

View File

@ -3,6 +3,7 @@
const {Server} = require('./server/Server');
const {StaticRequestHandler} = require('./server/StaticRequestHandler');
const {RenderRequestHandler} = require('./handlers/RenderRequestHandler');
const {PreviewRequestHandler} = require('./handlers/PreviewRequestHandler');
const path = require('path');
const DEV = process.argv.includes('dev');
@ -45,8 +46,9 @@ const statics = new StaticRequestHandler('')
'connect-src \'self\'',
'font-src \'self\' data:',
'img-src \'self\' blob:',
'form-action \'none\'',
'frame-ancestors \'none\'',
'form-action \'self\'',
'frame-ancestors \'self\'',
'frame-src \'self\'',
].join('; '))
.addHeader('X-Content-Type-Options', 'nosniff')
.addHeader('X-Frame-Options', 'DENY')
@ -96,8 +98,23 @@ const render = new RenderRequestHandler('/render')
].join('; '))
.addHeader('X-Content-Type-Options', 'nosniff');
const preview = new PreviewRequestHandler('/preview')
.setCacheMaxAge(DEV ? 0 : RENDER_MAX_AGE)
.addHeader('Content-Security-Policy', [
'base-uri \'self\'',
'default-src \'none\'',
'style-src \'self\'',
'connect-src \'none\'',
'img-src \'self\'',
'form-action \'none\'',
'frame-ancestors \'self\'',
'frame-src \'self\'',
].join('; '))
.addHeader('X-Content-Type-Options', 'nosniff');
new Server()
.addHandler(render)
.addHandler(preview)
.addHandler(statics)
.listen(PORT, HOSTNAME)
.then((server) => server.printListeningInfo(process.stdout));

View File

@ -121,6 +121,7 @@ cd - > /dev/null;
EOF
sudo tee /var/www/https/index.htm <<EOF > /dev/null;
<!DOCTYPE html>
<html lang="en">
<head>
<title>$DOMAIN</title>

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
@ -9,7 +10,8 @@
connect-src 'self';
font-src 'self' data:;
img-src 'self' blob:;
form-action 'none';
frame-src 'self';
form-action 'self';
">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, minimal-ui">
<meta name="apple-mobile-web-app-capable" content="yes">
@ -78,7 +80,6 @@
<div id="loader">
<h1>Sequence Diagram Online Editor</h1>
<p class="loadmsg">Loading&hellip;</p>
<noscript><p class="noscript">This tool requires Javascript!</p></noscript>
<nav>
<a href="#" data-touch="Files">My Diagrams</a>
<a href="library.htm" target="_blank" data-touch="API">Library</a>
@ -86,5 +87,37 @@
</nav>
</div>
<noscript>
<link rel="stylesheet" href="web/styles/noscript.css">
<main>
<section class="banner">
Enable JavaScript for live rendering, templates, PNG export, and local file saving.
<nav><a href="library.htm" target="_blank">Library</a><a href="https://github.com/davidje13/SequenceDiagram" target="_blank">GitHub</a></nav>
</section>
<form target="preview" action="preview" method="GET">
<textarea name="c">
title Labyrinth
Bowie -> Goblin: You remind me of the babe
Goblin -> Bowie: What babe?
Bowie -> Goblin: The babe with the power
Goblin -> Bowie: What power?
note right of Bowie, Goblin: Most people get muddled here!
Bowie -> Goblin: "The power of voodoo"
Goblin -> Bowie: "Who-do?"
Bowie -> Goblin: You do!
Goblin -> Bowie: Do what?
Bowie -> Goblin: Remind me of the babe!
Bowie -> Audience: Sings
terminators box
</textarea>
<button type="submit">Update</button>
</form>
<iframe name="preview" src="web/resources/preview.htm"></iframe>
</main>
</noscript>
</body>
</html>

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="../styles/preview.css">
</head>
<body>
<div class="initial-action">&larr; Press "Update" to see the diagram.</div>
</body>
</html>

View File

@ -29,12 +29,6 @@ html, body {
line-height: 1.3;
}
#loader p.noscript {
position: relative;
top: -2.3em;
background: #FFFFFF;
}
#loader nav {
margin: 80px 0 0;
font-size: 0.8em;
@ -598,7 +592,7 @@ svg a:active, svg a:hover {
}
@media (prefers-color-scheme: dark) {
body, #loader p.noscript {
body {
background: #222222;
color: #EEEEEE;
}

98
web/styles/noscript.css Normal file
View File

@ -0,0 +1,98 @@
#loader {
display: none;
}
main {
display: grid;
width: 100%;
height: 100%;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr auto;
}
.banner {
grid-column: 1 / 3;
padding: 10px 150px;
background: #FFFFCC;
text-align: center;
border-bottom: 1px solid #808080;
}
nav {
position: absolute;
top: 0;
right: 0;
padding: 10px;
line-height: 1rem;
font-size: 0.9rem;
text-align: right;
}
nav a {
padding: 0 10px;
}
form {
margin: 0;
padding: 0;
position: relative;
}
textarea {
width: 30vw;
height: 100%;
resize: horizontal;
min-width: 200px;
max-width: 80vw;
padding: 10px 50px 100px 10px;
white-space: pre;
box-sizing: border-box;
border: none;
border-right: 1px solid #808080;
background: #EEEEEE;
}
button {
position: absolute;
top: 10px;
right: 10px;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
a:link, a:visited {
color: #556688;
text-decoration: underline;
}
a:hover, a:active {
color: #5577AA;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
.banner {
background: #554400;
}
textarea {
color: #FFEEDD;
background: #333333;
}
iframe {
background: #111111; /* avoid flicker when reloading */
}
a:link, a:visited {
color: #99AAEE;
}
a:hover, a:active {
color: #6699FF;
}
}

36
web/styles/preview.css Normal file
View File

@ -0,0 +1,36 @@
html, body {
margin: 0;
padding: 0;
height: 100%;
}
body {
padding: 10px;
font-family: sans-serif;
background: white;
box-sizing: border-box;
}
img {
width: 100%;
height: 100%;
}
.error {
color: #800000;
}
@media (prefers-color-scheme: dark) {
body {
background: #111111;
color: #DDDDDD;
}
img {
filter: invert(1) hue-rotate(180deg);
}
.error {
color: #FF8080;
}
}