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
This commit is contained in:
Florian 2023-04-29 00:29:29 +02:00 committed by PlantUML
parent 763976abdd
commit f727c6dd13
46 changed files with 2638 additions and 761 deletions

19
.editorconfig Normal file
View 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
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -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
}

View File

@ -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
View File

@ -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>

View File

@ -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) {

View File

@ -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()));
}
}

View File

@ -1,237 +1,246 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:web="https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0"
>
<!-- ========================================================== -->
<!-- General -->
<!-- ========================================================== -->
<!-- Name the application -->
<display-name>PlantUML</display-name>
<description>PlantUML Online Server</description>
<!-- This app is cluster-ready -->
<distributable />
<!-- Set timeout to 120 minutes -->
<session-config>
<session-timeout>120</session-timeout>
</session-config>
<!-- ========================================================== -->
<!-- Custom Tag Libraries -->
<!-- ========================================================== -->
<!-- Taglib declarations are no longer required since JSP 2.0, see Removing taglib from web.xml -->
<!-- The <taglib> did not need to be a child of <jsp-config> in earlier versions but is required as of Tomcat 7 -->
<!-- Note that you can only have one <jsp-config> element per web.xml -->
<!--
<jsp-config>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/core</taglib-uri>
<taglib-location>/WEB-INF/lib/c.tld</taglib-location>
</taglib>
</jsp-config>
-->
<!-- ========================================================== -->
<!-- Context Parameters -->
<!-- ========================================================== -->
<context-param>
<param-name>org.eclipse.jetty.servlet.Default.welcomeServlets</param-name>
<param-value>exact</param-value>
</context-param>
<!-- ========================================================== -->
<!-- Servlets -->
<!-- ========================================================== -->
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.eclipse.jetty.jsp.JettyJspServlet</servlet-class>
<init-param>
<param-name>compilerSourceVM</param-name>
<param-value>1.7</param-value>
</init-param>
<init-param>
<param-name>compilerTargetVM</param-name>
<param-value>1.7</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>plantumlservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.PlantUmlServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>plantumlservlet</servlet-name>
<url-pattern>/welcome</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>plantumlservlet</servlet-name>
<url-pattern>/uml/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>plantumlservlet</servlet-name>
<url-pattern>/form</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>plantumlservlet</servlet-name>
<url-pattern>/start/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>imgservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.ImgServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>imgservlet</servlet-name>
<url-pattern>/png/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>imgservlet</servlet-name>
<url-pattern>/img/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>svgservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.SvgServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>svgservlet</servlet-name>
<url-pattern>/svg/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>pdfservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.PdfServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>pdfservlet</servlet-name>
<url-pattern>/pdf/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>epsservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.EpsServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>epsservlet</servlet-name>
<url-pattern>/eps/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>epstextservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.EpsTextServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>epstextservlet</servlet-name>
<url-pattern>/epstext/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>base64servlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.Base64Servlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>base64servlet</servlet-name>
<url-pattern>/base64/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>asciiservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.AsciiServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>asciiservlet</servlet-name>
<url-pattern>/txt/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>proxyservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.ProxyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>proxyservlet</servlet-name>
<url-pattern>/proxy</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>oldproxyservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.OldProxyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>oldproxyservlet</servlet-name>
<url-pattern>/proxy/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>mapservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.MapServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>mapservlet</servlet-name>
<url-pattern>/map/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>checkservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.CheckSyntaxServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>checkservlet</servlet-name>
<url-pattern>/check/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>languageservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.LanguageServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>languageservlet</servlet-name>
<url-pattern>/language</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>asciicoder</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.AsciiCoderServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>asciicoder</servlet-name>
<url-pattern>/coder/*</url-pattern>
</servlet-mapping>
<!-- ========================================================== -->
<!-- Error Handler -->
<!-- ========================================================== -->
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error.jsp</location>
</error-page>
<!-- ========================================================== -->
<!-- Welcome Files -->
<!-- ========================================================== -->
<welcome-file-list>
<welcome-file>welcome</welcome-file>
</welcome-file-list>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:web="https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0"
>
<!-- ========================================================== -->
<!-- General -->
<!-- ========================================================== -->
<!-- Name the application -->
<display-name>PlantUML</display-name>
<description>PlantUML Online Server</description>
<!-- This app is cluster-ready -->
<distributable />
<!-- Set timeout to 120 minutes -->
<session-config>
<session-timeout>120</session-timeout>
</session-config>
<!-- ========================================================== -->
<!-- Custom Tag Libraries -->
<!-- ========================================================== -->
<!-- Taglib declarations are no longer required since JSP 2.0, see Removing taglib from web.xml -->
<!-- The <taglib> did not need to be a child of <jsp-config> in earlier versions but is required as of Tomcat 7 -->
<!-- Note that you can only have one <jsp-config> element per web.xml -->
<!--
<jsp-config>
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/core</taglib-uri>
<taglib-location>/WEB-INF/lib/c.tld</taglib-location>
</taglib>
</jsp-config>
-->
<!-- ========================================================== -->
<!-- Context Parameters -->
<!-- ========================================================== -->
<context-param>
<param-name>org.eclipse.jetty.servlet.Default.welcomeServlets</param-name>
<param-value>exact</param-value>
</context-param>
<!-- ========================================================== -->
<!-- Servlets -->
<!-- ========================================================== -->
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.eclipse.jetty.jsp.JettyJspServlet</servlet-class>
<init-param>
<param-name>compilerSourceVM</param-name>
<param-value>1.8</param-value>
</init-param>
<init-param>
<param-name>compilerTargetVM</param-name>
<param-value>1.8</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>plantumlservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.PlantUmlServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>plantumlservlet</servlet-name>
<url-pattern>/welcome</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>plantumlservlet</servlet-name>
<url-pattern>/uml/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>plantumlservlet</servlet-name>
<url-pattern>/form</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>plantumlservlet</servlet-name>
<url-pattern>/start/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>imgservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.ImgServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>imgservlet</servlet-name>
<url-pattern>/png/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>imgservlet</servlet-name>
<url-pattern>/img/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>svgservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.SvgServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>svgservlet</servlet-name>
<url-pattern>/svg/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>pdfservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.PdfServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>pdfservlet</servlet-name>
<url-pattern>/pdf/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>epsservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.EpsServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>epsservlet</servlet-name>
<url-pattern>/eps/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>epstextservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.EpsTextServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>epstextservlet</servlet-name>
<url-pattern>/epstext/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>base64servlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.Base64Servlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>base64servlet</servlet-name>
<url-pattern>/base64/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>asciiservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.AsciiServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>asciiservlet</servlet-name>
<url-pattern>/txt/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>proxyservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.ProxyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>proxyservlet</servlet-name>
<url-pattern>/proxy</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>oldproxyservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.OldProxyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>oldproxyservlet</servlet-name>
<url-pattern>/proxy/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>mapservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.MapServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>mapservlet</servlet-name>
<url-pattern>/map/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>checkservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.CheckSyntaxServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>checkservlet</servlet-name>
<url-pattern>/check/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>languageservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.LanguageServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>languageservlet</servlet-name>
<url-pattern>/language</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>asciicoderservlet</servlet-name>
<servlet-class>net.sourceforge.plantuml.servlet.AsciiCoderServlet</servlet-class>
</servlet>
<servlet-mapping>
<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 -->
<!-- ========================================================== -->
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error.jsp</location>
</error-page>
<!-- ========================================================== -->
<!-- Welcome Files -->
<!-- ========================================================== -->
<welcome-file-list>
<welcome-file>welcome</welcome-file>
</welcome-file-list>
</web-app>

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View 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

View 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

View File

@ -1,4 +0,0 @@
<div id="footer">
<p>PlantUML Server Version <%= net.sourceforge.plantuml.version.Version.version() %>
</p>
</div>

View File

@ -1,93 +1,57 @@
<%@ page info="index" contentType="text/html; charset=utf-8" pageEncoding="utf-8" session="false" %>
<%
// diagram sources
String decoded = request.getAttribute("decoded").toString();
// 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();
// 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");
%>
<!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 --%>
<h1>PlantUML Server</h1>
<% if (showSocialButtons) { %>
<%@ include file="resource/socialbuttons1.html" %>
<% } %>
<% if (showGithubRibbon) { %>
<%@ include file="resource/githubribbon.html" %>
<% } %>
<p>Create your <a href="https://plantuml.com">PlantUML</a> diagrams directly in your browser!</p>
<div class="app flex-rows">
<div class="header">
<h1>PlantUML Server</h1>
<% if (showSocialButtons) { %>
<%@ include file="resource/socialbuttons1.html" %>
<% } %>
<% if (showGithubRibbon) { %>
<%@ include file="resource/githubribbon.html" %>
<% } %>
<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"/>&nbsp;
<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"/>&nbsp;
<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>&nbsp;
<a href="<%= svgurl %>" title="View diagram as SVG">View as SVG</a>&nbsp;
<a href="<%= txturl %>" title="View diagram as ASCII Art">View as ASCII Art</a>&nbsp;
<a href="<%= pdfurl %>" title="View diagram as PDF">View as PDF</a>&nbsp;
<% 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>
<%-- FOOTER --%>
<%@ include file="footer.jspf" %>
<div class="footer">
<%@ include file="resource/footer.jsp" %>
</div>
</div>
</body>
</html>
</html>

View File

@ -2,67 +2,394 @@
* PlantUMLServlet style sheet *
******************************/
/* Font */
h1, p, #content a {
font-family: arial,helvetica,sans-serif;
/* font-size: medium; */
/************* 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;
}
/* Header */
#header {
margin-left: auto;
margin-right: auto;
text-align: center;
/************* default settings *************/
html, body {
margin: 0;
padding: 0;
}
html {
font-family: arial,helvetica,sans-serif;
}
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%;
}
}
/* Content */
#content {
margin-left: auto;
margin-right: auto;
width: 95%;
/************* ruler *************/
.hr {
padding: 1rem 0;
width: 100%;
}
.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;
}
/* Form inputs */
#content textarea, #content .CodeMirror, #content input[type=text] {
background-color: #ffffff;
font-family: monospace;
font-size: medium;
width: 100%;
resize: vertical;
border: 3px solid #ccc !important;
/************* 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=text] {
border: 0;
/*******************************************************************/
/************* header, main, footer *************/
.header {
margin-left: auto;
margin-right: auto;
text-align: center;
}
.main {
margin: 1% 5%;
z-index: 1;
}
.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;
margin: 0;
padding: 0.5em;
text-align: center;
}
#content input[type="submit"] {
margin-top: 10px;
padding: 5px;
background: #fafafa;
border: 1px solid black;
/*******************************************************************/
/************* 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;
}
#content input[type="submit"]:hover {
background: #f0f0f0;
/************* 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;
}
/* Diagram */
#content #diagram {
text-align: center;
/************* 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;
}
#content #diagram img {
border: medium solid green;
padding: 10px;
/*******************************************************************/
/************* 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;
}
}
/* Footer */
#footer p {
background-color: #eee;
color: #666;
font-size: 0.7em;
padding: 2px;
text-align: center;
width: 100%;
}
/************* 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;
}

View File

@ -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) {
if (path.startsWith("http")) return path;
if (path.startsWith("/")) return window.location.origin + 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;
if (base.slice(-1) == "/") base = base.slice(0, -1);
return base + "/" + 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;
}
window.onload = function() {
loadCodeMirror();
function prepareUrl(url) {
if (!(url instanceof URL)) {
url = new URL(resolvePath(url));
}
// 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 ];
}
// resolve relative path inside url input once
const url = document.getElementById("url");
url.value = resolvePath(url.value);
function analyseUrl(url) {
let match;
[url, _, match] = prepareUrl(url);
return {
index: match.groups.idx,
encodedDiagram: match.groups.encoded || url.searchParams.get("url"),
};
}
// 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 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,
});
});
navigator.permissions.query({ name: "clipboard-read" }).then((result) => {
if (result.state == "granted" || result.state == "prompt") {
clipboard_read = 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;
};

View 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 + ") &nbsp; " + 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;
})();

View 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>

View File

@ -0,0 +1 @@
<p><%= net.sourceforge.plantuml.version.Version.fullDescription() %></p>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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);

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View 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
}
}

View File

@ -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);
}
}

View File

@ -1,4 +1,4 @@
package net.sourceforge.plantuml.servlet;
package net.sourceforge.plantuml.servlet.utils;
/**

View File

@ -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();
}
}
}

View File

@ -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"));
}
}