From ed1110d74fae6ab810969134e2fb25d4ddbfe3aa Mon Sep 17 00:00:00 2001 From: David Evans Date: Mon, 20 Jan 2020 19:23:17 +0000 Subject: [PATCH] Support open-in-editor URLs [#59] --- web/lib/editor.js | 150 ++++++++++++++++++--- web/lib/editor.min.js | 2 +- web/scripts/editor.mjs | 27 ++++ web/scripts/interface/Interface.mjs | 75 ++++++++++- web/scripts/interface/URLExporter.mjs | 44 ++++-- web/scripts/interface/URLExporter_spec.mjs | 24 ++-- web/scripts/slots/HashSlotNav.mjs | 4 + web/styles/editor.css | 13 +- 8 files changed, 290 insertions(+), 49 deletions(-) diff --git a/web/lib/editor.js b/web/lib/editor.js index 2448958..586796a 100644 --- a/web/lib/editor.js +++ b/web/lib/editor.js @@ -370,6 +370,10 @@ }); } + getRawHash() { + return getHash(); + } + maxSlots() { // Capacity of localStorage is limited // So avoid allowing too many documents @@ -1176,20 +1180,34 @@ } class URLExporter { - constructor(base = '') { - this.base = base; + constructor(renderBase = '', editBase = '') { + this.renderBase = renderBase; + this.editBase = editBase; } - setBase(base) { - this.base = base; + setRenderBase(renderBase) { + this.renderBase = renderBase; } - _convertCode(code) { - return code + setEditBase(editBase) { + this.editBase = editBase; + } + + _convertCode(code, keepBlankLines = false) { + let lines = code .split('\n') - .map(encodeURIComponent) - .filter((ln) => ln !== '') - .join('/'); + .map(encodeURIComponent); + + if(keepBlankLines) { + // Always trim trailing blank lines + while(lines.length > 0 && lines[lines.length - 1] === '') { + -- lines.length; + } + } else { + lines = lines.filter((ln) => ln !== ''); + } + + return lines.join('/'); } _convertWidthHeight(width, height) { @@ -1220,14 +1238,22 @@ return ''; } - getURL(code, size = {}) { + getRenderURL(code, size = {}) { return ( - this.base + + this.renderBase + this._convertSize(size) + this._convertCode(code) + '.svg' ); } + + getEditURL(code) { + return ( + this.editBase + + '#edit:' + + this._convertCode(code, true) + ); + } } class VoidStorage { @@ -1277,6 +1303,14 @@ }); } + function setDocumentTitle(diagramTitle) { + let title = diagramTitle.trim(); + if(title.length > 20) { + title = title.substr(0, 18).trim() + '\u2026'; + } + document.title = (title ? `${title} \u2014 ` : '') + 'Sequence Diagram'; + } + class Interface { constructor({ sequenceDiagram, @@ -1313,6 +1347,7 @@ this.diagram .on('render', () => { this.updateMinSize(this.diagram.getSize()); + setDocumentTitle(this.diagram.getTitle()); this.pngDirty = true; }) .on('mouseover', (element) => this.code.markLineHover(element.ln)) @@ -1365,6 +1400,32 @@ setTimeout(() => copied.styles({'display': 'none'}), 1500); }); + const updateMode = () => { + this.renderOpts.styles({ + 'display': this.modeEdit.element.checked ? 'none' : 'block', + }); + this._refreshURL(); + }; + + this.modeRender = this.dom.el('input').attrs({ + 'checked': 'checked', + 'name': 'export-mode', + 'type': 'radio', + 'value': 'render', + }).on('change', updateMode); + + this.modeEdit = this.dom.el('input').attrs({ + 'name': 'export-mode', + 'type': 'radio', + 'value': 'edit', + }).on('change', updateMode); + + this.modeMarkdown = this.dom.el('input').attrs({ + 'name': 'export-mode', + 'type': 'radio', + 'value': 'markdown', + }).on('change', updateMode); + this.urlWidth = this.dom.el('input').attrs({ 'min': 0, 'placeholder': 'auto', @@ -1396,12 +1457,21 @@ this._refreshURL(); }); - const urlOpts = this.dom.el('div').setClass('config').add( + this.renderOpts = this.dom.el('div').add( this.dom.el('label').add('width ', this.urlWidth), ', ', this.dom.el('label').add('height ', this.urlHeight), this.dom.el('span').setClass('or').add('or'), - this.dom.el('label').add('zoom ', this.urlZoom), + this.dom.el('label').add('zoom ', this.urlZoom) + ); + + const urlOpts = this.dom.el('div').setClass('config').add( + this.dom.el('div').setClass('export-mode').add( + this.dom.el('label').add(this.modeRender, 'View'), + this.dom.el('label').add(this.modeEdit, 'Edit'), + this.dom.el('label').add(this.modeMarkdown, 'Markdown') + ), + this.renderOpts, this.urlOutput, copy, copied @@ -1414,7 +1484,10 @@ .add('Loading\u2026') ); + const ownURL = (typeof window === 'undefined') ? 'http://localhost' : window.location.href; this.renderService = new URLExporter(); + this.renderService.setEditBase(new URL('.', ownURL).href); + const relativePath = 'render/'; fetchResource(relativePath) .then((response) => response.text()) @@ -1423,9 +1496,7 @@ if(!path || path.startsWith('<')) { path = relativePath; } - this.renderService.setBase( - new URL(path, window.location.href).href - ); + this.renderService.setRenderBase(new URL(path, ownURL).href); urlBuilder.empty().add(urlOpts); this._refreshURL(); }) @@ -1440,11 +1511,27 @@ } _refreshURL() { - this.urlOutput.val(this.renderService.getURL(this.code.value(), { + const code = this.code.value(); + const viewOpts = { height: Number.parseFloat(this.urlHeight.element.value), width: Number.parseFloat(this.urlWidth.element.value), zoom: Number.parseFloat(this.urlZoom.element.value || '1'), - })); + }; + + let url = ''; + if(this.modeMarkdown.element.checked) { + const edit = this.renderService.getEditURL(code); + const view = this.renderService.getRenderURL(code, viewOpts); + const title = this.diagram.getTitle() + .replace(/[^a-zA-Z0-9 \-_'"]/g, '') + .trim(); + url = `[![${title}](${view})](${edit})`; + } else if(this.modeEdit.element.checked) { + url = this.renderService.getEditURL(code); + } else { + url = this.renderService.getRenderURL(code, viewOpts); + } + this.urlOutput.val(url); } _showURLBuilder() { @@ -2214,6 +2301,32 @@ } } + function loadHashDocument(hashNav, slotStorage) { + const editPrefix = 'edit:'; + const hash = hashNav.getRawHash(); + if(!hash.startsWith(editPrefix)) { + return; + } + + let doc = hash + .substr(editPrefix.length) + .split('/') + .map(decodeURIComponent) + .join('\n'); + + if(!doc) { + return; + } + + if(!doc.endsWith('\n')) { + doc += '\n'; + } + + const newSlot = slotStorage.nextAvailableSlot(); + slotStorage.set(newSlot, doc); + hashNav.setSlot(newSlot); + } + window.addEventListener('load', () => { const loader = window.document.getElementById('loader'); const [nav] = loader.getElementsByTagName('nav'); @@ -2235,6 +2348,7 @@ // If the slot is changed by the user, reload to force a document load window.location.reload(); }); + loadHashDocument(hashNav, slotStorage); loader.parentNode.removeChild(loader); diff --git a/web/lib/editor.min.js b/web/lib/editor.min.js index 792897a..75306bf 100644 --- a/web/lib/editor.min.js +++ b/web/lib/editor.min.js @@ -1 +1 @@ -!function(){"use strict";function e(){const e=window.location.hash;return e?e.substr(1):""}function t(e){return null===e?null:e.element?e.element:e}function i(e){return e.length>0&&"\n"!==e.charAt(e.length-1)?e+"\n":e}function s(e,t){let i=0,s=0;for(;;){const n=e.indexOf("\n",i)+1;if(ti.line||e.line===i.line&&e.ch>i.ch)}function o(e,t){const i=e.toString(),s=i.indexOf(".");return-1===s||i.length-s-1<=t?i:e.toFixed(t)}function r(e=null){return null!==e&&!Number.isNaN(e)}var l=[{code:"{Agent1} -> {Agent2}: {Message}",title:"Simple arrow (synchronous)"},{code:"{Agent1} --\x3e {Agent2}: {Message}",title:"Arrow with dotted line (response)"},{code:"{Agent1} ->> {Agent2}: {Message}",title:"Open arrow (asynchronous)"},{code:"{Agent1} -x {Agent2}: {Message}",title:"Lost message"},{code:"{Agent1} ~> {Agent2}: {Message}",title:"Wavy line"},{code:"{Agent1} -> {Agent1}: {Message}",title:"Self-connection"},{code:"{Agent1} -> ...{id}\n...{id} -> {Agent2}: {Message}",preview:"begin A, B\nA -> ...x\n...x -> B: Message",title:"Asynchronous message"},{code:"* -> {Agent1}: {Message}",title:"Found message"},{code:"{Agent1} -> {Agent2}\n& {Agent1} -> {Agent3}: {Broadcast}",title:"Broadcast message"},{code:"{Agent1} -> +{Agent2}: {Request}\n{Agent1} <-- -{Agent2}: {Response}",title:"Request/response pair"},{code:"{Agent1} -> *{Agent2}: {Request}\n{Agent1} <-- !{Agent2}: {Response}",title:"Inline agent creation / destruction"},{code:"{Agent1} -> {Agent2}: {Request}\n{Agent1} <-- {Agent2}: {Response}\nend {Agent2}",preview:"begin A\n::\nA -> B: Request\nA <-- B: Response\nend B",title:"Agent creation / destruction"},{code:'autolabel "[]