frontend 2.0: initial version
- auto refresh function - light and dark theme - monaco editor (vscode) with "apex" as syntax highlighting language * apex seems to work quite fine (better than no highlighting) * future possibility: own plantuml language syntax support * future possibility: autocomplete (to much work but maybe partial) - implemented example for `!theme ...` - implemented example for `<&icon>` * future possibility: code validation - implemented example for `@start...` and `@end...`: * should be the first or last command * should be of the some type (e.g. `@startyaml` and @endyaml) * should be used exactly once per document/diagram - editor and preview is splitable into two windows like the "Extract window" functionality on (plantuml.com)[https://www.plantuml.com/plantuml] - multi index / multi paging diagram support - diagram can be displayed/rended as PNG, SVG, ASCII Art or PDF - Ctrl+s download the PlantUML Code as code file (diagram.puml) - Ctrl+, opens the settings and Esc closes the settings
19
.editorconfig
Normal file
@ -0,0 +1,19 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.java]
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.puml]
|
||||
insert_final_newline = false
|
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
8
.vscode/settings.json
vendored
@ -2,21 +2,23 @@
|
||||
"java.configuration.updateBuildConfiguration": "automatic",
|
||||
"cSpell.words": [
|
||||
"Arnaud",
|
||||
"buildx",
|
||||
"ditaa",
|
||||
"endditaa",
|
||||
"enduml",
|
||||
"epstext",
|
||||
"etag",
|
||||
"ghaction",
|
||||
"Lalloni",
|
||||
"monaco",
|
||||
"plantuml",
|
||||
"Roques",
|
||||
"servlet",
|
||||
"servlets",
|
||||
"startditaa",
|
||||
"startuml",
|
||||
"utxt",
|
||||
"ghaction",
|
||||
"buildx"
|
||||
"undock",
|
||||
"utxt"
|
||||
],
|
||||
"cSpell.allowCompoundWords": true
|
||||
}
|
34
pom.jdk8.xml
@ -56,7 +56,7 @@
|
||||
<plantuml.version>1.2023.6</plantuml.version>
|
||||
<!-- Please keep the jetty version identical with the docker image -->
|
||||
<jetty.version>11.0.7</jetty.version>
|
||||
<codemirror.version>5.63.0</codemirror.version>
|
||||
<monaco-editor.version>0.36.1</monaco-editor.version>
|
||||
|
||||
<!-- dependencies -->
|
||||
<jstl.version>1.2</jstl.version>
|
||||
@ -71,7 +71,9 @@
|
||||
|
||||
<!-- Testing -->
|
||||
<junit.version>4.13.2</junit.version>
|
||||
<htmlunit.version>2.53.0</htmlunit.version>
|
||||
<selenium.version>4.8.3</selenium.version>
|
||||
<selenium-webdrivermanager.version>5.3.2</selenium-webdrivermanager.version>
|
||||
<commons-io.version>2.11.0</commons-io.version>
|
||||
<jetty-server.version>${jetty.version}</jetty-server.version>
|
||||
|
||||
<!-- build plugin management -->
|
||||
@ -106,8 +108,8 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
<artifactId>codemirror</artifactId>
|
||||
<version>${codemirror.version}</version>
|
||||
<artifactId>monaco-editor</artifactId>
|
||||
<version>${monaco-editor.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
@ -175,9 +177,21 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.htmlunit</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<version>${htmlunit.version}</version>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>${selenium.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.bonigarcia</groupId>
|
||||
<artifactId>webdrivermanager</artifactId>
|
||||
<version>${selenium-webdrivermanager.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -365,9 +379,9 @@
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
<artifactId>codemirror</artifactId>
|
||||
<version>${codemirror.version}</version>
|
||||
<includes>**/lib/*.js,**/lib/*.css</includes>
|
||||
<artifactId>monaco-editor</artifactId>
|
||||
<version>${monaco-editor.version}</version>
|
||||
<includes>**/min/vs/loader.js,**/min/vs/**/*,**/min-maps/vs/**/*</includes>
|
||||
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
|
34
pom.xml
@ -56,7 +56,7 @@
|
||||
<plantuml.version>1.2023.6</plantuml.version>
|
||||
<!-- Please keep the jetty version identical with the docker image -->
|
||||
<jetty.version>11.0.7</jetty.version>
|
||||
<codemirror.version>5.63.0</codemirror.version>
|
||||
<monaco-editor.version>0.36.1</monaco-editor.version>
|
||||
|
||||
<!-- dependencies -->
|
||||
<jstl.version>1.2</jstl.version>
|
||||
@ -71,7 +71,9 @@
|
||||
|
||||
<!-- Testing -->
|
||||
<junit.version>4.13.2</junit.version>
|
||||
<htmlunit.version>2.53.0</htmlunit.version>
|
||||
<selenium.version>4.8.3</selenium.version>
|
||||
<selenium-webdrivermanager.version>5.3.2</selenium-webdrivermanager.version>
|
||||
<commons-io.version>2.11.0</commons-io.version>
|
||||
<jetty-server.version>${jetty.version}</jetty-server.version>
|
||||
|
||||
<!-- build plugin management -->
|
||||
@ -106,8 +108,8 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
<artifactId>codemirror</artifactId>
|
||||
<version>${codemirror.version}</version>
|
||||
<artifactId>monaco-editor</artifactId>
|
||||
<version>${monaco-editor.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
@ -153,9 +155,21 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.htmlunit</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<version>${htmlunit.version}</version>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>${selenium.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.bonigarcia</groupId>
|
||||
<artifactId>webdrivermanager</artifactId>
|
||||
<version>${selenium-webdrivermanager.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -346,9 +360,9 @@
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
<artifactId>codemirror</artifactId>
|
||||
<version>${codemirror.version}</version>
|
||||
<includes>**/lib/*.js,**/lib/*.css</includes>
|
||||
<artifactId>monaco-editor</artifactId>
|
||||
<version>${monaco-editor.version}</version>
|
||||
<includes>**/min/vs/loader.js,**/min/vs/**/*,**/min-maps/vs/**/*</includes>
|
||||
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
|
@ -36,6 +36,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import net.sourceforge.plantuml.OptionFlags;
|
||||
import net.sourceforge.plantuml.api.PlantumlUtils;
|
||||
import net.sourceforge.plantuml.code.NoPlantumlCompressionException;
|
||||
import net.sourceforge.plantuml.png.MetadataTag;
|
||||
import net.sourceforge.plantuml.servlet.utility.Configuration;
|
||||
import net.sourceforge.plantuml.servlet.utility.UmlExtractor;
|
||||
@ -134,8 +135,15 @@ public class PlantUmlServlet extends AsciiCoderServlet {
|
||||
final int idx = UrlDataExtractor.getIndex(request.getRequestURI());
|
||||
|
||||
// forward to index.jsp
|
||||
final String path;
|
||||
final String view = request.getParameter("view");
|
||||
if (view != null && view.equalsIgnoreCase("previewer")) {
|
||||
path = "/previewer.jsp";
|
||||
} else {
|
||||
path = "/index.jsp";
|
||||
}
|
||||
prepareRequestForDispatch(request, text, idx);
|
||||
final RequestDispatcher dispatcher = request.getRequestDispatcher("/index.jsp");
|
||||
final RequestDispatcher dispatcher = request.getRequestDispatcher(path);
|
||||
dispatcher.forward(request, response);
|
||||
}
|
||||
|
||||
@ -183,6 +191,10 @@ public class PlantUmlServlet extends AsciiCoderServlet {
|
||||
if (text != null && !text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
} catch (NoPlantumlCompressionException e) {
|
||||
// no textual diagram source available from Url
|
||||
// ignore and try 2. method (metadata) below
|
||||
// do not spam output console
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@ -229,26 +241,16 @@ public class PlantUmlServlet extends AsciiCoderServlet {
|
||||
*/
|
||||
private void prepareRequestForDispatch(HttpServletRequest request, String text, int idx) throws IOException {
|
||||
final String encoded = getTranscoder().encode(text);
|
||||
final String index = (idx < 0) ? "" : idx + "/";
|
||||
// diagram sources
|
||||
request.setAttribute("encoded", encoded);
|
||||
request.setAttribute("decoded", text);
|
||||
request.setAttribute("index", idx);
|
||||
request.setAttribute("index", (idx < 0) ? "" : idx);
|
||||
// properties
|
||||
request.setAttribute("showSocialButtons", Configuration.get("SHOW_SOCIAL_BUTTONS"));
|
||||
request.setAttribute("showGithubRibbon", Configuration.get("SHOW_GITHUB_RIBBON"));
|
||||
// image URLs
|
||||
final boolean hasImg = !text.isEmpty();
|
||||
request.setAttribute("hasImg", hasImg);
|
||||
request.setAttribute("imgurl", "png/" + index + encoded);
|
||||
request.setAttribute("svgurl", "svg/" + index + encoded);
|
||||
request.setAttribute("pdfurl", "pdf/" + index + encoded);
|
||||
request.setAttribute("txturl", "txt/" + index + encoded);
|
||||
request.setAttribute("mapurl", "map/" + index + encoded);
|
||||
// map for diagram source if necessary
|
||||
final boolean hasMap = PlantumlUtils.hasCMapData(text);
|
||||
request.setAttribute("hasMap", hasMap);
|
||||
String map = "";
|
||||
if (hasMap) {
|
||||
if (PlantumlUtils.hasCMapData(text)) {
|
||||
try {
|
||||
map = UmlExtractor.extractMap(text);
|
||||
} catch (Exception e) {
|
||||
|
@ -0,0 +1,174 @@
|
||||
/* ========================================================================
|
||||
* PlantUML : a free UML diagram generator
|
||||
* ========================================================================
|
||||
*
|
||||
* Project Info: https://plantuml.com
|
||||
*
|
||||
* This file is part of PlantUML.
|
||||
*
|
||||
* PlantUML is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* PlantUML distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
|
||||
* USA.
|
||||
*/
|
||||
package net.sourceforge.plantuml.servlet;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import net.sourceforge.plantuml.theme.ThemeUtils;
|
||||
import net.sourceforge.plantuml.openiconic.data.DummyIcon;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
* Small PlantUML frontend or UI helper.
|
||||
*/
|
||||
@SuppressWarnings("SERIAL")
|
||||
public class PlantUmlUIHelperServlet extends HttpServlet {
|
||||
|
||||
private interface HelperConsumer {
|
||||
void accept(HttpServletRequest request, HttpServletResponse response) throws IOException;
|
||||
}
|
||||
|
||||
private final Map<String, HelperConsumer> helpers = new HashMap<>();
|
||||
private String svgIconsSpriteCache = null;
|
||||
|
||||
public PlantUmlUIHelperServlet() {
|
||||
// add all supported request items/helper methods
|
||||
helpers.put("icons.svg", this::sendIconsSprite);
|
||||
helpers.put("icons", this::sendIcons);
|
||||
helpers.put("themes", this::sendThemes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
request.setCharacterEncoding("UTF-8");
|
||||
|
||||
final String requestItem = request.getParameter("request");
|
||||
final HelperConsumer requestHelper = this.helpers.get(requestItem);
|
||||
String errorMsg = null;
|
||||
if (requestItem == null) {
|
||||
errorMsg = "Request item not set.";
|
||||
} else if (requestHelper == null) {
|
||||
errorMsg = "Unknown requested item: " + requestItem;
|
||||
}
|
||||
if (errorMsg != null) {
|
||||
response.addHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setContentType("text/plain;charset=UTF-8");
|
||||
response.getWriter().write(errorMsg);
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
requestHelper.accept(request, response);
|
||||
}
|
||||
|
||||
private String toJson(List<String> list) {
|
||||
return "[" + list.stream()
|
||||
.map(item -> "\"" + item.replace("\"", "\\\"") + "\"")
|
||||
.collect(Collectors.joining(",")) + "]";
|
||||
}
|
||||
|
||||
private void sendJson(HttpServletResponse response, String json) throws IOException {
|
||||
response.addHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(json);
|
||||
}
|
||||
|
||||
private List<String> getIcons() throws IOException {
|
||||
InputStream in = DummyIcon.class.getResourceAsStream("all.txt");
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
|
||||
return br.lines().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendIconsSprite(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
if (svgIconsSpriteCache == null) {
|
||||
// NOTE: all icons has the following svg tag attributes: width="8" height="8" viewBox="0 0 8 8"
|
||||
List<String> iconNames = getIcons();
|
||||
StringBuilder sprite = new StringBuilder();
|
||||
sprite.append("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"8\" height=\"8\" viewBox=\"0 0 8 8\">\n");
|
||||
sprite.append("<defs>\n");
|
||||
sprite.append(" <style><![CDATA[\n");
|
||||
sprite.append(" .sprite { display: none; }\n");
|
||||
sprite.append(" .sprite:target { display: inline; }\n");
|
||||
sprite.append(" ]]></style>\n");
|
||||
sprite.append("</defs>\n");
|
||||
for (String name : iconNames) {
|
||||
try (InputStream in = DummyIcon.class.getResourceAsStream(name + ".svg")) {
|
||||
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
Document doc = db.parse(in);
|
||||
Writer out = new StringWriter();
|
||||
out.write("<g class=\"sprite\" id=\"" + name + "\">");
|
||||
|
||||
Transformer tf = TransformerFactory.newInstance().newTransformer();
|
||||
tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||
tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||
tf.setOutputProperty(OutputKeys.INDENT, "no");
|
||||
NodeList svgInnerNodes = doc.getElementsByTagName("svg").item(0).getChildNodes();
|
||||
for (int index = 0; index < svgInnerNodes.getLength(); index++) {
|
||||
tf.transform(new DOMSource(svgInnerNodes.item(index)), new StreamResult(out));
|
||||
}
|
||||
|
||||
out.write("</g>");
|
||||
sprite.append(out.toString() + "\n");
|
||||
} catch (ParserConfigurationException | SAXException | TransformerException ex) {
|
||||
// skip icons which can not be parsed/read
|
||||
Logger logger = Logger.getLogger("com.plantuml");
|
||||
logger.log(Level.WARNING, "SVG icon '{0}' could not be parsed. Skip!", name);
|
||||
}
|
||||
}
|
||||
sprite.append("</svg>\n");
|
||||
svgIconsSpriteCache = sprite.toString();
|
||||
}
|
||||
response.addHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setContentType("image/svg+xml;charset=UTF-8");
|
||||
response.getWriter().write(svgIconsSpriteCache);
|
||||
}
|
||||
|
||||
private void sendIcons(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
sendJson(response, toJson(getIcons()));
|
||||
}
|
||||
|
||||
private void sendThemes(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
sendJson(response, toJson(ThemeUtils.getAllThemeNames()));
|
||||
}
|
||||
}
|
@ -60,11 +60,11 @@
|
||||
<servlet-class>org.eclipse.jetty.jsp.JettyJspServlet</servlet-class>
|
||||
<init-param>
|
||||
<param-name>compilerSourceVM</param-name>
|
||||
<param-value>1.7</param-value>
|
||||
<param-value>1.8</param-value>
|
||||
</init-param>
|
||||
<init-param>
|
||||
<param-name>compilerTargetVM</param-name>
|
||||
<param-value>1.7</param-value>
|
||||
<param-value>1.8</param-value>
|
||||
</init-param>
|
||||
</servlet>
|
||||
|
||||
@ -202,14 +202,23 @@
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>asciicoder</servlet-name>
|
||||
<servlet-name>asciicoderservlet</servlet-name>
|
||||
<servlet-class>net.sourceforge.plantuml.servlet.AsciiCoderServlet</servlet-class>
|
||||
</servlet>
|
||||
<servlet-mapping>
|
||||
<servlet-name>asciicoder</servlet-name>
|
||||
<servlet-name>asciicoderservlet</servlet-name>
|
||||
<url-pattern>/coder/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>plantumluihelperservlet</servlet-name>
|
||||
<servlet-class>net.sourceforge.plantuml.servlet.PlantUmlUIHelperServlet</servlet-class>
|
||||
</servlet>
|
||||
<servlet-mapping>
|
||||
<servlet-name>plantumluihelperservlet</servlet-name>
|
||||
<url-pattern>/ui-helper/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
|
||||
<!-- ========================================================== -->
|
||||
<!-- Error Handler -->
|
||||
|
1
src/main/webapp/assets/copy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M16 16v4c0 1.152-.848 2-2 2H4c-1.152 0-2-.848-2-2V10c0-1.152.848-2 2-2h4V4c0-1.152.848-2 2-2h10c1.152 0 2 .848 2 2v10c0 1.152-.848 2-2 2h-4zm-2 0h-4c-1.152 0-2-.848-2-2v-4H4v10h10v-4zM10 4v10h10V4H10z" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 301 B |
1
src/main/webapp/assets/dock.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M384 224v184a40 40 0 01-40 40H104a40 40 0 01-40-40V168a40 40 0 0140-40h167.48" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M216 184v112h112m-104-8L440 72"/></svg>
|
After Width: | Height: | Size: 384 B |
1
src/main/webapp/assets/file-types/ascii.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg class="bi bi-filetype-ascii" fill="currentColor" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M14 4.5V11h-1V4.5h-2A1.5 1.5 0 019.5 3V1H4a1 1 0 00-1 1v9H2V2a2 2 0 012-2h5.5zM2.404 14.903l-.313 1.028h-.8l1.342-3.999h.926l1.335 4h-.84l-.314-1.03H2.404zm1.178-.59l-.49-1.616h-.034l-.49 1.617h1.014zm1.782.977a1.178 1.178 0 01-.111-.449h.764a.58.58 0 00.255.384c.07.049.154.087.25.114.095.028.2.041.319.041.164 0 .3-.023.413-.07a.558.558 0 00.255-.193.507.507 0 00.085-.29.387.387 0 00-.153-.326c-.101-.08-.256-.144-.463-.193l-.618-.143a1.72 1.72 0 01-.54-.214 1.002 1.002 0 01-.35-.367 1.068 1.068 0 01-.123-.524c0-.244.063-.457.19-.639.127-.181.303-.322.527-.422.225-.1.484-.149.777-.149.304 0 .564.05.779.152.217.102.384.239.5.41.12.17.186.359.2.566h-.75a.56.56 0 00-.12-.258.623.623 0 00-.246-.181.923.923 0 00-.37-.068c-.216 0-.387.05-.512.152a.472.472 0 00-.184.384c0 .121.047.22.143.3a.97.97 0 00.404.175l.62.143c.218.05.407.12.567.211.16.09.285.21.375.358.09.148.135.335.135.56 0 .247-.063.466-.188.656-.133.196-.32.348-.54.439-.233.105-.52.158-.857.158a2.191 2.191 0 01-.665-.09 1.404 1.404 0 01-.478-.252 1.131 1.131 0 01-.29-.375zm4.383-2.246a1.732 1.732 0 00-.103.633v.495c0 .246.035.455.103.627a.834.834 0 00.299.393c.142.09.308.136.477.13a.872.872 0 00.402-.087.699.699 0 00.272-.248.8.8 0 00.117-.364h.765v.076c-.01.241-.088.475-.226.674-.136.194-.32.345-.55.454a1.81 1.81 0 01-.785.164c-.36 0-.665-.072-.915-.216a1.424 1.424 0 01-.57-.627c-.13-.272-.194-.597-.194-.976v-.498c0-.38.065-.705.196-.978.13-.274.32-.485.57-.633.253-.15.557-.223.913-.223.218 0 .42.032.606.097.187.062.35.153.49.272.283.241.452.591.465.964v.073h-.765a.85.85 0 00-.12-.38.7.7 0 00-.272-.261.802.802 0 00-.4-.097.814.814 0 00-.473.138.868.868 0 00-.302.398zm3.628-1.106v4h-.79v-4h.79zm1.337.005v3.999h-.791v-4h.79z" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
1
src/main/webapp/assets/file-types/map.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg class="bi bi-filetype-map" fill="currentColor" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M14 4.5V14a2 2 0 01-2 2v-1a1 1 0 001-1V4.5h-2A1.5 1.5 0 019.5 3V1H4a1 1 0 00-1 1v9H2V2a2 2 0 012-2h5.5zM.706 15.849v-2.66h.038l.952 2.16h.516l.946-2.16h.038v2.66h.715V11.85h-.8l-1.14 2.596h-.026L.805 11.85H0v3.999zm7.31-3.999h1.6c.289 0 .533.06.732.179.201.117.355.276.46.477.106.201.158.427.158.677 0 .25-.053.476-.16.677-.106.199-.26.357-.464.474a1.46 1.46 0 01-.732.173h-.803v1.342h-.79V11.85zm2.06 1.714a.795.795 0 00.085-.381c0-.226-.062-.4-.185-.521-.123-.122-.294-.182-.513-.182h-.659v1.406h.66a.794.794 0 00.374-.082.574.574 0 00.238-.24zm-5.12 2.306l.313-1.028h1.336l.314 1.028h.84l-1.336-3.999h-.925l-1.329 3.96m1.79-3.195l.488 1.617H5.433l.49-1.617z" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 808 B |
1
src/main/webapp/assets/file-types/pdf.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg class="bi bi-filetype-pdf" fill="currentColor" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M14 4.5V14a2 2 0 01-2 2h-1v-1h1a1 1 0 001-1V4.5h-2A1.5 1.5 0 019.5 3V1H4a1 1 0 00-1 1v9H2V2a2 2 0 012-2h5.5L14 4.5zM1.6 11.85H0v3.999h.791v-1.342h.803c.287 0 .531-.057.732-.173.203-.117.358-.275.463-.474a1.42 1.42 0 00.161-.677c0-.25-.053-.476-.158-.677a1.176 1.176 0 00-.46-.477c-.2-.12-.443-.179-.732-.179zm.545 1.333a.795.795 0 01-.085.38.574.574 0 01-.238.241.794.794 0 01-.375.082H.788V12.48h.66c.218 0 .389.06.512.181.123.122.185.296.185.522zm1.217-1.333v3.999h1.46c.401 0 .734-.08.998-.237a1.45 1.45 0 00.595-.689c.13-.3.196-.662.196-1.084 0-.42-.065-.778-.196-1.075a1.426 1.426 0 00-.589-.68c-.264-.156-.599-.234-1.005-.234H3.362zm.791.645h.563c.248 0 .45.05.609.152a.89.89 0 01.354.454c.079.201.118.452.118.753a2.3 2.3 0 01-.068.592 1.14 1.14 0 01-.196.422.8.8 0 01-.334.252 1.298 1.298 0 01-.483.082h-.563v-2.707zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638H7.896z" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
src/main/webapp/assets/file-types/png.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg class="bi bi-filetype-png" fill="currentColor" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M14 4.5V14a2 2 0 01-2 2v-1a1 1 0 001-1V4.5h-2A1.5 1.5 0 019.5 3V1H4a1 1 0 00-1 1v9H2V2a2 2 0 012-2h5.5L14 4.5zm-3.76 8.132c.076.153.123.317.14.492h-.776a.797.797 0 00-.097-.249.689.689 0 00-.17-.19.707.707 0 00-.237-.126.96.96 0 00-.299-.044c-.285 0-.506.1-.665.302-.156.201-.234.484-.234.85v.498c0 .234.032.439.097.615a.881.881 0 00.304.413.87.87 0 00.519.146.967.967 0 00.457-.096.67.67 0 00.272-.264c.06-.11.091-.23.091-.363v-.255H8.82v-.59h1.576v.798c0 .193-.032.377-.097.55a1.29 1.29 0 01-.293.458 1.37 1.37 0 01-.495.313c-.197.074-.43.111-.697.111a1.98 1.98 0 01-.753-.132 1.447 1.447 0 01-.533-.377 1.58 1.58 0 01-.32-.58 2.482 2.482 0 01-.105-.745v-.506c0-.362.067-.678.2-.95.134-.271.328-.482.582-.633.256-.152.565-.228.926-.228.238 0 .45.033.636.1.187.066.348.158.48.275.133.117.238.253.314.407zm-8.64-.706H0v4h.791v-1.343h.803c.287 0 .531-.057.732-.172.203-.118.358-.276.463-.475a1.42 1.42 0 00.161-.677c0-.25-.053-.475-.158-.677a1.176 1.176 0 00-.46-.477c-.2-.12-.443-.179-.732-.179zm.545 1.333a.795.795 0 01-.085.381.574.574 0 01-.238.24.794.794 0 01-.375.082H.788v-1.406h.66c.218 0 .389.06.512.182.123.12.185.295.185.521zm1.964 2.666V13.25h.032l1.761 2.675h.656v-3.999h-.75v2.66h-.032l-1.752-2.66h-.662v4h.747z" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
src/main/webapp/assets/file-types/svg.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg class="bi bi-filetype-svg" fill="currentColor" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M14 4.5V14a2 2 0 01-2 2v-1a1 1 0 001-1V4.5h-2A1.5 1.5 0 019.5 3V1H4a1 1 0 00-1 1v9H2V2a2 2 0 012-2h5.5L14 4.5zM0 14.841a1.13 1.13 0 00.401.823c.13.108.288.192.478.252.19.061.411.091.665.091.338 0 .624-.053.858-.158.237-.105.416-.252.54-.44a1.17 1.17 0 00.187-.656c0-.224-.045-.41-.135-.56a1 1 0 00-.375-.357 2.027 2.027 0 00-.565-.21l-.621-.144a.97.97 0 01-.405-.176.37.37 0 01-.143-.299c0-.156.061-.284.184-.384.125-.101.296-.152.513-.152.143 0 .266.023.37.068a.625.625 0 01.245.181.56.56 0 01.12.258h.75a1.092 1.092 0 00-.199-.566 1.21 1.21 0 00-.5-.41 1.813 1.813 0 00-.78-.152c-.293 0-.552.05-.776.15-.225.099-.4.24-.528.421-.127.182-.19.395-.19.639 0 .201.04.376.123.524.082.149.199.27.351.367.153.095.332.167.54.213l.618.144c.207.049.36.113.462.193a.387.387 0 01.153.326.512.512 0 01-.085.29.559.559 0 01-.256.193c-.111.047-.249.07-.413.07-.117 0-.224-.013-.32-.04a.837.837 0 01-.248-.115.578.578 0 01-.255-.384H0zm4.575 1.09h.952l1.327-3.999h-.879l-.887 3.138H5.05l-.897-3.138h-.917l1.339 4zm5.483-3.293c.076.152.123.316.14.492h-.776a.797.797 0 00-.096-.249.689.689 0 00-.17-.19.707.707 0 00-.237-.126.963.963 0 00-.3-.044c-.284 0-.506.1-.664.302-.157.2-.235.484-.235.85v.497c0 .235.033.44.097.616a.881.881 0 00.305.413.87.87 0 00.518.146.965.965 0 00.457-.097.67.67 0 00.273-.263c.06-.11.09-.23.09-.364v-.254h-.823v-.59h1.576v.798c0 .193-.032.377-.096.55a1.29 1.29 0 01-.293.457 1.37 1.37 0 01-.495.314c-.198.074-.43.111-.698.111a1.98 1.98 0 01-.752-.132 1.447 1.447 0 01-.534-.377 1.58 1.58 0 01-.319-.58 2.482 2.482 0 01-.105-.745v-.507c0-.36.066-.677.199-.949.134-.271.329-.482.583-.633.256-.152.564-.228.926-.228.238 0 .45.033.635.1.188.066.348.158.48.275.134.117.238.253.314.407z" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
1
src/main/webapp/assets/file-types/txt.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg class="bi bi-filetype-txt" fill="currentColor" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M14 4.5V14a2 2 0 01-2 2h-2v-1h2a1 1 0 001-1V4.5h-2A1.5 1.5 0 019.5 3V1H4a1 1 0 00-1 1v9H2V2a2 2 0 012-2h5.5L14 4.5zM1.928 15.849v-3.337h1.136v-.662H0v.662h1.134v3.337h.794zm4.689-3.999h-.894L4.9 13.289h-.035l-.832-1.439h-.932l1.228 1.983-1.24 2.016h.862l.853-1.415h.035l.85 1.415h.907l-1.253-1.992 1.274-2.007zm1.93.662v3.337h-.794v-3.337H6.619v-.662h3.064v.662H8.546z" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 516 B |
BIN
src/main/webapp/assets/github-fork-me.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
1
src/main/webapp/assets/settings.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M262.29 192.31a64 64 0 1057.4 57.4 64.13 64.13 0 00-57.4-57.4zM416.39 256a154.34 154.34 0 01-1.53 20.79l45.21 35.46a10.81 10.81 0 012.45 13.75l-42.77 74a10.81 10.81 0 01-13.14 4.59l-44.9-18.08a16.11 16.11 0 00-15.17 1.75A164.48 164.48 0 01325 400.8a15.94 15.94 0 00-8.82 12.14l-6.73 47.89a11.08 11.08 0 01-10.68 9.17h-85.54a11.11 11.11 0 01-10.69-8.87l-6.72-47.82a16.07 16.07 0 00-9-12.22 155.3 155.3 0 01-21.46-12.57 16 16 0 00-15.11-1.71l-44.89 18.07a10.81 10.81 0 01-13.14-4.58l-42.77-74a10.8 10.8 0 012.45-13.75l38.21-30a16.05 16.05 0 006-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16 16 0 00-6.07-13.94l-38.19-30A10.81 10.81 0 0149.48 186l42.77-74a10.81 10.81 0 0113.14-4.59l44.9 18.08a16.11 16.11 0 0015.17-1.75A164.48 164.48 0 01187 111.2a15.94 15.94 0 008.82-12.14l6.73-47.89A11.08 11.08 0 01213.23 42h85.54a11.11 11.11 0 0110.69 8.87l6.72 47.82a16.07 16.07 0 009 12.22 155.3 155.3 0 0121.46 12.57 16 16 0 0015.11 1.71l44.89-18.07a10.81 10.81 0 0113.14 4.58l42.77 74a10.8 10.8 0 01-2.45 13.75l-38.21 30a16.05 16.05 0 00-6.05 14.08c.33 4.14.55 8.3.55 12.47z" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
src/main/webapp/assets/undock.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M384 224v184a40 40 0 01-40 40H104a40 40 0 01-40-40V168a40 40 0 0140-40h167.48M336 64h112v112M224 288L440 72" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>
|
After Width: | Height: | Size: 281 B |
@ -1,4 +0,0 @@
|
||||
<div id="footer">
|
||||
<p>PlantUML Server Version <%= net.sourceforge.plantuml.version.Version.version() %>
|
||||
</p>
|
||||
</div>
|
@ -1,42 +1,27 @@
|
||||
<%@ page info="index" contentType="text/html; charset=utf-8" pageEncoding="utf-8" session="false" %>
|
||||
|
||||
<%
|
||||
// diagram sources
|
||||
String encoded = request.getAttribute("encoded").toString();
|
||||
String decoded = request.getAttribute("decoded").toString();
|
||||
String index = request.getAttribute("index").toString();
|
||||
String diagramUrl = ((index.isEmpty()) ? "" : index + "/") + encoded;
|
||||
// map for diagram source if necessary
|
||||
String map = request.getAttribute("map").toString();
|
||||
boolean hasMap = !map.isEmpty();
|
||||
// properties
|
||||
boolean showSocialButtons = (boolean)request.getAttribute("showSocialButtons");
|
||||
boolean showGithubRibbon = (boolean)request.getAttribute("showGithubRibbon");
|
||||
// image URLs
|
||||
boolean hasImg = (boolean)request.getAttribute("hasImg");
|
||||
String imgurl = request.getAttribute("imgurl").toString();
|
||||
String svgurl = request.getAttribute("svgurl").toString();
|
||||
String txturl = request.getAttribute("txturl").toString();
|
||||
String pdfurl = request.getAttribute("pdfurl").toString();
|
||||
String mapurl = request.getAttribute("mapurl").toString();
|
||||
// map for diagram source if necessary
|
||||
boolean hasMap = (boolean)request.getAttribute("hasMap");
|
||||
String map = request.getAttribute("map").toString();
|
||||
%>
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<base href="<%= request.getContextPath() %>/" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache, must-revalidate" />
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon"/>
|
||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
|
||||
<link rel="stylesheet" href="plantuml.css" />
|
||||
<link rel="stylesheet" href="webjars/codemirror/5.63.0/lib/codemirror.css" />
|
||||
<script src="plantuml.js"></script>
|
||||
<script src="webjars/codemirror/5.63.0/lib/codemirror.js"></script>
|
||||
<title>PlantUMLServer</title>
|
||||
<%@ include file="resource/htmlheadbase.jsp" %>
|
||||
<title>PlantUML Server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<%-- PAGE TITLE --%>
|
||||
<div class="app flex-rows">
|
||||
<div class="header">
|
||||
<h1>PlantUML Server</h1>
|
||||
<% if (showSocialButtons) { %>
|
||||
<%@ include file="resource/socialbuttons1.html" %>
|
||||
@ -46,48 +31,27 @@
|
||||
<% } %>
|
||||
<p>Create your <a href="https://plantuml.com">PlantUML</a> diagrams directly in your browser!</p>
|
||||
</div>
|
||||
<div id="content">
|
||||
<%-- CONTENT --%>
|
||||
<form method="post" accept-charset="utf-8" action="form">
|
||||
<p> <label for="text">UML Editor Content</label>
|
||||
<textarea id="text" name="text" cols="120" rows="10"><%= net.sourceforge.plantuml.servlet.PlantUmlServlet.stringToHTMLString(decoded) %></textarea>
|
||||
<input type="submit" value="Submit" title="Submit Code and generate diagram"/>
|
||||
<input type="submit" value="Copy Content to Clipboard" title="Copy Content to the clipboard" onclick="copyToClipboard('text','Content');return false; ">
|
||||
</p>
|
||||
</form>
|
||||
<hr/>
|
||||
<p>You can enter here a previously generated URL:</p>
|
||||
<form method="post" action="form">
|
||||
<p> <label for="url">previously generated URL</label>
|
||||
<input id="url" name="url" type="text" size="150" value="<%= imgurl %>" />
|
||||
<br/>
|
||||
<input type="submit" value="Decode URL" title="Decode URL and show code and diagram"/>
|
||||
<input type="submit" value="Copy URL to Clipboard" title="Copy URL to the clipboard" onclick="copyToClipboard('url','URL');return false; ">
|
||||
</p>
|
||||
</form>
|
||||
<% if (hasImg) { %>
|
||||
<hr/>
|
||||
<a href="<%= imgurl %>" title="View diagram as PNG">View as PNG</a>
|
||||
<a href="<%= svgurl %>" title="View diagram as SVG">View as SVG</a>
|
||||
<a href="<%= txturl %>" title="View diagram as ASCII Art">View as ASCII Art</a>
|
||||
<a href="<%= pdfurl %>" title="View diagram as PDF">View as PDF</a>
|
||||
<% if (hasMap) { %>
|
||||
<a href="<%= mapurl %>">View Map Data</a>
|
||||
<% } %>
|
||||
<% if (showSocialButtons) { %>
|
||||
<%@ include file="resource/socialbuttons2.jspf" %>
|
||||
<% } %>
|
||||
<p id="diagram">
|
||||
<% if (!hasMap) { %>
|
||||
<img src="<%= imgurl %>" alt="PlantUML diagram" />
|
||||
<% } else { %>
|
||||
<img src="<%= imgurl %>" alt="PlantUML diagram" usemap="#plantuml_map" />
|
||||
<%= map %>
|
||||
<% } %>
|
||||
</p>
|
||||
<% } %>
|
||||
<div class="main flex-main flex-columns">
|
||||
<div id="editor-main-container" class="editor flex-main flex-rows">
|
||||
<div>
|
||||
<div class="btn-input">
|
||||
<input id="url" type="text" name="url" value="png/<%= diagramUrl %>" />
|
||||
<input type="image" alt="copy" src="assets/copy.svg" onclick="copyUrlToClipboard()" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-main monaco-editor-container">
|
||||
<textarea id="initCode" name="initCode" style="display: none;"><%= net.sourceforge.plantuml.servlet.PlantUmlServlet.stringToHTMLString(decoded) %></textarea>
|
||||
<div id="monaco-editor"></div>
|
||||
<input type="image" alt="copy" src="assets/copy.svg" onclick="copyCodeToClipboard()" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="previewer-main-container" class="previewer flex-main">
|
||||
<%@ include file="resource/preview.jsp" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<%@ include file="resource/footer.jsp" %>
|
||||
</div>
|
||||
</div>
|
||||
<%-- FOOTER --%>
|
||||
<%@ include file="footer.jspf" %>
|
||||
</body>
|
||||
</html>
|
@ -2,67 +2,394 @@
|
||||
* PlantUMLServlet style sheet *
|
||||
******************************/
|
||||
|
||||
/* Font */
|
||||
h1, p, #content a {
|
||||
/************* variables *************/
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--border-color: #ccc;
|
||||
--bg-color: inherit;
|
||||
--font-color: inherit;
|
||||
--settings-bg-color: #fefefe;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--border-color: #848484;
|
||||
--bg-color: #212121;
|
||||
--settings-bg-color: #424242;
|
||||
--font-color: #ccc;
|
||||
}
|
||||
|
||||
/************* default settings *************/
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html {
|
||||
font-family: arial,helvetica,sans-serif;
|
||||
/* font-size: medium; */
|
||||
}
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
overflow: auto;
|
||||
}
|
||||
@media screen and (min-width: 900px) {
|
||||
body {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#header {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
#content {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
/* Form inputs */
|
||||
#content textarea, #content .CodeMirror, #content input[type=text] {
|
||||
background-color: #ffffff;
|
||||
font-family: monospace;
|
||||
font-size: medium;
|
||||
/************* ruler *************/
|
||||
.hr {
|
||||
padding: 1rem 0;
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
border: 3px solid #ccc !important;
|
||||
}
|
||||
.flex-columns > .hr {
|
||||
padding: 0 1rem;
|
||||
width: initial;
|
||||
height: 100%;
|
||||
}
|
||||
.hr:after {
|
||||
content: "";
|
||||
display: block;
|
||||
background-color: var(--border-color);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 3px;
|
||||
min-width: 3px;
|
||||
}
|
||||
|
||||
#content input[type=text] {
|
||||
border: 0;
|
||||
/************* flex rows and columns *************/
|
||||
.flex-columns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flex-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-main {
|
||||
flex: 1 1 1px;
|
||||
overflow: auto;
|
||||
}
|
||||
.flex-columns > *, .flex-rows > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#content input[type="submit"] {
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
background: #fafafa;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#content input[type="submit"]:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Diagram */
|
||||
#content #diagram {
|
||||
/*******************************************************************/
|
||||
/************* header, main, footer *************/
|
||||
.header {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#content #diagram img {
|
||||
border: medium solid green;
|
||||
padding: 10px;
|
||||
.main {
|
||||
margin: 1% 5%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
#footer p {
|
||||
.main > div {
|
||||
margin: 0 1.75%;
|
||||
}
|
||||
.main > div:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.main > div:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.main {
|
||||
display: block;
|
||||
overflow: inherit;
|
||||
}
|
||||
.main > div {
|
||||
margin: 1.75% 0;
|
||||
}
|
||||
.main > div:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.main > div:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.footer p {
|
||||
background-color: #eee;
|
||||
color: #666;
|
||||
font-size: 0.7em;
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*******************************************************************/
|
||||
/************* editor *************/
|
||||
.editor {
|
||||
border: 3px solid var(--border-color);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.editor {
|
||||
height: 20em;
|
||||
}
|
||||
}
|
||||
.monaco-editor-container {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
#monaco-editor {
|
||||
height: 100%;
|
||||
}
|
||||
/* Hack to display the icons in the icon auto completion documentation in a visible size.
|
||||
* (see PlantUmlLanguageFeatures.registerIconCompletion) */
|
||||
#monaco-editor .overlayWidgets .suggest-details p img[alt="icon"] {
|
||||
height: 1.2rem;
|
||||
}
|
||||
|
||||
/************* URL input + copy button *************/
|
||||
.btn-input {
|
||||
align-items: center;
|
||||
border-bottom: 3px solid var(--border-color);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.btn-input input[type=text] {
|
||||
border: 0;
|
||||
flex: 1 1 1px;
|
||||
font-family: monospace;
|
||||
font-size: medium;
|
||||
padding: 0.2em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.btn-input input[type=text]:focus {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
.btn-input input[type=image] {
|
||||
height: 1rem;
|
||||
margin-left: 0.7em;
|
||||
padding: 0 0.3em;
|
||||
}
|
||||
|
||||
/************* Monaco editor copy button *************/
|
||||
.monaco-editor-container input[type=image] {
|
||||
height: 1.5rem;
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.monaco-editor-container input[type=image]:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*******************************************************************/
|
||||
/************* previewer *************/
|
||||
.content.viewer-content {
|
||||
margin: 5%;
|
||||
}
|
||||
.content.viewer-content, .previewer-container {
|
||||
height: 100%;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.previewer-container {
|
||||
height: initial;
|
||||
}
|
||||
.previewer-main {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
/************* menu *************/
|
||||
.preview-menu {
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
}
|
||||
.diagram-link img, .btn-dock {
|
||||
width: 2.5rem;
|
||||
}
|
||||
.btn-settings {
|
||||
width: 2.2rem;
|
||||
margin-left: auto;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.menu-r {
|
||||
min-width: 3rem;
|
||||
}
|
||||
.menu-r .btn-float-r {
|
||||
float: right;
|
||||
margin-left: 0.25rem;
|
||||
text-align: right;
|
||||
}
|
||||
.diagram-links {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.diagram-link {
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.diagram-links .diagram-link:first-of-type {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.diagram-links .diagram-link:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/************* paginator *************/
|
||||
#paginator {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/************* diagram *************/
|
||||
.diagram {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.diagram[data-diagram-type="pdf"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
.diagram > div {
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
.diagram[data-diagram-type="pdf"] > div {
|
||||
height: 20em;
|
||||
width: 100%;
|
||||
}
|
||||
.diagram img, .diagram svg, .diagram pre {
|
||||
border: 3px solid var(--border-color);
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
@media screen and (min-width: 900px) {
|
||||
.diagram {
|
||||
position: relative;
|
||||
}
|
||||
.diagram > div {
|
||||
margin: 0;
|
||||
}
|
||||
.diagram:not([data-diagram-type="pdf"]) > div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.diagram[data-diagram-type="pdf"] > div {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************/
|
||||
/************* settings *************/
|
||||
/************* modal *************/
|
||||
.modal {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
padding: 5%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: auto;
|
||||
background-color: rgb(0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.modal-content {
|
||||
background-color: var(--settings-bg-color);
|
||||
margin: auto;
|
||||
padding: 2rem;
|
||||
border: 3px solid var(--border-color);
|
||||
max-width: 30rem;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
-webkit-animation-name: animatetop;
|
||||
-webkit-animation-duration: 0.4s;
|
||||
animation-name: animatetop;
|
||||
animation-duration: 0.4s;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
@-webkit-keyframes animatetop {
|
||||
from { top: -50%; opacity: 0; }
|
||||
to { top: 50%; opacity: 1; }
|
||||
}
|
||||
@keyframes animatetop {
|
||||
from { top: -50%; opacity: 0; }
|
||||
to { top: 50%; opacity: 1; }
|
||||
}
|
||||
/************* header, main, footer *************/
|
||||
#settings .settings-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
#settings .settings-main {
|
||||
flex: 1;
|
||||
}
|
||||
#settings .settings-footer {
|
||||
text-align: right;
|
||||
}
|
||||
/************* label + input *************/
|
||||
#settings .setting {
|
||||
margin: 1rem 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#settings .setting:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
#settings .setting label {
|
||||
display: inline-block;
|
||||
min-width: 15rem;
|
||||
}
|
||||
#settings .setting label + input, #settings .setting label + select {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
min-width: 10rem;
|
||||
}
|
||||
#settings input, #settings select {
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
#settings input:not(:focus):invalid {
|
||||
border-bottom-color: red;
|
||||
}
|
||||
/************* settings editor *************/
|
||||
#settings #settings-monaco-editor {
|
||||
height: 17rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
/************* ok + cancel buttons *************/
|
||||
#settings input.ok, #settings input.cancel {
|
||||
min-width: 5rem;
|
||||
}
|
||||
#settings input.ok:hover {
|
||||
border-bottom-color: green;
|
||||
}
|
||||
#settings input.cancel:hover {
|
||||
border-bottom-color: darkred;
|
||||
}
|
||||
|
||||
/*******************************************************************/
|
||||
/************* color themes *************/
|
||||
[data-theme="dark"] img:not(#diagram-png):not(.no-filter) {
|
||||
filter: invert() contrast(30%);
|
||||
}
|
||||
[data-theme="dark"] input[type=image] {
|
||||
filter: invert() contrast(30%);
|
||||
}
|
||||
[data-theme="dark"] input:not([type=image]) {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
}
|
||||
[data-theme="dark"] .footer p {
|
||||
background-color: black;
|
||||
color: var(--font-color);
|
||||
}
|
||||
[data-theme="dark"] a {
|
||||
color: white;
|
||||
}
|
||||
|
@ -2,57 +2,742 @@
|
||||
* PlantUMLServlet script *
|
||||
**************************/
|
||||
|
||||
let clipboard_write = false;
|
||||
let clipboard_read = false;
|
||||
// ==========================================================================================================
|
||||
// == global configuration ==
|
||||
|
||||
function copyToClipboard(fieldid, fielddesc) {
|
||||
if (clipboard_write) {
|
||||
navigator.clipboard.writeText(document.getElementById(fieldid).value);
|
||||
alert(fielddesc + " copied to clipboard");
|
||||
document.appConfig = Object.assign({}, window.opener?.document.appConfig);
|
||||
if (Object.keys(document.appConfig).length === 0) {
|
||||
document.appConfig = JSON.parse(localStorage.getItem("document.appConfig")) || {
|
||||
changeEventsEnabled: true,
|
||||
// `autoRefreshState` is mostly used for unit testing puposes.
|
||||
// states: disabled | waiting | started | syncing | complete
|
||||
autoRefreshState: "disabled",
|
||||
theme: undefined, // dark | light (will be set via `initTheme` if undefined)
|
||||
diagramPreviewType: "png",
|
||||
editorWatcherTimeout: 500,
|
||||
editorCreateOptions: {
|
||||
automaticLayout: true,
|
||||
fixedOverflowWidgets: true,
|
||||
minimap: { enabled: false },
|
||||
scrollbar: { alwaysConsumeMouseWheel: false },
|
||||
scrollBeyondLastLine: false,
|
||||
tabSize: 2,
|
||||
theme: "vs", // "vs-dark"
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
function loadCodeMirror() {
|
||||
document.myCodeMirror = CodeMirror.fromTextArea(
|
||||
document.getElementById("text"),
|
||||
{
|
||||
lineNumbers: true,
|
||||
extraKeys: {Tab: false, "Shift-Tab": false}
|
||||
|
||||
// ==========================================================================================================
|
||||
// == DOM helpers ==
|
||||
|
||||
function removeChildren(el) {
|
||||
if (el.replaceChildren) {
|
||||
el.replaceChildren();
|
||||
} else {
|
||||
el.innerHTML = "";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
// `offsetParent` returns `null` if the element, or any of its parents,
|
||||
// is hidden via the display style property.
|
||||
// see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
||||
return (el.offsetParent === null)
|
||||
}
|
||||
|
||||
function setVisibility(el, visibility, focus=false) {
|
||||
if (visibility) {
|
||||
el.style.removeProperty("display");
|
||||
if (focus) el.focus();
|
||||
} else {
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == URL helpers ==
|
||||
|
||||
function resolvePath(path) {
|
||||
// also see `PlantUmlLanguageFeatures.resolvePath(path)`
|
||||
if (path.startsWith("http")) return path;
|
||||
if (path.startsWith("/")) return window.location.origin + path;
|
||||
|
||||
if (path.slice(0, 2) == "./") path = path.slice(2);
|
||||
let base = (document.querySelector('base') || {}).href || window.location.origin;
|
||||
let base = (document.querySelector("base") || {}).href || window.location.origin;
|
||||
if (base.slice(-1) == "/") base = base.slice(0, -1);
|
||||
return base + "/" + path;
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
loadCodeMirror();
|
||||
|
||||
// resolve relative path inside url input once
|
||||
const url = document.getElementById("url");
|
||||
url.value = resolvePath(url.value);
|
||||
|
||||
// clipboard check (from PR#250)
|
||||
// TODO: not supported by Firefox, Safari or WebView Android
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API#browser_compatibility
|
||||
if (navigator.permissions){
|
||||
navigator.permissions.query({ name: "clipboard-write" }).then((result) => {
|
||||
if (result.state == "granted" || result.state == "prompt") {
|
||||
clipboard_write = true;
|
||||
function prepareUrl(url) {
|
||||
if (!(url instanceof URL)) {
|
||||
url = new URL(resolvePath(url));
|
||||
}
|
||||
});
|
||||
navigator.permissions.query({ name: "clipboard-read" }).then((result) => {
|
||||
if (result.state == "granted" || result.state == "prompt") {
|
||||
clipboard_read = true;
|
||||
}
|
||||
});
|
||||
// pathname excluding context path
|
||||
let base = new URL((document.querySelector("base") || {}).href || window.location.origin).pathname;
|
||||
if (base.slice(-1) === "/") base = base.slice(0, -1);
|
||||
const pathname = url.pathname.startsWith(base) ? url.pathname.slice(base.length) : url.pathname;
|
||||
// same as `UrlDataExtractor.URL_PATTERN`
|
||||
const regex = /\/\w+(?:\/(?<idx>\d+))?(?:\/(?<encoded>[^\/]+))?\/?$/gm;
|
||||
const match = regex.exec(pathname);
|
||||
return [ url, pathname, match ];
|
||||
}
|
||||
|
||||
function analyseUrl(url) {
|
||||
let match;
|
||||
[url, _, match] = prepareUrl(url);
|
||||
return {
|
||||
index: match.groups.idx,
|
||||
encodedDiagram: match.groups.encoded || url.searchParams.get("url"),
|
||||
};
|
||||
}
|
||||
|
||||
function replaceUrl(url, encodedDiagram, index) {
|
||||
let oldPathname, match;
|
||||
[url, oldPathname, match] = prepareUrl(url);
|
||||
let pathname = oldPathname.slice(1);
|
||||
pathname = pathname.slice(0, pathname.indexOf("/"));
|
||||
if (index && index >= 0) pathname += "/" + index;
|
||||
if (match.groups.encoded) pathname += "/" + encodedDiagram;
|
||||
if (oldPathname.slice(-1) === "/") pathname += "/";
|
||||
url.pathname = new URL(resolvePath(pathname)).pathname;
|
||||
if (url.searchParams.get("url")) {
|
||||
url.searchParams.set("url", encodedDiagram);
|
||||
}
|
||||
return { url, pathname };
|
||||
}
|
||||
|
||||
function buildUrl(serletpath, encodedDiagram, index) {
|
||||
let pathname = serletpath;
|
||||
if (index && index >= 0) pathname += "/" + index;
|
||||
pathname += "/" + encodedDiagram;
|
||||
return pathname;
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == clipboard helpers ==
|
||||
|
||||
function copyUrlToClipboard() {
|
||||
const input = document.getElementById("url");
|
||||
input.focus();
|
||||
input.select();
|
||||
navigator.clipboard?.writeText(input.value).catch(() => {});
|
||||
}
|
||||
|
||||
function copyCodeToClipboard() {
|
||||
const range = document.editor.getModel().getFullModelRange();
|
||||
document.editor.focus();
|
||||
document.editor.setSelection(range);
|
||||
const code = document.editor.getValue();
|
||||
navigator.clipboard?.writeText(code).catch(() => {});
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == theme helpers ==
|
||||
|
||||
function getBrowserThemePreferences() {
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
return "dark";
|
||||
}
|
||||
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
||||
return "light";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == asynchron server calls ==
|
||||
|
||||
function call(method, url, data, callback) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
||||
callback(xhr.responseText);
|
||||
}
|
||||
}
|
||||
xhr.open(method, url, true);
|
||||
xhr.setRequestHeader("Content-Type", "text/plain");
|
||||
xhr.send(data);
|
||||
}
|
||||
|
||||
function decodeDiagram(encodedDiagram, callback) {
|
||||
call("GET", "coder/" + encodedDiagram, null, callback);
|
||||
}
|
||||
|
||||
function encodeDiagram(diagram, callback) {
|
||||
call("POST", "coder", diagram, callback);
|
||||
}
|
||||
|
||||
function requestDiagram(type, encodedDiagram, index, callback) {
|
||||
call("GET", buildUrl(type, encodedDiagram, index), null, callback);
|
||||
}
|
||||
|
||||
function requestDiagramMap(encodedDiagram, index, callback) {
|
||||
requestDiagram("map", encodedDiagram, index, callback);
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == settings ==
|
||||
|
||||
function initSettings() {
|
||||
document.getElementById("settings").addEventListener("keydown", (event) => {
|
||||
event.preventDefault();
|
||||
console.log(event);
|
||||
if (event.key === "Escape" || event.key === "Esc") {
|
||||
closeSettings();
|
||||
}
|
||||
}, false);
|
||||
document.getElementById("theme").addEventListener("change", (event) => {
|
||||
const theme = event.target.value;
|
||||
const editorCreateOptionsString = document.settingsEditor.getValue();
|
||||
const replaceTheme = (theme === "dark") ? "vs" : "vs-dark";
|
||||
const substituteTheme = (theme === "dark") ? "vs-dark" : "vs";
|
||||
const regex = new RegExp('("theme"\\s*:\\s*)"' + replaceTheme + '"', "gm");
|
||||
document.settingsEditor.getModel().setValue(
|
||||
editorCreateOptionsString.replace(regex, '$1"' + substituteTheme + '"')
|
||||
);
|
||||
});
|
||||
document.settingsEditor = monaco.editor.create(document.getElementById("settings-monaco-editor"), {
|
||||
language: "json", ...document.appConfig.editorCreateOptions
|
||||
});
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
setVisibility(document.getElementById("settings"), true, true);
|
||||
if (!document.settingsEditor) {
|
||||
initSettings();
|
||||
}
|
||||
// fill settings form
|
||||
document.getElementById("theme").value = document.appConfig.theme;
|
||||
document.getElementById("diagramPreviewType").value = document.appConfig.diagramPreviewType;
|
||||
document.getElementById("editorWatcherTimeout").value = document.appConfig.editorWatcherTimeout;
|
||||
document.settingsEditor.getModel().setValue(
|
||||
JSON.stringify(document.appConfig.editorCreateOptions, null, " ")
|
||||
);
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
setVisibility(document.getElementById("settings"), false);
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
const appConfig = Object.assign({}, document.appConfig);
|
||||
appConfig.theme = document.getElementById("theme").value;
|
||||
appConfig.editorWatcherTimeout = document.getElementById("editorWatcherTimeout").value;
|
||||
appConfig.diagramPreviewType = document.getElementById("diagramPreviewType").value;
|
||||
appConfig.editorCreateOptions = JSON.parse(document.settingsEditor.getValue());
|
||||
broadcastSettings(appConfig);
|
||||
closeSettings();
|
||||
}
|
||||
|
||||
function broadcastSettings(appConfig) {
|
||||
localStorage.setItem("document.appConfig", JSON.stringify(appConfig));
|
||||
sendMessage({
|
||||
sender: "settings",
|
||||
data: { appConfig },
|
||||
synchronize: true,
|
||||
});
|
||||
}
|
||||
|
||||
function applySettings() {
|
||||
setTheme(document.appConfig.theme);
|
||||
document.editor?.updateOptions(document.appConfig.editorCreateOptions);
|
||||
document.settingsEditor?.updateOptions(document.appConfig.editorCreateOptions);
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == dock (pop in) and undock (pop out) previewer ==
|
||||
|
||||
function getDockUndockElements() {
|
||||
return {
|
||||
"btnUndock": document.getElementById("btn-undock"),
|
||||
"btnDock": document.getElementById("btn-dock"),
|
||||
"editorContainer": document.getElementById("editor-main-container"),
|
||||
"previewContainer": document.getElementById("previewer-main-container"),
|
||||
};
|
||||
}
|
||||
|
||||
function hidePreview() {
|
||||
const elements = getDockUndockElements();
|
||||
setVisibility(elements.btnUndock, false);
|
||||
// if not opened via button and therefore a popup, `window.close` won't work
|
||||
setVisibility(elements.btnDock, window.opener);
|
||||
if (elements.editorContainer) elements.editorContainer.style.width = "100%";
|
||||
if (elements.previewContainer) setVisibility(elements.previewContainer, false);
|
||||
}
|
||||
|
||||
function showPreview() {
|
||||
const elements = getDockUndockElements();
|
||||
setVisibility(elements.btnUndock, true);
|
||||
setVisibility(elements.btnDock, false);
|
||||
if (elements.editorContainer) elements.editorContainer.style.removeProperty("width");
|
||||
if (elements.previewContainer) setVisibility(elements.previewContainer, true);
|
||||
}
|
||||
|
||||
function undock() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("view", "previewer");
|
||||
const previewer = window.open(url, "PlantUML Diagram Previewer", "popup");
|
||||
if (previewer) {
|
||||
previewer.onbeforeunload = showPreview;
|
||||
hidePreview();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == paginator ==
|
||||
|
||||
function getNumberOfDiagramPagesFromCode(code) {
|
||||
// count `newpage` inside code
|
||||
// known issue: a `newpage` starting in a newline inside a multiline comment will also be counted
|
||||
return code.match(/^\s*newpage\s?.*$/gm)?.length + 1 || 1;
|
||||
}
|
||||
|
||||
function updateNumberOfPagingElements(paginator, pages) {
|
||||
// remove elements (buttons) if there are to many
|
||||
while (paginator.childElementCount > pages) {
|
||||
paginator.removeChild(paginator.lastChild)
|
||||
}
|
||||
// add elements (buttons) if there are to less
|
||||
while (paginator.childElementCount < pages) {
|
||||
const radioBtn = document.createElement("input");
|
||||
radioBtn.name = "paginator";
|
||||
radioBtn.type = "radio";
|
||||
radioBtn.value = paginator.childElementCount;
|
||||
radioBtn.addEventListener("click", (event) => {
|
||||
sendMessage({
|
||||
sender: "paginator",
|
||||
data: { index: event.target.value },
|
||||
synchronize: true,
|
||||
});
|
||||
});
|
||||
paginator.appendChild(radioBtn);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaginator() {
|
||||
const paginator = document.getElementById("paginator");
|
||||
const pages = document.appData.numberOfDiagramPages;
|
||||
if (pages > 1) {
|
||||
updateNumberOfPagingElements(paginator, pages);
|
||||
setVisibility(paginator, true);
|
||||
} else {
|
||||
setVisibility(paginator, false);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaginatorSelection() {
|
||||
const paginator = document.getElementById("paginator");
|
||||
const index = document.appData.index;
|
||||
if (index === undefined) {
|
||||
for (const node of paginator.childNodes) {
|
||||
node.checked = false;
|
||||
}
|
||||
} else {
|
||||
paginator.childNodes[index].checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == sync data ==
|
||||
|
||||
function updateDiagramMap(mapString, mapEl) {
|
||||
const mapBtn = document.getElementById("map-diagram-link");
|
||||
mapEl = mapEl || document.getElementById("plantuml_map");
|
||||
if (mapString) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = mapString;
|
||||
mapEl.parentNode.replaceChild(div.firstChild, mapEl);
|
||||
setVisibility(mapBtn, true);
|
||||
} else {
|
||||
removeChildren(mapEl);
|
||||
setVisibility(mapBtn, false);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSvgDiagram(svgString, svgEl) {
|
||||
svgEl = svgEl || document.getElementById("diagram-svg");
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = svgString;
|
||||
const newSvg = div.querySelector("svg");
|
||||
newSvg.id = "diagram-svg";
|
||||
svgEl.parentNode.replaceChild(newSvg, svgEl);
|
||||
}
|
||||
|
||||
function updateTxtDiagram(txtString, txtEl) {
|
||||
txtEl = txtEl || document.getElementById("diagram-txt");
|
||||
txtEl.innerHTML = txtString;
|
||||
}
|
||||
|
||||
function syncDiagram(type, encodedDiagram, index) {
|
||||
const container = document.getElementById("diagram");
|
||||
const png = document.getElementById("diagram-png");
|
||||
const map = document.getElementById("plantuml_map");
|
||||
const svg = document.getElementById("diagram-svg");
|
||||
const txt = document.getElementById("diagram-txt");
|
||||
const pdf = document.getElementById("diagram-pdf");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (type === "png") {
|
||||
png.src = buildUrl(type, encodedDiagram, index);
|
||||
requestDiagramMap(encodedDiagram, index, (mapString) => {
|
||||
updateDiagramMap(mapString, map);
|
||||
resolve();
|
||||
});
|
||||
} else if (type === "svg") {
|
||||
requestDiagram(type, encodedDiagram, index, (svgString) => {
|
||||
updateSvgDiagram(svgString, svg);
|
||||
resolve();
|
||||
});
|
||||
} else if (type === "txt") {
|
||||
requestDiagram(type, encodedDiagram, index, (svgString) => {
|
||||
updateTxtDiagram(svgString, txt);
|
||||
resolve();
|
||||
});
|
||||
} else if (type === "pdf") {
|
||||
pdf.data = buildUrl(type, encodedDiagram, index);
|
||||
resolve();
|
||||
} else {
|
||||
(console.error || console.log)("unknown diagram type:", type);
|
||||
reject();
|
||||
}
|
||||
}).then(() => {
|
||||
container.setAttribute("data-diagram-type", type);
|
||||
setVisibility(png, type === "png");
|
||||
setVisibility(map, type === "png");
|
||||
setVisibility(svg, type === "svg");
|
||||
setVisibility(txt, type === "txt");
|
||||
setVisibility(pdf, type === "pdf");
|
||||
});
|
||||
}
|
||||
|
||||
function syncUrlTextInput(encodedDiagram, index) {
|
||||
const target = document.getElementById("url");
|
||||
document.appConfig.changeEventsEnabled = false;
|
||||
target.value = resolvePath(buildUrl("png", encodedDiagram, index));
|
||||
target.title = target.value;
|
||||
document.appConfig.changeEventsEnabled = true;
|
||||
}
|
||||
|
||||
function syncCodeEditor(code) {
|
||||
document.appConfig.changeEventsEnabled = false;
|
||||
document.editor.getModel().setValue(code);
|
||||
document.appConfig.changeEventsEnabled = true;
|
||||
}
|
||||
|
||||
function syncBrowserHistory(encodedDiagram, index) {
|
||||
const url = replaceUrl(window.location.href, encodedDiagram, index).url;
|
||||
history.replaceState(history.stat, document.title, url);
|
||||
}
|
||||
|
||||
function syncStaticPageData(includePaginatorUpdates) {
|
||||
document.appConfig.autoRefreshState = "syncing";
|
||||
const encodedDiagram = document.appData.encodedDiagram;
|
||||
const index = document.appData.index;
|
||||
return Promise.all([
|
||||
// update URL input
|
||||
new Promise((resolve, _reject) => {
|
||||
if (document.getElementById("url")) {
|
||||
syncUrlTextInput(encodedDiagram, index);
|
||||
}
|
||||
resolve();
|
||||
}),
|
||||
// update diagram image
|
||||
syncDiagram(document.appConfig.diagramPreviewType, encodedDiagram, index),
|
||||
// update external diagram links
|
||||
new Promise((resolve, _reject) => {
|
||||
for (let target of document.getElementsByClassName("diagram-link")) {
|
||||
target.href = buildUrl(target.dataset.imgType, encodedDiagram, index);
|
||||
}
|
||||
resolve();
|
||||
}),
|
||||
// update paginator
|
||||
new Promise((resolve, _reject) => {
|
||||
if (includePaginatorUpdates) {
|
||||
updatePaginator();
|
||||
updatePaginatorSelection();
|
||||
}
|
||||
resolve();
|
||||
}),
|
||||
// update browser url as well as the browser history
|
||||
new Promise((resolve, _reject) => {
|
||||
syncBrowserHistory(encodedDiagram, index);
|
||||
resolve();
|
||||
}),
|
||||
]).then(() => {
|
||||
// set auto refresh state to complete
|
||||
document.appConfig.autoRefreshState = "complete";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == initialize app ==
|
||||
|
||||
async function initializeApp(view) {
|
||||
await loadCodeEditor();
|
||||
if (view !== "previewer") {
|
||||
initializeCodeEditor();
|
||||
initializeUrlInput();
|
||||
}
|
||||
initializeAppData();
|
||||
initTheme();
|
||||
await initializeDiagram();
|
||||
initializePaginator();
|
||||
if (view !== "previewer") {
|
||||
addSavePlantumlDocumentEvent();
|
||||
}
|
||||
if (["previewer", "editor"].includes(view)) {
|
||||
hidePreview();
|
||||
}
|
||||
document.appConfig.autoRefreshState = "complete";
|
||||
}
|
||||
|
||||
function loadCodeEditor() {
|
||||
// load Monaco editor asynchron
|
||||
return new Promise((resolve, _reject) => {
|
||||
require.config({ paths: { vs: "webjars/monaco-editor/0.36.1/min/vs" } });
|
||||
require(["vs/editor/editor.main"], resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeCodeEditor() {
|
||||
// create editor model including editor watcher
|
||||
let timer = 0;
|
||||
const uri = monaco.Uri.parse("inmemory://plantuml");
|
||||
const initCodeEl = document.getElementById("initCode");
|
||||
const initCode = initCodeEl.value;
|
||||
initCodeEl.remove();
|
||||
const plantumlFeatures = new PlantUmlLanguageFeatures();
|
||||
const model = monaco.editor.createModel(initCode, "apex", uri);
|
||||
model.onDidChangeContent(() => {
|
||||
clearTimeout(timer);
|
||||
if (document.appConfig.changeEventsEnabled) {
|
||||
document.appConfig.autoRefreshState = "waiting";
|
||||
timer = setTimeout(() => {
|
||||
document.appConfig.autoRefreshState = "started";
|
||||
const code = model.getValue();
|
||||
const numberOfDiagramPages = getNumberOfDiagramPagesFromCode(code);
|
||||
let index = document.appData.index;
|
||||
if (index === undefined || numberOfDiagramPages === 1) {
|
||||
index = undefined;
|
||||
} else if (index >= numberOfDiagramPages) {
|
||||
index = numberOfDiagramPages - 1;
|
||||
}
|
||||
encodeDiagram(code, (encodedDiagram) => {
|
||||
sendMessage({
|
||||
sender: "editor",
|
||||
data: { encodedDiagram, numberOfDiagramPages, index },
|
||||
synchronize: true,
|
||||
});
|
||||
});
|
||||
plantumlFeatures.validateCode(model)
|
||||
.then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers));
|
||||
}, document.appConfig.editorWatcherTimeout);
|
||||
}
|
||||
});
|
||||
// create editor
|
||||
document.editor = monaco.editor.create(document.getElementById("monaco-editor"), {
|
||||
model, ...document.appConfig.editorCreateOptions
|
||||
});
|
||||
// sometimes the monaco editor has resize problems
|
||||
document.addEventListener("resize", () => document.editor.layout());
|
||||
}
|
||||
|
||||
function initializeUrlInput() {
|
||||
// resolve relative path inside url input once
|
||||
const urlInput = document.getElementById("url");
|
||||
urlInput.value = resolvePath(urlInput.value);
|
||||
urlInput.title = urlInput.value;
|
||||
|
||||
// update editor and everything else if the URL input is changed
|
||||
urlInput.addEventListener("change", (event) => {
|
||||
if (document.appConfig.changeEventsEnabled) {
|
||||
document.appConfig.autoRefreshState = "started";
|
||||
const analysedUrl = analyseUrl(event.target.value);
|
||||
decodeDiagram(analysedUrl.encodedDiagram, (code) => {
|
||||
syncCodeEditor(code);
|
||||
sendMessage({
|
||||
sender: "url",
|
||||
data: {
|
||||
encodedDiagram: analysedUrl.encodedDiagram,
|
||||
index: analysedUrl.index,
|
||||
},
|
||||
synchronize: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initializeAppData() {
|
||||
const analysedUrl = analyseUrl(window.location.href);
|
||||
const code = document.editor?.getValue();
|
||||
document.appData = Object.assign({}, window.opener?.document.appData);
|
||||
if (Object.keys(document.appData).length === 0) {
|
||||
document.appData = {
|
||||
encodedDiagram: analysedUrl.encodedDiagram,
|
||||
index: analysedUrl.index,
|
||||
numberOfDiagramPages: (code) ? getNumberOfDiagramPagesFromCode(code) : 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme() {
|
||||
function changeEditorThemeSettingIfNecessary(theme) {
|
||||
if (theme === "dark" && document.appConfig.editorCreateOptions.theme === "vs") {
|
||||
document.appConfig.editorCreateOptions.theme = "vs-dark";
|
||||
}
|
||||
if (theme === "light" && document.appConfig.editorCreateOptions.theme === "vs-dark") {
|
||||
document.appConfig.editorCreateOptions.theme = "vs";
|
||||
}
|
||||
}
|
||||
// set theme to last saved settings or browser preference or "light"
|
||||
document.appConfig.theme = document.appConfig.theme || getBrowserThemePreferences() || "light";
|
||||
setTheme(document.appConfig.theme);
|
||||
changeEditorThemeSettingIfNecessary(document.appConfig.theme);
|
||||
// listen to browser change event
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", event => {
|
||||
const theme = event.matches ? "dark" : "light";
|
||||
document.appConfig.theme = theme
|
||||
changeEditorThemeSettingIfNecessary(theme);
|
||||
broadcastSettings(document.appConfig);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeDiagram() {
|
||||
if (document.appConfig.diagramPreviewType === "png") {
|
||||
return Promise.resolve(); // png is initialized by default
|
||||
}
|
||||
return syncDiagram(
|
||||
document.appConfig.diagramPreviewType,
|
||||
document.appData.encodedDiagram,
|
||||
document.appData.index
|
||||
);
|
||||
}
|
||||
|
||||
function initializePaginator() {
|
||||
if (document.appData.numberOfDiagramPages > 1) {
|
||||
updatePaginator();
|
||||
updatePaginatorSelection();
|
||||
}
|
||||
}
|
||||
|
||||
function addSavePlantumlDocumentEvent() {
|
||||
const PLATFORM = navigator?.userAgentData?.platform || navigator?.platform || "unknown";
|
||||
document.addEventListener("keydown", function(e) {
|
||||
if (e.key === "s" && (PLATFORM.match("Mac") ? e.metaKey : e.ctrlKey)) {
|
||||
// support Ctrl+S to download diagram
|
||||
e.preventDefault();
|
||||
const code = document.editor.getValue();
|
||||
const name = Array.from(
|
||||
code.matchAll(/^\s*@start[a-zA-Z]*\s+([a-zA-Z-_äöüÄÖÜß ]+)\s*$/gm),
|
||||
m => m[1]
|
||||
)[0] || "diagram";
|
||||
// download via link
|
||||
const link = document.createElement("a");
|
||||
link.download = name + ".puml";
|
||||
link.href = "data:," + encodeURIComponent(code);
|
||||
link.click();
|
||||
}
|
||||
if (e.key === "," && (PLATFORM.match("Mac") ? e.metaKey : e.ctrlKey)) {
|
||||
// support Ctrl+, to open the settings
|
||||
e.preventDefault();
|
||||
if (document.getElementById("settings")?.style?.display === "none") {
|
||||
openSettings();
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == communication ==
|
||||
//
|
||||
// send and receive data: {
|
||||
// sender: string = ["editor"|"url"|"paginator"|"settings"],
|
||||
// data: {
|
||||
// encodedDiagram: string | undefined,
|
||||
// index: integer | undefined,
|
||||
// numberOfDiagramPages: integer | undefined,
|
||||
// appConfig: object | undefined
|
||||
// } | undefined,
|
||||
// synchronize: boolean = false,
|
||||
// reload: boolean = false, // reload page
|
||||
// force: boolean = false // force synchronize or reload
|
||||
// }
|
||||
|
||||
function sendMessage(data) {
|
||||
(new BroadcastChannel("plantuml-server")).postMessage(data, window.location.origin);
|
||||
}
|
||||
|
||||
function updateReceiveMessageData(data) {
|
||||
if (!data || Object.keys(data).length === 0) return {};
|
||||
|
||||
const changedFlags = {};
|
||||
if ("encodedDiagram" in data && data.encodedDiagram !== document.appData.encodedDiagram) {
|
||||
document.appData.encodedDiagram = data.encodedDiagram;
|
||||
changedFlags.diagram = true;
|
||||
}
|
||||
if ("index" in data && data.index !== document.appData.index) {
|
||||
document.appData.index = data.index;
|
||||
changedFlags.index = true;
|
||||
}
|
||||
if ("numberOfDiagramPages" in data && data.numberOfDiagramPages !== document.appData.numberOfDiagramPages) {
|
||||
document.appData.numberOfDiagramPages = data.numberOfDiagramPages;
|
||||
changedFlags.numberOfDiagramPages = true;
|
||||
}
|
||||
if ("appConfig" in data && data.appConfig !== document.appConfig) {
|
||||
document.appConfig = data.appConfig;
|
||||
changedFlags.appConfig = true;
|
||||
}
|
||||
return changedFlags;
|
||||
}
|
||||
|
||||
async function receiveMessage(event) {
|
||||
const data = event.data.data;
|
||||
const force = event.data.force || false;
|
||||
const changedFlags = updateReceiveMessageData(data);
|
||||
if (event.data.synchronize === true) {
|
||||
if (force || changedFlags.diagram || changedFlags.index || changedFlags.appConfig) {
|
||||
await syncStaticPageData(false);
|
||||
}
|
||||
if (force || changedFlags.numberOfDiagramPages) {
|
||||
updatePaginator();
|
||||
}
|
||||
if (force || changedFlags.numberOfDiagramPages || changedFlags.index) {
|
||||
updatePaginatorSelection();
|
||||
}
|
||||
if (changedFlags.appConfig) {
|
||||
applySettings();
|
||||
}
|
||||
}
|
||||
if (event.data.reload === true) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == main entry ==
|
||||
|
||||
window.onload = function() {
|
||||
const view = new URL(window.location.href).searchParams.get("view")?.toLowerCase();
|
||||
initializeApp(view);
|
||||
|
||||
// broadcast channel
|
||||
const bc = new BroadcastChannel("plantuml-server");
|
||||
bc.onmessage = receiveMessage;
|
||||
};
|
||||
|
384
src/main/webapp/plantumllanguage.js
Normal file
@ -0,0 +1,384 @@
|
||||
/*******************************************
|
||||
* Monaco Editor PlantUML language features *
|
||||
********************************************/
|
||||
|
||||
/**
|
||||
* Monaco Editor PlantUML Language Features.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* plantumlFeatures = new PlantUmlLanguageFeatures();
|
||||
* const model = monaco.editor.createModel(initCode, "apex", uri);
|
||||
* model.onDidChangeContent(() => plantumlFeatures.validateCode(model));
|
||||
* ```
|
||||
*/
|
||||
const PlantUmlLanguageFeatures = (function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Create Monaco Editor PlantUML Language Features instance.
|
||||
*
|
||||
* @param {object} [options] global instance options
|
||||
*/
|
||||
function PlantUmlLanguageFeatures({
|
||||
baseUrl = "",
|
||||
languageSelector = ["apex", "plantuml"],
|
||||
initialize = true
|
||||
} = {}) {
|
||||
|
||||
const validationEventListeners = {};
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == PlantUML valdation methods ==
|
||||
|
||||
/**
|
||||
* Add validation event listener.
|
||||
*
|
||||
* Validation Event Order:
|
||||
* before -> code -> line -> after
|
||||
*
|
||||
* @param {("before"|"code"|"line"|"after")} type before|code|line|after event type
|
||||
* @param {(event: any) => Promise<editor.IMarkerData>|editor.IMarkerData|Promise<editor.IMarkerData[]>|editor.IMarkerData[]|Promise<void>|void} listener event listener
|
||||
*/
|
||||
this.addValidationEventListener = (type, listener) => {
|
||||
if (!["before", "code", "line", "after"].includes(type)) {
|
||||
throw Error("Unknown validation event type: " + type);
|
||||
}
|
||||
validationEventListeners[type] = validationEventListeners[type] || [];
|
||||
validationEventListeners[type].push(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate PlantUML language of monaco editor model.
|
||||
*
|
||||
* @param {editor.ITextModel} model editor model to validate
|
||||
*
|
||||
* @returns editor markers as promise
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* validateCode(editor.getModel())
|
||||
* .then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers));
|
||||
* ```
|
||||
*/
|
||||
this.validateCode = async (model) => {
|
||||
const promises = [];
|
||||
|
||||
// raise before events
|
||||
promises.push(validationEventListeners.before?.map(listener => listener({ model })));
|
||||
|
||||
// raise code events
|
||||
promises.push(validationEventListeners.code?.map(listener => listener({ model, code: model.getValue() })));
|
||||
|
||||
if (validationEventListeners.line && validationEventListeners.line.length > 0) {
|
||||
// NOTE: lines and columns start at 1
|
||||
const lineCount = model.getLineCount();
|
||||
for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
|
||||
const range = {
|
||||
startLineNumber: lineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: lineNumber,
|
||||
endColumn: model.getLineLength(lineNumber) + 1,
|
||||
};
|
||||
const line = model.getValueInRange(range);
|
||||
// raise line events
|
||||
promises.push(validationEventListeners.line?.map(listener => listener({ model, range, line, lineNumber, lineCount })));
|
||||
}
|
||||
}
|
||||
|
||||
// raise after events
|
||||
promises.push(validationEventListeners.after?.map(listener => listener({ model })));
|
||||
|
||||
// collect all markers and ...
|
||||
// - since each event can results in an array of markers -> `flat(1)`
|
||||
// - since not each event has to results in markers and can be `undef
|
||||
return Promise.all(promises).then(results => results.flat(1).filter(marker => marker));
|
||||
};
|
||||
|
||||
/**
|
||||
* Add PlantUML `@start` and `@end` command validation.
|
||||
*/
|
||||
this.addStartEndValidationListeners = () => {
|
||||
let diagramType = undefined;
|
||||
let startCounter = 0;
|
||||
let endCounter = 0;
|
||||
|
||||
// reset validation cache
|
||||
this.addValidationEventListener("before", () => {
|
||||
diagramType = undefined;
|
||||
startCounter = 0;
|
||||
endCounter = 0;
|
||||
});
|
||||
|
||||
// @start should be the first command
|
||||
this.addValidationEventListener("code", ({ model, code }) => {
|
||||
const match = code.match(/^(('.*)|\s)*@start(?<type>\w+)/);
|
||||
if (match) {
|
||||
diagramType = match.groups.type;
|
||||
return; // diagram code starts with a `@start`
|
||||
}
|
||||
return {
|
||||
message: "PlantUML diagrams should begin with the `@start` command and `@start` should also be the first command.",
|
||||
severity: monaco.MarkerSeverity.Warning,
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: model.getLineLength(1) + 1,
|
||||
};
|
||||
});
|
||||
|
||||
// @end should be the last command and should be of the same type (e.g. @startjson ... @endjson)
|
||||
this.addValidationEventListener("code", ({ model, code }) => {
|
||||
const lineCount = model.getLineCount();
|
||||
const match = code.match(/\s+@end(?<type>\w+)(('.*)|\s)*$/);
|
||||
if (match) {
|
||||
if (diagramType === match.groups.type) {
|
||||
return; // diagram code ends with a `@end` of the same type as the `@start`
|
||||
}
|
||||
return {
|
||||
message: "PlantUML diagrams should start and end with the type.\nExample: `@startjson ... @endjson`",
|
||||
severity: monaco.MarkerSeverity.Error,
|
||||
startLineNumber: lineCount,
|
||||
startColumn: 1,
|
||||
endLineNumber: lineCount,
|
||||
endColumn: model.getLineLength(lineCount) + 1,
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: "PlantUML diagrams should end with the `@end` command and `@end` should also be the last command.",
|
||||
severity: monaco.MarkerSeverity.Warning,
|
||||
startLineNumber: lineCount,
|
||||
startColumn: 1,
|
||||
endLineNumber: lineCount,
|
||||
endColumn: model.getLineLength(lineCount) + 1,
|
||||
};
|
||||
});
|
||||
|
||||
// @start should only be used once
|
||||
this.addValidationEventListener("line", ({ range, line }) => {
|
||||
const match = line.match(/^\s*@start(?<type>\w+)(\s+.*)?$/);
|
||||
if (!match) return;
|
||||
|
||||
startCounter += 1;
|
||||
if (startCounter > 1) {
|
||||
const word = "@start" + match.groups.type;
|
||||
const wordIndex = line.indexOf(word);
|
||||
return {
|
||||
message: "Multiple @start commands detected.",
|
||||
severity: monaco.MarkerSeverity.Warning,
|
||||
startLineNumber: range.startLineNumber,
|
||||
startColumn: wordIndex + 1,
|
||||
endLineNumber: range.endLineNumber,
|
||||
endColumn: wordIndex + word.length + 1,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// @end should only be used once
|
||||
this.addValidationEventListener("line", ({ range, line }) => {
|
||||
const match = line.match(/^\s*@end(?<type>\w+)(\s+.*)?$/);
|
||||
if (!match) return;
|
||||
|
||||
endCounter += 1;
|
||||
if (endCounter > 1) {
|
||||
const word = "@end" + match.groups.type;
|
||||
const wordIndex = line.indexOf(word);
|
||||
return {
|
||||
message: "Multiple @end commands detected.",
|
||||
severity: monaco.MarkerSeverity.Warning,
|
||||
startLineNumber: range.startLineNumber,
|
||||
startColumn: wordIndex + 1,
|
||||
endLineNumber: range.endLineNumber,
|
||||
endColumn: wordIndex + word.length + 1,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == PlantUML code completion methods ==
|
||||
|
||||
this.registerThemeCompletion = () => {
|
||||
const createThemeProposals = async (range, filter = undefined) => {
|
||||
const themes = await this.getThemes();
|
||||
return themes?.filter(theme => filter ? theme.includes(filter) : true)
|
||||
.map(theme => ({
|
||||
label: theme,
|
||||
kind: monaco.languages.CompletionItemKind.Text,
|
||||
documentation: "PlantUML " + theme + " theme",
|
||||
insertText: theme,
|
||||
range: range,
|
||||
})) || [];
|
||||
};
|
||||
|
||||
monaco.languages.registerCompletionItemProvider(languageSelector, {
|
||||
triggerCharacters: [" "],
|
||||
provideCompletionItems: async (model, position) => {
|
||||
const textUntilPosition = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column,
|
||||
});
|
||||
if (textUntilPosition.match(/^\s*!(t(h(e(m(e)?)?)?)?)?$/)) {
|
||||
return {
|
||||
suggestions: [
|
||||
{
|
||||
label: 'theme',
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
documentation: "PlantUML theme command",
|
||||
insertText: 'theme',
|
||||
range: getWordRange(model, position),
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
const match = textUntilPosition.match(/^\s*!theme\s+(?<theme>[^\s]*)$/);
|
||||
if (match) {
|
||||
const suggestions = await createThemeProposals(getWordRange(model, position), match.groups.theme);
|
||||
return { suggestions };
|
||||
}
|
||||
return { suggestions: [] };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.registerIconCompletion = () => {
|
||||
const createIconProposals = async (range, filter = undefined) => {
|
||||
const icons = await this.getIcons();
|
||||
return icons?.filter(icon => filter ? icon.includes(filter) : true)
|
||||
.map(icon => {
|
||||
// NOTE: markdown image path inside suggestions seems to have rendering issues while using relative paths
|
||||
const iconUrl = this.resolvePath(baseUrl + "ui-helper?request=icons.svg#" + icon);
|
||||
return {
|
||||
label: icon,
|
||||
kind: monaco.languages.CompletionItemKind.Constant,
|
||||
documentation: {
|
||||
//supportHtml: true, // also a possibility but quite limited html
|
||||
value: "![icon](" + iconUrl + ") " + icon
|
||||
},
|
||||
insertText: icon + ">",
|
||||
range: range
|
||||
};
|
||||
}) || [];
|
||||
};
|
||||
|
||||
monaco.languages.registerCompletionItemProvider(languageSelector, {
|
||||
triggerCharacters: ["&"],
|
||||
provideCompletionItems: async (model, position) => {
|
||||
const textUntilPosition = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column,
|
||||
});
|
||||
const match = textUntilPosition.match(/<&(?<icon>[^\s>]*)$/);
|
||||
if (match) {
|
||||
const suggestions = await createIconProposals(getWordRange(model, position), match.groups.icon);
|
||||
return { suggestions };
|
||||
}
|
||||
return { suggestions: [] };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == helper methods ==
|
||||
|
||||
this.resolvePath = (path) => {
|
||||
if (path.startsWith("http")) return path;
|
||||
if (path.startsWith("/")) return window.location.origin + path;
|
||||
|
||||
if (path.slice(0, 2) == "./") path = path.slice(2);
|
||||
let base = (document.querySelector("base") || {}).href || window.location.origin;
|
||||
if (base.slice(-1) == "/") base = base.slice(0, -1);
|
||||
return base + "/" + path;
|
||||
};
|
||||
|
||||
this.getIcons = (function(){
|
||||
let icons = undefined;
|
||||
return async () => {
|
||||
if (icons === undefined) {
|
||||
icons = await makeRequest("GET", "ui-helper?request=icons", { responseType: "json" });
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
})();
|
||||
|
||||
this.getThemes = (function(){
|
||||
let themes = undefined;
|
||||
return async () => {
|
||||
if (themes === undefined) {
|
||||
themes = await makeRequest("GET", "ui-helper?request=themes", { responseType: "json" });
|
||||
}
|
||||
return themes;
|
||||
}
|
||||
})();
|
||||
|
||||
const makeRequest = (
|
||||
method,
|
||||
url,
|
||||
{ data = null, headers = { "Content-Type": "text/plain" }, responseType = "text", ignoreBaseUrl = false } = {}
|
||||
) => {
|
||||
const targetUrl = (ignoreBaseUrl === true) ? url : baseUrl + url;
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status >= 200 && xhr.status <= 300) {
|
||||
if (responseType === "json") {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
resolve(xhr.responseText);
|
||||
}
|
||||
} else {
|
||||
if (responseType === "json") {
|
||||
reject({ status: xhr.status, response: xhr.response });
|
||||
} else {
|
||||
reject({ status: xhr.status, responseText: xhr.responseText });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open(method, targetUrl, true);
|
||||
xhr.responseType = responseType;
|
||||
headers && Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]));
|
||||
xhr.send(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getWordRange = (model, position) => {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
return {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================================================
|
||||
// == constructor running code ==
|
||||
|
||||
// prepare base URL
|
||||
if (baseUrl === null || baseUrl === undefined) {
|
||||
baseUrl = "";
|
||||
} else if (baseUrl !== "") {
|
||||
// add tailing "/"
|
||||
if (baseUrl.slice(-1) !== "/") baseUrl = baseUrl + "/";
|
||||
}
|
||||
|
||||
// initialize default validation and code completion
|
||||
if (initialize) {
|
||||
this.addStartEndValidationListeners();
|
||||
this.registerThemeCompletion();
|
||||
this.registerIconCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
return PlantUmlLanguageFeatures;
|
||||
})();
|
26
src/main/webapp/previewer.jsp
Normal file
@ -0,0 +1,26 @@
|
||||
<%@ page info="index" contentType="text/html; charset=utf-8" pageEncoding="utf-8" session="false" %>
|
||||
<%
|
||||
// diagram sources
|
||||
String encoded = request.getAttribute("encoded").toString();
|
||||
String index = request.getAttribute("index").toString();
|
||||
String diagramUrl = ((index.isEmpty()) ? "" : index + "/") + encoded;
|
||||
// map for diagram source if necessary
|
||||
String map = request.getAttribute("map").toString();
|
||||
boolean hasMap = !map.isEmpty();
|
||||
// properties
|
||||
boolean showSocialButtons = (boolean)request.getAttribute("showSocialButtons");
|
||||
%>
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<%@ include file="resource/htmlheadbase.jsp" %>
|
||||
<title>PlantUML Server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content viewer-content">
|
||||
<%-- Preview --%>
|
||||
<%@ include file="resource/preview.jsp" %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1
src/main/webapp/resource/footer.jsp
Normal file
@ -0,0 +1 @@
|
||||
<p><%= net.sourceforge.plantuml.version.Version.fullDescription() %></p>
|
@ -1 +1,17 @@
|
||||
<a href="https://github.com/plantuml/plantuml-server"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://github-camo.global.ssl.fastly.net/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"></a>
|
||||
<div>
|
||||
<img
|
||||
style="display: inline; position: absolute; top: 0; right: 0; border: 0; max-width: 35%;"
|
||||
class="no-filter"
|
||||
src="assets/github-fork-me.png"
|
||||
alt="Fork me on GitHub"
|
||||
usemap="#github-banner"
|
||||
/>
|
||||
<map id="github-banner" name="github-banner" style="cursor: pointer;">
|
||||
<area
|
||||
shape="poly"
|
||||
coords="15,0 59,0 149,90 149,134"
|
||||
href="https://github.com/plantuml/plantuml-server"
|
||||
alt="Fork me on GitHub"
|
||||
/>
|
||||
</map>
|
||||
</div>
|
16
src/main/webapp/resource/htmlheadbase.jsp
Normal file
@ -0,0 +1,16 @@
|
||||
<base href="<%= request.getContextPath() %>/" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache, must-revalidate" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon"/>
|
||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
|
||||
<link rel="stylesheet" href="plantuml.css" />
|
||||
<script src="webjars/monaco-editor/0.36.1/min/vs/loader.js"></script>
|
||||
<script src="plantumllanguage.js"></script>
|
||||
<script src="plantuml.js"></script>
|
||||
<script>
|
||||
const VERSION = <%= net.sourceforge.plantuml.version.Version.version() %>;
|
||||
const VERSION_STRING = "<%= net.sourceforge.plantuml.version.Version.versionString() %>";
|
||||
</script>
|
89
src/main/webapp/resource/preview.jsp
Normal file
@ -0,0 +1,89 @@
|
||||
<div class="previewer-container flex-rows">
|
||||
<div class="preview-menu">
|
||||
<div class="diagram-links flex-columns">
|
||||
<span>View as:</span>
|
||||
<a class="diagram-link" data-img-type="png" href="png/<%= diagramUrl %>" title="View diagram as PNG">
|
||||
<img src="assets/file-types/png.svg" alt="PNG" />
|
||||
</a>
|
||||
<a class="diagram-link" data-img-type="svg" href="svg/<%= diagramUrl %>" title="View diagram as SVG">
|
||||
<img src="assets/file-types/svg.svg" alt="SVG" />
|
||||
</a>
|
||||
<a class="diagram-link" data-img-type="txt" href="txt/<%= diagramUrl %>" title="View diagram as ASCII Art">
|
||||
<img src="assets/file-types/ascii.svg" alt="ASCII Art" />
|
||||
</a>
|
||||
<a class="diagram-link" data-img-type="pdf" href="pdf/<%= diagramUrl %>" title="View diagram as PDF">
|
||||
<img src="assets/file-types/pdf.svg" alt="PDF" />
|
||||
</a>
|
||||
<a
|
||||
id="map-diagram-link"
|
||||
class="diagram-link"
|
||||
data-img-type="map"
|
||||
href="map/<%= diagramUrl %>"
|
||||
title="View diagram as Map Data"
|
||||
<% if (!hasMap) { %>
|
||||
style="display: none;"
|
||||
<% } %>
|
||||
>
|
||||
<img src="assets/file-types/map.svg" alt="MAP" />
|
||||
</a>
|
||||
<div class="flex-main menu-r">
|
||||
<div class="btn-float-r">
|
||||
<input
|
||||
id="btn-settings"
|
||||
class="btn-settings"
|
||||
type="image"
|
||||
src="assets/settings.svg"
|
||||
alt="settings"
|
||||
onclick="openSettings();"
|
||||
/>
|
||||
<input
|
||||
id="btn-undock"
|
||||
class="btn-dock"
|
||||
type="image"
|
||||
src="assets/undock.svg"
|
||||
alt="undock"
|
||||
onclick="undock();"
|
||||
/>
|
||||
<input
|
||||
id="btn-dock"
|
||||
class="btn-dock"
|
||||
type="image"
|
||||
src="assets/dock.svg"
|
||||
alt="dock"
|
||||
onclick="window.close();"
|
||||
style="display: none;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr"></div>
|
||||
<div id="paginator" data-number-of-diagram-pages="1" style="display: none;"></div>
|
||||
<div class="previewer-main flex-main">
|
||||
<div id="diagram" class="diagram">
|
||||
<div>
|
||||
<!-- PNG -->
|
||||
<img id="diagram-png" src="png/<%= diagramUrl %>" alt="PlantUML diagram" usemap="#plantuml_map" />
|
||||
<% if (hasMap) { %>
|
||||
<%= map %>
|
||||
<% } else { %>
|
||||
<map id="plantuml_map" name="plantuml_map"></map>
|
||||
<% } %>
|
||||
<!-- SVG -->
|
||||
<svg id="diagram-svg" style="display: none;"></svg>
|
||||
<!-- ASCII Art -->
|
||||
<pre id="diagram-txt" style="display: none;"></pre>
|
||||
<!-- PDF -->
|
||||
<object id="diagram-pdf" data="" type="application/pdf" width="100%" height="100%" style="display: none;">
|
||||
<p>Unable to display PDF file.</p>
|
||||
</object>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% if (showSocialButtons) { %>
|
||||
<div>
|
||||
<%@ include file="socialbuttons2.jsp" %>
|
||||
</div>
|
||||
<% } %>
|
||||
<%@ include file="settings.jsp" %>
|
||||
</div>
|
39
src/main/webapp/resource/settings.jsp
Normal file
@ -0,0 +1,39 @@
|
||||
<div id="settings" class="modal" style="display: none;" tabindex="-1">
|
||||
<div class="modal-content flex-rows">
|
||||
<div class="settings-header">
|
||||
<h2>Settings</h2>
|
||||
<div class="hr"></div>
|
||||
</div>
|
||||
<div class="settings-main flex-main">
|
||||
<div class="setting flex-columns">
|
||||
<label for="theme">Theme:</label>
|
||||
<select class="flex-main" id="theme" name="theme">
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting flex-columns">
|
||||
<label for="diagramPreviewType">Diagram Preview Type:</label>
|
||||
<select class="flex-main" id="diagramPreviewType" name="diagramPreviewType">
|
||||
<option value="png">PNG</option>
|
||||
<option value="svg">SVG</option>
|
||||
<option value="txt">ASCII Art</option>
|
||||
<option value="pdf">PDF</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting flex-columns">
|
||||
<label for="editorWatcherTimeout">Editor Watcher Timeout:</label>
|
||||
<input class="flex-main" id="editorWatcherTimeout" type="number" pattern="[1-9]+[0-9]*" value="" />
|
||||
</div>
|
||||
<div class="setting flex-main">
|
||||
<label for="editorCreateOptions">Monaco Editor Create Options:</label>
|
||||
<br />
|
||||
<div id="settings-monaco-editor"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<input class="ok" type="button" value="Save" onclick="saveSettings();" />
|
||||
<input class="cancel" type="button" value="Cancel" onclick="closeSettings();" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -8,7 +8,7 @@ public class AllTests extends TestSuite {
|
||||
public static Test suite() {
|
||||
TestSuite suite = new TestSuite(AllTests.class.getName());
|
||||
// $JUnit-BEGIN$
|
||||
suite.addTestSuite(TestForm.class);
|
||||
suite.addTestSuite(TestWebUI.class);
|
||||
suite.addTestSuite(TestImage.class);
|
||||
suite.addTestSuite(TestAsciiArt.class);
|
||||
suite.addTestSuite(TestSVG.class);
|
||||
|
@ -4,6 +4,9 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.TestUtils;
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappTestCase;
|
||||
|
||||
|
||||
public class TestAsciiArt extends WebappTestCase {
|
||||
|
||||
|
@ -6,6 +6,9 @@ import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.TestUtils;
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappTestCase;
|
||||
|
||||
|
||||
public class TestAsciiCoder extends WebappTestCase {
|
||||
|
||||
|
@ -4,6 +4,8 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappTestCase;
|
||||
|
||||
|
||||
public class TestCharset extends WebappTestCase {
|
||||
|
||||
|
@ -4,6 +4,9 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.TestUtils;
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappTestCase;
|
||||
|
||||
|
||||
public class TestCheck extends WebappTestCase {
|
||||
|
||||
|
@ -1,303 +0,0 @@
|
||||
package net.sourceforge.plantuml.servlet;
|
||||
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.DomElement;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlForm;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlImage;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlInput;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlTextArea;
|
||||
|
||||
|
||||
public class TestForm extends WebappTestCase {
|
||||
|
||||
/**
|
||||
* Verifies that the welcome page has exactly two form with the Bob --> Alice sample
|
||||
*/
|
||||
public void testWelcomePage() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getServerUrl());
|
||||
// Analyze response
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals(TestUtils.SEQBOBCODE, text);
|
||||
// Ensure the URL field is correct
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/" + TestUtils.SEQBOB));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
assertNotEquals(0, img.getImageReader().getHeight(0)); // 131
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 120
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the version image is generated
|
||||
*/
|
||||
public void testVersion() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getServerUrl());
|
||||
page.initialize();
|
||||
// Fill the form and submit it
|
||||
page.executeJavaScript("document.myCodeMirror.setValue('version')");
|
||||
HtmlForm form = page.getForms().get(0);
|
||||
HtmlSubmitInput btn = form.getFirstByXPath("//input[contains(@type, 'submit')]");
|
||||
page = btn.click();
|
||||
// Analyze response
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals(TestUtils.VERSIONCODE, text);
|
||||
// Ensure the URL field is correct
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/" + TestUtils.VERSION));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
assertNotEquals(0, img.getImageReader().getHeight(0)); // 186
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 519
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when the UML text is empty, default page and image is generated
|
||||
*/
|
||||
public void testEmptyText() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getServerUrl());
|
||||
page.initialize();
|
||||
// Fill the form and submit it
|
||||
page.executeJavaScript("document.myCodeMirror.setValue('')");
|
||||
HtmlForm form = page.getForms().get(0);
|
||||
HtmlSubmitInput btn = form.getFirstByXPath("//input[contains(@type, 'submit')]");
|
||||
page = btn.click();
|
||||
// Analyze response
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals(TestUtils.SEQBOBCODE, text);
|
||||
// Ensure the URL field is correct
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/" + TestUtils.SEQBOB));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
assertNotEquals(0, img.getImageReader().getHeight(0)); // 131
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 120
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when the encoded URL is empty, default page and image is generated
|
||||
*/
|
||||
public void testEmptyUrl() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getServerUrl());
|
||||
page.initialize();
|
||||
// Fill the form and submit it
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
url.setAttribute("value", "");
|
||||
HtmlSubmitInput btn = forms.get(1).getFirstByXPath("//input[contains(@type, 'submit')]");
|
||||
page = btn.click();
|
||||
// Analyze response
|
||||
forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals(TestUtils.SEQBOBCODE, text);
|
||||
// Ensure the URL field is correct
|
||||
url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/" + TestUtils.SEQBOB));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
assertNotEquals(0, img.getImageReader().getHeight(0)); // 131
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 120
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a ditaa diagram is generated
|
||||
*/
|
||||
public void testDitaaText() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getServerUrl());
|
||||
page.initialize();
|
||||
// Fill the form and submit it
|
||||
page.executeJavaScript("document.myCodeMirror.setValue(`@startditaa \n*--> \n@endditaa`)");
|
||||
HtmlForm form = page.getForms().get(0);
|
||||
HtmlSubmitInput btn = form.getFirstByXPath("//input[contains(@type, 'submit')]");
|
||||
page = btn.click();
|
||||
// Analyze response
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals("@startditaa \n*--> \n@endditaa", text);
|
||||
// Ensure the URL field is correct
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/SoWkIImgISaiIKnKuDBIrRLJu798pKi12m00"));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
assertNotEquals(0, img.getImageReader().getHeight(0)); // 70
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 90
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an image map is produced if the diagram contains a link
|
||||
*/
|
||||
public void testImageMap() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getServerUrl());
|
||||
page.initialize();
|
||||
// Fill the form and submit it
|
||||
page.executeJavaScript("document.myCodeMirror.setValue(`@startuml\nBob -> Alice : [[http://yahoo.com]] Hello\n@enduml`)");
|
||||
HtmlForm form = page.getForms().get(0);
|
||||
HtmlSubmitInput btn = form.getFirstByXPath("//input[contains(@type, 'submit')]");
|
||||
page = btn.click();
|
||||
// Analyze response
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals("@startuml\nBob -> Alice : [[http://yahoo.com]] Hello\n@enduml", text);
|
||||
// Ensure the URL field is correct
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/SyfFKj2rKt3CoKnELR1IY8xEA2afiDBNhqpCoC_NIyxFZOrLy4ZDoSa70000"));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
assertNotEquals(0, img.getImageReader().getHeight(0)); // 131
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 231
|
||||
// Ensure the image map is present
|
||||
DomElement map = page.getElementById("plantuml_map");
|
||||
assertNotNull(map);
|
||||
assertEquals(1, map.getChildElementCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when the encoded source is specified as an URL parameter
|
||||
* the diagram is displayed and the source is decoded
|
||||
*/
|
||||
public void testUrlParameter() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
// Submit the request with a url parameter
|
||||
HtmlPage page = webClient.getPage(getServerUrl() + "/form?url=" + TestUtils.SEQBOB);
|
||||
page.initialize();
|
||||
// Analyze response
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals(TestUtils.SEQBOBCODE, text);
|
||||
// Ensure the URL field is correct
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/" + TestUtils.SEQBOB));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
assertNotEquals(0, img.getImageReader().getHeight(0)); // 131
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 120
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an multipage diagram renders correct given index.
|
||||
*
|
||||
* Bob -> Alice : hello
|
||||
* newpage
|
||||
* Bob <- Alice : hello
|
||||
* Bob -> Alice : let's talk
|
||||
* Bob <- Alice : better not
|
||||
* Bob -> Alice : <&rain> bye
|
||||
* newpage
|
||||
* Bob <- Alice : bye
|
||||
*/
|
||||
public void testIndexPage() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(
|
||||
getServerUrl() + "/uml/1/" +
|
||||
"SyfFKj2rKt3CoKnELR1Io4ZDoSddoaijBqXCJ-Lo0ahQwA99Eg7go4ajKIzMA4dCoKPNdfHQKf9Qf92NNuAknqQjA34ppquXgJ8Lbrr0AG00"
|
||||
);
|
||||
// Analyze response
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals(
|
||||
"@startuml\nBob -> Alice : hello\nnewpage\nBob <- Alice : hello\nBob -> Alice : let's talk\nBob <- Alice : better not\nBob -> Alice : <&rain> bye\nnewpage\nBob <- Alice : bye\n@enduml",
|
||||
text
|
||||
);
|
||||
// Ensure the URL field is correct
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/1/SyfFKj2rKt3CoKnELR1Io4ZDoSddoaijBqXCJ-Lo0ahQwA99Eg7go4ajKIzMA4dCoKPNdfHQKf9Qf92NNuAknqQjA34ppquXgJ8Lbrr0AG00"));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
int height = img.getImageReader().getHeight(0);
|
||||
assertNotEquals(0, height); // 222
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 152
|
||||
// Ensure the correct index was generated
|
||||
assertTrue(height > 200); // 222
|
||||
assertTrue(height < 250); // 222
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an multipage diagram renders correct even if no index is specified.
|
||||
*
|
||||
* Bob -> Alice : hello
|
||||
* newpage
|
||||
* Bob <- Alice : hello
|
||||
* Bob -> Alice : let's talk
|
||||
* Bob <- Alice : better not
|
||||
* Bob -> Alice : <&rain> bye
|
||||
* newpage
|
||||
* Bob <- Alice : bye
|
||||
*/
|
||||
public void testIndexPageWithNoDefinedIndex() throws IOException {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(
|
||||
getServerUrl() + "/uml/" +
|
||||
"SyfFKj2rKt3CoKnELR1Io4ZDoSddoaijBqXCJ-Lo0ahQwA99Eg7go4ajKIzMA4dCoKPNdfHQKf9Qf92NNuAknqQjA34ppquXgJ8Lbrr0AG00"
|
||||
);
|
||||
// Analyze response
|
||||
List<HtmlForm> forms = page.getForms();
|
||||
assertEquals(2, forms.size());
|
||||
// Ensure the Text field is correct
|
||||
String text = ((HtmlTextArea)(forms.get(0).getFirstByXPath("//textarea[contains(@name, 'text')]"))).getTextContent();
|
||||
assertEquals(
|
||||
"@startuml\nBob -> Alice : hello\nnewpage\nBob <- Alice : hello\nBob -> Alice : let's talk\nBob <- Alice : better not\nBob -> Alice : <&rain> bye\nnewpage\nBob <- Alice : bye\n@enduml",
|
||||
text
|
||||
);
|
||||
// Ensure the URL field is correct
|
||||
HtmlInput url = forms.get(1).getInputByName("url");
|
||||
assertNotNull(url);
|
||||
assertTrue(url.getAttribute("value").endsWith("/png/SyfFKj2rKt3CoKnELR1Io4ZDoSddoaijBqXCJ-Lo0ahQwA99Eg7go4ajKIzMA4dCoKPNdfHQKf9Qf92NNuAknqQjA34ppquXgJ8Lbrr0AG00"));
|
||||
// Ensure the generated image is present
|
||||
HtmlImage img = page.getFirstByXPath("//img[contains(@alt, 'PlantUML diagram')]");
|
||||
int height = img.getImageReader().getHeight(0);
|
||||
assertNotEquals(0, height); // 132
|
||||
assertNotEquals(0, img.getImageReader().getWidth(0)); // 152
|
||||
// Ensure the correct index was generated
|
||||
assertTrue(height > 100); // 132
|
||||
assertTrue(height < 150); // 132
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -8,6 +8,9 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.TestUtils;
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappTestCase;
|
||||
|
||||
|
||||
public class TestImage extends WebappTestCase {
|
||||
|
||||
|
@ -3,6 +3,8 @@ package net.sourceforge.plantuml.servlet;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappTestCase;
|
||||
|
||||
|
||||
public class TestLanguage extends WebappTestCase {
|
||||
|
||||
|
@ -4,6 +4,9 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.TestUtils;
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappTestCase;
|
||||
|
||||
|
||||
public class TestMap extends WebappTestCase {
|
||||
|
||||
|
@ -8,6 +8,9 @@ import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Scanner;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.TestUtils;
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappTestCase;
|
||||
|
||||
|
||||
public class TestSVG extends WebappTestCase {
|
||||
|
||||
|
209
src/test/java/net/sourceforge/plantuml/servlet/TestWebUI.java
Normal file
@ -0,0 +1,209 @@
|
||||
package net.sourceforge.plantuml.servlet;
|
||||
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Dimension;
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.utils.TestUtils;
|
||||
import net.sourceforge.plantuml.servlet.utils.WebappUITestCase;
|
||||
|
||||
|
||||
public class TestWebUI extends WebappUITestCase {
|
||||
|
||||
/**
|
||||
* Verifies that the welcome page has exactly two form with the Bob --> Alice sample
|
||||
*/
|
||||
public void testWelcomePage() {
|
||||
driver.get(getServerUrl());
|
||||
assertTrue("UI loading completed", waitUntilUIIsLoaded());
|
||||
// ensure the editor text is correct
|
||||
String text = getEditorValue();
|
||||
assertEquals(TestUtils.SEQBOBCODE, text);
|
||||
// ensure the URL field is correct
|
||||
String url = getURLValue();
|
||||
assertTrue(url.endsWith("/png/" + TestUtils.SEQBOB));
|
||||
// ensure the generated image is present
|
||||
Dimension dim = getImageSize();
|
||||
assertNotEquals(0, dim.getHeight()); // 145
|
||||
assertNotEquals(0, dim.getWidth()); // 134
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the version image is generated
|
||||
*/
|
||||
public void testVersion() {
|
||||
driver.get(getServerUrl());
|
||||
assertTrue("UI loading completed", waitUntilUIIsLoaded());
|
||||
// change code and observe result
|
||||
setEditorValue(TestUtils.VERSIONCODE);
|
||||
assertTrue("Auto update done", waitUntilAutoRefreshCompleted());
|
||||
// ensure the editor text is correct
|
||||
String text = getEditorValue();
|
||||
assertEquals(TestUtils.VERSIONCODE, text);
|
||||
// ensure the URL field is correct
|
||||
String url = getURLValue();
|
||||
assertTrue(url.endsWith("/png/" + TestUtils.VERSION));
|
||||
// ensure the generated image is present
|
||||
Dimension dim = getImageSize();
|
||||
assertNotEquals(0, dim.getHeight()); // 242
|
||||
assertNotEquals(0, dim.getWidth()); // 472
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Verifies that when the UML text is empty, ...
|
||||
// * old behavior: default page and image is generated
|
||||
// */
|
||||
// public void testEmptyText() {
|
||||
// // ...
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Verifies that when the encoded URL is empty, ...
|
||||
// * old behavior: default page and image is generated
|
||||
// */
|
||||
// public void testEmptyUrl() {
|
||||
// // ...
|
||||
// }
|
||||
|
||||
/**
|
||||
* Verifies that a ditaa diagram is generated
|
||||
*/
|
||||
public void testDitaaText() {
|
||||
driver.get(getServerUrl());
|
||||
assertTrue("UI loading completed", waitUntilUIIsLoaded());
|
||||
// change code and observe result
|
||||
setEditorValue("@startditaa \n*--> \n@endditaa");
|
||||
assertTrue("Auto update done", waitUntilAutoRefreshCompleted());
|
||||
// ensure the editor text is correct
|
||||
String text = getEditorValue();
|
||||
assertEquals("@startditaa \n*--> \n@endditaa", text);
|
||||
// ensure the URL field is correct
|
||||
String url = getURLValue();
|
||||
assertTrue(url.endsWith("/png/SoWkIImgISaiIKnKuDBIrRLJu798pKi12m00"));
|
||||
// ensure the generated image is present
|
||||
Dimension dim = getImageSize();
|
||||
assertNotEquals(0, dim.getHeight());
|
||||
assertNotEquals(0, dim.getWidth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an image map is produced if the diagram contains a link
|
||||
*/
|
||||
public void testImageMap() {
|
||||
driver.get(getServerUrl());
|
||||
assertTrue("UI loading completed", waitUntilUIIsLoaded());
|
||||
// change code and observe result
|
||||
setEditorValue("@startuml\nBob -> Alice : [[http://yahoo.com]] Hello\n@enduml");
|
||||
assertTrue("Auto update done", waitUntilAutoRefreshCompleted());
|
||||
// ensure the editor text is correct
|
||||
String text = getEditorValue();
|
||||
assertEquals("@startuml\nBob -> Alice : [[http://yahoo.com]] Hello\n@enduml", text);
|
||||
// ensure the URL field is correct
|
||||
String url = getURLValue();
|
||||
assertTrue(url.endsWith("/png/SyfFKj2rKt3CoKnELR1IY8xEA2afiDBNhqpCoC_NIyxFZOrLy4ZDoSa70000"));
|
||||
// ensure the generated image is present
|
||||
Dimension dim = getImageSize();
|
||||
assertNotEquals(0, dim.getHeight());
|
||||
assertNotEquals(0, dim.getWidth());
|
||||
// ensure the image map is present
|
||||
WebElement map = getImageMap();
|
||||
assertNotNull(map);
|
||||
assertEquals(1, Integer.parseInt(map.getAttribute("childElementCount")));
|
||||
// ensure the map button is visible
|
||||
WebElement btnMap = driver.findElement(By.id("map-diagram-link"));
|
||||
assertTrue(btnMap.isDisplayed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when the encoded source is specified as an URL parameter
|
||||
* the diagram is displayed and the source is decoded
|
||||
*/
|
||||
public void testUrlParameter() {
|
||||
driver.get(getServerUrl() + "/form?url=" + TestUtils.SEQBOB);
|
||||
assertTrue("UI loading completed", waitUntilUIIsLoaded());
|
||||
// ensure the editor text is correct
|
||||
String text = getEditorValue();
|
||||
assertEquals(TestUtils.SEQBOBCODE, text);
|
||||
// ensure the URL field is correct
|
||||
String url = getURLValue();
|
||||
assertTrue(url.endsWith("/png/" + TestUtils.SEQBOB));
|
||||
// ensure the generated image is present
|
||||
Dimension dim = getImageSize();
|
||||
assertNotEquals(0, dim.getHeight());
|
||||
assertNotEquals(0, dim.getWidth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an multipage diagram renders correct given index.
|
||||
*
|
||||
* Bob -> Alice : hello
|
||||
* newpage
|
||||
* Bob <- Alice : hello
|
||||
* Bob -> Alice : let's talk
|
||||
* Bob <- Alice : better not
|
||||
* Bob -> Alice : <&rain> bye
|
||||
* newpage
|
||||
* Bob <- Alice : bye
|
||||
*/
|
||||
public void testIndexPage() {
|
||||
driver.get(
|
||||
getServerUrl() + "/uml/1/" +
|
||||
"SyfFKj2rKt3CoKnELR1Io4ZDoSddoaijBqXCJ-Lo0ahQwA99Eg7go4ajKIzMA4dCoKPNdfHQKf9Qf92NNuAknqQjA34ppquXgJ8Lbrr0AG00"
|
||||
);
|
||||
assertTrue("UI loading completed", waitUntilUIIsLoaded());
|
||||
// ensure the editor text is correct
|
||||
String text = getEditorValue();
|
||||
assertEquals(
|
||||
"@startuml\nBob -> Alice : hello\nnewpage\nBob <- Alice : hello\nBob -> Alice : let's talk\nBob <- Alice : better not\nBob -> Alice : <&rain> bye\nnewpage\nBob <- Alice : bye\n@enduml",
|
||||
text
|
||||
);
|
||||
// ensure the URL field is correct
|
||||
String url = getURLValue();
|
||||
assertTrue(url.endsWith("/png/1/SyfFKj2rKt3CoKnELR1Io4ZDoSddoaijBqXCJ-Lo0ahQwA99Eg7go4ajKIzMA4dCoKPNdfHQKf9Qf92NNuAknqQjA34ppquXgJ8Lbrr0AG00"));
|
||||
// ensure the generated image is present
|
||||
Dimension dim = getImageSize();
|
||||
assertNotEquals(0, dim.getHeight());
|
||||
assertNotEquals(0, dim.getWidth());
|
||||
// ensure the correct index was generated
|
||||
assertTrue(dim.getHeight() > 200); // 222
|
||||
assertTrue(dim.getHeight() < 250); // 222
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an multipage diagram renders correct even if no index is specified.
|
||||
*
|
||||
* Bob -> Alice : hello
|
||||
* newpage
|
||||
* Bob <- Alice : hello
|
||||
* Bob -> Alice : let's talk
|
||||
* Bob <- Alice : better not
|
||||
* Bob -> Alice : <&rain> bye
|
||||
* newpage
|
||||
* Bob <- Alice : bye
|
||||
*/
|
||||
public void testIndexPageWithNoDefinedIndex() {
|
||||
driver.get(
|
||||
getServerUrl() + "/uml/" +
|
||||
"SyfFKj2rKt3CoKnELR1Io4ZDoSddoaijBqXCJ-Lo0ahQwA99Eg7go4ajKIzMA4dCoKPNdfHQKf9Qf92NNuAknqQjA34ppquXgJ8Lbrr0AG00"
|
||||
);
|
||||
assertTrue("UI loading completed", waitUntilUIIsLoaded());
|
||||
// ensure the editor text is correct
|
||||
String text = getEditorValue();
|
||||
assertEquals(
|
||||
"@startuml\nBob -> Alice : hello\nnewpage\nBob <- Alice : hello\nBob -> Alice : let's talk\nBob <- Alice : better not\nBob -> Alice : <&rain> bye\nnewpage\nBob <- Alice : bye\n@enduml",
|
||||
text
|
||||
);
|
||||
// ensure the URL field is correct
|
||||
String url = getURLValue();
|
||||
assertTrue(url.endsWith("/png/SyfFKj2rKt3CoKnELR1Io4ZDoSddoaijBqXCJ-Lo0ahQwA99Eg7go4ajKIzMA4dCoKPNdfHQKf9Qf92NNuAknqQjA34ppquXgJ8Lbrr0AG00"));
|
||||
// ensure the generated image is present
|
||||
Dimension dim = getImageSize();
|
||||
assertNotEquals(0, dim.getHeight());
|
||||
assertNotEquals(0, dim.getWidth());
|
||||
// ensure the correct index was generated
|
||||
assertTrue(dim.getHeight() > 100); // 132
|
||||
assertTrue(dim.getHeight() < 150); // 132
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package net.sourceforge.plantuml.servlet.utils;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.openqa.selenium.Dimension;
|
||||
import org.openqa.selenium.PageLoadStrategy;
|
||||
import org.openqa.selenium.Point;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.openqa.selenium.edge.EdgeDriver;
|
||||
import org.openqa.selenium.edge.EdgeOptions;
|
||||
import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
import org.openqa.selenium.firefox.FirefoxOptions;
|
||||
|
||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||
|
||||
|
||||
public abstract class JUnitWebDriver {
|
||||
|
||||
public static final String browser;
|
||||
|
||||
static {
|
||||
browser = System.getProperty("system.test.browser", "firefox");
|
||||
}
|
||||
|
||||
public static WebDriver getDriver() {
|
||||
WebDriver driver;
|
||||
switch (browser.toLowerCase()) {
|
||||
case "chrome":
|
||||
driver = getChromeDriver();
|
||||
break;
|
||||
case "edge":
|
||||
driver = SystemUtils.IS_OS_WINDOWS ? getEdgeDriver() : getChromiumDriver();
|
||||
break;
|
||||
case "firefox":
|
||||
driver = getFirefoxDriver();
|
||||
break;
|
||||
default:
|
||||
driver = getChromiumDriver();
|
||||
}
|
||||
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
|
||||
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(10));
|
||||
driver.manage().window().setPosition(new Point(0, 0));
|
||||
driver.manage().window().setSize(new Dimension(1024, 768));
|
||||
return driver;
|
||||
}
|
||||
|
||||
private static WebDriver getChromiumDriver() {
|
||||
WebDriverManager.chromiumdriver().setup();
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
options.addArguments("--headless", "--no-sandbox", "--disable-gpu");
|
||||
options.setPageLoadStrategy(PageLoadStrategy.NONE);
|
||||
return new ChromeDriver(options);
|
||||
}
|
||||
|
||||
private static WebDriver getChromeDriver() {
|
||||
WebDriverManager.chromedriver().setup();
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
options.addArguments("--headless", "--no-sandbox", "--disable-gpu");
|
||||
options.setPageLoadStrategy(PageLoadStrategy.NONE);
|
||||
return new ChromeDriver(options);
|
||||
}
|
||||
|
||||
private static WebDriver getFirefoxDriver() {
|
||||
WebDriverManager.firefoxdriver().setup();
|
||||
FirefoxOptions options = new FirefoxOptions();
|
||||
options.addArguments("--headless");
|
||||
return new FirefoxDriver(options);
|
||||
}
|
||||
|
||||
private static WebDriver getEdgeDriver() {
|
||||
WebDriverManager.edgedriver().setup();
|
||||
EdgeOptions options = new EdgeOptions();
|
||||
options.addArguments("headless");
|
||||
return new EdgeDriver(options);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.sourceforge.plantuml.servlet;
|
||||
package net.sourceforge.plantuml.servlet.utils;
|
||||
|
||||
|
||||
/**
|
@ -1,4 +1,4 @@
|
||||
package net.sourceforge.plantuml.servlet;
|
||||
package net.sourceforge.plantuml.servlet.utils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@ -9,6 +9,7 @@ import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import net.sourceforge.plantuml.servlet.server.EmbeddedJettyServer;
|
||||
import net.sourceforge.plantuml.servlet.server.ExternalServer;
|
||||
import net.sourceforge.plantuml.servlet.server.ServerUtils;
|
||||
@ -23,27 +24,19 @@ public abstract class WebappTestCase extends TestCase {
|
||||
}
|
||||
|
||||
public WebappTestCase(String name) {
|
||||
super(name);
|
||||
// logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
String uri = System.getProperty("system.test.server", "");
|
||||
//uri = "http://localhost:8080/plantuml";
|
||||
if (!uri.isEmpty()) {
|
||||
// mvn test -DskipTests=false -DargLine="-Dsystem.test.server=http://localhost:8080/plantuml"
|
||||
// logger.info("Test against external server: " + uri);
|
||||
serverUtils = new ExternalServer(uri);
|
||||
return;
|
||||
}
|
||||
|
||||
// mvn test -DskipTests=false
|
||||
// logger.info("Test against embedded jetty server.");
|
||||
serverUtils = new EmbeddedJettyServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
serverUtils.startServer();
|
||||
// logger.info(getServerUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -51,7 +44,7 @@ public abstract class WebappTestCase extends TestCase {
|
||||
serverUtils.stopServer();
|
||||
}
|
||||
|
||||
protected String getServerUrl() {
|
||||
public String getServerUrl() {
|
||||
return serverUtils.getServerUrl();
|
||||
}
|
||||
|
||||
@ -104,5 +97,4 @@ public abstract class WebappTestCase extends TestCase {
|
||||
return byteStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package net.sourceforge.plantuml.servlet.utils;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Dimension;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.ui.ExpectedCondition;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
|
||||
|
||||
public abstract class WebappUITestCase extends WebappTestCase {
|
||||
|
||||
public WebDriver driver;
|
||||
public JavascriptExecutor js;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
driver = JUnitWebDriver.getDriver();
|
||||
js = (JavascriptExecutor)driver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
driver.close();
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
public boolean waitUntilJavascriptIsLoaded() {
|
||||
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));
|
||||
return wait.until(new ExpectedCondition<Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(WebDriver driver) {
|
||||
return js.executeScript("return document.readyState").toString().equals("complete");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean waitUntilEditorIsLoaded() {
|
||||
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));
|
||||
return wait.until(new ExpectedCondition<Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(WebDriver driver) {
|
||||
return js.executeScript("return document.editor === undefined").toString().equals("false");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean waitUntilAutoRefreshCompleted() {
|
||||
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
|
||||
return wait.until(new ExpectedCondition<Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(WebDriver driver) {
|
||||
return js.executeScript("return document.appConfig.autoRefreshState").toString().equals("complete");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean waitUntilUIIsLoaded() {
|
||||
return waitUntilEditorIsLoaded();
|
||||
}
|
||||
|
||||
public String getEditorValue() {
|
||||
return (String)js.executeScript("return document.editor.getValue();");
|
||||
}
|
||||
|
||||
public void setEditorValue(String code) {
|
||||
js.executeScript("return document.editor.getModel().setValue(`" + code.replace("`", "\\`") + "`);");
|
||||
}
|
||||
|
||||
public String getURLValue() {
|
||||
return driver.findElement(By.id("url")).getAttribute("value");
|
||||
}
|
||||
|
||||
public Dimension getImageSize() {
|
||||
WebElement img = driver.findElement(By.id("diagram-png"));
|
||||
return new Dimension(
|
||||
Integer.parseInt(img.getAttribute("width")),
|
||||
Integer.parseInt(img.getAttribute("height"))
|
||||
);
|
||||
// return driver.findElement(By.id("diagram-png")).getSize();
|
||||
}
|
||||
|
||||
public WebElement getImageMap() {
|
||||
return driver.findElement(By.id("plantuml_map"));
|
||||
}
|
||||
}
|