├── .gitattributes ├── src ├── main │ ├── webapp │ │ ├── META-INF │ │ │ └── MANIFEST.MF │ │ ├── components │ │ │ ├── header │ │ │ │ ├── social-buttons.jsp │ │ │ │ ├── header.jsp │ │ │ │ └── github-ribbon.jsp │ │ │ ├── footer │ │ │ │ └── footer.jsp │ │ │ ├── preview │ │ │ │ ├── paginator │ │ │ │ │ ├── paginator.jsp │ │ │ │ │ ├── paginator.css │ │ │ │ │ └── paginator.js │ │ │ │ ├── social-buttons.jsp │ │ │ │ ├── preview.css │ │ │ │ ├── preview.jsp │ │ │ │ ├── diagram │ │ │ │ │ ├── preview-diagram.jsp │ │ │ │ │ ├── preview-diagram.css │ │ │ │ │ └── preview-diagram.js │ │ │ │ ├── menu │ │ │ │ │ ├── preview-menu.css │ │ │ │ │ └── preview-menu.jsp │ │ │ │ └── preview.js │ │ │ ├── modals │ │ │ │ ├── diagram-export │ │ │ │ │ ├── diagram-export.css │ │ │ │ │ ├── diagram-export.jsp │ │ │ │ │ └── diagram-export.js │ │ │ │ ├── settings │ │ │ │ │ ├── settings.css │ │ │ │ │ ├── settings.jsp │ │ │ │ │ └── settings.js │ │ │ │ ├── diagram-import │ │ │ │ │ ├── diagram-import.jsp │ │ │ │ │ └── diagram-import.css │ │ │ │ ├── modals.js │ │ │ │ └── modals.css │ │ │ ├── editor │ │ │ │ ├── url-input │ │ │ │ │ ├── editor-url-input.jsp │ │ │ │ │ ├── editor-url-input.css │ │ │ │ │ └── editor-url-input.js │ │ │ │ ├── editor.jsp │ │ │ │ ├── menu │ │ │ │ │ ├── editor-menu.js │ │ │ │ │ ├── editor-menu.jsp │ │ │ │ │ └── editor-menu.css │ │ │ │ └── editor.css │ │ │ ├── app-head.jsp │ │ │ ├── app.js │ │ │ └── app.css │ │ ├── favicon.ico │ │ ├── resource │ │ │ └── test │ │ │ │ ├── bob.png │ │ │ │ ├── test2diagrams.txt │ │ │ │ └── bob.svg │ │ ├── js │ │ │ ├── utilities │ │ │ │ ├── os-helpers.js │ │ │ │ ├── dom-helpers.js │ │ │ │ ├── theme-helpers.js │ │ │ │ └── url-helpers.js │ │ │ ├── communication │ │ │ │ └── server.js │ │ │ ├── language │ │ │ │ ├── completion │ │ │ │ │ ├── utils.js │ │ │ │ │ ├── icons.js │ │ │ │ │ ├── themes.js │ │ │ │ │ └── emojis.js │ │ │ │ ├── validation │ │ │ │ │ ├── validation.js │ │ │ │ │ └── listeners │ │ │ │ │ │ └── start-end-validation.js │ │ │ │ └── language.js │ │ │ └── config │ │ │ │ └── config.js │ │ ├── assets │ │ │ ├── actions │ │ │ │ ├── download.svg │ │ │ │ ├── upload.svg │ │ │ │ ├── undock.svg │ │ │ │ ├── copy.svg │ │ │ │ ├── dock.svg │ │ │ │ └── settings.svg │ │ │ └── file-types │ │ │ │ ├── txt.svg │ │ │ │ ├── map.svg │ │ │ │ ├── pdf.svg │ │ │ │ ├── png.svg │ │ │ │ ├── svg.svg │ │ │ │ └── ascii.svg │ │ ├── previewer.jsp │ │ ├── error.jsp │ │ └── index.jsp │ ├── resources │ │ ├── config.properties │ │ └── log4j.properties │ ├── java │ │ └── net │ │ │ └── sourceforge │ │ │ └── plantuml │ │ │ └── servlet │ │ │ ├── utility │ │ │ ├── package.html │ │ │ ├── NullOutputStream.java │ │ │ ├── Configuration.java │ │ │ └── UrlDataExtractor.java │ │ │ ├── package.html │ │ │ ├── EpsServlet.java │ │ │ ├── PdfServlet.java │ │ │ ├── SvgServlet.java │ │ │ ├── ImgServlet.java │ │ │ ├── AsciiServlet.java │ │ │ ├── Base64Servlet.java │ │ │ ├── EpsTextServlet.java │ │ │ ├── diagrams.txt │ │ │ ├── LanguageServlet.java │ │ │ ├── CheckSyntaxServlet.java │ │ │ └── MapServlet.java │ └── config │ │ └── rules.xml └── test │ └── java │ └── net │ └── sourceforge │ └── plantuml │ └── servlet │ ├── server │ ├── ServerUtils.java │ ├── ExternalServer.java │ └── EmbeddedJettyServer.java │ ├── AllTests.java │ ├── TestLanguage.java │ ├── TestEPS.java │ ├── TestAsciiArt.java │ ├── utils │ ├── TestUtils.java │ ├── JUnitWebDriver.java │ ├── WebappUITestCase.java │ └── WebappTestCase.java │ ├── TestPDF.java │ ├── TestCheck.java │ ├── TestDependencies.java │ ├── TestCharset.java │ ├── TestAsciiCoder.java │ ├── TestOldProxy.java │ ├── TestImage.java │ └── TestMap.java ├── .dockerignore ├── docs ├── screenshot.png ├── WebUI │ ├── gifs │ │ ├── alice-bob.gif │ │ ├── multipaging.gif │ │ ├── diagram-export.gif │ │ ├── diagram-import.gif │ │ ├── settings-theme.gif │ │ ├── split-screen.gif │ │ ├── mobile-alice-bob.gif │ │ ├── mobile-settings.gif │ │ ├── auto-completion-icons.gif │ │ ├── validation-start-end.gif │ │ ├── auto-completion-emojis.gif │ │ ├── auto-completion-themes.gif │ │ ├── settings-rendering-type.gif │ │ └── auto-completion-themes+icons.gif │ ├── mobile.md │ ├── import-export.md │ ├── README.md │ ├── settings.md │ └── language-features.md └── contribution │ └── front-end.md ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── bug_report.md │ └── documentation_request.md ├── .gitignore ├── SECURITY.md ├── docker-compose.yml ├── examples ├── README.md ├── additional-fonts │ ├── docker-compose.yml │ └── README.md ├── nginx-simple │ ├── docker-compose.yml │ ├── nginx.conf │ └── README.md ├── nginx-contextpath │ ├── docker-compose.yml │ ├── nginx.conf │ └── README.md └── kubernetes-simple │ ├── README.md │ └── deployment.yaml ├── ROOT.jetty.xml ├── docker-entrypoint.jetty.sh ├── .editorconfig ├── pom.xml ├── docker-entrypoint.tomcat.sh ├── .vscode └── settings.json ├── Dockerfile.tomcat ├── Dockerfile.jetty-alpine ├── pom.jdk8.xml └── Dockerfile.jetty /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /src/main/webapp/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Class-Path: 3 | 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # .dockerignore -- stuff we don't need during image builds 2 | 3 | /.git/ 4 | -------------------------------------------------------------------------------- /src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | #PlantUML configuration file 2 | # 3 | SHOW_GITHUB_RIBBON=on -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /src/main/webapp/components/header/social-buttons.jsp: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/WebUI/gifs/alice-bob.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/alice-bob.gif -------------------------------------------------------------------------------- /src/main/webapp/components/footer/footer.jsp: -------------------------------------------------------------------------------- 1 |

<%= net.sourceforge.plantuml.version.Version.fullDescription() %>

2 | -------------------------------------------------------------------------------- /src/main/webapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/src/main/webapp/favicon.ico -------------------------------------------------------------------------------- /docs/WebUI/gifs/multipaging.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/multipaging.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/diagram-export.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/diagram-export.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/diagram-import.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/diagram-import.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/settings-theme.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/settings-theme.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/split-screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/split-screen.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/mobile-alice-bob.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/mobile-alice-bob.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/mobile-settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/mobile-settings.gif -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: plantuml 4 | patreon: plantuml 5 | liberapay: plantuml 6 | -------------------------------------------------------------------------------- /src/main/webapp/resource/test/bob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/src/main/webapp/resource/test/bob.png -------------------------------------------------------------------------------- /docs/WebUI/gifs/auto-completion-icons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/auto-completion-icons.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/validation-start-end.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/validation-start-end.gif -------------------------------------------------------------------------------- /src/main/webapp/components/preview/paginator/paginator.jsp: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/WebUI/gifs/auto-completion-emojis.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/auto-completion-emojis.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/auto-completion-themes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/auto-completion-themes.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/settings-rendering-type.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/settings-rendering-type.gif -------------------------------------------------------------------------------- /docs/WebUI/gifs/auto-completion-themes+icons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaheenJawadi/plantuml-server/HEAD/docs/WebUI/gifs/auto-completion-themes+icons.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse Ignores 2 | target 3 | .settings 4 | .classpath 5 | .project 6 | .checkstyle 7 | 8 | # IntelliJ ignores 9 | .idea/ 10 | out/ 11 | *.iml 12 | *.ipr 13 | *.iws -------------------------------------------------------------------------------- /src/main/webapp/components/preview/social-buttons.jsp: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/paginator/paginator.css: -------------------------------------------------------------------------------- 1 | /**************** 2 | * Paginator CSS * 3 | *****************/ 4 | 5 | #paginator { 6 | text-align: center; 7 | margin-bottom: 1rem; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/diagram-export/diagram-export.css: -------------------------------------------------------------------------------- 1 | /********************* 2 | * Diagram Export CSS * 3 | **********************/ 4 | 5 | #diagram-export.modal .label-input-pair label { 6 | min-width: 8rem; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/settings/settings.css: -------------------------------------------------------------------------------- 1 | /*************** 2 | * Settings CSS * 3 | ****************/ 4 | 5 | #settings #settings-monaco-editor { 6 | height: 17rem; 7 | border: 1px solid var(--border-color); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/webapp/resource/test/test2diagrams.txt: -------------------------------------------------------------------------------- 1 | ' This file is used by the TestProxy unit test. 2 | ' It contains 2 diagrams description. 3 | 4 | @startuml 5 | Bob -> Alice : hello 6 | @enduml 7 | 8 | @startuml 9 | version 10 | @enduml 11 | -------------------------------------------------------------------------------- /src/main/webapp/js/utilities/os-helpers.js: -------------------------------------------------------------------------------- 1 | /************* 2 | * OS Helpers * 3 | **************/ 4 | 5 | const isMac = (function() { 6 | const PLATFORM = navigator?.userAgentData?.platform || navigator?.platform || "unknown"; 7 | return PLATFORM.match("Mac"); 8 | })(); 9 | -------------------------------------------------------------------------------- /src/main/webapp/components/editor/url-input/editor-url-input.jsp: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /src/main/webapp/assets/actions/download.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/main/webapp/assets/actions/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you find any security concern, please send a mail to plantuml@gmail.com 8 | with title **Security concern**. 9 | 10 | We will then study the concern and will answer back by email. 11 | 12 | Thanks! 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | plantuml-server: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile.jetty 6 | image: plantuml/plantuml-server:jetty 7 | container_name: plantuml-server 8 | ports: 9 | - 8080:8080 10 | environment: 11 | - BASE_URL=plantuml 12 | -------------------------------------------------------------------------------- /src/main/webapp/assets/actions/undock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Logger configuration file 2 | # 3 | log4j.rootCategory=INFO, stdout 4 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 5 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 6 | log4j.appender.stdout.layout.ConversionPattern=%d{yy-MM-dd HH:mm:ss:SSS} %5p %t %c{2}:%L - %m%n 7 | -------------------------------------------------------------------------------- /src/main/webapp/assets/actions/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/preview.css: -------------------------------------------------------------------------------- 1 | /************** 2 | * Preview CSS * 3 | ***************/ 4 | 5 | .previewer-container { 6 | height: 100%; 7 | } 8 | @media screen and (max-width: 900px) { 9 | .previewer-container { 10 | height: initial; 11 | } 12 | .previewer-main { 13 | flex: none; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [Additional fonts inside the PlantUML docker container](./additional-fonts) 4 | - [Nginx simple reverse proxy example](./nginx-simple) 5 | - [Nginx reverse proxy example with defined location directive (different context path)](./nginx-contextpath) 6 | - [Kubernetes simple deployment](./kubernetes-simple) 7 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/server/ServerUtils.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet.server; 2 | 3 | 4 | public interface ServerUtils { 5 | 6 | public void startServer() throws Exception; 7 | 8 | public void stopServer() throws Exception; 9 | 10 | public abstract String getServerUrl(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ROOT.jetty.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | /plantuml.war 9 | 10 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/utility/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

This package contains utility classes of the JEE PlantUml Server.

4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/webapp/components/header/header.jsp: -------------------------------------------------------------------------------- 1 |

PlantUML Server

2 | <% if (showSocialButtons) { %> 3 | <%@ include file="/components/header/social-buttons.jsp" %> 4 | <% } %> 5 | <% if (showGithubRibbon) { %> 6 | <%@ include file="/components/header/github-ribbon.jsp" %> 7 | <% } %> 8 |

Create your PlantUML diagrams directly in your browser!

9 | -------------------------------------------------------------------------------- /examples/additional-fonts/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | plantuml-server: 5 | image: plantuml/plantuml-server:jetty 6 | container_name: plantuml-server 7 | ports: 8 | - "80:8080" 9 | environment: 10 | - TZ=Europe/Berlin 11 | - BASE_URL=plantuml 12 | volumes: 13 | - /usr/share/fonts:/var/lib/jetty/.local/share/fonts/host:ro 14 | -------------------------------------------------------------------------------- /docker-entrypoint.jetty.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # cspell:words mkdir 3 | # cspell:enableCompoundWords 4 | ########################################################### 5 | 6 | # ensure context path starts with a slash 7 | export CONTEXT_PATH="/${BASE_URL#'/'}" 8 | 9 | # base image entrypoint 10 | if [ -x /docker-entrypoint.sh ]; then 11 | /docker-entrypoint.sh "$@" 12 | else 13 | exec "$@" 14 | fi 15 | -------------------------------------------------------------------------------- /docs/WebUI/mobile.md: -------------------------------------------------------------------------------- 1 | # Mobile Version 2 | 3 | PlantUML Server is mobile ready. 4 | 5 | ## First example: "Alice and Bob" 6 | 7 | ![alice-bob](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/mobile-alice-bob.gif) 8 | 9 | 10 | ## Settings 11 | 12 | ![settings](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/mobile-settings.gif) 13 | -------------------------------------------------------------------------------- /src/main/webapp/assets/actions/dock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nginx-simple/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | plantuml-server: 5 | image: plantuml/plantuml-server:jetty 6 | container_name: plantuml-server 7 | environment: 8 | - TZ=Europe/Berlin 9 | 10 | nginx: 11 | image: nginx:alpine 12 | container_name: nginx 13 | ports: 14 | - "80:80" 15 | environment: 16 | - TZ=Europe/Berlin 17 | volumes: 18 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [*.java] 12 | indent_size = 4 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | 18 | [*.puml] 19 | insert_final_newline = false 20 | 21 | [{Dockerfile,Dockerfile.*}] 22 | indent_size = 4 23 | 24 | [.vscode/*.json] 25 | indent_size = 4 26 | -------------------------------------------------------------------------------- /src/main/webapp/js/communication/server.js: -------------------------------------------------------------------------------- 1 | /*********************** 2 | * Server Communication * 3 | ************************/ 4 | 5 | function makeRequest( 6 | method, 7 | url, 8 | { 9 | data = null, 10 | headers = { "Content-Type": "text/plain" }, 11 | responseType = "text", 12 | baseUrl = "", 13 | } = {} 14 | ) { 15 | return PlantUmlLanguageFeatures.makeRequest( 16 | method, 17 | url, 18 | { data, headers, responseType, baseUrl } 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /examples/nginx-contextpath/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | plantuml-server: 5 | image: plantuml/plantuml-server:jetty 6 | container_name: plantuml-server 7 | environment: 8 | - TZ=Europe/Berlin 9 | - BASE_URL=plantuml 10 | 11 | nginx: 12 | image: nginx:alpine 13 | container_name: nginx 14 | ports: 15 | - "80:80" 16 | environment: 17 | - TZ=Europe/Berlin 18 | volumes: 19 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 20 | -------------------------------------------------------------------------------- /src/main/webapp/assets/file-types/txt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/webapp/js/language/completion/utils.js: -------------------------------------------------------------------------------- 1 | /********************************************** 2 | * PlantUML Language Completion Provider Utils * 3 | ***********************************************/ 4 | 5 | PlantUmlLanguageFeatures.prototype.getWordRange = function(model, position) { 6 | const word = model.getWordUntilPosition(position); 7 | return { 8 | startLineNumber: position.lineNumber, 9 | endLineNumber: position.lineNumber, 10 | startColumn: word.startColumn, 11 | endColumn: word.endColumn, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/webapp/components/editor/editor.jsp: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%@ include file="/components/editor/url-input/editor-url-input.jsp" %> 4 |
5 |
6 | 7 |
8 | <%@ include file="/components/editor/menu/editor-menu.jsp" %> 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/main/webapp/components/editor/menu/editor-menu.js: -------------------------------------------------------------------------------- 1 | /***************** 2 | * Editor Menu JS * 3 | ******************/ 4 | 5 | function initEditorMenu() { 6 | function copyCodeToClipboard() { 7 | const range = document.editor.getModel().getFullModelRange(); 8 | document.editor.focus(); 9 | document.editor.setSelection(range); 10 | const code = document.editor.getValue(); 11 | navigator.clipboard?.writeText(code).catch(() => {}); 12 | } 13 | // add listener 14 | document.getElementById("menu-item-editor-code-copy").addEventListener("click", copyCodeToClipboard); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/webapp/components/header/github-ribbon.jsp: -------------------------------------------------------------------------------- 1 |
2 | Fork me on GitHub 9 | 10 | Fork me on GitHub 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/server/ExternalServer.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet.server; 2 | 3 | 4 | public class ExternalServer implements ServerUtils { 5 | 6 | private final String uri; 7 | 8 | public ExternalServer(String uri) { 9 | this.uri = uri; 10 | } 11 | 12 | @Override 13 | public void startServer() throws Exception { } 14 | 15 | @Override 16 | public void stopServer() throws Exception { } 17 | 18 | @Override 19 | public String getServerUrl() { 20 | return this.uri; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/AllTests.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import org.junit.platform.suite.api.SelectClasses; 4 | import org.junit.platform.suite.api.Suite; 5 | 6 | @Suite 7 | @SelectClasses({ 8 | TestAsciiArt.class, 9 | TestAsciiCoder.class, 10 | TestCharset.class, 11 | TestCheck.class, 12 | TestEPS.class, 13 | TestImage.class, 14 | TestLanguage.class, 15 | TestMap.class, 16 | TestMultipageUml.class, 17 | TestOldProxy.class, 18 | TestProxy.class, 19 | TestSVG.class, 20 | TestWebUI.class 21 | }) 22 | public class AllTests {} 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 4.0.0 8 | 9 | 10 | org.sourceforge.plantuml 11 | plantumlservlet-parent 12 | 1-SNAPSHOT 13 | pom.parent.xml 14 | 15 | 16 | plantumlservlet 17 | war 18 | 19 | 20 | 11 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/preview.jsp: -------------------------------------------------------------------------------- 1 |
2 | <%@ include file="/components/preview/menu/preview-menu.jsp" %> 3 |
4 | <%@ include file="/components/preview/paginator/paginator.jsp" %> 5 | 6 |
7 | <%@ include file="/components/preview/diagram/preview-diagram.jsp" %> 8 |
9 | <% if (showSocialButtons) { %> 10 |
11 | <%@ include file="/components/preview/social-buttons.jsp" %> 12 |
13 | <% } %> 14 | 15 | <%@ include file="/components/modals/settings/settings.jsp" %> 16 |
17 | -------------------------------------------------------------------------------- /docker-entrypoint.tomcat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # cspell:words mkdir 3 | # cspell:enableCompoundWords 4 | ########################################################### 5 | 6 | # choose war file name so that context path is correctly set based on BASE_URL, 7 | # following the rules from https://tomcat.apache.org/tomcat-9.0-doc/config/context.html#Naming, 8 | # specifically remove leading and trailing slashes and replace the remaining ones by hashes. 9 | export FILE_NAME="$(echo "$BASE_URL" | sed -e 's:^/::' -e 's:/$::' -e 's:/:#:g')" 10 | export FILE_PATH="$WEBAPP_PATH/$FILE_NAME.war" 11 | mv /plantuml.war "$FILE_PATH" 12 | 13 | # base image entrypoint 14 | if [ -x /docker-entrypoint.sh ]; then 15 | /docker-entrypoint.sh "$@" 16 | else 17 | exec "$@" 18 | fi 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic", 3 | "cSpell.words": [ 4 | "Arnaud", 5 | "buildx", 6 | "ditaa", 7 | "endditaa", 8 | "enduml", 9 | "epstext", 10 | "etag", 11 | "ghaction", 12 | "inmemory", 13 | "Lalloni", 14 | "monaco", 15 | "plantuml", 16 | "puml", 17 | "Roques", 18 | "servlet", 19 | "servlets", 20 | "startditaa", 21 | "startuml", 22 | "undock", 23 | "utxt" 24 | ], 25 | "cSpell.allowCompoundWords": true, 26 | "svg.preview.background": "dark-transparent", 27 | "files.associations": { 28 | "*.jspf": "html" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/diagram/preview-diagram.jsp: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | PlantUML diagram 5 | <% if (hasMap) { %> 6 | <%= map %> 7 | <% } else { %> 8 | 9 | <% } %> 10 | 11 | 12 | 13 | 14 | 15 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/main/webapp/components/editor/url-input/editor-url-input.css: -------------------------------------------------------------------------------- 1 | /*********************** 2 | * Editor URL Input CSS * 3 | ************************/ 4 | 5 | .editor .btn-input { 6 | align-items: center; 7 | border-bottom: 3px solid var(--border-color); 8 | box-sizing: border-box; 9 | display: flex; 10 | justify-content: center; 11 | } 12 | .editor .btn-input input[type=text] { 13 | border: 0; 14 | flex: 1 1 1px; 15 | font-family: monospace; 16 | font-size: medium; 17 | padding: 0.2em; 18 | text-overflow: ellipsis; 19 | } 20 | .editor .btn-input input[type=text]:focus { 21 | border: 0; 22 | box-shadow: none; 23 | outline: none; 24 | } 25 | .editor .btn-input input[type="image"] { 26 | height: 1rem; 27 | margin-left: 0.7em; 28 | padding: 0 0.3em; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/diagram-import/diagram-import.jsp: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/main/webapp/components/editor/editor.css: -------------------------------------------------------------------------------- 1 | /************* 2 | * Editor CSS * 3 | **************/ 4 | 5 | .editor { 6 | border: 3px solid var(--border-color); 7 | box-sizing: border-box; 8 | overflow: hidden; 9 | } 10 | @media screen and (max-width: 900px) { 11 | .editor { 12 | height: 20em; 13 | } 14 | } 15 | .editor .monaco-editor-container { 16 | overflow: hidden; 17 | position: relative; 18 | } 19 | 20 | #monaco-editor { 21 | height: 100%; 22 | } 23 | /* Hack to display the icons and emojis in the auto completion documentation in a visible size. 24 | * (see PlantUmlLanguageFeatures.register{Icon,Emoji}Completion) */ 25 | #monaco-editor .overlayWidgets .suggest-details p img[alt="icon"], 26 | #monaco-editor .overlayWidgets .suggest-details p img[alt="emoji"] { 27 | height: 1.2rem; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/webapp/assets/file-types/map.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/webapp/js/utilities/dom-helpers.js: -------------------------------------------------------------------------------- 1 | /************** 2 | * DOM Helpers * 3 | ***************/ 4 | 5 | function removeChildren(element) { 6 | if (element.replaceChildren) { 7 | element.replaceChildren(); 8 | } else { 9 | element.innerHTML = ""; 10 | } 11 | } 12 | 13 | function isVisible(element) { 14 | // `offsetParent` returns `null` if the element, or any of its parents, 15 | // is hidden via the display style property. 16 | // see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent 17 | return (element.offsetParent !== null); 18 | } 19 | 20 | function setVisibility(element, visibility, focus=false) { 21 | if (visibility) { 22 | element.style.removeProperty("display"); 23 | if (focus) element.focus(); 24 | } else { 25 | element.style.display = "none"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestLanguage.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 10 | 11 | 12 | public class TestLanguage extends WebappTestCase { 13 | 14 | /** 15 | * Tests that the language for the current PlantUML server can be obtained through HTTP 16 | */ 17 | @Test 18 | public void testRetrieveLanguage() throws IOException { 19 | final URL url = new URL(getServerUrl() + "/language"); 20 | String languageText = getContentText(url); 21 | Assertions.assertTrue(languageText.indexOf("@startuml") > 0, "Language contains @startuml"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/menu/preview-menu.css: -------------------------------------------------------------------------------- 1 | /******************* 2 | * Preview Menu CSS * 3 | ********************/ 4 | 5 | .preview-menu { 6 | margin-left: 5%; 7 | margin-right: 5%; 8 | } 9 | .diagram-link img, .btn-dock { 10 | width: 2.5rem; 11 | } 12 | .btn-settings { 13 | width: 2.2rem; 14 | margin-left: auto; 15 | margin-right: 0.25rem; 16 | } 17 | .menu-r { 18 | min-width: 3rem; 19 | } 20 | .menu-r .btn-float-r { 21 | float: right; 22 | margin-left: 0.25rem; 23 | text-align: right; 24 | } 25 | .diagram-links { 26 | align-items: center; 27 | display: flex; 28 | } 29 | .diagram-link { 30 | margin-left: 0.25rem; 31 | margin-right: 0.25rem; 32 | } 33 | .diagram-links .diagram-link:first-of-type { 34 | margin-left: 0.5rem; 35 | } 36 | .diagram-links .diagram-link:last-of-type { 37 | margin-right: 0; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/webapp/components/app-head.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 13 | 1. Go to '...' 14 | 2. Click on '....' 15 | 3. Scroll down to '....' 16 | 4. See error 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Desktop (please complete the following information):** 25 | 26 | - OS: [e.g. iOS] 27 | - Browser [e.g. chrome, safari] 28 | - Version [e.g. 22] 29 | 30 | **Smartphone (please complete the following information):** 31 | 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/diagram-import/diagram-import.css: -------------------------------------------------------------------------------- 1 | /********************* 2 | * Diagram Import CSS * 3 | **********************/ 4 | 5 | #diagram-import p.error-message { 6 | color: darkred; 7 | padding-left: 1rem; 8 | padding-right: 1rem; 9 | } 10 | #diagram-import input[type="file"] { 11 | display: block; 12 | width: 100%; 13 | border: 0.2rem dashed var(--border-color); 14 | border-radius: 0.4rem; 15 | box-sizing: border-box; 16 | padding: 5rem 2rem; 17 | } 18 | #diagram-import input[type="file"], 19 | #diagram-import input[type="file"]::file-selector-button { 20 | background-color: var(--modal-bg-color); 21 | } 22 | #diagram-import input[type="file"]:hover, 23 | #diagram-import input[type="file"].drop-able { 24 | border-color: var(--border-color-2); 25 | background-color: var(--file-drop-color); 26 | } 27 | #diagram-import input[type="file"]:hover::file-selector-button, 28 | #diagram-import input[type="file"].drop-able::file-selector-button { 29 | background-color: var(--file-drop-color); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/webapp/assets/file-types/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/webapp/components/editor/menu/editor-menu.jsp: -------------------------------------------------------------------------------- 1 |
2 | 9 | 35 |
36 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/diagram/preview-diagram.css: -------------------------------------------------------------------------------- 1 | /********************** 2 | * Preview Diagram CSS * 3 | ***********************/ 4 | 5 | .diagram { 6 | height: 100%; 7 | overflow: auto; 8 | } 9 | .diagram[data-diagram-type="pdf"] { 10 | overflow: hidden; 11 | } 12 | .diagram > div { 13 | margin: 1rem 0; 14 | text-align: center; 15 | } 16 | .diagram[data-diagram-type="pdf"] > div { 17 | height: 20em; 18 | width: 100%; 19 | } 20 | .diagram img, .diagram svg, .diagram pre { 21 | border: 3px solid var(--border-color); 22 | box-sizing: border-box; 23 | padding: 10px; 24 | } 25 | @media screen and (min-width: 900px) { 26 | .diagram { 27 | position: relative; 28 | } 29 | .diagram > div { 30 | margin: 0; 31 | } 32 | .diagram:not([data-diagram-type="pdf"]) > div { 33 | position: absolute; 34 | top: 50%; 35 | left: 50%; 36 | transform: translate(-50%, -50%); 37 | max-height: 100%; 38 | max-width: 100%; 39 | } 40 | .diagram[data-diagram-type="pdf"] > div { 41 | height: 100%; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/WebUI/import-export.md: -------------------------------------------------------------------------------- 1 | # Import and Export editable PlantUML Diagrams 2 | 3 | Similar to [draw.io](https://app.diagrams.net) it is possible to load and continue editing PlantUML diagram images. 4 | 5 | 6 | ## Export a diagram 7 | 8 | Via the editor menu or Ctrl + S (or Meta + S in the case of a Mac) you can open the file save dialog. 9 | Here you can edit the file name, choose a file/diagram type and download the diagram. 10 | The default is to download the PlantUML code. 11 | 12 | ![export](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/diagram-export.gif) 13 | 14 | 15 | ## Import a diagram 16 | 17 | This feature is based on the PlantUML meta data which currently **support only PNG and SVG** diagrams. 18 | Besides a diagram image, you can of course also load a diagram code file. 19 | Moreover, because it is so nice and convenient, we also added a drag-and-drop feature. 20 | 21 | ![import](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/diagram-import.gif) 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation request 3 | about: Identify an area for improvement in documentation 4 | 5 | --- 6 | 7 | **What is the URL of the documentation?** 8 | 9 | - **Example:** https://github.com/evantill/docker-cheerpj/blob/main/README.md#example-of-using-cheerpj-on-your-computer 10 | 11 | - *Note:* This URL includes the web page and the section of the documentation. 12 | 13 | **What can be improved?** 14 | 15 | A clear and concise description of what can be improved. 16 | Examples: 17 | 18 | - "I don't understand where the ${XYZ} variable is set." 19 | - "There seems to be a step missing between 'X' and 'Z'. I don't know how to get to 'Z'." 20 | - "When I run `command sub-command ...` I get the following error:" 21 | - "I don't know what is meant by 'gerble barb gazoink` in the instructions". 22 | 23 | **Additional context** 24 | 25 | Add any other context or screenshots to help describe the documentation improvement. 26 | If you think the documentation improvement is operating system specific, 27 | please indicate which operating system is being used. 28 | -------------------------------------------------------------------------------- /examples/nginx-contextpath/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | include /etc/nginx/modules-enabled/*.conf; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | http { 14 | 15 | server { 16 | listen 80; 17 | server_name localhost; 18 | 19 | # PlantUML 20 | location /plantuml/ { 21 | proxy_pass http://plantuml-server:8080/plantuml/; 22 | } 23 | } 24 | 25 | client_max_body_size 0; 26 | 27 | include /etc/nginx/mime.types; 28 | default_type application/octet-stream; 29 | 30 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 31 | '$status $body_bytes_sent "$http_referer" ' 32 | '"$http_user_agent" "$http_x_forwarded_for"'; 33 | 34 | access_log /var/log/nginx/access.log main; 35 | 36 | sendfile on; 37 | #tcp_nopush on; 38 | 39 | keepalive_timeout 65; 40 | 41 | #gzip on; 42 | 43 | include /etc/nginx/conf.d/*.conf; 44 | } 45 | -------------------------------------------------------------------------------- /docs/WebUI/README.md: -------------------------------------------------------------------------------- 1 | # PlantUML Server Web UI 2 | 3 | ## First example: "Alice and Bob" 4 | 5 | ![alice-bob](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/alice-bob.gif) 6 | 7 | ## Multipaging 8 | 9 | Just see what you want. 10 | And if its the second diagram page, so be it. 11 | 12 | ![multipaging](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/multipaging.gif) 13 | 14 | ## Split Screen 15 | 16 | You have multiple monitors? You want to share your Window but only show the diagram and not the code? Than do it! 17 | 18 | ![multipaging](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/split-screen.gif) 19 | 20 | ## More 21 | 22 | - [Settings](https://github.com/plantuml/plantuml-server/blob/master/docs/WebUI/settings.md) 23 | - [PlantUML Language Features](https://github.com/plantuml/plantuml-server/blob/master/docs/WebUI/language-features.md) 24 | - [Import/Export](https://github.com/plantuml/plantuml-server/blob/master/docs/WebUI/import-export.md) 25 | - [Mobile Version](https://github.com/plantuml/plantuml-server/blob/master/docs/WebUI/mobile.md) 26 | -------------------------------------------------------------------------------- /src/main/webapp/previewer.jsp: -------------------------------------------------------------------------------- 1 | <%@ page info="index" contentType="text/html; charset=utf-8" pageEncoding="utf-8" session="false" %> 2 | <% 3 | // diagram sources 4 | String encoded = request.getAttribute("encoded").toString(); 5 | String index = request.getAttribute("index").toString(); 6 | String diagramUrl = ((index.isEmpty()) ? "" : index + "/") + encoded; 7 | // map for diagram source if necessary 8 | String map = request.getAttribute("map").toString(); 9 | boolean hasMap = !map.isEmpty(); 10 | // properties 11 | boolean showSocialButtons = (boolean)request.getAttribute("showSocialButtons"); 12 | %> 13 | 14 | 15 | 16 | 17 | <%@ include file="/components/app-head.jsp" %> 18 | PlantUML Server 19 | 25 | 26 | 27 |
28 | <%-- Preview --%> 29 | <%@ include file="/components/preview/preview.jsp" %> 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/WebUI/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | Via the menu or Ctrl + , (or Meta + , in the case of a Mac) you can open the Setting dialog. 4 | 5 | ## Theme 6 | 7 | _The sun is too bright? You live on the dark side or only in the basement?_ 8 | Choose between the `dark` and `light` theme. 9 | 10 | ![theme](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/settings-theme.gif) 11 | 12 | ## Rendering Type 13 | 14 | You want always to work and see only the SVG version? Not Problem. 15 | Choose the rendering type you want to see. 16 | 17 | ![rendering-type](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/settings-rendering-type.gif) 18 | 19 | ## Editor Watcher Timeout 20 | 21 | You can change the Editor Watcher Timeout, by default it is `500 ms`. 22 | 23 | 24 | ## Editor Options 25 | 26 | You can change the options of the editor: 27 | 28 | ```yaml 29 | { 30 | automaticLayout: true, 31 | fixedOverflowWidgets: true, 32 | minimap: { enabled: false }, 33 | scrollbar: { alwaysConsumeMouseWheel: false }, 34 | scrollBeyondLastLine: false, 35 | tabSize: 2, 36 | theme: "vs", // "vs-dark" 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /src/main/webapp/assets/actions/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nginx-simple/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | include /etc/nginx/modules-enabled/*.conf; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | http { 14 | 15 | server { 16 | listen 80; 17 | server_name localhost; 18 | 19 | # PlantUML 20 | location / { 21 | proxy_set_header HOST $host; 22 | proxy_set_header X-Forwarded-Host $host; 23 | proxy_set_header X-Forwarded-Proto $scheme; 24 | 25 | proxy_pass http://plantuml-server:8080/; 26 | } 27 | } 28 | 29 | client_max_body_size 0; 30 | 31 | include /etc/nginx/mime.types; 32 | default_type application/octet-stream; 33 | 34 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 35 | '$status $body_bytes_sent "$http_referer" ' 36 | '"$http_user_agent" "$http_x_forwarded_for"'; 37 | 38 | access_log /var/log/nginx/access.log main; 39 | 40 | sendfile on; 41 | #tcp_nopush on; 42 | 43 | keepalive_timeout 65; 44 | 45 | #gzip on; 46 | 47 | include /etc/nginx/conf.d/*.conf; 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestEPS.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.net.URLConnection; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import net.sourceforge.plantuml.servlet.utils.TestUtils; 11 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 12 | 13 | 14 | public class TestEPS extends WebappTestCase { 15 | 16 | /** 17 | * Verifies the generation of the EPS for the Bob -> Alice sample 18 | */ 19 | @Test 20 | public void testSimpleSequenceDiagram() throws IOException { 21 | final URL url = new URL(getServerUrl() + "/eps/" + TestUtils.SEQBOB); 22 | final URLConnection conn = url.openConnection(); 23 | // Analyze response 24 | // Verifies the Content-Type header 25 | Assertions.assertEquals( 26 | "application/postscript", 27 | conn.getContentType().toLowerCase(), 28 | "Response content type is not EPS" 29 | ); 30 | // Get the content and verify its size 31 | String diagram = getContentText(conn); 32 | int diagramLen = diagram.length(); 33 | Assertions.assertTrue(diagramLen > 7000); 34 | Assertions.assertTrue(diagramLen < 10000); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/webapp/assets/file-types/png.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestAsciiArt.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.net.URLConnection; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import net.sourceforge.plantuml.servlet.utils.TestUtils; 11 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 12 | 13 | 14 | public class TestAsciiArt extends WebappTestCase { 15 | 16 | /** 17 | * Verifies the generation of the ascii art for the Bob -> Alice sample 18 | */ 19 | @Test 20 | public void testSimpleSequenceDiagram() throws IOException { 21 | final URL url = new URL(getServerUrl() + "/txt/" + TestUtils.SEQBOB); 22 | final URLConnection conn = url.openConnection(); 23 | // Analyze response 24 | // Verifies the Content-Type header 25 | Assertions.assertEquals( 26 | "text/plain;charset=utf-8", 27 | conn.getContentType().toLowerCase(), 28 | "Response content type is not TEXT PLAIN or UTF-8" 29 | ); 30 | // Get the content and verify its size 31 | String diagram = getContentText(conn); 32 | int diagramLen = diagram.length(); 33 | Assertions.assertTrue(diagramLen > 200); 34 | Assertions.assertTrue(diagramLen < 250); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /examples/kubernetes-simple/README.md: -------------------------------------------------------------------------------- 1 | # PlantUML Kubernetes Deployment 2 | 3 | In this example, PlantUML is deployed on an Kubernetes cluster using a `Deployment`, a `Service` and an `Ingress`. 4 | 5 | ## Quick start 6 | 7 | Install: 8 | 9 | ```bash 10 | # Hint: Adjust the Ingress host to your URL 11 | 12 | kubectl create ns plantuml 13 | kubectl -n plantuml apply -f deployment.yaml 14 | ``` 15 | 16 | Uninstall: 17 | 18 | ```bash 19 | kubectl -n plantuml delete -f deployment.yaml 20 | kubectl delete ns plantuml 21 | ``` 22 | 23 | ## TLS configuration 24 | 25 | Create a TLS `Secret` and extend the `Ingress` spec with a TLS configuration: 26 | 27 | ```bash 28 | [...] 29 | tls: 30 | - hosts: 31 | - plantuml-example.localhost 32 | secretName: plantuml-tls 33 | ``` 34 | 35 | Since the `Ingress Controller` terminates the TLS and routes `http` to the application, we might need to tell the application explicitly that it got a forwarded request. 36 | 37 | This configuration changes depending on the `Ingress Controller`. Here an nginx example: 38 | 39 | ``` 40 | annotations: 41 | kubernetes.io/ingress.class: nginx 42 | nginx.ingress.kubernetes.io/configuration-snippet: | 43 | more_set_headers "X-Forwarded-Proto: https"; 44 | ``` 45 | 46 | ## Useful commands 47 | 48 | ```bash 49 | # see whats going on inside your Deployment 50 | kubectl -n plantuml logs -l "app.kubernetes.io/name=plantuml" 51 | ``` 52 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/utils/TestUtils.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet.utils; 2 | 3 | 4 | /** 5 | * Utility class for the unit tests 6 | */ 7 | public abstract class TestUtils { 8 | 9 | // 10 | // Theses strings are the compressed form of a PlantUML diagram. 11 | // 12 | 13 | /** 14 | * version 15 | */ 16 | public static final String VERSION = "AqijAixCpmC0"; 17 | 18 | /** 19 | * version 20 | */ 21 | public static final String VERSIONCODE = "@startuml\nversion\n@enduml"; 22 | 23 | /** 24 | * Bob -> Alice : hello 25 | */ 26 | public static final String SEQBOB = "SyfFKj2rKt3CoKnELR1Io4ZDoSa70000"; 27 | 28 | /** 29 | * Bob -> Alice : hello 30 | */ 31 | public static final String SEQBOBCODE = "@startuml\nBob -> Alice : hello\n@enduml"; 32 | 33 | /** 34 | * Encoded/compressed diagram source to text multipage uml diagrams. 35 | * 36 | * Bob -> Alice : hello 37 | * newpage 38 | * Bob <- Alice : hello 39 | * Bob -> Alice : let's talk [[tel:0123456789]] 40 | * Bob <- Alice : better not 41 | * Bob -> Alice : <&rain> bye 42 | * newpage 43 | * Bob <- Alice : bye 44 | */ 45 | public static final String SEQMULTIPAGE = "SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vyfBBIz8J4y5IzheeagYwyX9BL4lLYX9pCbMY8ukISsnCZ0qCZOnDJEti8oDHJSXARMa9BL88I-_1DqO6xMYnCmyEuMaobGSreEb75BpKe3E1W00"; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/config/rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | (?i).*Alpha(?:-?\d+)? 11 | (?i).*a(?:-?\d+)? 12 | (?i).*Beta(?:-?\d+)? 13 | (?i).*-B(?:-?\d+)? 14 | (?i).*RC(?:-?\d+)? 15 | (?i).*CR(?:-?\d+)? 16 | (?i).*M(?:-?\d+)? 17 | (?i).*-dev((?:-?\d+)|(?:\.20\d{6}))? 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestPDF.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.HttpURLConnection; 5 | import java.net.URL; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import net.sourceforge.plantuml.servlet.utils.TestUtils; 11 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 12 | 13 | 14 | public class TestPDF extends WebappTestCase { 15 | 16 | /** 17 | * Verifies the generation of the PDF for the Bob -> Alice sample 18 | */ 19 | @Test 20 | public void testSimpleSequenceDiagram() throws IOException { 21 | final URL url = new URL(getServerUrl() + "/pdf/" + TestUtils.SEQBOB); 22 | final HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 23 | // Analyze response 24 | // Verifies HTTP status code and the Content-Type 25 | Assertions.assertEquals(200, conn.getResponseCode(), "Bad HTTP status received"); 26 | Assertions.assertEquals( 27 | "application/pdf", 28 | conn.getContentType().toLowerCase(), 29 | "Response content type is not PDF" 30 | ); 31 | // Get the content and verify its size 32 | String diagram = getContentText(conn); 33 | int diagramLen = diagram.length(); 34 | Assertions.assertTrue(diagramLen > 1500); 35 | Assertions.assertTrue(diagramLen < 2000); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/diagram-export/diagram-export.jsp: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /src/main/webapp/error.jsp: -------------------------------------------------------------------------------- 1 | <%@ page isErrorPage="true" contentType="text/html; charset=utf-8" pageEncoding="utf-8" session="false" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PlantUMLServer Error 16 | 17 | 18 |

19 | Sorry, but things didn't work out as planned. 20 |

21 |
22 | 23 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/webapp/components/app.js: -------------------------------------------------------------------------------- 1 | /********************************* 2 | * PlantUML Server Application JS * 3 | **********************************/ 4 | "use strict"; 5 | 6 | async function initApp() { 7 | const view = new URL(window.location.href).searchParams.get("view")?.toLowerCase(); 8 | 9 | function initializeAppData() { 10 | const analysedUrl = analyseUrl(window.location.href); 11 | const code = document.editor?.getValue(); 12 | document.appData = Object.assign({}, window.opener?.document.appData); 13 | if (Object.keys(document.appData).length === 0) { 14 | document.appData = { 15 | encodedDiagram: analysedUrl.encodedDiagram, 16 | index: analysedUrl.index, 17 | numberOfDiagramPages: (code) ? getNumberOfDiagramPagesFromCode(code) : 1, 18 | }; 19 | } 20 | } 21 | 22 | await initEditor(view); 23 | initializeAppData(); 24 | initTheme(); 25 | initAppCommunication(); 26 | await initPreview(view); 27 | initModals(view); 28 | 29 | if (document.editor) { 30 | document.editor.focus(); 31 | if (document.appData.encodedDiagram == "SyfFKj2rKt3CoKnELR1Io4ZDoSa70000") { 32 | // if default `Bob -> Alice : hello` example mark example code for faster editing 33 | document.editor.setSelection({ 34 | startLineNumber: 2, 35 | endLineNumber: 2, 36 | startColumn: 1, 37 | endColumn: 21, 38 | }); 39 | } 40 | } 41 | 42 | document.appConfig.autoRefreshState = "complete"; 43 | } 44 | 45 | // main entry 46 | window.onload = initApp; 47 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

This package is in charge of the JEE PlantUml Server.

4 |

There are 2 kind of servlets in this package:
5 | - Interactive servlets: Welcome, PlantUmlServlet that are in charge of the web pages dedicated to human users.
6 | - Service servlets: ImgServlet, SvgServlet, EpsServlet, EpsTextServlet, AsciiServlet, ProxyServlet that only produce a diagram as output.
7 |
8 | Structure of the service part of the PlantUmlServer:
9 | Class diagram of the service part of the PlantUmlServer 13 |

14 |

15 | Generation of a PNG image illustrated 19 |

20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/WebUI/language-features.md: -------------------------------------------------------------------------------- 1 | # PlantUML Language Features 2 | 3 | ## Auto Completion 4 | 5 | ### Icons 6 | 7 | - type `<&` to get a list of PlantUML available icons 8 | - see a preview of the suggested icon in its description 9 | - [PlantUML documentation](https://plantuml.com/openiconic) 10 | 11 | ![icons](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/auto-completion-icons.gif) 12 | 13 | ### Emojis 14 | 15 | - type `<:` to get a list of PlantUML available icons 16 | - see a preview of the suggested icon in its description 17 | - [PlantUML documentation](https://plantuml.com/creole#68305e25f5788db0) 18 | 19 | ![emojis](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/auto-completion-emojis.gif) 20 | 21 | ### Themes 22 | 23 | - type `!t` to get the suggestion `theme` 24 | - type `!theme ` to get a list of (local) available PlantUML themes. 25 | - [PlantUML documentation](https://plantuml.com/theme) 26 | 27 | ![themes](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/auto-completion-themes.gif) 28 | 29 | 30 | ## Validation 31 | 32 | ### `@start...` and `@end...` 33 | 34 | - `@start...` should always be the first command 35 | - `@end...` should alway be the last command 36 | - `@start...` should only exists once 37 | - `@end...` should only exists once 38 | - `@end...` should have the same type as `@start...` 39 | e.g.: `@startjson ... @endjson` 40 | 41 | ![start-end](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/validation-start-end.gif) 42 | -------------------------------------------------------------------------------- /src/main/webapp/js/utilities/theme-helpers.js: -------------------------------------------------------------------------------- 1 | /**************** 2 | * Theme Helpers * 3 | *****************/ 4 | 5 | function setTheme(theme) { 6 | document.documentElement.setAttribute("data-theme", theme); 7 | } 8 | 9 | function initTheme() { 10 | function getBrowserThemePreferences() { 11 | if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 12 | return "dark"; 13 | } 14 | if (window.matchMedia("(prefers-color-scheme: light)").matches) { 15 | return "light"; 16 | } 17 | return undefined; 18 | } 19 | function changeEditorThemeSettingIfNecessary(theme) { 20 | if (theme === "dark" && document.appConfig.editorCreateOptions.theme === "vs") { 21 | document.appConfig.editorCreateOptions.theme = "vs-dark"; 22 | } 23 | if (theme === "light" && document.appConfig.editorCreateOptions.theme === "vs-dark") { 24 | document.appConfig.editorCreateOptions.theme = "vs"; 25 | } 26 | } 27 | function onMediaColorPreferencesChanged(event) { 28 | const theme = event.matches ? "dark" : "light"; 29 | document.appConfig.theme = theme 30 | changeEditorThemeSettingIfNecessary(theme); 31 | updateConfig(document.appConfig); 32 | } 33 | // set theme to last saved settings or browser preference or "light" 34 | document.appConfig.theme = document.appConfig.theme || getBrowserThemePreferences() || "light"; 35 | setTheme(document.appConfig.theme); 36 | changeEditorThemeSettingIfNecessary(document.appConfig.theme); 37 | // listen to browser change event 38 | window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", onMediaColorPreferencesChanged); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/utility/NullOutputStream.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet.utility; 25 | 26 | import java.io.IOException; 27 | import java.io.OutputStream; 28 | 29 | /** 30 | * This output stream ignores everything and writes nothing. 31 | */ 32 | public class NullOutputStream extends OutputStream { 33 | 34 | /** 35 | * Writes to nowhere. 36 | * 37 | * @param b anything 38 | */ 39 | @Override 40 | public void write(int b) throws IOException { 41 | // Do nothing silently 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/preview.js: -------------------------------------------------------------------------------- 1 | /************* 2 | * Preview JS * 3 | **************/ 4 | 5 | async function initPreview(view) { 6 | const btnUndock = document.getElementById("btn-undock"); 7 | const btnDock = document.getElementById("btn-dock"); 8 | const editorContainer = document.getElementById("editor-main-container"); 9 | const previewContainer = document.getElementById("previewer-main-container"); 10 | 11 | function hidePreview() { 12 | setVisibility(btnUndock, false); 13 | // if not opened via button and therefore a popup, `window.close` won't work 14 | setVisibility(btnDock, window.opener); 15 | if (editorContainer) editorContainer.style.width = "100%"; 16 | if (previewContainer) setVisibility(previewContainer, false); 17 | } 18 | function showPreview() { 19 | setVisibility(btnUndock, true); 20 | setVisibility(btnDock, false); 21 | if (editorContainer) editorContainer.style.removeProperty("width"); 22 | if (previewContainer) setVisibility(previewContainer, true); 23 | } 24 | function undock() { 25 | const url = new URL(window.location.href); 26 | url.searchParams.set("view", "previewer"); 27 | const previewer = window.open(url, "PlantUML Diagram Previewer", "popup"); 28 | if (previewer) { 29 | previewer.onbeforeunload = showPreview; 30 | hidePreview(); 31 | } 32 | } 33 | // add listener 34 | btnUndock.addEventListener("click", undock); 35 | // init preview components 36 | await initializeDiagram(); 37 | initializePaginator() 38 | // check preview visibility 39 | if (["previewer", "editor"].includes(view)) { 40 | hidePreview(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/webapp/js/config/config.js: -------------------------------------------------------------------------------- 1 | /***************** 2 | * Configurations * 3 | ******************/ 4 | 5 | const { applyConfig, updateConfig } = (function() { 6 | const DEFAULT_APP_CONFIG = { 7 | changeEventsEnabled: true, 8 | // `autoRefreshState` is mostly used for unit testing puposes. 9 | // states: disabled | waiting | started | syncing | complete 10 | autoRefreshState: "disabled", 11 | theme: undefined, // dark | light (will be set via `initTheme` if undefined) 12 | diagramPreviewType: "png", 13 | editorWatcherTimeout: 500, 14 | editorCreateOptions: { 15 | automaticLayout: true, 16 | fixedOverflowWidgets: true, 17 | minimap: { enabled: false }, 18 | scrollbar: { alwaysConsumeMouseWheel: false }, 19 | scrollBeyondLastLine: false, 20 | tabSize: 2, 21 | theme: "vs", // "vs-dark" 22 | } 23 | }; 24 | 25 | function applyConfig() { 26 | setTheme(document.appConfig.theme); 27 | document.editor?.updateOptions(document.appConfig.editorCreateOptions); 28 | document.settingsEditor?.updateOptions(document.appConfig.editorCreateOptions); 29 | } 30 | function updateConfig(appConfig) { 31 | localStorage.setItem("document.appConfig", JSON.stringify(appConfig)); 32 | sendMessage({ 33 | sender: "config", 34 | data: { appConfig }, 35 | synchronize: true, 36 | }); 37 | } 38 | 39 | document.appConfig = Object.assign({}, window.opener?.document.appConfig); 40 | if (Object.keys(document.appConfig).length === 0) { 41 | document.appConfig = JSON.parse(localStorage.getItem("document.appConfig")) || DEFAULT_APP_CONFIG; 42 | } 43 | 44 | return { applyConfig, updateConfig }; 45 | })(); 46 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/EpsServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import net.sourceforge.plantuml.FileFormat; 27 | 28 | /** 29 | * EPS servlet of the webapp. 30 | * This servlet produces the UML diagram in EPS format. 31 | */ 32 | @SuppressWarnings("SERIAL") 33 | public class EpsServlet extends UmlDiagramService { 34 | 35 | /** 36 | * Gives the wished output format of the diagram. 37 | * This value is used by the DiagramResponse class. 38 | * 39 | * @return the format for EPS responses 40 | */ 41 | @Override 42 | public FileFormat getOutputFormat() { 43 | return FileFormat.EPS; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/PdfServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import net.sourceforge.plantuml.FileFormat; 27 | 28 | /** 29 | * PDF servlet of the webapp. 30 | * This servlet produces the UML diagram in PDF format. 31 | */ 32 | @SuppressWarnings("SERIAL") 33 | public class PdfServlet extends UmlDiagramService { 34 | 35 | /** 36 | * Gives the wished output format of the diagram. 37 | * This value is used by the DiagramResponse class. 38 | * 39 | * @return the format for pdf responses 40 | */ 41 | @Override 42 | public FileFormat getOutputFormat() { 43 | return FileFormat.PDF; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/SvgServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import net.sourceforge.plantuml.FileFormat; 27 | 28 | /** 29 | * SVG servlet of the webapp. 30 | * This servlet produces the UML diagram in SVG format. 31 | */ 32 | @SuppressWarnings("SERIAL") 33 | public class SvgServlet extends UmlDiagramService { 34 | 35 | /** 36 | * Gives the wished output format of the diagram. 37 | * This value is used by the DiagramResponse class. 38 | * 39 | * @return the format for svg responses 40 | */ 41 | @Override 42 | public FileFormat getOutputFormat() { 43 | return FileFormat.SVG; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/ImgServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import net.sourceforge.plantuml.FileFormat; 27 | 28 | /** 29 | * Image servlet of the webapp. 30 | * This servlet produces the UML diagram in PNG format. 31 | */ 32 | @SuppressWarnings("SERIAL") 33 | public class ImgServlet extends UmlDiagramService { 34 | 35 | /** 36 | * Gives the wished output format of the diagram. 37 | * This value is used by the DiagramResponse class. 38 | * 39 | * @return the format for image responses 40 | */ 41 | @Override 42 | public FileFormat getOutputFormat() { 43 | return FileFormat.PNG; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/AsciiServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import net.sourceforge.plantuml.FileFormat; 27 | 28 | /** 29 | * ASCII servlet of the webapp. 30 | * This servlet produces the UML sequence diagram in text format. 31 | */ 32 | @SuppressWarnings("SERIAL") 33 | public class AsciiServlet extends UmlDiagramService { 34 | 35 | /** 36 | * Gives the wished output format of the diagram. 37 | * This value is used by the DiagramResponse class. 38 | * 39 | * @return the format for ASCII responses 40 | */ 41 | @Override 42 | public FileFormat getOutputFormat() { 43 | return FileFormat.UTXT; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/Base64Servlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import net.sourceforge.plantuml.FileFormat; 27 | 28 | /** 29 | * Base64 servlet of the webapp. 30 | * This servlet produces the UML diagram in Base64 format. 31 | */ 32 | @SuppressWarnings("SERIAL") 33 | public class Base64Servlet extends UmlDiagramService { 34 | 35 | /** 36 | * Gives the wished output format of the diagram. 37 | * This value is used by the DiagramResponse class. 38 | * 39 | * @return the format for Base64 responses 40 | */ 41 | @Override 42 | public FileFormat getOutputFormat() { 43 | return FileFormat.BASE64; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/EpsTextServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import net.sourceforge.plantuml.FileFormat; 27 | 28 | /** 29 | * EPS Text servlet of the webapp. 30 | * This servlet produces the UML diagram in EPS Text format. 31 | */ 32 | @SuppressWarnings("SERIAL") 33 | public class EpsTextServlet extends UmlDiagramService { 34 | 35 | /** 36 | * Gives the wished output format of the diagram. 37 | * This value is used by the DiagramResponse class. 38 | * 39 | * @return the format for EPS Text responses 40 | */ 41 | @Override 42 | public FileFormat getOutputFormat() { 43 | return FileFormat.EPS_TEXT; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/settings/settings.jsp: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /src/main/webapp/assets/file-types/svg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page info="index" contentType="text/html; charset=utf-8" pageEncoding="utf-8" session="false" %> 2 | <% 3 | // diagram sources 4 | String encoded = request.getAttribute("encoded").toString(); 5 | String decoded = request.getAttribute("decoded").toString(); 6 | String index = request.getAttribute("index").toString(); 7 | String diagramUrl = ((index.isEmpty()) ? "" : index + "/") + encoded; 8 | // map for diagram source if necessary 9 | String map = request.getAttribute("map").toString(); 10 | boolean hasMap = !map.isEmpty(); 11 | // properties 12 | boolean showSocialButtons = (boolean)request.getAttribute("showSocialButtons"); 13 | boolean showGithubRibbon = (boolean)request.getAttribute("showGithubRibbon"); 14 | %> 15 | 16 | 17 | 18 | 19 | <%@ include file="/components/app-head.jsp" %> 20 | PlantUML Server 21 | 22 | 23 |
24 |
25 | <%@ include file="/components/header/header.jsp" %> 26 |
27 |
28 | <%@ include file="/components/editor/editor.jsp" %> 29 |
30 | <%@ include file="/components/preview/preview.jsp" %> 31 |
32 |
33 | 36 | 37 | <%@ include file="/components/modals/diagram-import/diagram-import.jsp" %> 38 | <%@ include file="/components/modals/diagram-export/diagram-export.jsp" %> 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/webapp/assets/file-types/ascii.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/modals.js: -------------------------------------------------------------------------------- 1 | /************ 2 | * Modals JS * 3 | *************/ 4 | 5 | const { registerModalListener, openModal, closeModal } = (function() { 6 | const modalListener = {}; 7 | return { 8 | registerModalListener: (id, fnOpen=undefined, fnClose=undefined) => { 9 | modalListener[id] = { fnOpen, fnClose }; 10 | }, 11 | openModal: (id, ...args) => { 12 | const fnOpen = modalListener[id]?.fnOpen; 13 | if (fnOpen) { 14 | fnOpen(...args); 15 | } else { 16 | setVisibility(document.getElementById(id), true, true); 17 | } 18 | }, 19 | closeModal: (id, ...args) => { 20 | const fnClose = modalListener[id]?.fnClose; 21 | if (fnClose) { 22 | fnClose(...args); 23 | } else { 24 | setVisibility(document.getElementById(id), false); 25 | } 26 | }, 27 | }; 28 | })(); 29 | 30 | function initModals(view) { 31 | function onModalKeydown(event) { 32 | if (event.key === "Escape" || event.key === "Esc") { 33 | event.preventDefault(); 34 | closeModal(event.target.closest(".modal").id); 35 | } else if (event.key === "Enter") { 36 | event.preventDefault(); 37 | const modal = event.target.closest(".modal"); 38 | const okBtn = modal.querySelector('input.ok[type="button"]'); 39 | if (okBtn && !okBtn.disabled) { 40 | okBtn.click(); 41 | } 42 | } 43 | } 44 | document.querySelectorAll(".modal").forEach(modal => { 45 | modal.addEventListener("keydown", onModalKeydown, false); 46 | }); 47 | // init modals 48 | initSettings(); 49 | if (view !== "previewer") { 50 | initDiagramExport(); 51 | initDiagramImport(); 52 | } 53 | } 54 | 55 | function isModalOpen(id) { 56 | return isVisible(document.getElementById(id)); 57 | } 58 | 59 | function closeAllModals() { 60 | document.querySelectorAll(".modal").forEach(modal => closeModal(modal.id)); 61 | } 62 | -------------------------------------------------------------------------------- /src/main/webapp/resource/test/bob.svg: -------------------------------------------------------------------------------- 1 | 2 | BobBobAliceAlicehello -------------------------------------------------------------------------------- /src/main/webapp/js/utilities/url-helpers.js: -------------------------------------------------------------------------------- 1 | /************** 2 | * URL Helpers * 3 | ***************/ 4 | 5 | function resolvePath(path) { 6 | return PlantUmlLanguageFeatures.absolutePath(path); 7 | } 8 | 9 | function prepareUrl(url) { 10 | if (!(url instanceof URL)) { 11 | url = new URL(resolvePath(url)); 12 | } 13 | // pathname excluding context path 14 | let base = new URL((document.querySelector("base") || {}).href || window.location.origin).pathname; 15 | if (base.slice(-1) === "/") base = base.slice(0, -1); 16 | const pathname = url.pathname.startsWith(base) ? url.pathname.slice(base.length) : url.pathname; 17 | // same as `UrlDataExtractor.URL_PATTERN` 18 | // regex = /\/\w+(?:\/(?\d+))?(?:\/(?[^\/]+))?\/?$/gm; 19 | const regex = /\/\w+(?:\/(\d+))?(?:\/([^/]+))?\/?$/gm; 20 | const match = regex.exec(pathname); 21 | return [ url, pathname, { idx: match[1], encoded: match[2] } ]; 22 | } 23 | 24 | function analyseUrl(url) { 25 | let _, idx, encoded; 26 | [url, _, { idx, encoded }] = prepareUrl(url); 27 | return { 28 | index: idx, 29 | encodedDiagram: encoded || url.searchParams.get("url"), 30 | }; 31 | } 32 | 33 | function replaceUrl(url, encodedDiagram, index) { 34 | let oldPathname, encoded; 35 | [url, oldPathname, { encoded }] = prepareUrl(url); 36 | let pathname = oldPathname.slice(1); 37 | pathname = pathname.slice(0, pathname.indexOf("/")); 38 | if (index && index >= 0) pathname += "/" + index; 39 | if (encoded) pathname += "/" + encodedDiagram; 40 | if (oldPathname.slice(-1) === "/") pathname += "/"; 41 | url.pathname = new URL(resolvePath(pathname)).pathname; 42 | if (url.searchParams.get("url")) { 43 | url.searchParams.set("url", encodedDiagram); 44 | } 45 | return { url, pathname }; 46 | } 47 | 48 | function buildUrl(serletpath, encodedDiagram, index) { 49 | let pathname = serletpath; 50 | if (index && index >= 0) pathname += "/" + index; 51 | pathname += "/" + encodedDiagram; 52 | return pathname; 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/diagrams.txt: -------------------------------------------------------------------------------- 1 | ## Class diagram ## 2 | ################### 3 | 4 | @startuml 5 | hide empty members 6 | hide empty methods 7 | hide empty fields 8 | abstract class UmlDiagramService { 9 | + doGet(request: HttpServletRequest, response: HttpServletResponse) : void 10 | + doPost(request: HttpServletRequest, response: HttpServletResponse) : void 11 | + {abstract} getOutputFormat() : FileFormat 12 | } 13 | class DiagramResponse { 14 | + DiagramResponse(res: HttpServletResponse, fmt: FileFormat, req: HttpServletRequest) 15 | + sendDiagram(uml: String, idx: int) : void 16 | + sendMap(uml: String, idx: int) : void 17 | + sendCheck(uml: String) : void 18 | } 19 | HttpServlet <|-- CheckSyntaxServlet 20 | HttpServlet <|-- LanguageServlet 21 | HttpServlet <|-- MapServlet 22 | HttpServlet <|-- PlantUmlServlet 23 | HttpServlet <|-- ProxyServlet 24 | HttpServlet <|-- OldProxyServlet 25 | HttpServlet <|-- UmlDiagramService 26 | UmlDiagramService <|-- AsciiServlet 27 | UmlDiagramService <|-- Base64Servlet 28 | UmlDiagramService <|-- EpsServlet 29 | UmlDiagramService <|-- EpsTextServlet 30 | UmlDiagramService <|-- ImgServlet 31 | UmlDiagramService <|-- SvgServlet 32 | UmlDiagramService o-- DiagramResponse 33 | DiagramResponse --o CheckSyntaxServlet 34 | DiagramResponse --o MapServlet 35 | DiagramResponse --o ProxyServlet 36 | @enduml 37 | 38 | ## Sequence diagram ## 39 | ###################### 40 | 41 | @startuml 42 | title Generation of a PNG image illustrated 43 | ImgServlet -> UrlDataExtractor : getEncodedDiagram() 44 | UrlDataExtractor --> ImgServlet : encoded 45 | ImgServlet -> UrlDataExtractor : getIndex() 46 | UrlDataExtractor --> ImgServlet : index 47 | ImgServlet -> UmlExtractor : getUmlSource() 48 | UmlExtractor --> ImgServlet : decoded 49 | ImgServlet -> ImgServlet : getOutputFormat() 50 | ImgServlet -> "dr:DiagramResponse" as dr ** : <> 51 | ImgServlet -> dr : sendDiagram() 52 | participant "PlantUML library" as Lib #99FF99 53 | dr -> Lib : generateImage() 54 | Lib --> dr 55 | dr --> ImgServlet 56 | @enduml 57 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestCheck.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.net.URLConnection; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import net.sourceforge.plantuml.servlet.utils.TestUtils; 11 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 12 | 13 | 14 | public class TestCheck extends WebappTestCase { 15 | 16 | /** 17 | * Verifies the generation of a syntax check for the following sample: 18 | * Bob -> Alice : hello 19 | */ 20 | @Test 21 | public void testCorrectSequenceDiagram() throws IOException { 22 | final URL url = new URL(getServerUrl() + "/check/" + TestUtils.SEQBOB); 23 | final URLConnection conn = url.openConnection(); 24 | // Analyze response 25 | // Verifies the Content-Type header 26 | Assertions.assertEquals( 27 | "text/plain;charset=utf-8", 28 | conn.getContentType().toLowerCase(), 29 | "Response content type is not TEXT PLAIN or UTF-8" 30 | ); 31 | // Get the content, check its first characters and verify its size 32 | String checkResult = getContentText(conn); 33 | Assertions.assertTrue( 34 | checkResult.startsWith("(2 participants)"), 35 | "Response content is not starting with (2 participants)" 36 | ); 37 | int checkLen = checkResult.length(); 38 | Assertions.assertTrue(checkLen > 1); 39 | Assertions.assertTrue(checkLen < 100); 40 | } 41 | 42 | /** 43 | * Check the syntax of an invalid sequence diagram : 44 | * Bob - 45 | */ 46 | @Test 47 | public void testWrongDiagramSyntax() throws IOException { 48 | final URL url = new URL(getServerUrl() + "/check/SyfFKj050000"); 49 | // Analyze response 50 | String checkResult = getContentText(url); 51 | Assertions.assertTrue(checkResult.startsWith("(Error)"), "Response is not an error"); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/webapp/components/editor/url-input/editor-url-input.js: -------------------------------------------------------------------------------- 1 | /********************** 2 | * Editor URL Input JS * 3 | ***********************/ 4 | 5 | const { setUrlValue, initEditorUrlInput } = (function() { 6 | function setUrlValue( 7 | url=undefined, 8 | { encodedDiagram=undefined, index=undefined } = {}, 9 | { suppressEditorChangedMessage=false } = {} 10 | ) { 11 | if (!url && !encodedDiagram) return; 12 | if (suppressEditorChangedMessage) { 13 | suppressNextMessage("url"); 14 | } 15 | document.getElementById("url").value = url ? url : resolvePath(buildUrl("png", encodedDiagram, index)); 16 | } 17 | 18 | function initEditorUrlInput() { 19 | const input = document.getElementById("url"); 20 | 21 | function copyUrlToClipboard() { 22 | input.focus(); 23 | input.select(); 24 | navigator.clipboard?.writeText(input.value).catch(() => {}); 25 | } 26 | async function onInputChanged(event) { 27 | document.appConfig.autoRefreshState = "started"; 28 | event.target.title = event.target.value; 29 | const analysedUrl = analyseUrl(event.target.value); 30 | // decode diagram (server request) 31 | const code = await makeRequest("GET", "coder/" + analysedUrl.encodedDiagram); 32 | // change editor content without sending the editor change message 33 | setEditorValue(document.editor, code, { suppressEditorChangedMessage: true }); 34 | sendMessage({ 35 | sender: "url", 36 | data: { 37 | encodedDiagram: analysedUrl.encodedDiagram, 38 | index: analysedUrl.index, 39 | }, 40 | synchronize: true, 41 | }); 42 | } 43 | 44 | // resolve relative path inside url input once 45 | setUrlValue(resolvePath(input.value)); 46 | // update editor and everything else if the URL input is changed 47 | input.addEventListener("change", onInputChanged); 48 | // add listener 49 | document.getElementById("url-copy-btn").addEventListener("click", copyUrlToClipboard); 50 | } 51 | 52 | return { setUrlValue, initEditorUrlInput }; 53 | })(); 54 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/menu/preview-menu.jsp: -------------------------------------------------------------------------------- 1 |
2 | 57 |
58 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestDependencies.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.HttpURLConnection; 5 | import java.net.URL; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Tag; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import net.sourceforge.plantuml.servlet.utils.TestUtils; 12 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 13 | 14 | 15 | public class TestDependencies extends WebappTestCase { 16 | 17 | /** 18 | * Verifies that Graphviz is installed and can be found 19 | */ 20 | @Test 21 | @Tag("graphviz-test") 22 | public void testGraphviz() throws IOException { 23 | final URL url = new URL(getServerUrl() + "/txt/" + TestUtils.VERSION); 24 | final HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 25 | // Analyze response 26 | // Verifies HTTP status code and the Content-Type 27 | Assertions.assertEquals(200, conn.getResponseCode(), "Bad HTTP status received"); 28 | Assertions.assertEquals( 29 | "text/plain;charset=utf-8", 30 | conn.getContentType().toLowerCase(), 31 | "Response content type is not TEXT PLAIN or UTF-8" 32 | ); 33 | // Get the content and check installation status 34 | String diagram = getContentText(conn); 35 | Assertions.assertTrue( 36 | diagram.contains("Installation seems OK. File generation OK"), 37 | "Version diagram was:\n" + diagram 38 | ); 39 | } 40 | 41 | /** 42 | * Verifies that the Monaco Editor webjar can be loaded 43 | */ 44 | @Test 45 | public void testMonacoEditorWebJar() throws IOException { 46 | final URL url = new URL(getServerUrl() + "/webjars/monaco-editor/0.36.1/min/vs/loader.js"); 47 | final HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 48 | // Analyze response 49 | // Verifies HTTP status code and the Content-Type 50 | Assertions.assertEquals(200, conn.getResponseCode(), "Bad HTTP status received"); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/paginator/paginator.js: -------------------------------------------------------------------------------- 1 | /*************** 2 | * Paginator JS * 3 | ***************/ 4 | 5 | function getNumberOfDiagramPagesFromCode(code) { 6 | // count `newpage` inside code 7 | // known issue: a `newpage` starting in a newline inside a multiline comment will also be counted 8 | return code.match(/^\s*newpage\s?.*$/gm)?.length + 1 || 1; 9 | } 10 | 11 | function updatePaginatorSelection() { 12 | const paginator = document.getElementById("paginator"); 13 | const index = document.appData.index; 14 | if (index === undefined || paginator.childNodes.length <= index) { 15 | for (const node of paginator.childNodes) { 16 | node.checked = false; 17 | } 18 | } else { 19 | paginator.childNodes[index].checked = true; 20 | } 21 | } 22 | 23 | const updatePaginator = (function() { 24 | function updateNumberOfPagingElements(paginator, pages) { 25 | // remove elements (buttons) if there are to many 26 | while (paginator.childElementCount > pages) { 27 | paginator.removeChild(paginator.lastChild) 28 | } 29 | // add elements (buttons) if there are to less 30 | while (paginator.childElementCount < pages) { 31 | const radioBtn = document.createElement("input"); 32 | radioBtn.name = "paginator"; 33 | radioBtn.type = "radio"; 34 | radioBtn.value = paginator.childElementCount; 35 | radioBtn.addEventListener("click", (event) => { 36 | sendMessage({ 37 | sender: "paginator", 38 | data: { index: event.target.value }, 39 | synchronize: true, 40 | }); 41 | }); 42 | paginator.appendChild(radioBtn); 43 | } 44 | } 45 | return function() { 46 | const paginator = document.getElementById("paginator"); 47 | const pages = document.appData.numberOfDiagramPages; 48 | if (pages > 1) { 49 | updateNumberOfPagingElements(paginator, pages); 50 | setVisibility(paginator, true); 51 | } else { 52 | setVisibility(paginator, false); 53 | } 54 | }; 55 | })(); 56 | 57 | function initializePaginator() { 58 | if (document.appData.numberOfDiagramPages > 1) { 59 | updatePaginator(); 60 | updatePaginatorSelection(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/webapp/js/language/completion/icons.js: -------------------------------------------------------------------------------- 1 | /********************************************* 2 | * PlantUML Language Icon Completion Provider * 3 | **********************************************/ 4 | 5 | PlantUmlLanguageFeatures.prototype.getIcons = (function(){ 6 | let icons = undefined; 7 | return async function() { 8 | if (icons === undefined) { 9 | icons = await PlantUmlLanguageFeatures.makeRequest("GET", "ui-helper?request=icons"); 10 | } 11 | return icons; 12 | } 13 | })(); 14 | 15 | PlantUmlLanguageFeatures.prototype.registerIconCompletion = function() { 16 | const createIconProposals = async (range, filter = undefined) => { 17 | const icons = await this.getIcons(); 18 | return icons?.filter(icon => filter ? icon.includes(filter) : true) 19 | .map(icon => { 20 | // NOTE: markdown image path inside suggestions seems to have rendering issues while using relative paths 21 | const iconUrl = PlantUmlLanguageFeatures.absolutePath( 22 | PlantUmlLanguageFeatures.baseUrl + "ui-helper?request=icons.svg#" + icon 23 | ); 24 | return { 25 | label: icon, 26 | kind: monaco.languages.CompletionItemKind.Constant, 27 | documentation: { 28 | //supportHtml: true, // also a possibility but quite limited html 29 | value: "![icon](" + iconUrl + ")   " + icon 30 | }, 31 | insertText: icon + ">", 32 | range: range 33 | }; 34 | }) || []; 35 | }; 36 | 37 | monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, { 38 | triggerCharacters: ["&"], 39 | provideCompletionItems: async (model, position) => { 40 | const textUntilPosition = model.getValueInRange({ 41 | startLineNumber: position.lineNumber, 42 | startColumn: 1, 43 | endLineNumber: position.lineNumber, 44 | endColumn: position.column, 45 | }); 46 | const match = textUntilPosition.match(/<&([^\s>]*)$/); 47 | if (match) { 48 | const suggestions = await createIconProposals(this.getWordRange(model, position), match[1]); 49 | return { suggestions }; 50 | } 51 | return { suggestions: [] }; 52 | } 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/main/webapp/js/language/completion/themes.js: -------------------------------------------------------------------------------- 1 | /********************************************** 2 | * PlantUML Language Theme Completion Provider * 3 | ***********************************************/ 4 | 5 | PlantUmlLanguageFeatures.prototype.getThemes = (function(){ 6 | let themes = undefined; 7 | return async function() { 8 | if (themes === undefined) { 9 | themes = await PlantUmlLanguageFeatures.makeRequest("GET", "ui-helper?request=themes"); 10 | } 11 | return themes; 12 | } 13 | })(); 14 | 15 | PlantUmlLanguageFeatures.prototype.registerThemeCompletion = function() { 16 | const createThemeProposals = async (range, filter = undefined) => { 17 | const themes = await this.getThemes(); 18 | return themes?.filter(theme => filter ? theme.includes(filter) : true) 19 | .map(theme => ({ 20 | label: theme, 21 | kind: monaco.languages.CompletionItemKind.Text, 22 | documentation: "PlantUML " + theme + " theme", 23 | insertText: theme, 24 | range: range, 25 | })) || []; 26 | }; 27 | 28 | monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, { 29 | triggerCharacters: [" "], 30 | provideCompletionItems: async (model, position) => { 31 | const textUntilPosition = model.getValueInRange({ 32 | startLineNumber: position.lineNumber, 33 | startColumn: 1, 34 | endLineNumber: position.lineNumber, 35 | endColumn: position.column, 36 | }); 37 | if (textUntilPosition.match(/^\s*!(t(h(e(m(e)?)?)?)?)?$/)) { 38 | return { 39 | suggestions: [ 40 | { 41 | label: 'theme', 42 | kind: monaco.languages.CompletionItemKind.Keyword, 43 | documentation: "PlantUML theme command", 44 | insertText: 'theme', 45 | range: this.getWordRange(model, position), 46 | } 47 | ] 48 | }; 49 | } 50 | const match = textUntilPosition.match(/^\s*!theme\s+([^\s]*)$/); 51 | if (match) { 52 | const suggestions = await createThemeProposals(this.getWordRange(model, position), match[1]); 53 | return { suggestions }; 54 | } 55 | return { suggestions: [] }; 56 | } 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /examples/kubernetes-simple/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: plantuml 6 | labels: 7 | app.kubernetes.io/name: plantuml 8 | app.kubernetes.io/instance: plantuml 9 | spec: 10 | replicas: 3 # Can be adjusted 11 | selector: 12 | matchLabels: 13 | app.kubernetes.io/name: plantuml 14 | app.kubernetes.io/instance: plantuml 15 | template: 16 | metadata: 17 | labels: 18 | app.kubernetes.io/name: plantuml 19 | app.kubernetes.io/instance: plantuml 20 | spec: 21 | containers: 22 | - name: plantuml 23 | securityContext: 24 | allowPrivilegeEscalation: false 25 | image: plantuml/plantuml-server:jetty-v1.2022.6 26 | imagePullPolicy: Always 27 | # env: # In case of different base URL 28 | # - name: BASE_URL 29 | # value: plantuml 30 | ports: 31 | - name: http 32 | containerPort: 8080 33 | protocol: TCP 34 | livenessProbe: 35 | tcpSocket: 36 | port: http 37 | readinessProbe: 38 | tcpSocket: 39 | port: http 40 | resources: 41 | limits: 42 | cpu: 500m 43 | memory: 2048Mi 44 | requests: 45 | cpu: 250m 46 | memory: 1024Mi 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: plantuml 52 | labels: 53 | app.kubernetes.io/name: plantuml 54 | app.kubernetes.io/instance: plantuml 55 | spec: 56 | type: ClusterIP 57 | ports: 58 | - port: 80 59 | targetPort: http 60 | protocol: TCP 61 | name: http 62 | selector: 63 | app.kubernetes.io/name: plantuml 64 | app.kubernetes.io/instance: plantuml 65 | --- 66 | apiVersion: networking.k8s.io/v1 67 | kind: Ingress 68 | metadata: 69 | name: plantuml 70 | labels: 71 | app.kubernetes.io/name: plantuml 72 | app.kubernetes.io/instance: plantuml 73 | spec: 74 | rules: 75 | - host: plantuml-example.localhost 76 | http: 77 | paths: 78 | - backend: 79 | service: 80 | name: plantuml 81 | port: 82 | number: 80 83 | path: / 84 | pathType: Prefix 85 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestCharset.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.net.URLConnection; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 11 | 12 | 13 | public class TestCharset extends WebappTestCase { 14 | 15 | /** 16 | * Verifies the preservation of unicode characters for the "Bob -> Alice : hell‽" sample 17 | */ 18 | @Test 19 | public void testUnicodeSupport() throws IOException { 20 | final URL url = new URL(getServerUrl() + "/txt/SyfFKj2rKt3CoKnELR1Io4ZDoNdKi1S0"); 21 | final URLConnection conn = url.openConnection(); 22 | // Analyze response 23 | // Verifies the Content-Type header 24 | Assertions.assertEquals( 25 | "text/plain;charset=utf-8", 26 | conn.getContentType().toLowerCase(), 27 | "Response content type is not TEXT PLAIN or UTF-8" 28 | ); 29 | // Get the content and verify that the interrobang unicode character is present 30 | String diagram = getContentText(conn); 31 | Assertions.assertTrue(diagram.contains("‽"), "Interrobang unicode character is not preserved"); 32 | } 33 | 34 | /** 35 | * Verifies the preservation of unicode characters for the 36 | * "participant Bob [[http://www.snow.com/❄]]\nBob -> Alice" sample 37 | */ 38 | @Test 39 | public void testUnicodeInCMap() throws IOException { 40 | final URL url = new URL( 41 | getServerUrl() + 42 | "/map/AqWiAibCpYn8p2jHSCfFKeYEpYWfAR3IroylBzShpiilrqlEpzL_DBSbDfOB9Azhf-2OavcS2W00" 43 | ); 44 | final URLConnection conn = url.openConnection(); 45 | // Analyze response 46 | // Verifies the Content-Type header 47 | Assertions.assertEquals( 48 | "text/plain;charset=utf-8", 49 | conn.getContentType().toLowerCase(), 50 | "Response content type is not TEXT PLAIN or UTF-8" 51 | ); 52 | // Get the content and verify that the snow unicode character is present 53 | String map = getContentText(conn); 54 | Assertions.assertTrue(map.contains("❄"), "Snow unicode character is not preserved"); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Dockerfile.tomcat: -------------------------------------------------------------------------------- 1 | FROM maven:3-eclipse-temurin-11 AS builder 2 | 3 | COPY pom.xml pom.parent.xml /app/ 4 | COPY src/main /app/src/main/ 5 | 6 | WORKDIR /app 7 | RUN mvn --batch-mode --define java.net.useSystemProxies=true -Dapache-jsp.scope=compile package 8 | 9 | ######################################################################################## 10 | 11 | FROM tomcat:10-jdk11 12 | 13 | RUN apt-get update && \ 14 | apt-get install -y --no-install-recommends \ 15 | curl \ 16 | fonts-noto-cjk \ 17 | libgd3 \ 18 | && \ 19 | rm -rf /var/lib/apt/lists/* 20 | 21 | # Build Graphviz from source because there are no binary distributions for recent versions 22 | ARG GRAPHVIZ_VERSION 23 | ARG GRAPHVIZ_BUILD_DIR=/tmp/graphiz-build 24 | RUN apt-get update && \ 25 | apt-get install -y --no-install-recommends \ 26 | build-essential \ 27 | jq \ 28 | libexpat1-dev \ 29 | libgd-dev \ 30 | zlib1g-dev \ 31 | && \ 32 | mkdir -p $GRAPHVIZ_BUILD_DIR && \ 33 | cd $GRAPHVIZ_BUILD_DIR && \ 34 | GRAPHVIZ_VERSION=${GRAPHVIZ_VERSION:-$(curl -s https://gitlab.com/api/v4/projects/4207231/releases/ | jq -r '.[] | .name' | sort -V -r | head -1)} && \ 35 | curl -o graphviz.tar.gz https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/${GRAPHVIZ_VERSION}/graphviz-${GRAPHVIZ_VERSION}.tar.gz && \ 36 | tar -xzf graphviz.tar.gz && \ 37 | cd graphviz-$GRAPHVIZ_VERSION && \ 38 | ./configure && \ 39 | make && \ 40 | make install && \ 41 | apt-get remove -y \ 42 | build-essential \ 43 | jq \ 44 | libexpat1-dev \ 45 | libgd-dev \ 46 | zlib1g-dev \ 47 | && \ 48 | apt-get autoremove -y && \ 49 | apt-get clean && \ 50 | rm -rf /var/lib/apt/lists/* && \ 51 | rm -rf $GRAPHVIZ_BUILD_DIR 52 | 53 | COPY docker-entrypoint.tomcat.sh /entrypoint.sh 54 | RUN chmod +x /entrypoint.sh 55 | 56 | ENV WEBAPP_PATH=$CATALINA_HOME/webapps 57 | RUN rm -rf $WEBAPP_PATH && \ 58 | mkdir -p $WEBAPP_PATH 59 | COPY --from=builder /app/target/plantuml.war /plantuml.war 60 | 61 | # Openshift https://docs.openshift.com/container-platform/4.9/openshift_images/create-images.html#images-create-guide-openshift_create-images 62 | RUN chgrp -R 0 $CATALINA_HOME && chmod -R g=u $CATALINA_HOME 63 | 64 | ENTRYPOINT ["/entrypoint.sh"] 65 | CMD ["catalina.sh", "run"] 66 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/LanguageServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import java.io.IOException; 27 | import java.io.PrintStream; 28 | 29 | import jakarta.servlet.ServletException; 30 | import jakarta.servlet.http.HttpServlet; 31 | import jakarta.servlet.http.HttpServletRequest; 32 | import jakarta.servlet.http.HttpServletResponse; 33 | 34 | import net.sourceforge.plantuml.syntax.LanguageDescriptor; 35 | 36 | /** 37 | * Servlet used to inspect the language keywords of the running PlantUML server. 38 | * Same as {@code java -jar plantuml.jar -language} 39 | */ 40 | public class LanguageServlet extends HttpServlet { 41 | 42 | @Override 43 | protected void doPost(HttpServletRequest request, HttpServletResponse response) 44 | throws ServletException, IOException { 45 | throw new ServletException(new UnsupportedOperationException("The Language servlet only handles GET requests")); 46 | } 47 | 48 | @Override 49 | protected void doGet(HttpServletRequest request, HttpServletResponse response) 50 | throws ServletException, IOException { 51 | final PrintStream ps = new PrintStream(response.getOutputStream()); 52 | response.setContentType("text/text"); 53 | new LanguageDescriptor().print(ps); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/webapp/js/language/completion/emojis.js: -------------------------------------------------------------------------------- 1 | /********************************************** 2 | * PlantUML Language Emoji Completion Provider * 3 | ***********************************************/ 4 | 5 | PlantUmlLanguageFeatures.prototype.getEmojis = (function(){ 6 | let emojis = undefined; 7 | return async function() { 8 | if (emojis === undefined) { 9 | emojis = await PlantUmlLanguageFeatures.makeRequest("GET", "ui-helper?request=emojis"); 10 | } 11 | return emojis; 12 | } 13 | })(); 14 | 15 | PlantUmlLanguageFeatures.prototype.registerEmojiCompletion = function() { 16 | const createEmojiProposals = async (range, filter = undefined) => { 17 | const emojis = await this.getEmojis(); 18 | return emojis?.filter(([unicode, name]) => filter ? unicode.includes(filter) || name?.includes(filter) : true) 19 | .map(([unicode, name]) => { 20 | // NOTE: load images direct from GitHub source: https://github.com/twitter/twemoji#download 21 | const emojiUrl = "https://raw.githubusercontent.com/twitter/twemoji/gh-pages/v/13.1.0/svg/" + unicode + ".svg"; 22 | const docHint = (name) ? name + " (" + unicode + ")" : unicode; 23 | const isUnicode = !name || (filter && unicode.includes(filter)); 24 | const label = isUnicode ? unicode : name; 25 | return { 26 | label: label, 27 | kind: monaco.languages.CompletionItemKind.Constant, 28 | documentation: { 29 | //supportHtml: true, // also a possibility but quite limited html 30 | value: "![emoji](" + emojiUrl + ")   " + docHint 31 | }, 32 | insertText: label + ":>", 33 | range: range 34 | }; 35 | }) || []; 36 | }; 37 | 38 | monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, { 39 | triggerCharacters: [":"], 40 | provideCompletionItems: async (model, position) => { 41 | const textUntilPosition = model.getValueInRange({ 42 | startLineNumber: position.lineNumber, 43 | startColumn: 1, 44 | endLineNumber: position.lineNumber, 45 | endColumn: position.column, 46 | }); 47 | const match = textUntilPosition.match(/<:([^\s>]*)$/); 48 | if (match) { 49 | const suggestions = await createEmojiProposals(this.getWordRange(model, position), match[1]); 50 | return { suggestions }; 51 | } 52 | return { suggestions: [] }; 53 | } 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /examples/additional-fonts/README.md: -------------------------------------------------------------------------------- 1 | # Additional fonts inside the PlantUML docker container 2 | 3 | It is possible to make additional fonts available to PlantUML by mapping them via a volume within the docker container. 4 | 5 | Since the base image from the docker container is using Ubuntu, fonts can easily be provided by just adding them somewhere inside the `~/.local/share/fonts` directory. 6 | 7 | **Tipp**: to not overwrite the container fonts add the additional files inside an own sub-folder, e.g., `custom` or `host`. 8 | 9 | In the following you can find an example how to provide all fonts of your host machine in the PlantUML docker container for Jetty and Tomcat. 10 | 11 | 12 | ## Jetty 13 | 14 | In the case of the Jetty docker container the home directory is `/var/lib/jetty`. 15 | 16 | ```yaml 17 | services: 18 | plantuml-server: 19 | image: plantuml/plantuml-server:jetty 20 | container_name: plantuml-server 21 | ports: 22 | - "80:8080" 23 | environment: 24 | - TZ=Europe/Berlin 25 | - BASE_URL=plantuml 26 | volumes: 27 | - /usr/share/fonts:/var/lib/jetty/.local/share/fonts/host:ro 28 | ``` 29 | 30 | 31 | ## Tomcat 32 | 33 | In the case of the Tomcat docker container the home directory is `/root`. 34 | _Yes, the tomcat container is running as `root` which is basically a really bad idea w.r.t. security. Create a pull request and maintain it if you want to change that._ 35 | 36 | ```yaml 37 | services: 38 | plantuml-server: 39 | image: plantuml/plantuml-server:tomcat 40 | container_name: plantuml-server 41 | ports: 42 | - "80:8080" 43 | environment: 44 | - TZ=Europe/Berlin 45 | - BASE_URL=plantuml 46 | volumes: 47 | - /usr/share/fonts:/root/.local/share/fonts/host:ro 48 | ``` 49 | 50 | 51 | ## Verification 52 | 53 | The following command will print a list of all available fonts inside the docker container: 54 | 55 | ```bash 56 | docker exec -it plantuml-server fc-list 57 | ``` 58 | 59 | To find a special font add a grep filter to the command: 60 | 61 | ```bash 62 | docker exec -it plantuml-server fc-list | grep "" 63 | ``` 64 | 65 | Naturally, it is also possible to check this via PlantUML itself by rendering the following diagram: 66 | 67 | ```plantuml 68 | @startuml 69 | listfonts 70 | @enduml 71 | ``` 72 | 73 | **Note**: If you have added a lot of fonts: (a) this diagram may take a few seconds to generate, and (b) eventually the PNG image may be clipped. To avoid the latter, render the diagram as an SVG image. 74 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/settings/settings.js: -------------------------------------------------------------------------------- 1 | /************** 2 | * Settings JS * 3 | ***************/ 4 | 5 | function initSettings() { 6 | const themeElement = document.getElementById("theme"); 7 | const diagramPreviewTypeElement = document.getElementById("diagramPreviewType"); 8 | const editorWatcherTimeoutElement = document.getElementById("editorWatcherTimeout"); 9 | 10 | function openSettings() { 11 | setVisibility(document.getElementById("settings"), true, true); 12 | // fill settings form 13 | themeElement.value = document.appConfig.theme; 14 | diagramPreviewTypeElement.value = document.appConfig.diagramPreviewType; 15 | editorWatcherTimeoutElement.value = document.appConfig.editorWatcherTimeout; 16 | setEditorValue(document.settingsEditor, JSON.stringify(document.appConfig.editorCreateOptions, null, " ")); 17 | } 18 | function saveSettings() { 19 | const appConfig = Object.assign({}, document.appConfig); 20 | appConfig.theme = themeElement.value; 21 | appConfig.editorWatcherTimeout = editorWatcherTimeoutElement.value; 22 | appConfig.diagramPreviewType = diagramPreviewTypeElement.value; 23 | appConfig.editorCreateOptions = JSON.parse(document.settingsEditor.getValue()); 24 | updateConfig(appConfig); 25 | closeModal("settings"); 26 | } 27 | function onThemeChanged(event) { 28 | const theme = event.target.value; 29 | const editorCreateOptionsString = document.settingsEditor.getValue(); 30 | const replaceTheme = (theme === "dark") ? "vs" : "vs-dark"; 31 | const substituteTheme = (theme === "dark") ? "vs-dark" : "vs"; 32 | const regex = new RegExp('("theme"\\s*:\\s*)"' + replaceTheme + '"', "gm"); 33 | setEditorValue(document.settingsEditor, editorCreateOptionsString.replace(regex, '$1"' + substituteTheme + '"')); 34 | } 35 | 36 | // create app config monaco editor 37 | document.settingsEditor = monaco.editor.create(document.getElementById("settings-monaco-editor"), { 38 | language: "json", ...document.appConfig.editorCreateOptions 39 | }); 40 | // add listeners 41 | themeElement.addEventListener("change", onThemeChanged); 42 | document.getElementById("settings-ok-btn").addEventListener("click", saveSettings); 43 | // support Ctrl+, to open the settings 44 | window.addEventListener("keydown", function(e) { 45 | if (e.key === "," && (isMac ? e.metaKey : e.ctrlKey)) { 46 | e.preventDefault(); 47 | if (!isModalOpen("settings")) { 48 | openSettings(); 49 | } 50 | } 51 | }, false); 52 | // register model listeners 53 | registerModalListener("settings", openSettings); 54 | } 55 | -------------------------------------------------------------------------------- /Dockerfile.jetty-alpine: -------------------------------------------------------------------------------- 1 | FROM maven:3-eclipse-temurin-17-alpine AS builder 2 | 3 | COPY pom.xml pom.parent.xml /app/ 4 | COPY src/main /app/src/main/ 5 | 6 | WORKDIR /app 7 | RUN mvn --batch-mode --define java.net.useSystemProxies=true package 8 | 9 | ######################################################################################## 10 | 11 | FROM jetty:11.0.18-jre17-alpine-eclipse-temurin 12 | 13 | # Proxy and OldProxy need empty path segments support in URIs 14 | # Hence: allow AMBIGUOUS_EMPTY_SEGMENT 15 | # Changes are only active if `/generate-jetty-start.sh` is called! 16 | RUN sed -i 's/# jetty\.httpConfig\.uriCompliance=DEFAULT/jetty.httpConfig.uriCompliance=DEFAULT,AMBIGUOUS_EMPTY_SEGMENT/g' /var/lib/jetty/start.d/server.ini 17 | 18 | USER root 19 | 20 | RUN apk add --no-cache \ 21 | curl \ 22 | font-noto-cjk \ 23 | libgd \ 24 | && \ 25 | /generate-jetty-start.sh 26 | 27 | #RUN apk add --no-cache graphviz 28 | ARG GRAPHVIZ_VERSION 29 | ARG GRAPHVIZ_BUILD_DIR=/tmp/graphiz-build 30 | RUN apk add --no-cache \ 31 | g++ \ 32 | jq \ 33 | expat-dev \ 34 | make \ 35 | zlib \ 36 | pkgconf \ 37 | && \ 38 | mkdir -p $GRAPHVIZ_BUILD_DIR && \ 39 | cd $GRAPHVIZ_BUILD_DIR && \ 40 | GRAPHVIZ_VERSION=${GRAPHVIZ_VERSION:-$(curl -s https://gitlab.com/api/v4/projects/4207231/releases/ | jq -r '.[] | .name' | sort -V -r | head -1)} && \ 41 | curl -o graphviz.tar.gz https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/${GRAPHVIZ_VERSION}/graphviz-${GRAPHVIZ_VERSION}.tar.gz && \ 42 | tar -xzf graphviz.tar.gz && \ 43 | cd graphviz-$GRAPHVIZ_VERSION && \ 44 | ./configure && \ 45 | make && \ 46 | make install && \ 47 | apk del --no-cache \ 48 | g++ \ 49 | jq \ 50 | expat-dev \ 51 | make \ 52 | zlib \ 53 | && \ 54 | rm -rf $GRAPHVIZ_BUILD_DIR 55 | 56 | COPY docker-entrypoint.jetty.sh /entrypoint.sh 57 | RUN chmod +x /entrypoint.sh 58 | 59 | USER jetty 60 | 61 | ENV WEBAPP_PATH=$JETTY_BASE/webapps 62 | RUN rm -rf $WEBAPP_PATH && \ 63 | mkdir -p $WEBAPP_PATH 64 | COPY --from=builder /app/target/plantuml.war /plantuml.war 65 | COPY ROOT.jetty.xml $WEBAPP_PATH/ROOT.xml 66 | 67 | # Openshift https://docs.openshift.com/container-platform/4.9/openshift_images/create-images.html#images-create-guide-openshift_create-images 68 | USER root 69 | RUN chgrp -R 0 $JETTY_BASE && \ 70 | chmod -R g=u $JETTY_BASE 71 | RUN chgrp -R 0 /tmp && \ 72 | chmod -R g=u /tmp 73 | USER jetty 74 | 75 | ENTRYPOINT ["/entrypoint.sh"] 76 | VOLUME ["/tmp/jetty"] 77 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/modals.css: -------------------------------------------------------------------------------- 1 | /************* 2 | * Modals CSS * 3 | **************/ 4 | 5 | .modal { 6 | display: block; 7 | position: fixed; 8 | z-index: 1; 9 | padding: 5%; 10 | left: 0; 11 | top: 0; 12 | bottom: 0; 13 | right: 0; 14 | overflow: auto; 15 | background-color: rgb(0, 0, 0); 16 | background-color: rgba(0, 0, 0, 0.4); 17 | } 18 | .modal .modal-content { 19 | background-color: var(--modal-bg-color); 20 | margin: auto; 21 | padding: 2rem; 22 | border: 3px solid var(--border-color); 23 | max-width: 30rem; 24 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 25 | -webkit-animation-name: modal-animatetop; 26 | -webkit-animation-duration: 0.4s; 27 | animation-name: modal-animatetop; 28 | animation-duration: 0.4s; 29 | position: relative; 30 | top: 50%; 31 | transform: translateY(-50%); 32 | } 33 | @-webkit-keyframes modal-animatetop { 34 | from { top: -50%; opacity: 0; } 35 | to { top: 50%; opacity: 1; } 36 | } 37 | @keyframes modal-animatetop { 38 | from { top: -50%; opacity: 0; } 39 | to { top: 50%; opacity: 1; } 40 | } 41 | /************* header, main, footer *************/ 42 | .modal .modal-header h2 { 43 | margin: 0; 44 | } 45 | .modal .modal-main { 46 | flex: 1; 47 | } 48 | .modal .modal-footer { 49 | margin-top: 1rem; 50 | text-align: right; 51 | } 52 | /************* inputs *************/ 53 | .modal input, .modal select { 54 | border: 1px solid var(--border-color); 55 | } 56 | .modal input:not(:focus):invalid { 57 | border-bottom-color: red; 58 | } 59 | .modal input[type="file"]::file-selector-button { 60 | border: 1px solid var(--border-color); 61 | } 62 | /************* ok + cancel buttons *************/ 63 | .modal input.ok, .modal input.cancel { 64 | min-width: 5rem; 65 | } 66 | .modal input.ok[disabled], .modal input.cancel[disabled] { 67 | color: var(--font-color-disabled); 68 | } 69 | .modal input.ok:not([disabled]):hover { 70 | border-bottom-color: green; 71 | } 72 | .modal input.cancel:not([disabled]):hover { 73 | border-bottom-color: darkred; 74 | } 75 | /************* label + input pair *************/ 76 | .modal .label-input-pair { 77 | margin: 1rem 0; 78 | overflow: hidden; 79 | } 80 | .modal .label-input-pair:first-child { 81 | margin-top: 0; 82 | } 83 | .modal .label-input-pair:last-child { 84 | margin-bottom: 0; 85 | } 86 | .modal .label-input-pair label { 87 | display: inline-block; 88 | min-width: 15rem; 89 | } 90 | .modal .label-input-pair label + input, 91 | .modal .label-input-pair label + select { 92 | box-sizing: border-box; 93 | display: inline-block; 94 | min-width: 10rem; 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestAsciiCoder.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStreamWriter; 5 | import java.net.HttpURLConnection; 6 | import java.net.URL; 7 | import java.net.URLConnection; 8 | 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import net.sourceforge.plantuml.servlet.utils.TestUtils; 13 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 14 | 15 | 16 | public class TestAsciiCoder extends WebappTestCase { 17 | 18 | /** 19 | * Verifies the decoding for the Bob -> Alice sample 20 | */ 21 | @Test 22 | public void testBobAliceSampleDiagramDecoding() throws IOException { 23 | final URL url = new URL(getServerUrl() + "/coder/" + TestUtils.SEQBOB); 24 | final URLConnection conn = url.openConnection(); 25 | // Analyze response 26 | // Verifies the Content-Type header 27 | Assertions.assertEquals( 28 | "text/plain;charset=utf-8", 29 | conn.getContentType().toLowerCase(), 30 | "Response content type is not TEXT PLAIN or UTF-8" 31 | ); 32 | // Get and verify the content 33 | final String diagram = getContentText(conn); 34 | Assertions.assertEquals(TestUtils.SEQBOBCODE, diagram); 35 | } 36 | 37 | /** 38 | * Verifies the encoding for the Bob -> Alice sample 39 | */ 40 | @Test 41 | public void testBobAliceSampleDiagramEncoding() throws IOException { 42 | final URL url = new URL(getServerUrl() + "/coder"); 43 | final HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 44 | conn.setRequestMethod("POST"); 45 | conn.setDoOutput(true); 46 | conn.setRequestProperty("Content-type", "text/plain"); 47 | try (final OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream())) { 48 | writer.write(TestUtils.SEQBOBCODE); 49 | writer.flush(); 50 | } 51 | // Analyze response 52 | // HTTP response 200 53 | Assertions.assertEquals(200, conn.getResponseCode(), "Bad HTTP status received"); 54 | // Verifies the Content-Type header 55 | Assertions.assertEquals( 56 | "text/plain;charset=utf-8", 57 | conn.getContentType().toLowerCase(), 58 | "Response content type is not TEXT PLAIN or UTF-8" 59 | ); 60 | // Get the content and verify its size 61 | final String diagram = getContentText(conn.getInputStream()); 62 | Assertions.assertEquals(TestUtils.SEQBOB, diagram); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/webapp/components/editor/menu/editor-menu.css: -------------------------------------------------------------------------------- 1 | /****************** 2 | * Editor Menu CSS * 3 | *******************/ 4 | 5 | .monaco-editor-container .editor-menu { 6 | position: absolute; 7 | right: 0; 8 | top: 0; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | flex: 1; 14 | } 15 | .monaco-editor-container .editor-menu > div.menu-kebab { 16 | width: 60px; 17 | height: 60px; 18 | display: flex; 19 | flex-wrap: wrap; 20 | justify-content: center; 21 | align-items: center; 22 | cursor: pointer; 23 | scale: 0.5; 24 | } 25 | .monaco-editor-container .editor-menu:hover > div.menu-kebab, 26 | .monaco-editor-container .editor-menu:focus > div.menu-kebab { 27 | outline: none; 28 | scale: 0.65; 29 | } 30 | .monaco-editor-container .menu-kebab .kebab-circle { 31 | width: 12px; 32 | height: 12px; 33 | margin: 3px; 34 | background: var(--font-color); 35 | border-radius: 50%; 36 | display: block; 37 | opacity: 0.8; 38 | } 39 | .monaco-editor-container .menu-kebab { 40 | flex-direction: column; 41 | position: relative; 42 | transition: all 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275); 43 | } 44 | .monaco-editor-container .menu-kebab .kebab-circle:nth-child(4), 45 | .monaco-editor-container .menu-kebab .kebab-circle:nth-child(5) { 46 | position: absolute; 47 | opacity: 0; 48 | top: 50%; 49 | margin-top: -6px; 50 | left: 50%; 51 | } 52 | .monaco-editor-container .menu-kebab .kebab-circle:nth-child(4) { 53 | margin-left: -25px; 54 | } 55 | .monaco-editor-container .menu-kebab .kebab-circle:nth-child(5) { 56 | margin-left: 13px; 57 | } 58 | .monaco-editor-container .editor-menu:hover .menu-kebab, 59 | .monaco-editor-container .editor-menu:focus .menu-kebab { 60 | transform: rotate(45deg); 61 | } 62 | .monaco-editor-container .editor-menu:hover .menu-kebab .kebab-circle, 63 | .monaco-editor-container .editor-menu:focus .menu-kebab .kebab-circle { 64 | opacity: 1; 65 | } 66 | 67 | .monaco-editor-container .editor-menu .menu-item { 68 | display: none; 69 | margin: 1rem 0; 70 | height: 1.75rem; 71 | opacity: 0.5; 72 | position: relative; 73 | -webkit-animation-name: editor-menu-animateitem; 74 | -webkit-animation-duration: 0.4s; 75 | animation-name: editor-menu-animateitem; 76 | animation-duration: 0.4s; 77 | } 78 | @-webkit-keyframes editor-menu-animateitem { 79 | from { top: -50%; opacity: 0; } 80 | to { top: 0; opacity: 0.5; } 81 | } 82 | @keyframes editor-menu-animateitem { 83 | from { top: -50%; opacity: 0; } 84 | to { top: 0; opacity: 0.5; } 85 | } 86 | .monaco-editor-container .editor-menu .menu-item:hover { 87 | opacity: 1; 88 | } 89 | .monaco-editor-container .editor-menu:hover .menu-item, 90 | .monaco-editor-container .editor-menu:focus .menu-item { 91 | display: block; 92 | } 93 | -------------------------------------------------------------------------------- /pom.jdk8.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 4.0.0 8 | 9 | 10 | org.sourceforge.plantuml 11 | plantumlservlet-parent 12 | 1-SNAPSHOT 13 | pom.parent.xml 14 | 15 | 16 | plantumlservlet 17 | war 18 | 19 | 20 | 21 | 8 22 | 23 | 24 | 25 | 9.3 26 | 27 | 28 | 29 | 2.4.4 30 | 31 | 32 | 33 | 34 | jakarta.servlet 35 | jakarta.servlet-api 36 | 5.0.0 37 | provided 38 | 39 | 40 | org.eclipse.jetty 41 | apache-jsp 42 | ${apache-jsp.version} 43 | ${apache-jsp.scope} 44 | 45 | 46 | org.eclipse.jetty.toolchain 47 | jetty-jakarta-servlet-api 48 | 49 | 50 | org.eclipse.jetty.toolchain 51 | jetty-schemas 52 | 53 | 54 | 55 | 56 | org.eclipse.jetty 57 | jetty-annotations 58 | ${jetty-annotations.version} 59 | provided 60 | 61 | 62 | org.eclipse.jetty.toolchain 63 | jetty-jakarta-servlet-api 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.eclipse.jetty 71 | jetty-server 72 | ${jetty-server.version} 73 | test 74 | 75 | 76 | org.eclipse.jetty.toolchain 77 | jetty-jakarta-servlet-api 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/CheckSyntaxServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import java.io.IOException; 27 | 28 | import javax.imageio.IIOException; 29 | 30 | import jakarta.servlet.ServletException; 31 | import jakarta.servlet.http.HttpServlet; 32 | import jakarta.servlet.http.HttpServletRequest; 33 | import jakarta.servlet.http.HttpServletResponse; 34 | 35 | import net.sourceforge.plantuml.FileFormat; 36 | import net.sourceforge.plantuml.servlet.utility.UmlExtractor; 37 | import net.sourceforge.plantuml.servlet.utility.UrlDataExtractor; 38 | 39 | /** 40 | * Check servlet of the webapp. 41 | * This servlet checks the syntax of the diagram and send a report in TEXT format. 42 | */ 43 | @SuppressWarnings("SERIAL") 44 | public class CheckSyntaxServlet extends HttpServlet { 45 | 46 | @Override 47 | public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 48 | 49 | // build the UML source from the compressed request parameter 50 | final String url = request.getRequestURI(); 51 | final String uml = UmlExtractor.getUmlSource(UrlDataExtractor.getEncodedDiagram(url, "")); 52 | 53 | // generate the response 54 | DiagramResponse dr = new DiagramResponse(response, getOutputFormat(), request); 55 | try { 56 | dr.sendCheck(uml); 57 | } catch (IIOException e) { 58 | // Browser has closed the connection, do nothing 59 | } 60 | } 61 | 62 | /** 63 | * Gives the wished output format of the diagram. 64 | * This value is used by the DiagramResponse class. 65 | * 66 | * @return the format for check responses 67 | */ 68 | public FileFormat getOutputFormat() { 69 | return FileFormat.UTXT; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/server/EmbeddedJettyServer.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet.server; 2 | 3 | import java.util.EnumSet; 4 | 5 | import org.eclipse.jetty.http.UriCompliance; 6 | import org.eclipse.jetty.http.UriCompliance.Violation; 7 | import org.eclipse.jetty.server.HttpConnectionFactory; 8 | import org.eclipse.jetty.server.Server; 9 | import org.eclipse.jetty.server.ServerConnector; 10 | import org.eclipse.jetty.server.handler.DefaultHandler; 11 | import org.eclipse.jetty.server.handler.HandlerList; 12 | import org.eclipse.jetty.webapp.WebAppContext; 13 | 14 | public class EmbeddedJettyServer implements ServerUtils { 15 | 16 | private static final String contextPath = "/plantuml"; 17 | private Server server; 18 | 19 | public EmbeddedJettyServer() { 20 | String[] virtualHosts = new String[]{"localhost", "test.localhost"}; 21 | server = new Server(); 22 | 23 | ServerConnector connector = new ServerConnector(server); 24 | // Proxy and OldProxy need empty path segments support in URIs 25 | // Hence: allow AMBIGUOUS_EMPTY_SEGMENT 26 | UriCompliance uriCompliance = UriCompliance.from(EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)); 27 | connector.getConnectionFactory(HttpConnectionFactory.class) 28 | .getHttpConfiguration() 29 | .setUriCompliance(uriCompliance); 30 | server.addConnector(connector); 31 | 32 | // PlantUML server web application 33 | WebAppContext context = new WebAppContext(server, "src/main/webapp", EmbeddedJettyServer.contextPath); 34 | context.addVirtualHosts(virtualHosts); 35 | 36 | // Add static webjars resource files 37 | // The maven-dependency-plugin in the pom.xml provides these files. 38 | WebAppContext res = new WebAppContext( 39 | server, 40 | "target/classes/META-INF/resources/webjars", 41 | EmbeddedJettyServer.contextPath + "/webjars" 42 | ); 43 | res.addVirtualHosts(virtualHosts); 44 | 45 | // Create server handler 46 | HandlerList handlers = new HandlerList(); 47 | handlers.addHandler(res); // provides: /plantuml/webjars 48 | handlers.addHandler(context); // provides: /plantuml 49 | handlers.addHandler(new DefaultHandler()); // provides: / 50 | 51 | server.setHandler(handlers); 52 | } 53 | 54 | public void startServer() throws Exception { 55 | server.start(); 56 | } 57 | 58 | public void stopServer() throws Exception { 59 | server.stop(); 60 | } 61 | 62 | public String getServerUrl() { 63 | return String.format( 64 | "%s://%s%s", 65 | server.getURI().getScheme(), 66 | server.getURI().getAuthority(), 67 | EmbeddedJettyServer.contextPath 68 | ); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Dockerfile.jetty: -------------------------------------------------------------------------------- 1 | FROM maven:3-eclipse-temurin-17 AS builder 2 | 3 | COPY pom.xml pom.parent.xml /app/ 4 | COPY src/main /app/src/main/ 5 | 6 | WORKDIR /app 7 | RUN mvn --batch-mode --define java.net.useSystemProxies=true package 8 | 9 | ######################################################################################## 10 | 11 | FROM jetty:11.0.18-jre17-eclipse-temurin 12 | 13 | # Proxy and OldProxy need empty path segments support in URIs 14 | # Hence: allow AMBIGUOUS_EMPTY_SEGMENT 15 | # Changes are only active if `/generate-jetty-start.sh` is called! 16 | RUN sed -i 's/# jetty\.httpConfig\.uriCompliance=DEFAULT/jetty.httpConfig.uriCompliance=DEFAULT,AMBIGUOUS_EMPTY_SEGMENT/g' /var/lib/jetty/start.d/server.ini 17 | 18 | USER root 19 | 20 | RUN apt-get update && \ 21 | apt-get install -y --no-install-recommends \ 22 | curl \ 23 | fonts-noto-cjk \ 24 | libgd3 \ 25 | && \ 26 | rm -rf /var/lib/apt/lists/* && \ 27 | /generate-jetty-start.sh 28 | 29 | # Build Graphviz from source because there are no binary distributions for recent versions 30 | ARG GRAPHVIZ_VERSION 31 | ARG GRAPHVIZ_BUILD_DIR=/tmp/graphiz-build 32 | RUN apt-get update && \ 33 | apt-get install -y --no-install-recommends \ 34 | build-essential \ 35 | jq \ 36 | libexpat1-dev \ 37 | libgd-dev \ 38 | zlib1g-dev \ 39 | && \ 40 | mkdir -p $GRAPHVIZ_BUILD_DIR && \ 41 | cd $GRAPHVIZ_BUILD_DIR && \ 42 | GRAPHVIZ_VERSION=${GRAPHVIZ_VERSION:-$(curl -s https://gitlab.com/api/v4/projects/4207231/releases/ | jq -r '.[] | .name' | sort -V -r | head -1)} && \ 43 | curl -o graphviz.tar.gz https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/${GRAPHVIZ_VERSION}/graphviz-${GRAPHVIZ_VERSION}.tar.gz && \ 44 | tar -xzf graphviz.tar.gz && \ 45 | cd graphviz-$GRAPHVIZ_VERSION && \ 46 | ./configure && \ 47 | make && \ 48 | make install && \ 49 | apt-get remove -y \ 50 | build-essential \ 51 | jq \ 52 | libexpat1-dev \ 53 | libgd-dev \ 54 | zlib1g-dev \ 55 | && \ 56 | apt-get autoremove -y && \ 57 | apt-get clean && \ 58 | rm -rf /var/lib/apt/lists/* && \ 59 | rm -rf $GRAPHVIZ_BUILD_DIR 60 | 61 | COPY docker-entrypoint.jetty.sh /entrypoint.sh 62 | RUN chmod +x /entrypoint.sh 63 | 64 | USER jetty 65 | 66 | ENV WEBAPP_PATH=$JETTY_BASE/webapps 67 | RUN rm -rf $WEBAPP_PATH && \ 68 | mkdir -p $WEBAPP_PATH 69 | COPY --from=builder /app/target/plantuml.war /plantuml.war 70 | COPY ROOT.jetty.xml $WEBAPP_PATH/ROOT.xml 71 | 72 | # Openshift https://docs.openshift.com/container-platform/4.9/openshift_images/create-images.html#images-create-guide-openshift_create-images 73 | USER root 74 | RUN chgrp -R 0 $JETTY_BASE && chmod -R g=u $JETTY_BASE 75 | RUN chgrp -R 0 /tmp && chmod -R g=u /tmp 76 | USER jetty 77 | 78 | ENTRYPOINT ["/entrypoint.sh"] 79 | VOLUME ["/tmp/jetty"] 80 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/MapServlet.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet; 25 | 26 | import java.io.IOException; 27 | 28 | import javax.imageio.IIOException; 29 | 30 | import jakarta.servlet.ServletException; 31 | import jakarta.servlet.http.HttpServlet; 32 | import jakarta.servlet.http.HttpServletRequest; 33 | import jakarta.servlet.http.HttpServletResponse; 34 | 35 | import net.sourceforge.plantuml.FileFormat; 36 | import net.sourceforge.plantuml.servlet.utility.UmlExtractor; 37 | import net.sourceforge.plantuml.servlet.utility.UrlDataExtractor; 38 | 39 | /** 40 | * MAP servlet of the webapp. 41 | * This servlet produces the image map of the diagram in HTML format. 42 | */ 43 | @SuppressWarnings("SERIAL") 44 | public class MapServlet extends HttpServlet { 45 | 46 | @Override 47 | public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 48 | 49 | // build the UML source from the compressed request parameter 50 | final String url = request.getRequestURI(); 51 | final String uml = UmlExtractor.getUmlSource(UrlDataExtractor.getEncodedDiagram(url, "")); 52 | final int idx = UrlDataExtractor.getIndex(url, 0); 53 | 54 | // generate the response 55 | DiagramResponse dr = new DiagramResponse(response, getOutputFormat(), request); 56 | try { 57 | dr.sendMap(uml, idx); 58 | } catch (IIOException e) { 59 | // Browser has closed the connection, do nothing 60 | } 61 | } 62 | 63 | /** 64 | * Gives the wished output format of the diagram. 65 | * This value is used by the DiagramResponse class. 66 | * 67 | * @return the format for map responses 68 | */ 69 | public FileFormat getOutputFormat() { 70 | return FileFormat.UTXT; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/webapp/js/language/validation/validation.js: -------------------------------------------------------------------------------- 1 | /******************************************** 2 | * PlantUML Language Validation Feature Base * 3 | *********************************************/ 4 | 5 | (function() { 6 | 7 | const validationEventListeners = {}; 8 | 9 | /** 10 | * Add validation event listener. 11 | * 12 | * Validation Event Order: 13 | * before -> code -> line -> after 14 | * 15 | * @param {("before"|"code"|"line"|"after")} type before|code|line|after event type 16 | * @param {(event: any) => Promise|editor.IMarkerData|Promise|editor.IMarkerData[]|Promise|void} listener event listener 17 | */ 18 | PlantUmlLanguageFeatures.prototype.addValidationEventListener = function(type, listener) { 19 | if (!["before", "code", "line", "after"].includes(type)) { 20 | throw Error("Unknown validation event type: " + type); 21 | } 22 | validationEventListeners[type] = validationEventListeners[type] || []; 23 | validationEventListeners[type].push(listener); 24 | }; 25 | 26 | /** 27 | * Validate PlantUML language of monaco editor model. 28 | * 29 | * @param {editor.ITextModel} model editor model to validate 30 | * 31 | * @returns editor markers as promise 32 | * 33 | * @example 34 | * ```js 35 | * validateCode(editor.getModel()) 36 | * .then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers)); 37 | * ``` 38 | */ 39 | PlantUmlLanguageFeatures.prototype.validateCode = async function(model) { 40 | const promises = []; 41 | 42 | // raise before events 43 | promises.push(validationEventListeners.before?.map(listener => listener({ model }))); 44 | 45 | // raise code events 46 | promises.push(validationEventListeners.code?.map(listener => listener({ model, code: model.getValue() }))); 47 | 48 | if (validationEventListeners.line && validationEventListeners.line.length > 0) { 49 | // NOTE: lines and columns start at 1 50 | const lineCount = model.getLineCount(); 51 | for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) { 52 | const range = { 53 | startLineNumber: lineNumber, 54 | startColumn: 1, 55 | endLineNumber: lineNumber, 56 | endColumn: model.getLineLength(lineNumber) + 1, 57 | }; 58 | const line = model.getValueInRange(range); 59 | // raise line events 60 | promises.push(validationEventListeners.line?.map(listener => listener({ model, range, line, lineNumber, lineCount }))); 61 | } 62 | } 63 | 64 | // raise after events 65 | promises.push(validationEventListeners.after?.map(listener => listener({ model }))); 66 | 67 | // collect all markers and ... 68 | // - since each event can results in an array of markers -> `flat(1)` 69 | // - since not each event has to results in markers and can be `undef 70 | return Promise.all(promises).then(results => results.flat(1).filter(marker => marker)); 71 | }; 72 | 73 | })(); 74 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/utility/Configuration.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet.utility; 25 | 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | 29 | import java.util.Properties; 30 | 31 | /** 32 | * Shared PlantUML Server configuration. 33 | */ 34 | public final class Configuration { 35 | 36 | /** 37 | * Singleton configuration instance. 38 | */ 39 | private static Configuration instance; 40 | /** 41 | * Configuration properties. 42 | */ 43 | private Properties config; 44 | 45 | /** 46 | * Singleton constructor. 47 | */ 48 | private Configuration() { 49 | config = new Properties(); 50 | 51 | // Default values 52 | config.setProperty("SHOW_SOCIAL_BUTTONS", "off"); 53 | config.setProperty("SHOW_GITHUB_RIBBON", "off"); 54 | // End of default values 55 | 56 | try { 57 | InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("config.properties"); 58 | if (is != null) { 59 | config.load(is); 60 | is.close(); 61 | } 62 | } catch (IOException e) { 63 | // Just log a warning 64 | e.printStackTrace(); 65 | } 66 | } 67 | 68 | /** 69 | * Get the configuration. 70 | * 71 | * @return the complete configuration 72 | */ 73 | public static Properties get() { 74 | if (instance == null) { 75 | instance = new Configuration(); 76 | } 77 | return instance.config; 78 | } 79 | 80 | /** 81 | * Get a boolean configuration value. 82 | * 83 | * @param key config property key 84 | * 85 | * @return true if the value is "on" 86 | */ 87 | public static boolean get(final String key) { 88 | if (get().getProperty(key) == null) { 89 | return false; 90 | } 91 | return get().getProperty(key).startsWith("on"); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/utils/JUnitWebDriver.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet.utils; 2 | 3 | import java.time.Duration; 4 | 5 | import org.apache.commons.lang3.SystemUtils; 6 | import org.openqa.selenium.Dimension; 7 | import org.openqa.selenium.PageLoadStrategy; 8 | import org.openqa.selenium.Point; 9 | import org.openqa.selenium.WebDriver; 10 | import org.openqa.selenium.chrome.ChromeDriver; 11 | import org.openqa.selenium.chrome.ChromeOptions; 12 | import org.openqa.selenium.edge.EdgeDriver; 13 | import org.openqa.selenium.edge.EdgeOptions; 14 | import org.openqa.selenium.firefox.FirefoxDriver; 15 | import org.openqa.selenium.firefox.FirefoxOptions; 16 | 17 | import io.github.bonigarcia.wdm.WebDriverManager; 18 | 19 | 20 | public abstract class JUnitWebDriver { 21 | 22 | public static final String browser; 23 | 24 | static { 25 | browser = System.getProperty("system.test.browser", "firefox"); 26 | } 27 | 28 | public static WebDriver getDriver() { 29 | WebDriver driver; 30 | switch (browser.toLowerCase()) { 31 | case "chrome": 32 | driver = getChromeDriver(); 33 | break; 34 | case "edge": 35 | driver = SystemUtils.IS_OS_WINDOWS ? getEdgeDriver() : getChromiumDriver(); 36 | break; 37 | case "firefox": 38 | driver = getFirefoxDriver(); 39 | break; 40 | default: 41 | driver = getChromiumDriver(); 42 | } 43 | driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); 44 | driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(10)); 45 | driver.manage().window().setPosition(new Point(0, 0)); 46 | driver.manage().window().setSize(new Dimension(1024, 768)); 47 | return driver; 48 | } 49 | 50 | private static WebDriver getChromiumDriver() { 51 | WebDriverManager.chromiumdriver().setup(); 52 | ChromeOptions options = new ChromeOptions(); 53 | options.addArguments("--headless", "--no-sandbox", "--disable-gpu"); 54 | options.setPageLoadStrategy(PageLoadStrategy.NONE); 55 | return new ChromeDriver(options); 56 | } 57 | 58 | private static WebDriver getChromeDriver() { 59 | WebDriverManager.chromedriver().setup(); 60 | ChromeOptions options = new ChromeOptions(); 61 | options.addArguments("--headless", "--no-sandbox", "--disable-gpu"); 62 | options.setPageLoadStrategy(PageLoadStrategy.NONE); 63 | return new ChromeDriver(options); 64 | } 65 | 66 | private static WebDriver getFirefoxDriver() { 67 | WebDriverManager.firefoxdriver().setup(); 68 | FirefoxOptions options = new FirefoxOptions(); 69 | options.addArguments("--headless"); 70 | return new FirefoxDriver(options); 71 | } 72 | 73 | private static WebDriver getEdgeDriver() { 74 | WebDriverManager.edgedriver().setup(); 75 | EdgeOptions options = new EdgeOptions(); 76 | options.addArguments("headless"); 77 | return new EdgeDriver(options); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestOldProxy.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.HttpURLConnection; 5 | import java.net.URL; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 11 | 12 | 13 | public class TestOldProxy extends WebappTestCase { 14 | 15 | private static final String TEST_RESOURCE = "test2diagrams.txt"; 16 | 17 | /** 18 | * Verifies the proxified reception of the default Bob and Alice diagram 19 | */ 20 | @Test 21 | public void testDefaultProxy() throws IOException { 22 | final URL url = new URL(getServerUrl() + "/proxy/" + getTestResourceUrl(TEST_RESOURCE)); 23 | final HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 24 | // Analyze response 25 | // Verifies HTTP status code and the Content-Type 26 | Assertions.assertEquals(200, conn.getResponseCode(), "Bad HTTP status received"); 27 | Assertions.assertEquals( 28 | "image/png", 29 | conn.getContentType().toLowerCase(), 30 | "Response content type is not PNG" 31 | ); 32 | // Get the image and verify its size (~2000 bytes) 33 | byte[] inMemoryImage = getContentAsBytes(conn); 34 | int diagramLen = inMemoryImage.length; 35 | Assertions.assertTrue(diagramLen > 2000); 36 | Assertions.assertTrue(diagramLen < 3000); 37 | } 38 | 39 | /** 40 | * Verifies the proxified reception of the default Bob and Alice diagram in a specific format (SVG) 41 | */ 42 | @Test 43 | public void testProxyWithFormat() throws IOException { 44 | final URL url = new URL(getServerUrl() + "/proxy/svg/" + getTestResourceUrl(TEST_RESOURCE)); 45 | final HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 46 | // Analyze response 47 | // Verifies HTTP status code and the Content-Type 48 | Assertions.assertEquals(200, conn.getResponseCode(), "Bad HTTP status received"); 49 | Assertions.assertEquals( 50 | "image/svg+xml", 51 | conn.getContentType().toLowerCase(), 52 | "Response content type is not SVG" 53 | ); 54 | // Get the content and verify its size 55 | String diagram = getContentText(conn); 56 | int diagramLen = diagram.length(); 57 | Assertions.assertTrue(diagramLen > 1000); 58 | Assertions.assertTrue(diagramLen < 3000); 59 | } 60 | 61 | /** 62 | * Verifies that the HTTP header of a diagram incites the browser to cache it. 63 | */ 64 | @Test 65 | public void testInvalidUrl() throws IOException { 66 | final URL url = new URL(getServerUrl() + "/proxy/invalidURL"); 67 | final HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 68 | // Analyze response 69 | // Check if status code is 400 70 | Assertions.assertEquals(400, conn.getResponseCode(), "Bad HTTP status received"); 71 | // Check error message 72 | Assertions.assertTrue( 73 | getContentText(conn.getErrorStream()).contains("URL malformed."), 74 | "Response is not malformed URL" 75 | ); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /examples/nginx-simple/README.md: -------------------------------------------------------------------------------- 1 | # Nginx simple reverse proxy example 2 | 3 | References: 4 | - [Nginx documentation](https://nginx.org/en/docs/) 5 | - [Nginx beginner's guide](https://nginx.org/en/docs/beginners_guide.html) 6 | 7 | 8 | ## Quick start 9 | 10 | Be sure to have [`docker-compose.yml`](./docker-compose.yml) and [`nginx.conf`](./nginx.conf) inside your current working directory. 11 | 12 | ```bash 13 | # start nginx and plantuml server 14 | docker-compose up -d 15 | 16 | # stop nginx and plantuml server 17 | docker-compose down 18 | ``` 19 | 20 | Check with `docker ps` if both container are up and running: 21 | 22 | ``` 23 | $ docker ps 24 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 25 | 217e753a0dcf plantuml/plantuml-server:jetty "/entrypoint.sh" 4 seconds ago Up 3 seconds 8080/tcp plantuml-server 26 | 9b1290c100f5 nginx:alpine "/docker-entrypoint.…" 4 seconds ago Up 3 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp nginx 27 | ``` 28 | 29 | Open [http://localhost](http://localhost) inside your browser. 30 | YEAH! You are now using PlantUML behind a simple Nginx reverse proxy. 31 | 32 | 33 | ## Nginx configuration 34 | 35 | ```nginx 36 | ... 37 | 38 | # PlantUML 39 | location / { 40 | proxy_set_header HOST $host; 41 | proxy_set_header X-Forwarded-Host $host; 42 | proxy_set_header X-Forwarded-Proto $scheme; 43 | 44 | proxy_pass http://plantuml-server:8080/; 45 | } 46 | 47 | ... 48 | ``` 49 | 50 | - `location /` to reverse complete server 51 | - `proxy_set_header HOST $host` and `proxy_set_header X-Forwarded-Host $host` to replaces local plantuml server ip with FQDN 52 | - `proxy_set_header X-Forwarded-Proto $scheme` to use reverse proxy protocol schema instead of communication schema between reverse proxy and plantuml server 53 | - `proxy_pass http://plantuml-server:8080/` to set reverse proxy path to plantuml server. 54 | Use the docker container name `plantuml-server` instead of ip addresses. 55 | 56 | 57 | ## Nginx and PlantUML server 58 | 59 | ```yaml 60 | version: "3" 61 | 62 | services: 63 | plantuml-server: 64 | image: plantuml/plantuml-server:jetty 65 | container_name: plantuml-server 66 | environment: 67 | - TZ=Europe/Berlin 68 | 69 | nginx: 70 | image: nginx:alpine 71 | container_name: nginx 72 | ports: 73 | - "80:80" 74 | environment: 75 | - TZ=Europe/Berlin 76 | volumes: 77 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 78 | ``` 79 | 80 | - Set `container_name` to use them instead of e.g. ip addresses 81 | - Set the environment `TZ` the ensure the same timezone. 82 | For example to server timezone (`cat /etc/timezone`)? 83 | - plantuml-server 84 | * plantuml-server already exposes port `8080` to it's own local network (but not outside). 85 | Since plantuml-server and nginx are sharing a network, nginx can reach plantuml-server without further settings. 86 | - nginx 87 | * open/link port `80` to the outside 88 | * `./nginx.conf:/etc/nginx/nginx.conf:ro` to use your own Nginx configuration (readonly) 89 | 90 | 91 | ## Useful commands 92 | 93 | ```bash 94 | # see whats going on inside your docker containers 95 | docker logs --tail 50 --follow --timestamps nginx 96 | docker logs --tail 50 --follow --timestamps plantuml-server 97 | ``` 98 | -------------------------------------------------------------------------------- /src/main/webapp/components/preview/diagram/preview-diagram.js: -------------------------------------------------------------------------------- 1 | /********************* 2 | * Preview Diagram JS * 3 | **********************/ 4 | 5 | async function initializeDiagram() { 6 | if (document.appConfig.diagramPreviewType !== "png") { 7 | // NOTE: "png" is preloaded from the server 8 | return setDiagram( 9 | document.appConfig.diagramPreviewType, 10 | document.appData.encodedDiagram, 11 | document.appData.index 12 | ); 13 | } 14 | } 15 | 16 | async function setDiagram(type, encodedDiagram, index) { 17 | const container = document.getElementById("diagram"); 18 | const png = document.getElementById("diagram-png"); 19 | const txt = document.getElementById("diagram-txt"); 20 | const pdf = document.getElementById("diagram-pdf"); 21 | // NOTE: the map and svg elements will be overwitten, hence can not be cached 22 | 23 | async function requestDiagram(type, encodedDiagram, index) { 24 | return makeRequest("GET", buildUrl(type, encodedDiagram, index)); 25 | } 26 | function setDiagramMap(mapString) { 27 | const mapEl = document.getElementById("plantuml_map"); 28 | const mapBtn = document.getElementById("map-diagram-link"); 29 | if (mapString) { 30 | const div = document.createElement("div"); 31 | div.innerHTML = mapString; 32 | mapEl.parentNode.replaceChild(div.firstChild, mapEl); 33 | setVisibility(mapBtn, true); 34 | } else { 35 | removeChildren(mapEl); 36 | setVisibility(mapBtn, false); 37 | } 38 | } 39 | function setSvgDiagram(svgString) { 40 | const svgEl = document.getElementById("diagram-svg"); 41 | const div = document.createElement("div"); 42 | div.innerHTML = svgString; 43 | const newSvg = div.querySelector("svg"); 44 | newSvg.id = "diagram-svg"; 45 | newSvg.classList = svgEl.classList; 46 | newSvg.style.cssText = svgEl.style.cssText; 47 | svgEl.parentNode.replaceChild(newSvg, svgEl); 48 | } 49 | function setDiagramVisibility(type) { 50 | const map = document.getElementById("plantuml_map"); 51 | const svg = document.getElementById("diagram-svg"); 52 | container.setAttribute("data-diagram-type", type); 53 | setVisibility(png, type === "png"); 54 | setVisibility(map, type === "png"); 55 | setVisibility(svg, type === "svg"); 56 | setVisibility(txt, type === "txt"); 57 | setVisibility(pdf, type === "pdf"); 58 | } 59 | // update diagram 60 | try { 61 | if (type === "png") { 62 | png.src = buildUrl("png", encodedDiagram, index); 63 | const map = await requestDiagram("map", encodedDiagram, index); 64 | setDiagramMap(map); 65 | } else if (type === "svg") { 66 | const svg = await requestDiagram("svg", encodedDiagram, index); 67 | setSvgDiagram(svg); 68 | } else if (type === "txt") { 69 | txt.innerHTML = await requestDiagram("txt", encodedDiagram, index); 70 | } else if (type === "pdf") { 71 | pdf.data = buildUrl("pdf", encodedDiagram, index); 72 | } else { 73 | const message = "unknown diagram type: " + type; 74 | (console.error || console.log)(message); 75 | return Promise.reject(message); 76 | } 77 | setDiagramVisibility(type); 78 | } catch (e) { 79 | // This should only happen if for example a broken diagram is requested. 80 | // Therefore, since the error message is already included in the response image, prevent further error messages. 81 | //(console.error || console.log)(e); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/utils/WebappUITestCase.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet.utils; 2 | 3 | import java.time.Duration; 4 | 5 | import org.junit.jupiter.api.AfterEach; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.openqa.selenium.By; 8 | import org.openqa.selenium.Dimension; 9 | import org.openqa.selenium.JavascriptExecutor; 10 | import org.openqa.selenium.WebDriver; 11 | import org.openqa.selenium.WebElement; 12 | import org.openqa.selenium.support.ui.ExpectedCondition; 13 | import org.openqa.selenium.support.ui.WebDriverWait; 14 | 15 | 16 | public abstract class WebappUITestCase extends WebappTestCase { 17 | 18 | public WebDriver driver; 19 | public JavascriptExecutor js; 20 | 21 | @BeforeEach 22 | public void setUp() throws Exception { 23 | super.setUp(); 24 | driver = JUnitWebDriver.getDriver(); 25 | js = (JavascriptExecutor)driver; 26 | } 27 | 28 | @AfterEach 29 | public void tearDown() throws Exception { 30 | driver.close(); 31 | super.tearDown(); 32 | } 33 | 34 | public boolean waitUntilJavascriptIsLoaded() { 35 | WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); 36 | return wait.until(new ExpectedCondition() { 37 | @Override 38 | public Boolean apply(WebDriver driver) { 39 | return js.executeScript("return document.readyState").toString().equals("complete"); 40 | } 41 | }); 42 | } 43 | 44 | public boolean waitUntilEditorIsLoaded() { 45 | WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); 46 | return wait.until(new ExpectedCondition() { 47 | @Override 48 | public Boolean apply(WebDriver driver) { 49 | return js.executeScript("return document.editor === undefined").toString().equals("false"); 50 | } 51 | }); 52 | } 53 | 54 | public boolean waitUntilAutoRefreshCompleted() { 55 | WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); 56 | return wait.until(new ExpectedCondition() { 57 | @Override 58 | public Boolean apply(WebDriver driver) { 59 | return js.executeScript("return document.appConfig.autoRefreshState").toString().equals("complete"); 60 | } 61 | }); 62 | } 63 | 64 | public boolean waitUntilUIIsLoaded() { 65 | return waitUntilEditorIsLoaded(); 66 | } 67 | 68 | public String getEditorValue() { 69 | return (String)js.executeScript("return document.editor.getValue();"); 70 | } 71 | 72 | public void setEditorValue(String code) { 73 | js.executeScript("return document.editor.getModel().setValue(`" + code.replace("`", "\\`") + "`);"); 74 | } 75 | 76 | public String getURLValue() { 77 | return driver.findElement(By.id("url")).getAttribute("value"); 78 | } 79 | 80 | public Dimension getImageSize() { 81 | WebElement img = driver.findElement(By.id("diagram-png")); 82 | return new Dimension( 83 | Integer.parseInt(img.getAttribute("width")), 84 | Integer.parseInt(img.getAttribute("height")) 85 | ); 86 | // return driver.findElement(By.id("diagram-png")).getSize(); 87 | } 88 | 89 | public WebElement getImageMap() { 90 | return driver.findElement(By.id("plantuml_map")); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/webapp/js/language/language.js: -------------------------------------------------------------------------------- 1 | /************************************************ 2 | * Monaco Editor PlantUML Language Features Base * 3 | *************************************************/ 4 | "use strict"; 5 | 6 | /** 7 | * Monaco Editor PlantUML Language Features. 8 | * 9 | * @param {boolean} [initialize] `true` if all default validation and code completion 10 | * functions should be activated; otherwise `false` 11 | * 12 | * @example 13 | * ```js 14 | * plantumlFeatures = new PlantUmlLanguageFeatures(); 15 | * const model = monaco.editor.createModel(initCode, "apex", uri); 16 | * model.onDidChangeContent(() => plantumlFeatures.validateCode(model)); 17 | * ``` 18 | */ 19 | const PlantUmlLanguageFeatures = function(initialize = true) { 20 | if (initialize) { 21 | // initialize all validation and code completion methods 22 | this.addStartEndValidationListeners(); 23 | this.registerThemeCompletion(); 24 | this.registerIconCompletion(); 25 | this.registerEmojiCompletion(); 26 | } 27 | }; 28 | 29 | PlantUmlLanguageFeatures.baseUrl = ""; 30 | PlantUmlLanguageFeatures.setBaseUrl = function(baseUrl) { 31 | if (baseUrl === null || baseUrl === undefined) { 32 | baseUrl = ""; 33 | } else if (baseUrl !== "") { 34 | if (baseUrl.slice(-1) !== "/") { 35 | baseUrl = baseUrl + "/"; // add tailing "/" 36 | } 37 | } 38 | PlantUmlLanguageFeatures.baseUrl = baseUrl; 39 | } 40 | 41 | PlantUmlLanguageFeatures.languageSelector = ["apex", "plantuml"]; 42 | PlantUmlLanguageFeatures.setLanguageSelector = function(languageSelector) { 43 | PlantUmlLanguageFeatures.languageSelector = languageSelector; 44 | } 45 | 46 | PlantUmlLanguageFeatures.makeRequest = function( 47 | method, 48 | url, 49 | { 50 | data = null, 51 | headers = { "Content-Type": "text/plain" }, 52 | responseType = "json", 53 | baseUrl = PlantUmlLanguageFeatures.baseUrl, 54 | } = {} 55 | ) { 56 | function getResolveResponse(xhr) { 57 | return responseType === "json" ? xhr.response : xhr.responseText; 58 | } 59 | function getRejectResponse(xhr) { 60 | return responseType === "json" 61 | ? { status: xhr.status, response: xhr.response } 62 | : { status: xhr.status, responseText: xhr.responseText }; 63 | } 64 | const targetUrl = !baseUrl ? url : baseUrl.replace(/\/*$/g, "/") + url; 65 | return new Promise((resolve, reject) => { 66 | const xhr = new XMLHttpRequest(); 67 | xhr.onreadystatechange = function() { 68 | if (xhr.readyState === XMLHttpRequest.DONE) { 69 | if (xhr.status >= 200 && xhr.status <= 300) { 70 | resolve(getResolveResponse(xhr)); 71 | } else { 72 | reject(getRejectResponse(xhr)); 73 | } 74 | } 75 | } 76 | xhr.open(method, targetUrl, true); 77 | xhr.responseType = responseType; 78 | headers && Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key])); 79 | xhr.send(data); 80 | }); 81 | } 82 | 83 | PlantUmlLanguageFeatures.absolutePath = function(path) { 84 | if (path.startsWith("http")) return path; 85 | if (path.startsWith("//")) return window.location.protocol + path; 86 | if (path.startsWith("/")) return window.location.origin + path; 87 | 88 | if (path.slice(0, 2) == "./") path = path.slice(2); 89 | let base = (document.querySelector("base") || {}).href || window.location.origin; 90 | if (base.slice(-1) == "/") base = base.slice(0, -1); 91 | return base + "/" + path; 92 | } 93 | -------------------------------------------------------------------------------- /src/main/webapp/components/modals/diagram-export/diagram-export.js: -------------------------------------------------------------------------------- 1 | /******************** 2 | * Diagram Export JS * 3 | *********************/ 4 | 5 | function initDiagramExport() { 6 | const filenameInput = document.getElementById("download-name"); 7 | const fileTypeSelect = document.getElementById("download-type"); 8 | 9 | function openDiagramExportDialog() { 10 | setVisibility(document.getElementById("diagram-export"), true, true); 11 | const code = document.editor.getValue(); 12 | const name = Array.from( 13 | code.matchAll(/^\s*@start[a-zA-Z]*\s+([a-zA-Z-_äöüÄÖÜß ]+)\s*$/gm), 14 | m => m[1] 15 | )[0] || "diagram"; 16 | filenameInput.value = name + ".puml"; 17 | fileTypeSelect.value = "code"; 18 | filenameInput.focus(); 19 | } 20 | function splitFilename(filename) { 21 | const idx = filename.lastIndexOf("."); 22 | if (idx < 1) { 23 | return { name: filename, ext: null }; 24 | } 25 | if (idx === filename.length - 1) { 26 | return { name: filename.slice(0, -1), ext: null }; 27 | } 28 | return { 29 | name: filename.substring(0, idx), 30 | ext: filename.substring(idx + 1), 31 | }; 32 | } 33 | function getExtensionByType(type) { 34 | switch (type) { 35 | case "epstext": return "eps"; 36 | case "code": return "puml"; 37 | default: return type; 38 | } 39 | } 40 | function getTypeByExtension(ext) { 41 | if (!ext) return ext; 42 | ext = ext.toLowerCase(); 43 | switch (ext) { 44 | case "puml": 45 | case "plantuml": 46 | case "code": 47 | return "code"; 48 | case "ascii": return "txt" 49 | default: return ext; 50 | } 51 | } 52 | function onTypeChanged(event) { 53 | const type = event.target.value; 54 | const ext = getExtensionByType(type); 55 | const { name } = splitFilename(filenameInput.value); 56 | filenameInput.value = name + "." + ext; 57 | } 58 | function onFilenameChanged(event) { 59 | const { ext } = splitFilename(event.target.value); 60 | const type = getTypeByExtension(ext); 61 | if (!type) return; 62 | fileTypeSelect.value = type; 63 | } 64 | function downloadFile() { 65 | const filename = filenameInput.value; 66 | const type = fileTypeSelect.value; 67 | const link = document.createElement("a"); 68 | link.download = filename; 69 | if (type === "code") { 70 | const code = document.editor.getValue(); 71 | link.href = "data:," + encodeURIComponent(code); 72 | } else { 73 | if (document.appData.index !== undefined) { 74 | link.href = type + "/" + document.appData.index + "/" + document.appData.encodedDiagram; 75 | } else { 76 | link.href = type + "/" + document.appData.encodedDiagram; 77 | } 78 | } 79 | link.click(); 80 | } 81 | 82 | // register modal 83 | registerModalListener("diagram-export", openDiagramExportDialog); 84 | // add listener 85 | filenameInput.addEventListener("change", onFilenameChanged); 86 | fileTypeSelect.addEventListener("change", onTypeChanged); 87 | document.getElementById("diagram-export-ok-btn").addEventListener("click", downloadFile); 88 | // add Ctrl+S or Meta+S (Mac) key shortcut to open export dialog 89 | window.addEventListener("keydown", event => { 90 | if (event.key === "s" && (isMac ? event.metaKey : event.ctrlKey)) { 91 | event.preventDefault(); 92 | if (!isModalOpen("diagram-export")) { 93 | openDiagramExportDialog(); 94 | } 95 | } 96 | }, false); 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestImage.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.net.URLConnection; 6 | import java.text.ParseException; 7 | import java.text.SimpleDateFormat; 8 | import java.util.Date; 9 | import java.util.Locale; 10 | 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import net.sourceforge.plantuml.servlet.utils.TestUtils; 15 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 16 | 17 | 18 | public class TestImage extends WebappTestCase { 19 | 20 | /** 21 | * Verifies the generation of the version image from an encoded URL 22 | */ 23 | @Test 24 | public void testVersionImage() throws IOException { 25 | final URL url = new URL(getServerUrl() + "/png/" + TestUtils.VERSION); 26 | final URLConnection conn = url.openConnection(); 27 | // Analyze response 28 | // Verifies the Content-Type header 29 | Assertions.assertEquals( 30 | "image/png", 31 | conn.getContentType().toLowerCase(), 32 | "Response content type is not PNG" 33 | ); 34 | // Get the image and verify its size 35 | byte[] inMemoryImage = getContentAsBytes(conn); 36 | int diagramLen = inMemoryImage.length; 37 | Assertions.assertTrue(diagramLen > 10000, "size = " + diagramLen); 38 | Assertions.assertTrue(diagramLen < 30000, "size = " + diagramLen); 39 | } 40 | 41 | /** 42 | * Verifies that the HTTP header of a diagram incites the browser to cache it. 43 | */ 44 | @Test 45 | public void testDiagramHttpHeader() throws IOException, ParseException { 46 | final URL url = new URL(getServerUrl() + "/png/" + TestUtils.SEQBOB); 47 | final URLConnection conn = url.openConnection(); 48 | // Analyze response 49 | // Verifies the Content-Type header 50 | Assertions.assertEquals( 51 | "image/png", 52 | conn.getContentType().toLowerCase(), 53 | "Response content type is not PNG" 54 | ); 55 | // Verifies the availability of the Expires entry in the response header 56 | Assertions.assertNotNull(conn.getHeaderField("Expires")); 57 | // Verifies the availability of the Last-Modified entry in the response header 58 | Assertions.assertNotNull(conn.getHeaderField("Last-Modified")); 59 | // Verifies the Last-Modified value is in the past 60 | SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ZZZ", Locale.ENGLISH); 61 | Date lastModified = format.parse(conn.getHeaderField("Last-Modified")); 62 | Assertions.assertTrue(lastModified.before(new Date()), "Last-Modified is not in the past"); 63 | // Consume the response but do nothing with it 64 | getContentAsBytes(conn); 65 | } 66 | 67 | /** 68 | * Verifies that the HTTP header of a diagram incites the browser to cache it. 69 | */ 70 | @Test 71 | public void testOldImgURL() throws IOException { 72 | final URL url = new URL(getServerUrl() + "/img/" + TestUtils.SEQBOB); 73 | final URLConnection conn = url.openConnection(); 74 | // Analyze response 75 | // Verifies the Content-Type header 76 | Assertions.assertEquals( 77 | "image/png", 78 | conn.getContentType().toLowerCase(), 79 | "Response content type is not PNG" 80 | ); 81 | // Consume the response but do nothing with it 82 | getContentAsBytes(conn); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/utils/WebappTestCase.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet.utils; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.net.URL; 9 | import java.net.URLConnection; 10 | import java.util.stream.Collectors; 11 | 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | 15 | import net.sourceforge.plantuml.servlet.server.EmbeddedJettyServer; 16 | import net.sourceforge.plantuml.servlet.server.ExternalServer; 17 | import net.sourceforge.plantuml.servlet.server.ServerUtils; 18 | 19 | 20 | public abstract class WebappTestCase { 21 | 22 | private final ServerUtils serverUtils; 23 | 24 | public WebappTestCase() { 25 | this(null); 26 | } 27 | 28 | public WebappTestCase(String name) { 29 | String uri = System.getProperty("system.test.server", ""); 30 | if (!uri.isEmpty()) { 31 | // mvn test -DskipTests=false -DargLine="-Dsystem.test.server=http://localhost:8080/plantuml" 32 | serverUtils = new ExternalServer(uri); 33 | return; 34 | } 35 | // mvn test -DskipTests=false 36 | serverUtils = new EmbeddedJettyServer(); 37 | } 38 | 39 | @BeforeEach 40 | public void setUp() throws Exception { 41 | serverUtils.startServer(); 42 | } 43 | 44 | @AfterEach 45 | public void tearDown() throws Exception { 46 | serverUtils.stopServer(); 47 | } 48 | 49 | public String getServerUrl() { 50 | return serverUtils.getServerUrl(); 51 | } 52 | 53 | public String getTestResourceUrl(String resource) { 54 | // NOTE: [Old]ProxyServlet.forbiddenURL do not allow URL with IP-Addresses or localhost. 55 | String serverUrl = getServerUrl().replace("/localhost", "/test.localhost"); 56 | return serverUrl + "/resource/test/" + resource; 57 | } 58 | 59 | public String getContentText(final URL url) throws IOException { 60 | try (final InputStream responseStream = url.openStream()) { 61 | return getContentText(responseStream); 62 | } 63 | } 64 | 65 | public String getContentText(final URLConnection conn) throws IOException { 66 | try (final InputStream responseStream = conn.getInputStream()) { 67 | return getContentText(responseStream); 68 | } 69 | } 70 | 71 | public String getContentText(final InputStream stream) throws IOException { 72 | try (final BufferedReader br = new BufferedReader(new InputStreamReader(stream))) { 73 | return br.lines().collect(Collectors.joining("\n")); 74 | } 75 | } 76 | 77 | public byte[] getContentAsBytes(final URL url) throws IOException { 78 | try (final InputStream responseStream = url.openStream()) { 79 | return getContentAsBytes(responseStream); 80 | } 81 | } 82 | 83 | public byte[] getContentAsBytes(final URLConnection conn) throws IOException { 84 | try (final InputStream responseStream = conn.getInputStream()) { 85 | return getContentAsBytes(responseStream); 86 | } 87 | } 88 | 89 | public byte[] getContentAsBytes(final InputStream stream) throws IOException { 90 | try (final ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) { 91 | byte[] buf = new byte[1024]; 92 | int n = 0; 93 | while ((n = stream.read(buf)) != -1) { 94 | byteStream.write(buf, 0, n); 95 | } 96 | return byteStream.toByteArray(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/net/sourceforge/plantuml/servlet/TestMap.java: -------------------------------------------------------------------------------- 1 | package net.sourceforge.plantuml.servlet; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.net.URLConnection; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import net.sourceforge.plantuml.servlet.utils.TestUtils; 11 | import net.sourceforge.plantuml.servlet.utils.WebappTestCase; 12 | 13 | 14 | public class TestMap extends WebappTestCase { 15 | 16 | /** 17 | * Verifies the generation of the MAP for the following sample: 18 | * 19 | * participant Bob [[http://www.yahoo.com]] 20 | * Bob -> Alice : [[http://www.google.com]] hello 21 | */ 22 | @Test 23 | public void testSimpleSequenceDiagram() throws IOException { 24 | final URL url = new URL( 25 | getServerUrl() + 26 | "/map/AqWiAibCpYn8p2jHSCfFKeYEpYWfAR3IroylBzUhJCp8pzTBpi-DZUK2IUhQAJZcP2QdAbYXgalFpq_FIOKeLCX8pSd91m00" 27 | ); 28 | final URLConnection conn = url.openConnection(); 29 | // Analyze response 30 | // Verifies the Content-Type header 31 | Assertions.assertEquals( 32 | "text/plain;charset=utf-8", 33 | conn.getContentType().toLowerCase(), 34 | "Response content type is not TEXT PLAIN or UTF-8" 35 | ); 36 | // Get the content, check its first characters and verify its size 37 | String diagram = getContentText(conn); 38 | Assertions.assertTrue( 39 | diagram.startsWith(" 200); 48 | Assertions.assertTrue(diagramLen < 300); 49 | } 50 | 51 | /** 52 | * Check the content of the MAP for the sequence diagram sample 53 | * Verify structure of the area tags 54 | */ 55 | @Test 56 | public void testSequenceDiagramContent() throws IOException { 57 | final URL url = new URL( 58 | getServerUrl() + 59 | "/map/AqWiAibCpYn8p2jHSCfFKeYEpYWfAR3IroylBzUhJCp8pzTBpi-DZUK2IUhQAJZcP2QdAbYXgalFpq_FIOKeLCX8pSd91m00" 60 | ); 61 | // Analyze response 62 | // Get the data contained in the XML 63 | String map = getContentText(url); 64 | // Verify shape: 65 | // 66 | // 67 | // 68 | // 69 | Assertions.assertTrue( 70 | map.matches("^\n(\n){2}\n*$"), 71 | "Response doesn't match shape" 72 | ); 73 | } 74 | 75 | /** 76 | * Check the empty MAP of a sequence diagram without link 77 | * This test uses the simple Bob -> Alice 78 | */ 79 | @Test 80 | public void testSequenceDiagramWithoutLink() throws IOException { 81 | final URL url = new URL(getServerUrl() + "/map/" + TestUtils.SEQBOB); 82 | final URLConnection conn = url.openConnection(); 83 | // Analyze response 84 | // Verifies the Content-Type header 85 | Assertions.assertEquals( 86 | "text/plain;charset=utf-8", 87 | conn.getContentType().toLowerCase(), 88 | "Response content type is not TEXT PLAIN or UTF-8" 89 | ); 90 | // Get the data contained in the XML 91 | String diagram = getContentText(conn); 92 | int diagramLen = diagram.length(); 93 | Assertions.assertEquals(0, diagramLen); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/net/sourceforge/plantuml/servlet/utility/UrlDataExtractor.java: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * PlantUML : a free UML diagram generator 3 | * ======================================================================== 4 | * 5 | * Project Info: https://plantuml.com 6 | * 7 | * This file is part of PlantUML. 8 | * 9 | * PlantUML is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * PlantUML distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 | * License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public 20 | * License along with this library; if not, write to the Free Software 21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 | * USA. 23 | */ 24 | package net.sourceforge.plantuml.servlet.utility; 25 | 26 | import java.util.regex.Matcher; 27 | import java.util.regex.Pattern; 28 | 29 | /** 30 | * Utility class to extract the index and diagram source from an URL, e.g., returned by `request.getRequestURI()`. 31 | */ 32 | public abstract class UrlDataExtractor { 33 | 34 | /** 35 | * URL regex pattern to easily extract index and encoded diagram. 36 | */ 37 | private static final Pattern URL_PATTERN = Pattern.compile( 38 | "/\\w+(?:/(?\\d+))?(?:/(?[^/]+))?/?$" 39 | ); 40 | 41 | /** 42 | * Get diagram index from URL. 43 | * 44 | * @param url URL to analyse, e.g., returned by `request.getRequestURI()` 45 | * 46 | * @return if exists diagram index; otherwise -1 47 | */ 48 | public static int getIndex(final String url) { 49 | return getIndex(url, -1); 50 | } 51 | 52 | /** 53 | * Get diagram index from URL. 54 | * 55 | * @param url URL to analyse, e.g., returned by `request.getRequestURI()` 56 | * @param fallback fallback index if no index exists in {@code url} 57 | * 58 | * @return if exists diagram index; otherwise {@code fallback} 59 | */ 60 | public static int getIndex(final String url, final int fallback) { 61 | final Matcher matcher = URL_PATTERN.matcher(url); 62 | if (!matcher.find()) { 63 | return fallback; 64 | } 65 | String idx = matcher.group("idx"); 66 | if (idx == null) { 67 | return fallback; 68 | } 69 | return Integer.parseInt(idx); 70 | } 71 | 72 | /** 73 | * Get encoded diagram source from URL. 74 | * 75 | * @param url URL to analyse, e.g., returned by `request.getRequestURI()` 76 | * 77 | * @return if exists diagram index; otherwise `null` 78 | */ 79 | public static String getEncodedDiagram(final String url) { 80 | return getEncodedDiagram(url, null); 81 | } 82 | 83 | /** 84 | * Get encoded diagram source from URL. 85 | * 86 | * @param url URL to analyse, e.g., returned by `request.getRequestURI()` 87 | * @param fallback fallback if no encoded diagram source exists in {@code url} 88 | * 89 | * @return if exists diagram index; otherwise {@code fallback} 90 | */ 91 | public static String getEncodedDiagram(final String url, final String fallback) { 92 | final Matcher matcher = URL_PATTERN.matcher(url); 93 | if (!matcher.find()) { 94 | return fallback; 95 | } 96 | String encoded = matcher.group("encoded"); 97 | if (encoded == null) { 98 | return fallback; 99 | } 100 | return encoded; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/webapp/js/language/validation/listeners/start-end-validation.js: -------------------------------------------------------------------------------- 1 | /**************************************** 2 | * Language Start-End Validation Feature * 3 | *****************************************/ 4 | 5 | /** 6 | * Add PlantUML `@start` and `@end` command validation. 7 | */ 8 | PlantUmlLanguageFeatures.prototype.addStartEndValidationListeners = function() { 9 | let diagramType = undefined; 10 | let startCounter = 0; 11 | let endCounter = 0; 12 | 13 | // reset validation cache 14 | this.addValidationEventListener("before", () => { 15 | diagramType = undefined; 16 | startCounter = 0; 17 | endCounter = 0; 18 | }); 19 | 20 | // @start should be the first command 21 | this.addValidationEventListener("code", ({ model, code }) => { 22 | const match = code.match(/^(?:(?:'.*)|\s)*@start(\w+)/); 23 | if (match) { 24 | diagramType = match[1]; 25 | return; // diagram code starts with a `@start` 26 | } 27 | return { 28 | message: "PlantUML diagrams should begin with the `@start` command and `@start` should also be the first command.", 29 | severity: monaco.MarkerSeverity.Warning, 30 | startLineNumber: 1, 31 | startColumn: 1, 32 | endLineNumber: 1, 33 | endColumn: model.getLineLength(1) + 1, 34 | }; 35 | }); 36 | 37 | // @end should be the last command and should be of the same type (e.g. @startjson ... @endjson) 38 | this.addValidationEventListener("code", ({ model, code }) => { 39 | const lineCount = model.getLineCount(); 40 | const match = code.match(/\s+@end(\w+)(?:(?:'.*)|\s)*$/); 41 | if (match) { 42 | if (diagramType === match[1]) { 43 | return; // diagram code ends with a `@end` of the same type as the `@start` 44 | } 45 | return { 46 | message: "PlantUML diagrams should start and end with the type.\nExample: `@startjson ... @endjson`", 47 | severity: monaco.MarkerSeverity.Error, 48 | startLineNumber: lineCount, 49 | startColumn: 1, 50 | endLineNumber: lineCount, 51 | endColumn: model.getLineLength(lineCount) + 1, 52 | }; 53 | } 54 | return { 55 | message: "PlantUML diagrams should end with the `@end` command and `@end` should also be the last command.", 56 | severity: monaco.MarkerSeverity.Warning, 57 | startLineNumber: lineCount, 58 | startColumn: 1, 59 | endLineNumber: lineCount, 60 | endColumn: model.getLineLength(lineCount) + 1, 61 | }; 62 | }); 63 | 64 | // @start should only be used once 65 | this.addValidationEventListener("line", ({ range, line }) => { 66 | const match = line.match(/^\s*@start(\w+)(?:\s+.*)?$/); 67 | if (!match) return; 68 | 69 | startCounter += 1; 70 | if (startCounter > 1) { 71 | const word = "@start" + match[1]; 72 | const wordIndex = line.indexOf(word); 73 | return { 74 | message: "Multiple @start commands detected.", 75 | severity: monaco.MarkerSeverity.Warning, 76 | startLineNumber: range.startLineNumber, 77 | startColumn: wordIndex + 1, 78 | endLineNumber: range.endLineNumber, 79 | endColumn: wordIndex + word.length + 1, 80 | }; 81 | } 82 | }); 83 | 84 | // @end should only be used once 85 | this.addValidationEventListener("line", ({ range, line }) => { 86 | const match = line.match(/^\s*@end(\w+)(?:\s+.*)?$/); 87 | if (!match) return; 88 | 89 | endCounter += 1; 90 | if (endCounter > 1) { 91 | const word = "@end" + match[1]; 92 | const wordIndex = line.indexOf(word); 93 | return { 94 | message: "Multiple @end commands detected.", 95 | severity: monaco.MarkerSeverity.Warning, 96 | startLineNumber: range.startLineNumber, 97 | startColumn: wordIndex + 1, 98 | endLineNumber: range.endLineNumber, 99 | endColumn: wordIndex + word.length + 1, 100 | }; 101 | } 102 | }); 103 | }; 104 | -------------------------------------------------------------------------------- /src/main/webapp/components/app.css: -------------------------------------------------------------------------------- 1 | /********************************** 2 | * PlantUML Server Application CSS * 3 | ***********************************/ 4 | 5 | /************* variables *************/ 6 | :root { 7 | color-scheme: light dark; 8 | --font-color: black; 9 | --font-color-disabled: #888; 10 | --bg-color: white; 11 | --border-color: #ccc; 12 | --border-color-2: #aaa; 13 | --footer-font-color: #666; 14 | --footer-bg-color: #eee; 15 | --modal-bg-color: #fefefe; 16 | --file-drop-color: #eee; 17 | } 18 | [data-theme="dark"] { 19 | --font-color: #ccc; 20 | --font-color-disabled: #777; 21 | --bg-color: #212121; 22 | --border-color: #848484; 23 | --border-color-2: #aaa; 24 | --footer-font-color: #ccc; 25 | --footer-bg-color: black; 26 | --modal-bg-color: #424242; 27 | --file-drop-color: #212121; 28 | } 29 | 30 | /************* default settings *************/ 31 | html, body { 32 | margin: 0; 33 | padding: 0; 34 | } 35 | html { 36 | font-family: arial,helvetica,sans-serif; 37 | } 38 | body { 39 | background-color: var(--bg-color); 40 | color: var(--font-color); 41 | overflow: auto; 42 | } 43 | @media screen and (min-width: 900px) { 44 | body { 45 | height: 100vh; 46 | overflow: hidden; 47 | } 48 | .app { 49 | height: 100%; 50 | } 51 | } 52 | input:not([type="image"]) { 53 | background-color: var(--bg-color); 54 | color: var(--font-color); 55 | } 56 | input[type="file"]::file-selector-button { 57 | background-color: var(--bg-color); 58 | color: var(--font-color); 59 | } 60 | select { 61 | background-color: var(--bg-color); 62 | color: var(--font-color); 63 | } 64 | 65 | /************* ruler *************/ 66 | .hr { 67 | padding: 1rem 0; 68 | width: 100%; 69 | } 70 | .flex-columns > .hr { 71 | padding: 0 1rem; 72 | width: initial; 73 | height: 100%; 74 | } 75 | .hr:after { 76 | content: ""; 77 | display: block; 78 | background-color: var(--border-color); 79 | height: 100%; 80 | width: 100%; 81 | min-height: 3px; 82 | min-width: 3px; 83 | } 84 | 85 | /************* wait cursor *************/ 86 | .wait { 87 | cursor: wait; 88 | } 89 | .wait > * { 90 | pointer-events: none; 91 | } 92 | 93 | /************* flex rows and columns *************/ 94 | .flex-columns { 95 | display: flex; 96 | flex-direction: row; 97 | flex-wrap: wrap; 98 | } 99 | .flex-rows { 100 | display: flex; 101 | flex-direction: column; 102 | } 103 | .flex-main { 104 | flex: 1 1 1px; 105 | overflow: auto; 106 | } 107 | .flex-columns > *, .flex-rows > * { 108 | flex-shrink: 0; 109 | } 110 | 111 | /*******************************************************************/ 112 | /************* header, main, footer *************/ 113 | .header { 114 | margin-left: auto; 115 | margin-right: auto; 116 | text-align: center; 117 | } 118 | .main { 119 | margin: 1% 5%; 120 | z-index: 1; 121 | } 122 | .main > div { 123 | margin: 0 1.75%; 124 | } 125 | .main > div:first-child { 126 | margin-left: 0; 127 | } 128 | .main > div:last-child { 129 | margin-right: 0; 130 | } 131 | @media screen and (max-width: 900px) { 132 | .main { 133 | display: block; 134 | overflow: inherit; 135 | } 136 | .main > div { 137 | margin: 1.75% 0; 138 | } 139 | .main > div:first-child { 140 | margin-top: 0; 141 | } 142 | .main > div:last-child { 143 | margin-bottom: 0; 144 | } 145 | } 146 | .footer p { 147 | background-color: var(--footer-bg-color); 148 | color: var(--footer-font-color); 149 | font-size: 0.7em; 150 | margin: 0; 151 | padding: 0.5em; 152 | text-align: center; 153 | } 154 | 155 | /*******************************************************************/ 156 | /************* color themes *************/ 157 | [data-theme="dark"] img:not(#diagram-png):not(.no-filter) { 158 | filter: invert() contrast(30%); 159 | } 160 | [data-theme="dark"] input[type="image"] { 161 | filter: invert() contrast(30%); 162 | } 163 | [data-theme="dark"] a { 164 | color: white; 165 | } 166 | -------------------------------------------------------------------------------- /docs/contribution/front-end.md: -------------------------------------------------------------------------------- 1 | # Front-end Contribution 2 | 3 | ## Web UI 4 | 5 | The Web UI uses vanilla javascript. 6 | 7 | As online editor Microsoft's [Monaco Editor](https://github.com/microsoft/monaco-editor). 8 | The documentation can be found [here](https://microsoft.github.io/monaco-editor/docs.html). 9 | You may recognize the editor since it's the code editor from [VS Code](https://github.com/microsoft/vscode). 10 | 11 | The main entry file are `index.jsp`, `previewer.jsp` and `error.jsp`. 12 | 13 | The code structure is mainly divided into `components` and `js`: 14 | - `components` are for example a modal or dialog. 15 | Anything that include things directly seen and rendered on the page. 16 | - `js` contains more the things that do not have a direct influence on the UI. For example the PlantUML language features or the methods for cross-browser/cross-tab communication. 17 | 18 | 19 | ## PlantUML Language Features 20 | 21 | At the moment there is no defined PlantUML language. 22 | Feel free to create one! 23 | But until then the syntax highlighting form `apex` is used. 24 | IMHO it works quite well. 25 | 26 | All PlantUML language features are bundled into a seperate file `plantuml-language.min.js`. 27 | Therefore anything under `js/language` should be independent! 28 | 29 | ### Code Completion 30 | What do you need to do to create a new code completion feature: 31 | 1. create a new JS file under `js/language/completion` - let's say `xxx.js` 32 | 2. create a new `registerXxxCompletion` method 33 | _It may help you if you look into the [documentation](https://microsoft.github.io/monaco-editor/docs.html#functions/languages.registerCompletionItemProvider.html) or at the provided [sample code](https://microsoft.github.io/monaco-editor/playground.html?source=v0.38.0#example-extending-language-services-completion-provider-example) to understand more about `monaco.languages.registerCompletionItemProvider`._ 34 | ```js 35 | PlantUmlLanguageFeatures.prototype.registerEmojiCompletion = function() { 36 | monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, { 37 | provideCompletionItems: async (model, position) => { 38 | // ... 39 | return { suggestions }; 40 | } 41 | }); 42 | }; 43 | ``` 44 | 4. add your new method inside the language initialization inside `js/language/language.js` 45 | ```diff 46 | const PlantUmlLanguageFeatures = function(initialize = true) { 47 | if (initialize) { 48 | // initialize all validation and code completion methods 49 | this.addStartEndValidationListeners(); 50 | this.registerThemeCompletion(); 51 | this.registerIconCompletion(); 52 | this.registerEmojiCompletion(); 53 | + this.registerXxxCompletion(); 54 | } 55 | }; 56 | ``` 57 | 58 | ### Code Validation 59 | What do you need to do to create a new code validation feature: 60 | 1. create a new JS file under `js/language/validation/listeners` - let's say `zzz-validation.js` 61 | 2. register your validation methods to the designated event listener 62 | The validation event order is: `before` → `code` → `line` → `after` 63 | You may look at `js/language/validation/listeners/start-end-validation.js` to get an idea how to register a new listener. 64 | 3. add your new method inside the language initialization inside `js/language/language.js` 65 | ```diff 66 | const PlantUmlLanguageFeatures = function(initialize = true) { 67 | if (initialize) { 68 | // initialize all validation and code completion methods 69 | this.addStartEndValidationListeners(); 70 | + this.addZzzValidationListeners(); 71 | this.registerThemeCompletion(); 72 | this.registerIconCompletion(); 73 | this.registerEmojiCompletion(); 74 | } 75 | }; 76 | ``` 77 | 78 | 79 | ### Tipps 80 | 81 | - `pom.xml`: set `withoutCSSJSCompress` to `true` to deactivate the minification 82 | - use `mvn fizzed-watcher:run` to watch changes and automatically update the bundled `plantuml.min.{css,js}` and `plantuml-language.min.js` files 83 | - if the browser get the error `ReferenceError: require is not defined` or something similar related to the webjars, try `mvn clean install` to get things straight 84 | -------------------------------------------------------------------------------- /examples/nginx-contextpath/README.md: -------------------------------------------------------------------------------- 1 | # Nginx reverse proxy example with defined location directive 2 | 3 | In this example, the reverse proxy is defined only under the `/plantuml` context path. 4 | All other context paths (locations) are not affected and are freely available. 5 | This allows the server to be used for more than "just" PlantUML. 6 | 7 | References: 8 | - [Nginx documentation](https://nginx.org/en/docs/) 9 | - [Nginx beginner's guide](https://nginx.org/en/docs/beginners_guide.html) 10 | 11 | 12 | ## Quick start 13 | 14 | Be sure to have [`docker-compose.yml`](./docker-compose.yml) and [`nginx.conf`](./nginx.conf) inside your current working directory. 15 | 16 | ```bash 17 | # start nginx and plantuml server 18 | docker-compose up -d 19 | 20 | # stop nginx and plantuml server 21 | docker-compose down 22 | ``` 23 | 24 | Check with `docker ps` if both container are up and running: 25 | 26 | ``` 27 | $ docker ps 28 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 29 | 217e753a0dcf plantuml/plantuml-server:jetty "/entrypoint.sh" 4 seconds ago Up 3 seconds 8080/tcp plantuml-server 30 | 9b1290c100f5 nginx:alpine "/docker-entrypoint.…" 4 seconds ago Up 3 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp nginx 31 | ``` 32 | 33 | Open [http://localhost/plantuml](http://localhost/plantuml) inside your browser. 34 | YEAH! You are now using PlantUML behind a simple Nginx reverse proxy. 35 | 36 | 37 | ## Nginx configuration 38 | 39 | ```nginx 40 | ... 41 | 42 | # PlantUML 43 | location /plantuml/ { 44 | proxy_pass http://plantuml-server:8080/plantuml/; 45 | } 46 | 47 | ... 48 | ``` 49 | 50 | - `location /plantuml/` to reverse only the context path `/plantuml` 51 | - `proxy_pass http://plantuml-server:8080/plantuml/` to set reverse proxy path to plantuml server. 52 | Use the docker container name `plantuml-server` instead of ip addresses. 53 | Also, use the same context path (`BASE_URL`) as PlantUML, which is configurable as an environment variable in the docker-compose file. 54 | 55 | NOTE: `BASE_URL`, `location` and therefore the `proxy_pass` should have the some context path! 56 | If that is not possible it may be possible to solve the problem by using NGINX `sub_filter`: 57 | ```nginx 58 | # PlantUML 59 | location /plantuml/ { 60 | sub_filter '' ''; 61 | sub_filter_types text/html; 62 | 63 | proxy_pass http://plantuml-server:8080/; 64 | } 65 | ``` 66 | 67 | NOTE: Since [PR#256](https://github.com/plantuml/plantuml-server/pull/256) it is possible to use deep base URLs. 68 | So with e.g. `BASE_URL=foo/bar` the following is possible: 69 | ```nginx 70 | # PlantUML 71 | location /foo/bar/ { 72 | proxy_pass http://plantuml-server:8080/foo/bar/; 73 | } 74 | ``` 75 | 76 | 77 | ## Nginx and PlantUML server 78 | 79 | ```yaml 80 | version: "3" 81 | 82 | services: 83 | plantuml-server: 84 | image: plantuml/plantuml-server:jetty 85 | container_name: plantuml-server 86 | environment: 87 | - TZ=Europe/Berlin 88 | - BASE_URL=plantuml 89 | 90 | nginx: 91 | image: nginx:alpine 92 | container_name: nginx 93 | ports: 94 | - "80:80" 95 | environment: 96 | - TZ=Europe/Berlin 97 | volumes: 98 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 99 | ``` 100 | 101 | - Set `container_name` to use them instead of e.g. ip addresses 102 | - Set the environment `TZ` the ensure the same timezone. 103 | For example to server timezone (`cat /etc/timezone`)? 104 | - plantuml-server 105 | * plantuml-server already exposes port `8080` to it's own local network (but not outside). 106 | Since plantuml-server and nginx are sharing a network, nginx can reach plantuml-server without further settings. 107 | * Set the environment `BASE_URL` to the preferred context path 108 | - nginx 109 | * open/link port `80` to the outside 110 | * `./nginx.conf:/etc/nginx/nginx.conf:ro` to use your own Nginx configuration (readonly) 111 | 112 | 113 | ## Useful commands 114 | 115 | ```bash 116 | # see whats going on inside your docker containers 117 | docker logs --tail 50 --follow --timestamps nginx 118 | docker logs --tail 50 --follow --timestamps plantuml-server 119 | ``` 120 | --------------------------------------------------------------------------------