front-end code refactoring

Since the front-end received much more code (features) than first expected, the files became much too large. For this reason, the JS and CSS code has now been split by component and thus into several small files. However, since there are now many small files, a JS and CSS bundle tool had to come :D.
This commit is contained in:
Florian 2023-05-19 00:35:34 +02:00 committed by PlantUML
parent 080cbaada9
commit 478ef3bce7
70 changed files with 2725 additions and 2186 deletions

View File

@ -9,6 +9,7 @@
"epstext",
"etag",
"ghaction",
"inmemory",
"Lalloni",
"monaco",
"plantuml",
@ -22,5 +23,8 @@
"utxt"
],
"cSpell.allowCompoundWords": true,
"svg.preview.background": "transparent"
"svg.preview.background": "dark-transparent",
"files.associations": {
"*.jspf": "html"
}
}

View File

@ -0,0 +1,83 @@
# Front-end Contribution
## Web UI
The Web UI uses vanilla javascript.
As online editor Microsoft's [Monaco Editor](https://github.com/microsoft/monaco-editor).
The documentation can be found [here](https://microsoft.github.io/monaco-editor/docs.html).
You may recognize the editor since it's the code editor from [VS Code](https://github.com/microsoft/vscode).
The main entry file are `index.jsp`, `previewer.jsp` and `error.jsp`.
The code structure is mainly divided into `components` and `js`:
- `components` are for example a modal or dialog.
Anything that include things directly seen and rendered on the page.
- `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.
## PlantUML Language Features
At the moment there is no defined PlantUML language.
Feel free to create one!
But until then the syntax highlighting form `apex` is used.
IMHO it works quite well.
All PlantUML language features are bundled into a seperate file `plantuml-language.min.js`.
Therefore anything under `js/language` should be independent!
### Code Completion
What do you need to do to create a new code completion feature:
1. create a new JS file under `js/language/completion` - let's say `xxx.js`
2. create a new `registerXxxCompletion` method
_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`._
```js
PlantUmlLanguageFeatures.prototype.registerEmojiCompletion = function() {
monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, {
provideCompletionItems: async (model, position) => {
// ...
return { suggestions };
}
});
};
```
4. add your new method inside the language initialization inside `js/language/language.js`
```diff
const PlantUmlLanguageFeatures = function(initialize = true) {
if (initialize) {
// initialize all validation and code completion methods
this.addStartEndValidationListeners();
this.registerThemeCompletion();
this.registerIconCompletion();
this.registerEmojiCompletion();
+ this.registerXxxCompletion();
}
};
```
### Code Validation
What do you need to do to create a new code validation feature:
1. create a new JS file under `js/language/validation/listeners` - let's say `zzz-validation.js`
2. register your validation methods to the designated event listener
The validation event order is: `before` → `code` → `line` → `after`
You may look at `js/language/validation/listeners/start-end-validation.js` to get an idea how to register a new listener.
3. add your new method inside the language initialization inside `js/language/language.js`
```diff
const PlantUmlLanguageFeatures = function(initialize = true) {
if (initialize) {
// initialize all validation and code completion methods
this.addStartEndValidationListeners();
+ this.addZzzValidationListeners();
this.registerThemeCompletion();
this.registerIconCompletion();
this.registerEmojiCompletion();
}
};
```
### Tipps
- `pom.xml`: set `withoutCSSJSCompress` to `true` to deactivate the minification
- use `mvn fizzed-watcher:run` to watch changes and automatically update the bundled `plantuml.min.{css,js}` and `plantuml-language.min.js` files
- 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

View File

@ -28,6 +28,10 @@
- mvn test -DskipTests=false -DargLine="-Dsystem.test.server=http://localhost:8080/plantuml"
-->
<skipTests>true</skipTests>
<!--
JS and CSS compression / minify
-->
<withoutCSSJSCompress>false</withoutCSSJSCompress>
<!--
This artifact is required for:
@ -75,7 +79,7 @@
<junit.version>5.9.3</junit.version>
<junit-suite.version>1.9.3</junit-suite.version>
<selenium.version>4.9.1</selenium.version>
<selenium-webdrivermanager.version>5.3.2</selenium-webdrivermanager.version>
<selenium-webdrivermanager.version>5.3.3</selenium-webdrivermanager.version>
<commons-io.version>2.11.0</commons-io.version>
<jetty-server.version>${jetty.version}</jetty-server.version>
@ -101,6 +105,7 @@
<jetty-maven-plugin.version>${jetty.version}</jetty-maven-plugin.version>
<duplicate-finder-maven-plugin.version>1.5.1</duplicate-finder-maven-plugin.version>
<maven-javadoc-plugin.version>3.5.0</maven-javadoc-plugin.version>
<resources-optimizer-maven-plugin.version>2.4.4</resources-optimizer-maven-plugin.version><!-- no JDK8 support starting version 2.5.0 -->
</properties>
<dependencies>
@ -479,6 +484,95 @@
</webResources>
</configuration>
</plugin>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<executions>
<execution>
<id>clean-minified-resources</id>
<phase>initialize</phase>
<goals>
<goal>clean</goal>
</goals>
<configuration>
<excludeDefaultDirectories>true</excludeDefaultDirectories>
<filesets>
<fileset>
<directory>${basedir}/src/main/webapp/min</directory>
</fileset>
</filesets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.primefaces.extensions</groupId>
<artifactId>resources-optimizer-maven-plugin</artifactId>
<version>${resources-optimizer-maven-plugin.version}</version>
<executions>
<execution>
<id>optimize</id>
<phase>generate-resources</phase>
<goals>
<goal>optimize</goal>
</goals>
</execution>
</executions>
<configuration>
<warningLevel>DEFAULT</warningLevel>
<failOnWarning>true</failOnWarning>
<suffix>.min</suffix>
<languageIn>ECMASCRIPT_2020</languageIn>
<languageOut>ECMASCRIPT5_STRICT</languageOut>
<emitUseStrict>true</emitUseStrict>
<resourcesSets>
<resourcesSet>
<inputDir>${basedir}/src/main/webapp</inputDir>
<includes>
<include>components/**/*.js</include>
<include>js/**/*.js</include>
</includes>
<excludes>
<exclude>js/language/**</exclude>
</excludes>
<aggregations>
<aggregation>
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
<removeIncluded>false</removeIncluded>
<outputFile>${basedir}/src/main/webapp/min/plantuml.min.js</outputFile>
</aggregation>
</aggregations>
</resourcesSet>
<resourcesSet>
<inputDir>${basedir}/src/main/webapp/js/language</inputDir>
<includes>
<include>language.js</include>
<include>validation/validation.js</include>
<include>**/*.js</include>
</includes>
<aggregations>
<aggregation>
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
<removeIncluded>false</removeIncluded>
<outputFile>${basedir}/src/main/webapp/min/plantuml-language.min.js</outputFile>
</aggregation>
</aggregations>
</resourcesSet>
<resourcesSet>
<inputDir>${basedir}/src/main/webapp/components</inputDir>
<includes>
<include>**/*.css</include>
</includes>
<aggregations>
<aggregation>
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
<removeIncluded>false</removeIncluded>
<outputFile>${basedir}/src/main/webapp/min/plantuml.min.css</outputFile>
</aggregation>
</aggregations>
</resourcesSet>
</resourcesSets>
</configuration>
</plugin>
</plugins>
</build>

128
pom.xml
View File

@ -28,6 +28,12 @@
- mvn test -DskipTests=false -DargLine="-Dsystem.test.server=http://localhost:8080/plantuml"
-->
<skipTests>true</skipTests>
<!--
JS and CSS compression / minify
If false minify is enabled.
Dev Tipp: set to `true` and run `mvn fizzed-watcher:run` while developing the frontend
-->
<withoutCSSJSCompress>false</withoutCSSJSCompress>
<!--
This artifact is required for:
@ -75,7 +81,7 @@
<junit.version>5.9.3</junit.version>
<junit-suite.version>1.9.3</junit-suite.version>
<selenium.version>4.9.1</selenium.version>
<selenium-webdrivermanager.version>5.3.2</selenium-webdrivermanager.version>
<selenium-webdrivermanager.version>5.3.3</selenium-webdrivermanager.version>
<commons-io.version>2.11.0</commons-io.version>
<jetty-server.version>${jetty.version}</jetty-server.version>
@ -93,7 +99,7 @@
<maven-site-plugin.version>3.12.1</maven-site-plugin.version>
<maven-project-info-reports-plugin.version>3.4.3</maven-project-info-reports-plugin.version>
<maven-checkstyle-plugin.version>3.2.2</maven-checkstyle-plugin.version>
<checkstyle.version>10.10.0</checkstyle.version>
<checkstyle.version>10.11.0</checkstyle.version>
<!-- plugins -->
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
@ -101,6 +107,8 @@
<jetty-maven-plugin.version>${jetty.version}</jetty-maven-plugin.version>
<duplicate-finder-maven-plugin.version>1.5.1</duplicate-finder-maven-plugin.version>
<maven-javadoc-plugin.version>3.5.0</maven-javadoc-plugin.version>
<resources-optimizer-maven-plugin.version>2.5.6</resources-optimizer-maven-plugin.version>
<fizzed-watcher-maven-plugin.verson>1.0.6</fizzed-watcher-maven-plugin.verson>
</properties>
<dependencies>
@ -173,7 +181,7 @@
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
@ -451,6 +459,120 @@
</webResources>
</configuration>
</plugin>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<executions>
<execution>
<id>clean-minified-resources</id>
<phase>initialize</phase>
<goals>
<goal>clean</goal>
</goals>
<configuration>
<excludeDefaultDirectories>true</excludeDefaultDirectories>
<filesets>
<fileset>
<directory>${basedir}/src/main/webapp/min</directory>
</fileset>
</filesets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.primefaces.extensions</groupId>
<artifactId>resources-optimizer-maven-plugin</artifactId>
<version>${resources-optimizer-maven-plugin.version}</version>
<executions>
<execution>
<id>optimize</id>
<phase>generate-resources</phase>
<goals>
<goal>optimize</goal>
</goals>
</execution>
</executions>
<configuration>
<warningLevel>DEFAULT</warningLevel>
<failOnWarning>true</failOnWarning>
<suffix>.min</suffix>
<languageIn>ECMASCRIPT_2020</languageIn>
<languageOut>ECMASCRIPT5_STRICT</languageOut>
<emitUseStrict>true</emitUseStrict>
<resourcesSets>
<resourcesSet>
<inputDir>${basedir}/src/main/webapp</inputDir>
<includes>
<include>components/**/*.js</include>
<include>js/**/*.js</include>
</includes>
<excludes>
<exclude>js/language/**</exclude>
</excludes>
<aggregations>
<aggregation>
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
<removeIncluded>false</removeIncluded>
<outputFile>${basedir}/src/main/webapp/min/plantuml.min.js</outputFile>
</aggregation>
</aggregations>
</resourcesSet>
<resourcesSet>
<inputDir>${basedir}/src/main/webapp/js/language</inputDir>
<includes>
<include>language.js</include>
<include>validation/validation.js</include>
<include>**/*.js</include>
</includes>
<aggregations>
<aggregation>
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
<removeIncluded>false</removeIncluded>
<outputFile>${basedir}/src/main/webapp/min/plantuml-language.min.js</outputFile>
</aggregation>
</aggregations>
</resourcesSet>
<resourcesSet>
<inputDir>${basedir}/src/main/webapp/components</inputDir>
<includes>
<include>**/*.css</include>
</includes>
<aggregations>
<aggregation>
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
<removeIncluded>false</removeIncluded>
<outputFile>${basedir}/src/main/webapp/min/plantuml.min.css</outputFile>
</aggregation>
</aggregations>
</resourcesSet>
</resourcesSets>
</configuration>
</plugin>
<plugin>
<groupId>com.fizzed</groupId>
<artifactId>fizzed-watcher-maven-plugin</artifactId>
<version>${fizzed-watcher-maven-plugin.verson}</version>
<configuration>
<watches>
<watch>
<directory>${basedir}/src/main/webapp/components</directory>
<recursive>true</recursive>
<includes>
<include>*.js</include>
<include>*.css</include>
</includes>
<excludes>
<exclude>*.min.js</exclude>
<exclude>*.min.css</exclude>
</excludes>
</watch>
</watches>
<goals>
<goal>clean:clean@clean-minified-resources</goal>
<goal>org.primefaces.extensions:resources-optimizer-maven-plugin:optimize</goal>
</goals>
</configuration>
</plugin>
</plugins>
</build>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -8,7 +8,7 @@
<meta name="color-scheme" content="light dark" />
<link rel="icon" href="favicon.ico" type="image/x-icon"/>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
<link rel="stylesheet" href="plantuml.css" />
<link rel="stylesheet" href="min/plantuml.min.css" />
<script src="min/plantuml.min.js"></script>
<script src="min/plantuml-language.min.js"></script>
<script src="webjars/monaco-editor/0.36.1/min/vs/loader.js"></script>
<script src="plantumllanguage.js"></script>
<script src="plantuml.js"></script>

View File

@ -0,0 +1,165 @@
/**********************************
* PlantUML Server Application CSS *
***********************************/
/************* variables *************/
:root {
color-scheme: light dark;
--font-color: black;
--font-color-disabled: #888;
--bg-color: white;
--border-color: #ccc;
--border-color-2: #aaa;
--footer-font-color: #666;
--footer-bg-color: #eee;
--modal-bg-color: #fefefe;
--file-drop-color: #eee;
}
[data-theme="dark"] {
--font-color: #ccc;
--font-color-disabled: #777;
--bg-color: #212121;
--border-color: #848484;
--border-color-2: #aaa;
--footer-font-color: #ccc;
--footer-bg-color: black;
--modal-bg-color: #424242;
--file-drop-color: #212121;
}
/************* default settings *************/
html, body {
margin: 0;
padding: 0;
}
html {
font-family: arial,helvetica,sans-serif;
}
body {
background-color: var(--bg-color);
color: var(--font-color);
overflow: auto;
}
@media screen and (min-width: 900px) {
body {
height: 100vh;
overflow: hidden;
}
.app {
height: 100%;
}
}
input:not([type="image"]) {
background-color: var(--bg-color);
color: var(--font-color);
}
input[type="file"]::file-selector-button {
background-color: var(--bg-color);
color: var(--font-color);
}
select {
background-color: var(--bg-color);
color: var(--font-color);
}
/************* ruler *************/
.hr {
padding: 1rem 0;
width: 100%;
}
.flex-columns > .hr {
padding: 0 1rem;
width: initial;
height: 100%;
}
.hr:after {
content: "";
display: block;
background-color: var(--border-color);
height: 100%;
width: 100%;
min-height: 3px;
min-width: 3px;
}
/************* wait cursor *************/
.wait {
cursor: wait;
}
.wait > * {
pointer-events: none;
}
/************* flex rows and columns *************/
.flex-columns {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.flex-rows {
display: flex;
flex-direction: column;
}
.flex-main {
flex: 1 1 1px;
overflow: auto;
}
.flex-columns > *, .flex-rows > * {
flex-shrink: 0;
}
/*******************************************************************/
/************* header, main, footer *************/
.header {
margin-left: auto;
margin-right: auto;
text-align: center;
}
.main {
margin: 1% 5%;
z-index: 1;
}
.main > div {
margin: 0 1.75%;
}
.main > div:first-child {
margin-left: 0;
}
.main > div:last-child {
margin-right: 0;
}
@media screen and (max-width: 900px) {
.main {
display: block;
overflow: inherit;
}
.main > div {
margin: 1.75% 0;
}
.main > div:first-child {
margin-top: 0;
}
.main > div:last-child {
margin-bottom: 0;
}
}
.footer p {
background-color: var(--footer-bg-color);
color: var(--footer-font-color);
font-size: 0.7em;
margin: 0;
padding: 0.5em;
text-align: center;
}
/*******************************************************************/
/************* color themes *************/
[data-theme="dark"] img:not(#diagram-png):not(.no-filter) {
filter: invert() contrast(30%);
}
[data-theme="dark"] input[type="image"] {
filter: invert() contrast(30%);
}
[data-theme="dark"] a {
color: white;
}

View File

@ -0,0 +1,46 @@
/*********************************
* PlantUML Server Application JS *
**********************************/
"use strict";
async function initApp() {
const view = new URL(window.location.href).searchParams.get("view")?.toLowerCase();
function initializeAppData() {
const analysedUrl = analyseUrl(window.location.href);
const code = document.editor?.getValue();
document.appData = Object.assign({}, window.opener?.document.appData);
if (Object.keys(document.appData).length === 0) {
document.appData = {
encodedDiagram: analysedUrl.encodedDiagram,
index: analysedUrl.index,
numberOfDiagramPages: (code) ? getNumberOfDiagramPagesFromCode(code) : 1,
};
}
}
await initEditor(view);
initializeAppData();
initTheme();
initAppCommunication();
await initPreview(view);
initModals(view);
if (document.editor) {
document.editor.focus();
if (document.appData.encodedDiagram == "SyfFKj2rKt3CoKnELR1Io4ZDoSa70000") {
// if default `Bob -> Alice : hello` example mark example code for faster editing
document.editor.setSelection({
startLineNumber: 2,
endLineNumber: 2,
startColumn: 1,
endColumn: 21,
});
}
}
document.appConfig.autoRefreshState = "complete";
}
// main entry
window.onload = initApp;

View File

@ -0,0 +1,28 @@
/*************
* Editor CSS *
**************/
.editor {
border: 3px solid var(--border-color);
box-sizing: border-box;
overflow: hidden;
}
@media screen and (max-width: 900px) {
.editor {
height: 20em;
}
}
.editor .monaco-editor-container {
overflow: hidden;
position: relative;
}
#monaco-editor {
height: 100%;
}
/* Hack to display the icons and emojis in the auto completion documentation in a visible size.
* (see PlantUmlLanguageFeatures.register{Icon,Emoji}Completion) */
#monaco-editor .overlayWidgets .suggest-details p img[alt="icon"],
#monaco-editor .overlayWidgets .suggest-details p img[alt="emoji"] {
height: 1.2rem;
}

View File

@ -0,0 +1,112 @@
/************
* Editor JS *
*************/
const { setEditorValue, initEditor } = (function() {
function setEditorValue(
editor,
text,
{ suppressEditorChangedMessage=false, forceMoveMarkers=undefined } = {}
) {
if (suppressEditorChangedMessage && editor === document.editor) {
suppressNextMessage("editor");
}
// replace editor value but preserve undo stack
editor.executeEdits("", [{ range: editor.getModel().getFullModelRange(), text, forceMoveMarkers }]);
}
async function initEditor(view) {
function loadMonacoCodeEditorAsync() {
return new Promise((resolve, _reject) => {
require.config({ paths: { vs: "webjars/monaco-editor/0.36.1/min/vs" } });
require(["vs/editor/editor.main"], resolve);
});
}
function createEditorModel() {
let plantumlFeatures;
function onPlantumlEditorContentChanged(code, sender=undefined, broadcastChanges=true) {
function broadcastCodeEditorChanges() {
document.appConfig.autoRefreshState = "started";
const numberOfDiagramPages = getNumberOfDiagramPagesFromCode(code);
let index = document.appData.index;
if (index === undefined || numberOfDiagramPages === 1) {
index = undefined;
} else if (index >= numberOfDiagramPages) {
index = numberOfDiagramPages - 1;
}
makeRequest("POST", "coder", { data: code }).then((encodedDiagram) => {
sendMessage({
sender,
data: { encodedDiagram, numberOfDiagramPages, index },
synchronize: true,
});
});
}
const updatePlantumlLanguageMarkers = (function() {
return function() {
const model = document.editor.getModel();
plantumlFeatures = plantumlFeatures || new PlantUmlLanguageFeatures();
plantumlFeatures.validateCode(model)
.then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers));
}
})();
if (sender && broadcastChanges) broadcastCodeEditorChanges();
updatePlantumlLanguageMarkers();
}
function getInitPlantumlCodeAndRemoveElement() {
const initCodeEl = document.getElementById("initCode");
const initCode = initCodeEl.value;
initCodeEl.remove();
return initCode;
}
// create editor model
const model = monaco.editor.createModel(
getInitPlantumlCodeAndRemoveElement(),
"apex",
monaco.Uri.parse("inmemory://plantuml")
);
// create editor model watcher
let timer = 0;
model.onDidChangeContent(() => {
clearTimeout(timer);
document.appConfig.autoRefreshState = "waiting";
timer = setTimeout(
() => onPlantumlEditorContentChanged(model.getValue(), "editor"),
document.appConfig.editorWatcherTimeout
);
});
return model;
}
function getDefaultStorageService() {
// create own storage service to expand suggestion documentation by default
return {
get() {},
getBoolean(key) { return key === "expandSuggestionDocs"; },
getNumber() { return 0; },
remove() {},
store() {},
onWillSaveState() {},
onDidChangeStorage() {},
onDidChangeValue() {},
};
}
// load monaco editor requirements
await loadMonacoCodeEditorAsync();
if (view !== "previewer") {
// create editor
const model = createEditorModel();
const storageService = getDefaultStorageService();
document.editor = monaco.editor.create(document.getElementById("monaco-editor"), {
model, ...document.appConfig.editorCreateOptions
}, { storageService });
// sometimes the monaco editor has resize problems
document.addEventListener("resize", () => document.editor.layout());
// init editor components
initEditorUrlInput();
initEditorMenu();
}
}
return { setEditorValue, initEditor };
})();

View File

@ -0,0 +1,10 @@
<div id="editor-main-container" class="editor flex-main flex-rows">
<div>
<%@ include file="/components/editor/url-input/editor-url-input.jsp" %>
</div>
<div class="flex-main monaco-editor-container">
<textarea id="initCode" name="initCode" style="display: none;"><%= net.sourceforge.plantuml.servlet.PlantUmlServlet.stringToHTMLString(decoded) %></textarea>
<div id="monaco-editor"></div>
<%@ include file="/components/editor/menu/editor-menu.jsp" %>
</div>
</div>

View File

@ -0,0 +1,92 @@
/******************
* Editor Menu CSS *
*******************/
.monaco-editor-container .editor-menu {
position: absolute;
right: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
}
.monaco-editor-container .editor-menu > div.menu-kebab {
width: 60px;
height: 60px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
cursor: pointer;
scale: 0.5;
}
.monaco-editor-container .editor-menu:hover > div.menu-kebab,
.monaco-editor-container .editor-menu:focus > div.menu-kebab {
outline: none;
scale: 0.65;
}
.monaco-editor-container .menu-kebab .kebab-circle {
width: 12px;
height: 12px;
margin: 3px;
background: var(--font-color);
border-radius: 50%;
display: block;
opacity: 0.8;
}
.monaco-editor-container .menu-kebab {
flex-direction: column;
position: relative;
transition: all 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(4),
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(5) {
position: absolute;
opacity: 0;
top: 50%;
margin-top: -6px;
left: 50%;
}
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(4) {
margin-left: -25px;
}
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(5) {
margin-left: 13px;
}
.monaco-editor-container .editor-menu:hover .menu-kebab,
.monaco-editor-container .editor-menu:focus .menu-kebab {
transform: rotate(45deg);
}
.monaco-editor-container .editor-menu:hover .menu-kebab .kebab-circle,
.monaco-editor-container .editor-menu:focus .menu-kebab .kebab-circle {
opacity: 1;
}
.monaco-editor-container .editor-menu .menu-item {
display: none;
margin: 1rem 0;
height: 1.75rem;
opacity: 0.5;
position: relative;
-webkit-animation-name: editor-menu-animateitem;
-webkit-animation-duration: 0.4s;
animation-name: editor-menu-animateitem;
animation-duration: 0.4s;
}
@-webkit-keyframes editor-menu-animateitem {
from { top: -50%; opacity: 0; }
to { top: 0; opacity: 0.5; }
}
@keyframes editor-menu-animateitem {
from { top: -50%; opacity: 0; }
to { top: 0; opacity: 0.5; }
}
.monaco-editor-container .editor-menu .menu-item:hover {
opacity: 1;
}
.monaco-editor-container .editor-menu:hover .menu-item,
.monaco-editor-container .editor-menu:focus .menu-item {
display: block;
}

View File

@ -0,0 +1,15 @@
/*****************
* Editor Menu JS *
******************/
function initEditorMenu() {
function copyCodeToClipboard() {
const range = document.editor.getModel().getFullModelRange();
document.editor.focus();
document.editor.setSelection(range);
const code = document.editor.getValue();
navigator.clipboard?.writeText(code).catch(() => {});
}
// add listener
document.getElementById("menu-item-editor-code-copy").addEventListener("click", copyCodeToClipboard);
}

View File

@ -0,0 +1,35 @@
<div class="editor-menu" tabindex="-1">
<div class="menu-kebab">
<div class="kebab-circle"></div>
<div class="kebab-circle"></div>
<div class="kebab-circle"></div>
<div class="kebab-circle"></div>
<div class="kebab-circle"></div>
</div>
<div class="menu-items">
<input
id="menu-item-editor-code-copy"
class="menu-item"
type="image"
alt="copy"
title="Copy code"
src="assets/actions/copy.svg"
/>
<input
class="menu-item"
type="image"
alt="import"
title="Import diagram"
src="assets/actions/upload.svg"
onclick="openModal('diagram-import')"
/>
<input
class="menu-item"
type="image"
alt="export"
title="Export diagram"
src="assets/actions/download.svg"
onclick="openModal('diagram-export')"
/>
</div>
</div>

View File

@ -0,0 +1,29 @@
/***********************
* Editor URL Input CSS *
************************/
.editor .btn-input {
align-items: center;
border-bottom: 3px solid var(--border-color);
box-sizing: border-box;
display: flex;
justify-content: center;
}
.editor .btn-input input[type=text] {
border: 0;
flex: 1 1 1px;
font-family: monospace;
font-size: medium;
padding: 0.2em;
text-overflow: ellipsis;
}
.editor .btn-input input[type=text]:focus {
border: 0;
box-shadow: none;
outline: none;
}
.editor .btn-input input[type="image"] {
height: 1rem;
margin-left: 0.7em;
padding: 0 0.3em;
}

View File

@ -0,0 +1,53 @@
/**********************
* Editor URL Input JS *
***********************/
const { setUrlValue, initEditorUrlInput } = (function() {
function setUrlValue(
url=undefined,
{ encodedDiagram=undefined, index=undefined } = {},
{ suppressEditorChangedMessage=false } = {}
) {
if (!url && !encodedDiagram) return;
if (suppressEditorChangedMessage) {
suppressNextMessage("url");
}
document.getElementById("url").value = url ? url : resolvePath(buildUrl("png", encodedDiagram, index));
}
function initEditorUrlInput() {
const input = document.getElementById("url");
function copyUrlToClipboard() {
input.focus();
input.select();
navigator.clipboard?.writeText(input.value).catch(() => {});
}
async function onInputChanged(event) {
document.appConfig.autoRefreshState = "started";
event.target.title = event.target.value;
const analysedUrl = analyseUrl(event.target.value);
// decode diagram (server request)
const code = await makeRequest("GET", "coder/" + analysedUrl.encodedDiagram);
// change editor content without sending the editor change message
setEditorValue(document.editor, code, { suppressEditorChangedMessage: true });
sendMessage({
sender: "url",
data: {
encodedDiagram: analysedUrl.encodedDiagram,
index: analysedUrl.index,
},
synchronize: true,
});
}
// resolve relative path inside url input once
setUrlValue(resolvePath(input.value));
// update editor and everything else if the URL input is changed
input.addEventListener("change", onInputChanged);
// add listener
document.getElementById("url-copy-btn").addEventListener("click", copyUrlToClipboard);
}
return { setUrlValue, initEditorUrlInput };
})();

View File

@ -0,0 +1,4 @@
<div class="btn-input">
<input id="url" type="text" name="url" value="png/<%= diagramUrl %>" />
<input id="url-copy-btn" type="image" alt="copy" src="assets/actions/copy.svg" tabindex="-1" />
</div>

View File

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

View File

@ -0,0 +1,17 @@
<div>
<img
style="display: inline; position: absolute; top: 0; right: 0; border: 0; max-width: 25%;"
class="no-filter"
src="assets/github-fork-me.svg"
alt="Fork me on GitHub"
usemap="#github-banner"
/>
<map id="github-banner" name="github-banner" style="cursor: pointer;">
<area
shape="poly"
coords="10,0 50,0 149,100 149,140"
href="https://github.com/plantuml/plantuml-server"
alt="Fork me on GitHub"
/>
</map>
</div>

View File

@ -0,0 +1,8 @@
<h1>PlantUML Server</h1>
<% if (showSocialButtons) { %>
<%@ include file="/components/header/social-buttons.jsp" %>
<% } %>
<% if (showGithubRibbon) { %>
<%@ include file="/components/header/github-ribbon.jsp" %>
<% } %>
<p>Create your <a href="https://plantuml.com">PlantUML</a> diagrams directly in your browser!</p>

View File

@ -0,0 +1 @@
<!-- Insert here the html code of your social buttons -->

View File

@ -0,0 +1,7 @@
/*********************
* Diagram Export CSS *
**********************/
#diagram-export.modal .label-input-pair label {
min-width: 8rem;
}

View File

@ -0,0 +1,97 @@
/********************
* Diagram Export JS *
*********************/
function initDiagramExport() {
const filenameInput = document.getElementById("download-name");
const fileTypeSelect = document.getElementById("download-type");
function openDiagramExportDialog() {
setVisibility(document.getElementById("diagram-export"), true, true);
const code = document.editor.getValue();
const name = Array.from(
code.matchAll(/^\s*@start[a-zA-Z]*\s+([a-zA-Z-_äöüÄÖÜß ]+)\s*$/gm),
m => m[1]
)[0] || "diagram";
filenameInput.value = name + ".puml";
fileTypeSelect.value = "code";
filenameInput.focus();
}
function splitFilename(filename) {
const idx = filename.lastIndexOf(".");
if (idx < 1) {
return { name: filename, ext: null };
}
if (idx === filename.length - 1) {
return { name: filename.slice(0, -1), ext: null };
}
return {
name: filename.substring(0, idx),
ext: filename.substring(idx + 1),
};
}
function getExtensionByType(type) {
switch (type) {
case "epstext": return "eps";
case "code": return "puml";
default: return type;
}
}
function getTypeByExtension(ext) {
if (!ext) return ext;
ext = ext.toLowerCase();
switch (ext) {
case "puml":
case "plantuml":
case "code":
return "code";
case "ascii": return "txt"
default: return ext;
}
}
function onTypeChanged(event) {
const type = event.target.value;
const ext = getExtensionByType(type);
const { name } = splitFilename(filenameInput.value);
filenameInput.value = name + "." + ext;
}
function onFilenameChanged(event) {
const { ext } = splitFilename(event.target.value);
const type = getTypeByExtension(ext);
if (!type) return;
fileTypeSelect.value = type;
}
function downloadFile() {
const filename = filenameInput.value;
const type = fileTypeSelect.value;
const link = document.createElement("a");
link.download = filename;
if (type === "code") {
const code = document.editor.getValue();
link.href = "data:," + encodeURIComponent(code);
} else {
if (document.appData.index !== undefined) {
link.href = type + "/" + document.appData.index + "/" + document.appData.encodedDiagram;
} else {
link.href = type + "/" + document.appData.encodedDiagram;
}
}
link.click();
}
// register modal
registerModalListener("diagram-export", openDiagramExportDialog);
// add listener
filenameInput.addEventListener("change", onFilenameChanged);
fileTypeSelect.addEventListener("change", onTypeChanged);
document.getElementById("diagram-export-ok-btn").addEventListener("click", downloadFile);
// add Ctrl+S or Meta+S (Mac) key shortcut to open export dialog
window.addEventListener("keydown", event => {
if (event.key === "s" && (isMac ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
if (!isModalOpen("diagram-export")) {
openDiagramExportDialog();
}
}
}, false);
}

View File

@ -0,0 +1,30 @@
/*********************
* Diagram Import CSS *
**********************/
#diagram-import p.error-message {
color: darkred;
padding-left: 1rem;
padding-right: 1rem;
}
#diagram-import input[type="file"] {
display: block;
width: 100%;
border: 0.2rem dashed var(--border-color);
border-radius: 0.4rem;
box-sizing: border-box;
padding: 5rem 2rem;
}
#diagram-import input[type="file"],
#diagram-import input[type="file"]::file-selector-button {
background-color: var(--modal-bg-color);
}
#diagram-import input[type="file"]:hover,
#diagram-import input[type="file"].drop-able {
border-color: var(--border-color-2);
background-color: var(--file-drop-color);
}
#diagram-import input[type="file"]:hover::file-selector-button,
#diagram-import input[type="file"].drop-able::file-selector-button {
background-color: var(--file-drop-color);
}

View File

@ -0,0 +1,152 @@
/********************
* Diagram Import JS *
*********************/
function initDiagramImport() {
const dialogElement = document.getElementById("diagram-import");
const fileInput = document.getElementById("diagram-import-input");
const okButton = document.getElementById("diagram-import-ok-btn");
const errorMessageElement = document.getElementById("diagram-import-error-message");
function openDialog(isOpenManually = true) {
setVisibility(dialogElement, true, true);
dialogElement.dataset.isOpenManually = isOpenManually.toString();
// reset or clear file input
fileInput.value = "";
onFileInputChange(fileInput);
}
function closeDialog() {
fileInput.value = ""; // reset or clear
onFileInputChange(fileInput);
dialogElement.removeAttribute("data-is-open-manually");
setVisibility(dialogElement, false);
}
function onFileInputChange(fileInput) {
errorMessageElement.innerText = "";
okButton.disabled = fileInput.files?.length < 1;
}
function checkFileLocally(file) {
function getImageFileType({name, type}) {
const supported = ["png", "svg"];
// get type by mime type
let fileType = supported.filter(t => type.toLowerCase().indexOf(t) !== -1)[0];
if (fileType) return fileType;
// fallback: get type by filename extension
if (name.indexOf(".") === -1) return undefined;
const ext = name.substring(name.lastIndexOf(".")+1).toLowerCase();
return supported.filter(t => ext === t)[0];
}
function isDiagramCode({name, type}) {
// get type by mime type
let supported = ["plain", "text", "plantuml", "puml"];
if (supported.filter(t => type.toLowerCase().indexOf(t) !== -1).length > 0) {
return true;
}
// fallback: get type by filename extension
if (name.indexOf(".") === -1) return false;
const ext = name.substring(name.lastIndexOf('.')+1).toLowerCase();
supported = ["txt", "puml", "plantuml"];
return supported.filter(t => ext === t).length > 0;
}
const type = getImageFileType(file);
const isCode = type === undefined ? isDiagramCode(file) : false;
if (!type && !isCode) {
errorMessageElement.innerText = "File not supported. " +
"Only PNG and SVG diagram images as well as PlantUML code text files are supported."
}
return { type, isDiagramCode: isCode, valid: type || isCode };
}
function importDiagram(file, fileCheck) {
function loadDiagram(code) {
setEditorValue(document.editor, code);
}
function requestMetadata(file) {
const fd = new FormData();
fd.append("diagram", file, file.name);
return makeRequest("POST", "metadata", {
data: fd,
responseType: "json",
headers: { "Accept": "application/json" },
});
}
dialogElement.classList.add("wait");
return new Promise((resolve, reject) => {
if (fileCheck.type) {
// upload diagram image, get meta data from server and load diagram from result
requestMetadata(file).then(
metadata => { loadDiagram(metadata.decoded); resolve(); },
({ response }) => { errorMessageElement.innerText = response.message || response; reject(); }
);
} else if (fileCheck.isDiagramCode) {
// read code (text) file
const reader = new FileReader();
reader.onload = event => loadDiagram(event.target.result);
reader.readAsText(file);
resolve();
} else {
// this error should already be handled.
errorMessageElement.innerText = "File not supported. " +
"Only PNG and SVG diagram images as well as PlantUML code text files are supported.";
reject();
}
}).then(() => closeDialog(), () => {}).finally(() => dialogElement.classList.remove("wait"));
}
function onGlobalDragEnter(event) {
event.stopPropagation();
event.preventDefault();
if (!isVisible(dialogElement)) {
openDialog(false);
}
}
function onFileInputDragOver(event) {
event.stopPropagation();
event.preventDefault();
if (event.dataTransfer !== null) {
event.dataTransfer.dropEffect = "copy";
}
}
function onFileInputDrop(event) {
function stop() {
event.stopPropagation();
event.preventDefault();
}
const files = event.dataTransfer.files || event.target.files;
if (!files || files.length < 1) {
return stop();
}
const file = files[0];
const fileCheck = checkFileLocally(file);
if (!fileCheck.valid) {
return stop();
}
if (dialogElement.dataset.isOpenManually === "true") {
return; // let file input handle this event => no `stop()`!
}
// drop and go - close modal without additional ok button click
stop();
importDiagram(file, fileCheck);
}
// global drag&drop events
window.addEventListener("dragenter", onGlobalDragEnter, false);
// diagram import dialog drag&drop events
fileInput.addEventListener("dragenter", event => event.target.classList.add("drop-able"), false);
fileInput.addEventListener("dragover", onFileInputDragOver, false);
fileInput.addEventListener("dragexit", event => event.target.classList.remove("drop-able"), false);
fileInput.addEventListener("drop", onFileInputDrop, false);
fileInput.addEventListener("change", event => onFileInputChange(event.target));
// ok button
okButton.addEventListener("click", () => {
const file = fileInput.files[0]; // should be always a valid file
importDiagram(file, checkFileLocally(file)); // otherwise button should be disabled
});
// register model listeners
registerModalListener("diagram-import", openDialog, closeDialog);
}

View File

@ -5,7 +5,7 @@
<div class="hr"></div>
</div>
<div class="modal-main flex-main">
<input id="diagram-import-input" type="file" name="diagram" onchange="onDiagramImportInputChange(this);" />
<input id="diagram-import-input" type="file" name="diagram" />
<p id="diagram-import-error-message" class="error-message"></p>
</div>
<div class="modal-footer">

View File

@ -0,0 +1,95 @@
/*************
* Modals CSS *
**************/
.modal {
display: block;
position: fixed;
z-index: 1;
padding: 5%;
left: 0;
top: 0;
bottom: 0;
right: 0;
overflow: auto;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
}
.modal .modal-content {
background-color: var(--modal-bg-color);
margin: auto;
padding: 2rem;
border: 3px solid var(--border-color);
max-width: 30rem;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
-webkit-animation-name: modal-animatetop;
-webkit-animation-duration: 0.4s;
animation-name: modal-animatetop;
animation-duration: 0.4s;
position: relative;
top: 50%;
transform: translateY(-50%);
}
@-webkit-keyframes modal-animatetop {
from { top: -50%; opacity: 0; }
to { top: 50%; opacity: 1; }
}
@keyframes modal-animatetop {
from { top: -50%; opacity: 0; }
to { top: 50%; opacity: 1; }
}
/************* header, main, footer *************/
.modal .modal-header h2 {
margin: 0;
}
.modal .modal-main {
flex: 1;
}
.modal .modal-footer {
margin-top: 1rem;
text-align: right;
}
/************* inputs *************/
.modal input, .modal select {
border: 1px solid var(--border-color);
}
.modal input:not(:focus):invalid {
border-bottom-color: red;
}
.modal input[type="file"]::file-selector-button {
border: 1px solid var(--border-color);
}
/************* ok + cancel buttons *************/
.modal input.ok, .modal input.cancel {
min-width: 5rem;
}
.modal input.ok[disabled], .modal input.cancel[disabled] {
color: var(--font-color-disabled);
}
.modal input.ok:not([disabled]):hover {
border-bottom-color: green;
}
.modal input.cancel:not([disabled]):hover {
border-bottom-color: darkred;
}
/************* label + input pair *************/
.modal .label-input-pair {
margin: 1rem 0;
overflow: hidden;
}
.modal .label-input-pair:first-child {
margin-top: 0;
}
.modal .label-input-pair:last-child {
margin-bottom: 0;
}
.modal .label-input-pair label {
display: inline-block;
min-width: 15rem;
}
.modal .label-input-pair label + input,
.modal .label-input-pair label + select {
box-sizing: border-box;
display: inline-block;
min-width: 10rem;
}

View File

@ -0,0 +1,61 @@
/************
* Modals JS *
*************/
const { registerModalListener, openModal, closeModal } = (function() {
const modalListener = {};
return {
registerModalListener: (id, fnOpen=undefined, fnClose=undefined) => {
modalListener[id] = { fnOpen, fnClose };
},
openModal: (id, ...args) => {
const fnOpen = modalListener[id]?.fnOpen;
if (fnOpen) {
fnOpen(...args);
} else {
setVisibility(document.getElementById(id), true, true);
}
},
closeModal: (id, ...args) => {
const fnClose = modalListener[id]?.fnClose;
if (fnClose) {
fnClose(...args);
} else {
setVisibility(document.getElementById(id), false);
}
},
};
})();
function initModals(view) {
function onModalKeydown(event) {
if (event.key === "Escape" || event.key === "Esc") {
event.preventDefault();
closeModal(event.target.closest(".modal").id);
} else if (event.key === "Enter") {
event.preventDefault();
const modal = event.target.closest(".modal");
const okBtn = modal.querySelector('input.ok[type="button"]');
if (okBtn && !okBtn.disabled) {
okBtn.click();
}
}
}
document.querySelectorAll(".modal").forEach(modal => {
modal.addEventListener("keydown", onModalKeydown, false);
});
// init modals
initSettings();
if (view !== "previewer") {
initDiagramExport();
initDiagramImport();
}
}
function isModalOpen(id) {
return isVisible(document.getElementById(id));
}
function closeAllModals() {
document.querySelectorAll(".modal").forEach(modal => closeModal(modal.id));
}

View File

@ -0,0 +1,8 @@
/***************
* Settings CSS *
****************/
#settings #settings-monaco-editor {
height: 17rem;
border: 1px solid var(--border-color);
}

View File

@ -0,0 +1,54 @@
/**************
* Settings JS *
***************/
function initSettings() {
const themeElement = document.getElementById("theme");
const diagramPreviewTypeElement = document.getElementById("diagramPreviewType");
const editorWatcherTimeoutElement = document.getElementById("editorWatcherTimeout");
function openSettings() {
setVisibility(document.getElementById("settings"), true, true);
// fill settings form
themeElement.value = document.appConfig.theme;
diagramPreviewTypeElement.value = document.appConfig.diagramPreviewType;
editorWatcherTimeoutElement.value = document.appConfig.editorWatcherTimeout;
setEditorValue(document.settingsEditor, JSON.stringify(document.appConfig.editorCreateOptions, null, " "));
}
function saveSettings() {
const appConfig = Object.assign({}, document.appConfig);
appConfig.theme = themeElement.value;
appConfig.editorWatcherTimeout = editorWatcherTimeoutElement.value;
appConfig.diagramPreviewType = diagramPreviewTypeElement.value;
appConfig.editorCreateOptions = JSON.parse(document.settingsEditor.getValue());
updateConfig(appConfig);
closeModal("settings");
}
function onThemeChanged(event) {
const theme = event.target.value;
const editorCreateOptionsString = document.settingsEditor.getValue();
const replaceTheme = (theme === "dark") ? "vs" : "vs-dark";
const substituteTheme = (theme === "dark") ? "vs-dark" : "vs";
const regex = new RegExp('("theme"\\s*:\\s*)"' + replaceTheme + '"', "gm");
setEditorValue(document.settingsEditor, editorCreateOptionsString.replace(regex, '$1"' + substituteTheme + '"'));
}
// create app config monaco editor
document.settingsEditor = monaco.editor.create(document.getElementById("settings-monaco-editor"), {
language: "json", ...document.appConfig.editorCreateOptions
});
// add listeners
themeElement.addEventListener("change", onThemeChanged);
document.getElementById("settings-ok-btn").addEventListener("click", saveSettings);
// support Ctrl+, to open the settings
window.addEventListener("keydown", function(e) {
if (e.key === "," && (isMac ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
if (!isModalOpen("settings")) {
openSettings();
}
}
}, false);
// register model listeners
registerModalListener("settings", openSettings);
}

View File

@ -32,7 +32,7 @@
</div>
</div>
<div class="modal-footer">
<input class="ok" type="button" value="Save" onclick="saveSettings();" />
<input id="settings-ok-btn" class="ok" type="button" value="Save" />
<input class="cancel" type="button" value="Cancel" onclick="closeModal('settings');" />
</div>
</div>

View File

@ -0,0 +1,43 @@
/**********************
* Preview Diagram CSS *
***********************/
.diagram {
height: 100%;
overflow: auto;
}
.diagram[data-diagram-type="pdf"] {
overflow: hidden;
}
.diagram > div {
margin: 1rem 0;
text-align: center;
}
.diagram[data-diagram-type="pdf"] > div {
height: 20em;
width: 100%;
}
.diagram img, .diagram svg, .diagram pre {
border: 3px solid var(--border-color);
box-sizing: border-box;
padding: 10px;
}
@media screen and (min-width: 900px) {
.diagram {
position: relative;
}
.diagram > div {
margin: 0;
}
.diagram:not([data-diagram-type="pdf"]) > div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-height: 100%;
max-width: 100%;
}
.diagram[data-diagram-type="pdf"] > div {
height: 100%;
}
}

View File

@ -0,0 +1,77 @@
/*********************
* Preview Diagram JS *
**********************/
async function initializeDiagram() {
if (document.appConfig.diagramPreviewType !== "png") {
// NOTE: "png" is preloaded from the server
return setDiagram(
document.appConfig.diagramPreviewType,
document.appData.encodedDiagram,
document.appData.index
);
}
}
async function setDiagram(type, encodedDiagram, index) {
const container = document.getElementById("diagram");
const png = document.getElementById("diagram-png");
const txt = document.getElementById("diagram-txt");
const pdf = document.getElementById("diagram-pdf");
// NOTE: the map and svg elements will be overwitten, hence can not be cached
async function requestDiagram(type, encodedDiagram, index) {
return makeRequest("GET", buildUrl(type, encodedDiagram, index));
}
function setDiagramMap(mapString) {
const mapEl = document.getElementById("plantuml_map");
const mapBtn = document.getElementById("map-diagram-link");
if (mapString) {
const div = document.createElement("div");
div.innerHTML = mapString;
mapEl.parentNode.replaceChild(div.firstChild, mapEl);
setVisibility(mapBtn, true);
} else {
removeChildren(mapEl);
setVisibility(mapBtn, false);
}
}
function setSvgDiagram(svgString) {
const svgEl = document.getElementById("diagram-svg");
const div = document.createElement("div");
div.innerHTML = svgString;
const newSvg = div.querySelector("svg");
newSvg.id = "diagram-svg";
newSvg.classList = svgEl.classList;
newSvg.style.cssText = svgEl.style.cssText;
svgEl.parentNode.replaceChild(newSvg, svgEl);
}
function setDiagramVisibility(type) {
const map = document.getElementById("plantuml_map");
const svg = document.getElementById("diagram-svg");
container.setAttribute("data-diagram-type", type);
setVisibility(png, type === "png");
setVisibility(map, type === "png");
setVisibility(svg, type === "svg");
setVisibility(txt, type === "txt");
setVisibility(pdf, type === "pdf");
}
// update diagram
if (type === "png") {
png.src = buildUrl("png", encodedDiagram, index);
const map = await requestDiagram("map", encodedDiagram, index);
setDiagramMap(map);
} else if (type === "svg") {
const svg = await requestDiagram("svg", encodedDiagram, index);
setSvgDiagram(svg);
} else if (type === "txt") {
txt.innerHTML = await requestDiagram("txt", encodedDiagram, index);
} else if (type === "pdf") {
pdf.data = buildUrl("pdf", encodedDiagram, index);
} else {
const message = "unknown diagram type: " + type;
(console.error || console.log)(message);
return Promise.reject(message);
}
setDiagramVisibility(type);
}

View File

@ -0,0 +1,19 @@
<div id="diagram" class="diagram">
<div>
<!-- PNG -->
<img id="diagram-png" src="png/<%= diagramUrl %>" alt="PlantUML diagram" usemap="#plantuml_map" />
<% if (hasMap) { %>
<%= map %>
<% } else { %>
<map id="plantuml_map" name="plantuml_map"></map>
<% } %>
<!-- SVG -->
<svg id="diagram-svg" style="display: none;"></svg>
<!-- ASCII Art -->
<pre id="diagram-txt" style="display: none;"></pre>
<!-- PDF -->
<object id="diagram-pdf" data="" type="application/pdf" width="100%" height="100%" style="display: none;">
<p>Unable to display PDF file.</p>
</object>
</div>
</div>

View File

@ -0,0 +1,38 @@
/*******************
* Preview Menu CSS *
********************/
.preview-menu {
margin-left: 5%;
margin-right: 5%;
}
.diagram-link img, .btn-dock {
width: 2.5rem;
}
.btn-settings {
width: 2.2rem;
margin-left: auto;
margin-right: 0.25rem;
}
.menu-r {
min-width: 3rem;
}
.menu-r .btn-float-r {
float: right;
margin-left: 0.25rem;
text-align: right;
}
.diagram-links {
align-items: center;
display: flex;
}
.diagram-link {
margin-left: 0.25rem;
margin-right: 0.25rem;
}
.diagram-links .diagram-link:first-of-type {
margin-left: 0.5rem;
}
.diagram-links .diagram-link:last-of-type {
margin-right: 0;
}

View File

@ -0,0 +1,57 @@
<div class="preview-menu">
<div class="diagram-links flex-columns">
<span>View as:</span>
<a class="diagram-link" data-img-type="png" href="png/<%= diagramUrl %>" title="View diagram as PNG">
<img src="assets/file-types/png.svg" alt="PNG" />
</a>
<a class="diagram-link" data-img-type="svg" href="svg/<%= diagramUrl %>" title="View diagram as SVG">
<img src="assets/file-types/svg.svg" alt="SVG" />
</a>
<a class="diagram-link" data-img-type="txt" href="txt/<%= diagramUrl %>" title="View diagram as ASCII Art">
<img src="assets/file-types/ascii.svg" alt="ASCII Art" />
</a>
<a class="diagram-link" data-img-type="pdf" href="pdf/<%= diagramUrl %>" title="View diagram as PDF">
<img src="assets/file-types/pdf.svg" alt="PDF" />
</a>
<a
id="map-diagram-link"
class="diagram-link"
data-img-type="map"
href="map/<%= diagramUrl %>"
title="View diagram as Map Data"
<% if (!hasMap) { %>
style="display: none;"
<% } %>
>
<img src="assets/file-types/map.svg" alt="MAP" />
</a>
<div class="flex-main menu-r">
<div class="btn-float-r">
<input
id="btn-settings"
class="btn-settings"
type="image"
src="assets/actions/settings.svg"
alt="settings"
onclick="openModal('settings')"
/>
<input
id="btn-undock"
class="btn-dock"
type="image"
src="assets/actions/undock.svg"
alt="undock"
/>
<input
id="btn-dock"
class="btn-dock"
type="image"
src="assets/actions/dock.svg"
alt="dock"
onclick="window.close();"
style="display: none;"
/>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,8 @@
/****************
* Paginator CSS *
*****************/
#paginator {
text-align: center;
margin-bottom: 1rem;
}

View File

@ -0,0 +1,62 @@
/***************
* Paginator JS *
***************/
function getNumberOfDiagramPagesFromCode(code) {
// count `newpage` inside code
// known issue: a `newpage` starting in a newline inside a multiline comment will also be counted
return code.match(/^\s*newpage\s?.*$/gm)?.length + 1 || 1;
}
function updatePaginatorSelection() {
const paginator = document.getElementById("paginator");
const index = document.appData.index;
if (index === undefined || paginator.childNodes.length <= index) {
for (const node of paginator.childNodes) {
node.checked = false;
}
} else {
paginator.childNodes[index].checked = true;
}
}
const updatePaginator = (function() {
function updateNumberOfPagingElements(paginator, pages) {
// remove elements (buttons) if there are to many
while (paginator.childElementCount > pages) {
paginator.removeChild(paginator.lastChild)
}
// add elements (buttons) if there are to less
while (paginator.childElementCount < pages) {
const radioBtn = document.createElement("input");
radioBtn.name = "paginator";
radioBtn.type = "radio";
radioBtn.value = paginator.childElementCount;
radioBtn.addEventListener("click", (event) => {
sendMessage({
sender: "paginator",
data: { index: event.target.value },
synchronize: true,
});
});
paginator.appendChild(radioBtn);
}
}
return function() {
const paginator = document.getElementById("paginator");
const pages = document.appData.numberOfDiagramPages;
if (pages > 1) {
updateNumberOfPagingElements(paginator, pages);
setVisibility(paginator, true);
} else {
setVisibility(paginator, false);
}
};
})();
function initializePaginator() {
if (document.appData.numberOfDiagramPages > 1) {
updatePaginator();
updatePaginatorSelection();
}
}

View File

@ -0,0 +1 @@
<div id="paginator" data-number-of-diagram-pages="1" style="display: none;"></div>

View File

@ -0,0 +1,15 @@
/**************
* Preview CSS *
***************/
.previewer-container {
height: 100%;
}
@media screen and (max-width: 900px) {
.previewer-container {
height: initial;
}
.previewer-main {
flex: none;
}
}

View File

@ -0,0 +1,42 @@
/*************
* Preview JS *
**************/
async function initPreview(view) {
const btnUndock = document.getElementById("btn-undock");
const btnDock = document.getElementById("btn-dock");
const editorContainer = document.getElementById("editor-main-container");
const previewContainer = document.getElementById("previewer-main-container");
function hidePreview() {
setVisibility(btnUndock, false);
// if not opened via button and therefore a popup, `window.close` won't work
setVisibility(btnDock, window.opener);
if (editorContainer) editorContainer.style.width = "100%";
if (previewContainer) setVisibility(previewContainer, false);
}
function showPreview() {
setVisibility(btnUndock, true);
setVisibility(btnDock, false);
if (editorContainer) editorContainer.style.removeProperty("width");
if (previewContainer) setVisibility(previewContainer, true);
}
function undock() {
const url = new URL(window.location.href);
url.searchParams.set("view", "previewer");
const previewer = window.open(url, "PlantUML Diagram Previewer", "popup");
if (previewer) {
previewer.onbeforeunload = showPreview;
hidePreview();
}
}
// add listener
btnUndock.addEventListener("click", undock);
// init preview components
await initializeDiagram();
initializePaginator()
// check preview visibility
if (["previewer", "editor"].includes(view)) {
hidePreview();
}
}

View File

@ -0,0 +1,16 @@
<div class="previewer-container flex-rows">
<%@ include file="/components/preview/menu/preview-menu.jsp" %>
<div class="hr"></div>
<%@ include file="/components/preview/paginator/paginator.jsp" %>
<div id="paginator" data-number-of-diagram-pages="1" style="display: none;"></div>
<div class="previewer-main flex-main">
<%@ include file="/components/preview/diagram/preview-diagram.jsp" %>
</div>
<% if (showSocialButtons) { %>
<div>
<%@ include file="/components/preview/social-buttons.jsp" %>
</div>
<% } %>
<!-- global modals -->
<%@ include file="/components/modals/settings/settings.jsp" %>
</div>

View File

@ -16,58 +16,26 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<%@ include file="resource/htmlheadbase.jsp" %>
<%@ include file="/components/app-head.jsp" %>
<title>PlantUML Server</title>
</head>
<body>
<div class="app flex-rows">
<div class="header">
<h1>PlantUML Server</h1>
<% if (showSocialButtons) { %>
<%@ include file="resource/socialbuttons1.html" %>
<% } %>
<% if (showGithubRibbon) { %>
<%@ include file="resource/githubribbon.html" %>
<% } %>
<p>Create your <a href="https://plantuml.com">PlantUML</a> diagrams directly in your browser!</p>
<%@ include file="/components/header/header.jsp" %>
</div>
<div class="main flex-main flex-columns">
<div id="editor-main-container" class="editor flex-main flex-rows">
<div>
<div class="btn-input">
<input id="url" type="text" name="url" value="png/<%= diagramUrl %>" />
<input type="image" alt="copy" src="assets/actions/copy.svg" onclick="copyUrlToClipboard()" />
</div>
</div>
<div class="flex-main monaco-editor-container">
<textarea id="initCode" name="initCode" style="display: none;"><%= net.sourceforge.plantuml.servlet.PlantUmlServlet.stringToHTMLString(decoded) %></textarea>
<div id="monaco-editor"></div>
<div class="editor-menu" tabindex="-1">
<div class="menu-kebab">
<div class="kebab-circle"></div>
<div class="kebab-circle"></div>
<div class="kebab-circle"></div>
<div class="kebab-circle"></div>
<div class="kebab-circle"></div>
</div>
<div class="menu-items">
<input class="menu-item" type="image" alt="copy" title="Copy code" src="assets/actions/copy.svg" onclick="copyCodeToClipboard()" />
<input class="menu-item" type="image" alt="import" title="Import diagram" src="assets/actions/upload.svg" onclick="openModal('diagram-import')" />
<input class="menu-item" type="image" alt="export" title="Export diagram" src="assets/actions/download.svg" onclick="openModal('diagram-export')" />
</div>
</div>
</div>
</div>
<%@ include file="/components/editor/editor.jsp" %>
<div id="previewer-main-container" class="previewer flex-main">
<%@ include file="resource/preview.jsp" %>
<%@ include file="/components/preview/preview.jsp" %>
</div>
</div>
<div class="footer">
<%@ include file="resource/footer.jsp" %>
<%@ include file="/components/footer/footer.jsp" %>
</div>
<!-- editor modals -->
<%@ include file="resource/diagram-import.jsp" %>
<%@ include file="resource/diagram-export.jsp" %>
<%@ include file="/components/modals/diagram-import/diagram-import.jsp" %>
<%@ include file="/components/modals/diagram-export/diagram-export.jsp" %>
</div>
</body>
</html>

View File

@ -0,0 +1,123 @@
/************************
* Browser Communication *
*************************
* send and receive data object:
* {
* sender: string = ["editor"|"url"|"paginator"|"settings"|"file-drop"],
* data: {
* encodedDiagram: string | undefined,
* index: integer | undefined,
* numberOfDiagramPages: integer | undefined,
* appConfig: object | undefined
* } | undefined,
* synchronize: boolean = false,
* reload: boolean = false, // reload page
* force: boolean = false // force synchronize or reload
* }
*************************/
const { sendMessage, suppressNextMessage, initAppCommunication } = (function() {
const BROADCAST_CHANNEL = "plantuml-server";
const { suppressNextMessage, isMessageSuppressed } = (function() {
const suppressMessages = [];
function suppressNextMessage(sender, condition=undefined) {
suppressMessages.push({ sender, condition });
}
function isMessageSuppressed(data) {
for (let i = 0; i < suppressMessages.length; i++) {
const suppressMessage = suppressMessages[i];
if (!suppressMessage.sender || suppressMessage.sender === data.sender) {
if (!suppressMessage.condition || suppressMessage.condition(data)) {
suppressMessages.splice(i, 1);
return true;
}
}
}
return false;
}
return { suppressNextMessage, isMessageSuppressed };
})();
function sendMessage(data) {
if (isMessageSuppressed(data)) return;
(new BroadcastChannel(BROADCAST_CHANNEL)).postMessage(data);
}
function initAppCommunication() {
function updateReceiveMessageData(data) {
if (!data || Object.keys(data).length === 0) return {};
const changedFlags = {};
if ("encodedDiagram" in data && data.encodedDiagram !== document.appData.encodedDiagram) {
document.appData.encodedDiagram = data.encodedDiagram;
changedFlags.diagram = true;
}
if ("index" in data && data.index !== document.appData.index) {
document.appData.index = data.index;
changedFlags.index = true;
}
if ("numberOfDiagramPages" in data && data.numberOfDiagramPages !== document.appData.numberOfDiagramPages) {
document.appData.numberOfDiagramPages = data.numberOfDiagramPages;
changedFlags.numberOfDiagramPages = true;
}
if ("appConfig" in data && data.appConfig !== document.appConfig) {
document.appConfig = data.appConfig;
changedFlags.appConfig = true;
}
return changedFlags;
}
async function receiveMessage(event) {
async function updateStaticPageData(sender) {
document.appConfig.autoRefreshState = "syncing";
const encodedDiagram = document.appData.encodedDiagram;
const index = document.appData.index;
if (sender !== "url" && document.getElementById("url")) {
// update URL input
setUrlValue(undefined, { encodedDiagram, index }, { suppressEditorChangedMessage: true });
}
// update diagram image
await setDiagram(document.appConfig.diagramPreviewType, encodedDiagram, index);
// update external diagram links
for (let target of document.getElementsByClassName("diagram-link")) {
target.href = buildUrl(target.dataset.imgType, encodedDiagram, index);
}
// update browser url as well as the browser history
const url = replaceUrl(window.location.href, encodedDiagram, index).url;
history.replaceState(history.stat, document.title, url);
// set auto refresh state to complete
document.appConfig.autoRefreshState = "complete";
}
const data = event.data.data;
const force = event.data.force || false;
const changedFlags = updateReceiveMessageData(data);
if (event.data.synchronize === true) {
if (force || changedFlags.diagram || changedFlags.index || changedFlags.appConfig) {
await updateStaticPageData(event.data.sender);
}
if (force || changedFlags.numberOfDiagramPages) {
updatePaginator();
}
if (force || changedFlags.numberOfDiagramPages || changedFlags.index) {
updatePaginatorSelection();
}
if (changedFlags.appConfig) {
applyConfig();
}
}
if (event.data.reload === true) {
window.location.reload();
}
}
// create broadcast channel
const bc = new BroadcastChannel(BROADCAST_CHANNEL);
bc.onmessage = receiveMessage;
}
return { sendMessage, suppressNextMessage, initAppCommunication };
})();

View File

@ -0,0 +1,20 @@
/***********************
* Server Communication *
************************/
function makeRequest(
method,
url,
{
data = null,
headers = { "Content-Type": "text/plain" },
responseType = "text",
baseUrl = "",
} = {}
) {
return PlantUmlLanguageFeatures.makeRequest(
method,
url,
{ data, headers, responseType, baseUrl }
);
}

View File

@ -0,0 +1,45 @@
/*****************
* Configurations *
******************/
const { applyConfig, updateConfig } = (function() {
const DEFAULT_APP_CONFIG = {
changeEventsEnabled: true,
// `autoRefreshState` is mostly used for unit testing puposes.
// states: disabled | waiting | started | syncing | complete
autoRefreshState: "disabled",
theme: undefined, // dark | light (will be set via `initTheme` if undefined)
diagramPreviewType: "png",
editorWatcherTimeout: 500,
editorCreateOptions: {
automaticLayout: true,
fixedOverflowWidgets: true,
minimap: { enabled: false },
scrollbar: { alwaysConsumeMouseWheel: false },
scrollBeyondLastLine: false,
tabSize: 2,
theme: "vs", // "vs-dark"
}
};
function applyConfig() {
setTheme(document.appConfig.theme);
document.editor?.updateOptions(document.appConfig.editorCreateOptions);
document.settingsEditor?.updateOptions(document.appConfig.editorCreateOptions);
}
function updateConfig(appConfig) {
localStorage.setItem("document.appConfig", JSON.stringify(appConfig));
sendMessage({
sender: "config",
data: { appConfig },
synchronize: true,
});
}
document.appConfig = Object.assign({}, window.opener?.document.appConfig);
if (Object.keys(document.appConfig).length === 0) {
document.appConfig = JSON.parse(localStorage.getItem("document.appConfig")) || DEFAULT_APP_CONFIG;
}
return { applyConfig, updateConfig };
})();

View File

@ -0,0 +1,55 @@
/**********************************************
* PlantUML Language Emoji Completion Provider *
***********************************************/
PlantUmlLanguageFeatures.prototype.getEmojis = (function(){
let emojis = undefined;
return async function() {
if (emojis === undefined) {
emojis = await PlantUmlLanguageFeatures.makeRequest("GET", "ui-helper?request=emojis");
}
return emojis;
}
})();
PlantUmlLanguageFeatures.prototype.registerEmojiCompletion = function() {
const createEmojiProposals = async (range, filter = undefined) => {
const emojis = await this.getEmojis();
return emojis?.filter(([unicode, name]) => filter ? unicode.includes(filter) || name?.includes(filter) : true)
.map(([unicode, name]) => {
// NOTE: load images direct from GitHub source: https://github.com/twitter/twemoji#download
const emojiUrl = "https://raw.githubusercontent.com/twitter/twemoji/gh-pages/v/13.1.0/svg/" + unicode + ".svg";
const docHint = (name) ? name + " (" + unicode + ")" : unicode;
const isUnicode = !name || (filter && unicode.includes(filter));
const label = isUnicode ? unicode : name;
return {
label: label,
kind: monaco.languages.CompletionItemKind.Constant,
documentation: {
//supportHtml: true, // also a possibility but quite limited html
value: "![emoji](" + emojiUrl + ") &nbsp; " + docHint
},
insertText: label + ":>",
range: range
};
}) || [];
};
monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, {
triggerCharacters: [":"],
provideCompletionItems: async (model, position) => {
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
const match = textUntilPosition.match(/<:([^\s>]*)$/);
if (match) {
const suggestions = await createEmojiProposals(this.getWordRange(model, position), match[1]);
return { suggestions };
}
return { suggestions: [] };
}
});
};

View File

@ -0,0 +1,54 @@
/*********************************************
* PlantUML Language Icon Completion Provider *
**********************************************/
PlantUmlLanguageFeatures.prototype.getIcons = (function(){
let icons = undefined;
return async function() {
if (icons === undefined) {
icons = await PlantUmlLanguageFeatures.makeRequest("GET", "ui-helper?request=icons");
}
return icons;
}
})();
PlantUmlLanguageFeatures.prototype.registerIconCompletion = function() {
const createIconProposals = async (range, filter = undefined) => {
const icons = await this.getIcons();
return icons?.filter(icon => filter ? icon.includes(filter) : true)
.map(icon => {
// NOTE: markdown image path inside suggestions seems to have rendering issues while using relative paths
const iconUrl = PlantUmlLanguageFeatures.absolutePath(
PlantUmlLanguageFeatures.baseUrl + "ui-helper?request=icons.svg#" + icon
);
return {
label: icon,
kind: monaco.languages.CompletionItemKind.Constant,
documentation: {
//supportHtml: true, // also a possibility but quite limited html
value: "![icon](" + iconUrl + ") &nbsp; " + icon
},
insertText: icon + ">",
range: range
};
}) || [];
};
monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, {
triggerCharacters: ["&"],
provideCompletionItems: async (model, position) => {
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
const match = textUntilPosition.match(/<&([^\s>]*)$/);
if (match) {
const suggestions = await createIconProposals(this.getWordRange(model, position), match[1]);
return { suggestions };
}
return { suggestions: [] };
}
});
};

View File

@ -0,0 +1,58 @@
/**********************************************
* PlantUML Language Theme Completion Provider *
***********************************************/
PlantUmlLanguageFeatures.prototype.getThemes = (function(){
let themes = undefined;
return async function() {
if (themes === undefined) {
themes = await PlantUmlLanguageFeatures.makeRequest("GET", "ui-helper?request=themes");
}
return themes;
}
})();
PlantUmlLanguageFeatures.prototype.registerThemeCompletion = function() {
const createThemeProposals = async (range, filter = undefined) => {
const themes = await this.getThemes();
return themes?.filter(theme => filter ? theme.includes(filter) : true)
.map(theme => ({
label: theme,
kind: monaco.languages.CompletionItemKind.Text,
documentation: "PlantUML " + theme + " theme",
insertText: theme,
range: range,
})) || [];
};
monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, {
triggerCharacters: [" "],
provideCompletionItems: async (model, position) => {
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
if (textUntilPosition.match(/^\s*!(t(h(e(m(e)?)?)?)?)?$/)) {
return {
suggestions: [
{
label: 'theme',
kind: monaco.languages.CompletionItemKind.Keyword,
documentation: "PlantUML theme command",
insertText: 'theme',
range: this.getWordRange(model, position),
}
]
};
}
const match = textUntilPosition.match(/^\s*!theme\s+([^\s]*)$/);
if (match) {
const suggestions = await createThemeProposals(this.getWordRange(model, position), match[1]);
return { suggestions };
}
return { suggestions: [] };
}
});
};

View File

@ -0,0 +1,13 @@
/**********************************************
* PlantUML Language Completion Provider Utils *
***********************************************/
PlantUmlLanguageFeatures.prototype.getWordRange = function(model, position) {
const word = model.getWordUntilPosition(position);
return {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
}

View File

@ -0,0 +1,92 @@
/************************************************
* Monaco Editor PlantUML Language Features Base *
*************************************************/
"use strict";
/**
* Monaco Editor PlantUML Language Features.
*
* @param {boolean} [initialize] `true` if all default validation and code completion
* functions should be activated; otherwise `false`
*
* @example
* ```js
* plantumlFeatures = new PlantUmlLanguageFeatures();
* const model = monaco.editor.createModel(initCode, "apex", uri);
* model.onDidChangeContent(() => plantumlFeatures.validateCode(model));
* ```
*/
const PlantUmlLanguageFeatures = function(initialize = true) {
if (initialize) {
// initialize all validation and code completion methods
this.addStartEndValidationListeners();
this.registerThemeCompletion();
this.registerIconCompletion();
this.registerEmojiCompletion();
}
};
PlantUmlLanguageFeatures.baseUrl = "";
PlantUmlLanguageFeatures.setBaseUrl = function(baseUrl) {
if (baseUrl === null || baseUrl === undefined) {
baseUrl = "";
} else if (baseUrl !== "") {
if (baseUrl.slice(-1) !== "/") {
baseUrl = baseUrl + "/"; // add tailing "/"
}
}
PlantUmlLanguageFeatures.baseUrl = baseUrl;
}
PlantUmlLanguageFeatures.languageSelector = ["apex", "plantuml"];
PlantUmlLanguageFeatures.setLanguageSelector = function(languageSelector) {
PlantUmlLanguageFeatures.languageSelector = languageSelector;
}
PlantUmlLanguageFeatures.makeRequest = function(
method,
url,
{
data = null,
headers = { "Content-Type": "text/plain" },
responseType = "json",
baseUrl = PlantUmlLanguageFeatures.baseUrl,
} = {}
) {
function getResolveResponse(xhr) {
return responseType === "json" ? xhr.response : xhr.responseText;
}
function getRejectResponse(xhr) {
return responseType === "json"
? { status: xhr.status, response: xhr.response }
: { status: xhr.status, responseText: xhr.responseText };
}
const targetUrl = !baseUrl ? url : baseUrl.replace(/\/*$/g, "/") + url;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status <= 300) {
resolve(getResolveResponse(xhr));
} else {
reject(getRejectResponse(xhr));
}
}
}
xhr.open(method, targetUrl, true);
xhr.responseType = responseType;
headers && Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]));
xhr.send(data);
});
}
PlantUmlLanguageFeatures.absolutePath = function(path) {
if (path.startsWith("http")) return path;
if (path.startsWith("//")) return window.location.protocol + path;
if (path.startsWith("/")) return window.location.origin + path;
if (path.slice(0, 2) == "./") path = path.slice(2);
let base = (document.querySelector("base") || {}).href || window.location.origin;
if (base.slice(-1) == "/") base = base.slice(0, -1);
return base + "/" + path;
}

View File

@ -0,0 +1,103 @@
/****************************************
* Language Start-End Validation Feature *
*****************************************/
/**
* Add PlantUML `@start` and `@end` command validation.
*/
PlantUmlLanguageFeatures.prototype.addStartEndValidationListeners = function() {
let diagramType = undefined;
let startCounter = 0;
let endCounter = 0;
// reset validation cache
this.addValidationEventListener("before", () => {
diagramType = undefined;
startCounter = 0;
endCounter = 0;
});
// @start should be the first command
this.addValidationEventListener("code", ({ model, code }) => {
const match = code.match(/^(?:(?:'.*)|\s)*@start(\w+)/);
if (match) {
diagramType = match[1];
return; // diagram code starts with a `@start`
}
return {
message: "PlantUML diagrams should begin with the `@start` command and `@start` should also be the first command.",
severity: monaco.MarkerSeverity.Warning,
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: model.getLineLength(1) + 1,
};
});
// @end should be the last command and should be of the same type (e.g. @startjson ... @endjson)
this.addValidationEventListener("code", ({ model, code }) => {
const lineCount = model.getLineCount();
const match = code.match(/\s+@end(\w+)(?:(?:'.*)|\s)*$/);
if (match) {
if (diagramType === match[1]) {
return; // diagram code ends with a `@end` of the same type as the `@start`
}
return {
message: "PlantUML diagrams should start and end with the type.\nExample: `@startjson ... @endjson`",
severity: monaco.MarkerSeverity.Error,
startLineNumber: lineCount,
startColumn: 1,
endLineNumber: lineCount,
endColumn: model.getLineLength(lineCount) + 1,
};
}
return {
message: "PlantUML diagrams should end with the `@end` command and `@end` should also be the last command.",
severity: monaco.MarkerSeverity.Warning,
startLineNumber: lineCount,
startColumn: 1,
endLineNumber: lineCount,
endColumn: model.getLineLength(lineCount) + 1,
};
});
// @start should only be used once
this.addValidationEventListener("line", ({ range, line }) => {
const match = line.match(/^\s*@start(\w+)(?:\s+.*)?$/);
if (!match) return;
startCounter += 1;
if (startCounter > 1) {
const word = "@start" + match[1];
const wordIndex = line.indexOf(word);
return {
message: "Multiple @start commands detected.",
severity: monaco.MarkerSeverity.Warning,
startLineNumber: range.startLineNumber,
startColumn: wordIndex + 1,
endLineNumber: range.endLineNumber,
endColumn: wordIndex + word.length + 1,
};
}
});
// @end should only be used once
this.addValidationEventListener("line", ({ range, line }) => {
const match = line.match(/^\s*@end(\w+)(?:\s+.*)?$/);
if (!match) return;
endCounter += 1;
if (endCounter > 1) {
const word = "@end" + match[1];
const wordIndex = line.indexOf(word);
return {
message: "Multiple @end commands detected.",
severity: monaco.MarkerSeverity.Warning,
startLineNumber: range.startLineNumber,
startColumn: wordIndex + 1,
endLineNumber: range.endLineNumber,
endColumn: wordIndex + word.length + 1,
};
}
});
};

View File

@ -0,0 +1,73 @@
/********************************************
* PlantUML Language Validation Feature Base *
*********************************************/
(function() {
const validationEventListeners = {};
/**
* Add validation event listener.
*
* Validation Event Order:
* before -> code -> line -> after
*
* @param {("before"|"code"|"line"|"after")} type before|code|line|after event type
* @param {(event: any) => Promise<editor.IMarkerData>|editor.IMarkerData|Promise<editor.IMarkerData[]>|editor.IMarkerData[]|Promise<void>|void} listener event listener
*/
PlantUmlLanguageFeatures.prototype.addValidationEventListener = function(type, listener) {
if (!["before", "code", "line", "after"].includes(type)) {
throw Error("Unknown validation event type: " + type);
}
validationEventListeners[type] = validationEventListeners[type] || [];
validationEventListeners[type].push(listener);
};
/**
* Validate PlantUML language of monaco editor model.
*
* @param {editor.ITextModel} model editor model to validate
*
* @returns editor markers as promise
*
* @example
* ```js
* validateCode(editor.getModel())
* .then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers));
* ```
*/
PlantUmlLanguageFeatures.prototype.validateCode = async function(model) {
const promises = [];
// raise before events
promises.push(validationEventListeners.before?.map(listener => listener({ model })));
// raise code events
promises.push(validationEventListeners.code?.map(listener => listener({ model, code: model.getValue() })));
if (validationEventListeners.line && validationEventListeners.line.length > 0) {
// NOTE: lines and columns start at 1
const lineCount = model.getLineCount();
for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
const range = {
startLineNumber: lineNumber,
startColumn: 1,
endLineNumber: lineNumber,
endColumn: model.getLineLength(lineNumber) + 1,
};
const line = model.getValueInRange(range);
// raise line events
promises.push(validationEventListeners.line?.map(listener => listener({ model, range, line, lineNumber, lineCount })));
}
}
// raise after events
promises.push(validationEventListeners.after?.map(listener => listener({ model })));
// collect all markers and ...
// - since each event can results in an array of markers -> `flat(1)`
// - since not each event has to results in markers and can be `undef
return Promise.all(promises).then(results => results.flat(1).filter(marker => marker));
};
})();

View File

@ -0,0 +1,27 @@
/**************
* DOM Helpers *
***************/
function removeChildren(element) {
if (element.replaceChildren) {
element.replaceChildren();
} else {
element.innerHTML = "";
}
}
function isVisible(element) {
// `offsetParent` returns `null` if the element, or any of its parents,
// is hidden via the display style property.
// see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
return (element.offsetParent !== null);
}
function setVisibility(element, visibility, focus=false) {
if (visibility) {
element.style.removeProperty("display");
if (focus) element.focus();
} else {
element.style.display = "none";
}
}

View File

@ -0,0 +1,8 @@
/*************
* OS Helpers *
**************/
const isMac = (function() {
const PLATFORM = navigator?.userAgentData?.platform || navigator?.platform || "unknown";
return PLATFORM.match("Mac");
})();

View File

@ -0,0 +1,39 @@
/****************
* Theme Helpers *
*****************/
function setTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
}
function initTheme() {
function getBrowserThemePreferences() {
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
return "light";
}
return undefined;
}
function changeEditorThemeSettingIfNecessary(theme) {
if (theme === "dark" && document.appConfig.editorCreateOptions.theme === "vs") {
document.appConfig.editorCreateOptions.theme = "vs-dark";
}
if (theme === "light" && document.appConfig.editorCreateOptions.theme === "vs-dark") {
document.appConfig.editorCreateOptions.theme = "vs";
}
}
function onMediaColorPreferencesChanged(event) {
const theme = event.matches ? "dark" : "light";
document.appConfig.theme = theme
changeEditorThemeSettingIfNecessary(theme);
updateConfig(document.appConfig);
}
// set theme to last saved settings or browser preference or "light"
document.appConfig.theme = document.appConfig.theme || getBrowserThemePreferences() || "light";
setTheme(document.appConfig.theme);
changeEditorThemeSettingIfNecessary(document.appConfig.theme);
// listen to browser change event
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", onMediaColorPreferencesChanged);
}

View File

@ -0,0 +1,53 @@
/**************
* URL Helpers *
***************/
function resolvePath(path) {
return PlantUmlLanguageFeatures.absolutePath(path);
}
function prepareUrl(url) {
if (!(url instanceof URL)) {
url = new URL(resolvePath(url));
}
// pathname excluding context path
let base = new URL((document.querySelector("base") || {}).href || window.location.origin).pathname;
if (base.slice(-1) === "/") base = base.slice(0, -1);
const pathname = url.pathname.startsWith(base) ? url.pathname.slice(base.length) : url.pathname;
// same as `UrlDataExtractor.URL_PATTERN`
// regex = /\/\w+(?:\/(?<idx>\d+))?(?:\/(?<encoded>[^\/]+))?\/?$/gm;
const regex = /\/\w+(?:\/(\d+))?(?:\/([^/]+))?\/?$/gm;
const match = regex.exec(pathname);
return [ url, pathname, { idx: match[1], encoded: match[2] } ];
}
function analyseUrl(url) {
let _, idx, encoded;
[url, _, { idx, encoded }] = prepareUrl(url);
return {
index: idx,
encodedDiagram: encoded || url.searchParams.get("url"),
};
}
function replaceUrl(url, encodedDiagram, index) {
let oldPathname, encoded;
[url, oldPathname, { encoded }] = prepareUrl(url);
let pathname = oldPathname.slice(1);
pathname = pathname.slice(0, pathname.indexOf("/"));
if (index && index >= 0) pathname += "/" + index;
if (encoded) pathname += "/" + encodedDiagram;
if (oldPathname.slice(-1) === "/") pathname += "/";
url.pathname = new URL(resolvePath(pathname)).pathname;
if (url.searchParams.get("url")) {
url.searchParams.set("url", encodedDiagram);
}
return { url, pathname };
}
function buildUrl(serletpath, encodedDiagram, index) {
let pathname = serletpath;
if (index && index >= 0) pathname += "/" + index;
pathname += "/" + encodedDiagram;
return pathname;
}

View File

@ -0,0 +1,45 @@
'use strict';var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.makeIterator=function(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];if(b)return b.call(a);if("number"==typeof a.length)return $jscomp.arrayIterator(a);throw Error(String(a)+" is not an iterable or ArrayLike");};
$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,d){if(a==Array.prototype||a==Object.prototype)return a;a[b]=d.value;return a};
$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var d=a[b];if(d&&d.Math==Math)return d}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};
$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(a,b,d){if(!d||null!=a){d=$jscomp.propertyToPolyfillSymbol[b];if(null==d)return a[b];d=a[d];return void 0!==d?d:a[b]}};$jscomp.polyfill=function(a,b,d,c){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,d,c):$jscomp.polyfillUnisolated(a,b,d,c))};
$jscomp.polyfillUnisolated=function(a,b,d,c){d=$jscomp.global;a=a.split(".");for(c=0;c<a.length-1;c++){var e=a[c];if(!(e in d))return;d=d[e]}a=a[a.length-1];c=d[a];b=b(c);b!=c&&null!=b&&$jscomp.defineProperty(d,a,{configurable:!0,writable:!0,value:b})};
$jscomp.polyfillIsolated=function(a,b,d,c){var e=a.split(".");a=1===e.length;c=e[0];c=!a&&c in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var g=0;g<e.length-1;g++){var h=e[g];if(!(h in c))return;c=c[h]}e=e[e.length-1];d=$jscomp.IS_SYMBOL_NATIVE&&"es6"===d?c[e]:null;b=b(d);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,e,{configurable:!0,writable:!0,value:b}):b!==d&&(void 0===$jscomp.propertyToPolyfillSymbol[e]&&(d=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[e]=$jscomp.IS_SYMBOL_NATIVE?
$jscomp.global.Symbol(e):$jscomp.POLYFILL_PREFIX+d+"$"+e),$jscomp.defineProperty(c,$jscomp.propertyToPolyfillSymbol[e],{configurable:!0,writable:!0,value:b})))};$jscomp.underscoreProtoCanBeSet=function(){var a={a:!0},b={};try{return b.__proto__=a,b.a}catch(d){}return!1};
$jscomp.setPrototypeOf=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.setPrototypeOf?Object.setPrototypeOf:$jscomp.underscoreProtoCanBeSet()?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null;$jscomp.generator={};$jscomp.generator.ensureIteratorResultIsObject_=function(a){if(!(a instanceof Object))throw new TypeError("Iterator result "+a+" is not an object");};
$jscomp.generator.Context=function(){this.isRunning_=!1;this.yieldAllIterator_=null;this.yieldResult=void 0;this.nextAddress=1;this.finallyAddress_=this.catchAddress_=0;this.finallyContexts_=this.abruptCompletion_=null};$jscomp.generator.Context.prototype.start_=function(){if(this.isRunning_)throw new TypeError("Generator is already running");this.isRunning_=!0};$jscomp.generator.Context.prototype.stop_=function(){this.isRunning_=!1};
$jscomp.generator.Context.prototype.jumpToErrorHandler_=function(){this.nextAddress=this.catchAddress_||this.finallyAddress_};$jscomp.generator.Context.prototype.next_=function(a){this.yieldResult=a};$jscomp.generator.Context.prototype.throw_=function(a){this.abruptCompletion_={exception:a,isException:!0};this.jumpToErrorHandler_()};$jscomp.generator.Context.prototype.return=function(a){this.abruptCompletion_={return:a};this.nextAddress=this.finallyAddress_};
$jscomp.generator.Context.prototype.jumpThroughFinallyBlocks=function(a){this.abruptCompletion_={jumpTo:a};this.nextAddress=this.finallyAddress_};$jscomp.generator.Context.prototype.yield=function(a,b){this.nextAddress=b;return{value:a}};$jscomp.generator.Context.prototype.yieldAll=function(a,b){a=$jscomp.makeIterator(a);var d=a.next();$jscomp.generator.ensureIteratorResultIsObject_(d);if(d.done)this.yieldResult=d.value,this.nextAddress=b;else return this.yieldAllIterator_=a,this.yield(d.value,b)};
$jscomp.generator.Context.prototype.jumpTo=function(a){this.nextAddress=a};$jscomp.generator.Context.prototype.jumpToEnd=function(){this.nextAddress=0};$jscomp.generator.Context.prototype.setCatchFinallyBlocks=function(a,b){this.catchAddress_=a;void 0!=b&&(this.finallyAddress_=b)};$jscomp.generator.Context.prototype.setFinallyBlock=function(a){this.catchAddress_=0;this.finallyAddress_=a||0};$jscomp.generator.Context.prototype.leaveTryBlock=function(a,b){this.nextAddress=a;this.catchAddress_=b||0};
$jscomp.generator.Context.prototype.enterCatchBlock=function(a){this.catchAddress_=a||0;a=this.abruptCompletion_.exception;this.abruptCompletion_=null;return a};$jscomp.generator.Context.prototype.enterFinallyBlock=function(a,b,d){d?this.finallyContexts_[d]=this.abruptCompletion_:this.finallyContexts_=[this.abruptCompletion_];this.catchAddress_=a||0;this.finallyAddress_=b||0};
$jscomp.generator.Context.prototype.leaveFinallyBlock=function(a,b){b=this.finallyContexts_.splice(b||0)[0];if(b=this.abruptCompletion_=this.abruptCompletion_||b){if(b.isException)return this.jumpToErrorHandler_();void 0!=b.jumpTo&&this.finallyAddress_<b.jumpTo?(this.nextAddress=b.jumpTo,this.abruptCompletion_=null):this.nextAddress=this.finallyAddress_}else this.nextAddress=a};$jscomp.generator.Context.prototype.forIn=function(a){return new $jscomp.generator.Context.PropertyIterator(a)};
$jscomp.generator.Context.PropertyIterator=function(a){this.object_=a;this.properties_=[];for(var b in a)this.properties_.push(b);this.properties_.reverse()};$jscomp.generator.Context.PropertyIterator.prototype.getNext=function(){for(;0<this.properties_.length;){var a=this.properties_.pop();if(a in this.object_)return a}return null};$jscomp.generator.Engine_=function(a){this.context_=new $jscomp.generator.Context;this.program_=a};
$jscomp.generator.Engine_.prototype.next_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_.next,a,this.context_.next_);this.context_.next_(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.return_=function(a){this.context_.start_();var b=this.context_.yieldAllIterator_;if(b)return this.yieldAllStep_("return"in b?b["return"]:function(d){return{value:d,done:!0}},a,this.context_.return);this.context_.return(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.throw_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_["throw"],a,this.context_.next_);this.context_.throw_(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.yieldAllStep_=function(a,b,d){try{var c=a.call(this.context_.yieldAllIterator_,b);$jscomp.generator.ensureIteratorResultIsObject_(c);if(!c.done)return this.context_.stop_(),c;var e=c.value}catch(g){return this.context_.yieldAllIterator_=null,this.context_.throw_(g),this.nextStep_()}this.context_.yieldAllIterator_=null;d.call(this.context_,e);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.nextStep_=function(){for(;this.context_.nextAddress;)try{var a=this.program_(this.context_);if(a)return this.context_.stop_(),{value:a.value,done:!1}}catch(b){this.context_.yieldResult=void 0,this.context_.throw_(b)}this.context_.stop_();if(this.context_.abruptCompletion_){a=this.context_.abruptCompletion_;this.context_.abruptCompletion_=null;if(a.isException)throw a.exception;return{value:a.return,done:!0}}return{value:void 0,done:!0}};
$jscomp.generator.Generator_=function(a){this.next=function(b){return a.next_(b)};this.throw=function(b){return a.throw_(b)};this.return=function(b){return a.return_(b)};this[Symbol.iterator]=function(){return this}};$jscomp.generator.createGenerator=function(a,b){b=new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(b));$jscomp.setPrototypeOf&&a.prototype&&$jscomp.setPrototypeOf(b,a.prototype);return b};
$jscomp.asyncExecutePromiseGenerator=function(a){function b(c){return a.next(c)}function d(c){return a.throw(c)}return new Promise(function(c,e){function g(h){h.done?c(h.value):Promise.resolve(h.value).then(b,d).then(g,e)}g(a.next())})};$jscomp.asyncExecutePromiseGeneratorFunction=function(a){return $jscomp.asyncExecutePromiseGenerator(a())};$jscomp.asyncExecutePromiseGeneratorProgram=function(a){return $jscomp.asyncExecutePromiseGenerator(new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(a)))};
var PlantUmlLanguageFeatures=function(a){if(void 0===a||a)this.addStartEndValidationListeners(),this.registerThemeCompletion(),this.registerIconCompletion(),this.registerEmojiCompletion()};PlantUmlLanguageFeatures.baseUrl="";PlantUmlLanguageFeatures.setBaseUrl=function(a){null===a||void 0===a?a="":""!==a&&"/"!==a.slice(-1)&&(a+="/");PlantUmlLanguageFeatures.baseUrl=a};PlantUmlLanguageFeatures.languageSelector=["apex","plantuml"];
PlantUmlLanguageFeatures.setLanguageSelector=function(a){PlantUmlLanguageFeatures.languageSelector=a};
PlantUmlLanguageFeatures.makeRequest=function(a,b,d){d=void 0===d?{}:d;var c=void 0===d.data?null:d.data,e=void 0===d.headers?{"Content-Type":"text/plain"}:d.headers,g=void 0===d.responseType?"json":d.responseType,h=(d=void 0===d.baseUrl?PlantUmlLanguageFeatures.baseUrl:d.baseUrl)?d.replace(/\/*$/g,"/")+b:b;return new Promise(function(f,l){var k=new XMLHttpRequest;k.onreadystatechange=function(){k.readyState===XMLHttpRequest.DONE&&(200<=k.status&&300>=k.status?f("json"===g?k.response:k.responseText):
l("json"===g?{status:k.status,response:k.response}:{status:k.status,responseText:k.responseText}))};k.open(a,h,!0);k.responseType=g;e&&Object.keys(e).forEach(function(m){return k.setRequestHeader(m,e[m])});k.send(c)})};
PlantUmlLanguageFeatures.absolutePath=function(a){if(a.startsWith("http"))return a;if(a.startsWith("//"))return window.location.protocol+a;if(a.startsWith("/"))return window.location.origin+a;"./"==a.slice(0,2)&&(a=a.slice(2));var b=(document.querySelector("base")||{}).href||window.location.origin;"/"==b.slice(-1)&&(b=b.slice(0,-1));return b+"/"+a};
(function(){var a={};PlantUmlLanguageFeatures.prototype.addValidationEventListener=function(b,d){if(!["before","code","line","after"].includes(b))throw Error("Unknown validation event type: "+b);a[b]=a[b]||[];a[b].push(d)};PlantUmlLanguageFeatures.prototype.validateCode=function(b){var d,c,e,g,h,f,l;return $jscomp.asyncExecutePromiseGeneratorProgram(function(k){d=[];d.push(null==(c=a.before)?void 0:c.map(function(m){return m({model:b})}));d.push(null==(e=a.code)?void 0:e.map(function(m){return m({model:b,
code:b.getValue()})}));if(a.line&&0<a.line.length)for(g=b.getLineCount(),h={$jscomp$loop$prop$lineNumber$3:1};h.$jscomp$loop$prop$lineNumber$3<=g;h={$jscomp$loop$prop$range$1:h.$jscomp$loop$prop$range$1,$jscomp$loop$prop$line$2:h.$jscomp$loop$prop$line$2,$jscomp$loop$prop$lineNumber$3:h.$jscomp$loop$prop$lineNumber$3},h.$jscomp$loop$prop$lineNumber$3++)h.$jscomp$loop$prop$range$1={startLineNumber:h.$jscomp$loop$prop$lineNumber$3,startColumn:1,endLineNumber:h.$jscomp$loop$prop$lineNumber$3,endColumn:b.getLineLength(h.$jscomp$loop$prop$lineNumber$3)+
1},h.$jscomp$loop$prop$line$2=b.getValueInRange(h.$jscomp$loop$prop$range$1),f=void 0,d.push(null==(f=a.line)?void 0:f.map(function(m){return function(n){return n({model:b,range:m.$jscomp$loop$prop$range$1,line:m.$jscomp$loop$prop$line$2,lineNumber:m.$jscomp$loop$prop$lineNumber$3,lineCount:g})}}(h)));d.push(null==(l=a.after)?void 0:l.map(function(m){return m({model:b})}));return k.return(Promise.all(d).then(function(m){return m.flat(1).filter(function(n){return n})}))})}})();
PlantUmlLanguageFeatures.prototype.getEmojis=function(){var a=void 0;return function(){return $jscomp.asyncExecutePromiseGeneratorProgram(function(b){if(1==b.nextAddress)return void 0!==a?b.jumpTo(2):b.yield(PlantUmlLanguageFeatures.makeRequest("GET","ui-helper?request\x3demojis"),3);2!=b.nextAddress&&(a=b.yieldResult);return b.return(a)})}}();
PlantUmlLanguageFeatures.prototype.registerEmojiCompletion=function(){var a=this,b=function(d,c){var e,g;return $jscomp.asyncExecutePromiseGeneratorProgram(function(h){if(1==h.nextAddress)return h.yield(a.getEmojis(),2);e=h.yieldResult;return h.return((null==(g=e)?void 0:g.filter(function(f){var l=$jscomp.makeIterator(f);f=l.next().value;l=l.next().value;return c?f.includes(c)||(null==l?void 0:l.includes(c)):!0}).map(function(f){var l=$jscomp.makeIterator(f);f=l.next().value;var k=l.next().value;
l="https://raw.githubusercontent.com/twitter/twemoji/gh-pages/v/13.1.0/svg/"+f+".svg";var m=k?k+" ("+f+")":f;f=!k||c&&f.includes(c)?f:k;return{label:f,kind:monaco.languages.CompletionItemKind.Constant,documentation:{value:"![emoji]("+l+") \x26nbsp; "+m},insertText:f+":\x3e",range:d}}))||[])})};monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector,{triggerCharacters:[":"],provideCompletionItems:function(d,c){var e,g,h;return $jscomp.asyncExecutePromiseGeneratorProgram(function(f){return 1==
f.nextAddress?(e=d.getValueInRange({startLineNumber:c.lineNumber,startColumn:1,endLineNumber:c.lineNumber,endColumn:c.column}),(g=e.match(/<:([^\s>]*)$/))?f.yield(b(a.getWordRange(d,c),g[1]),3):f.jumpTo(2)):2!=f.nextAddress?(h=f.yieldResult,f.return({suggestions:h})):f.return({suggestions:[]})})}})};
PlantUmlLanguageFeatures.prototype.getIcons=function(){var a=void 0;return function(){return $jscomp.asyncExecutePromiseGeneratorProgram(function(b){if(1==b.nextAddress)return void 0!==a?b.jumpTo(2):b.yield(PlantUmlLanguageFeatures.makeRequest("GET","ui-helper?request\x3dicons"),3);2!=b.nextAddress&&(a=b.yieldResult);return b.return(a)})}}();
PlantUmlLanguageFeatures.prototype.registerIconCompletion=function(){var a=this,b=function(d,c){var e,g;return $jscomp.asyncExecutePromiseGeneratorProgram(function(h){if(1==h.nextAddress)return h.yield(a.getIcons(),2);e=h.yieldResult;return h.return((null==(g=e)?void 0:g.filter(function(f){return c?f.includes(c):!0}).map(function(f){var l=PlantUmlLanguageFeatures.absolutePath(PlantUmlLanguageFeatures.baseUrl+"ui-helper?request\x3dicons.svg#"+f);return{label:f,kind:monaco.languages.CompletionItemKind.Constant,
documentation:{value:"![icon]("+l+") \x26nbsp; "+f},insertText:f+"\x3e",range:d}}))||[])})};monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector,{triggerCharacters:["\x26"],provideCompletionItems:function(d,c){var e,g,h;return $jscomp.asyncExecutePromiseGeneratorProgram(function(f){return 1==f.nextAddress?(e=d.getValueInRange({startLineNumber:c.lineNumber,startColumn:1,endLineNumber:c.lineNumber,endColumn:c.column}),(g=e.match(/<&([^\s>]*)$/))?f.yield(b(a.getWordRange(d,
c),g[1]),3):f.jumpTo(2)):2!=f.nextAddress?(h=f.yieldResult,f.return({suggestions:h})):f.return({suggestions:[]})})}})};PlantUmlLanguageFeatures.prototype.getThemes=function(){var a=void 0;return function(){return $jscomp.asyncExecutePromiseGeneratorProgram(function(b){if(1==b.nextAddress)return void 0!==a?b.jumpTo(2):b.yield(PlantUmlLanguageFeatures.makeRequest("GET","ui-helper?request\x3dthemes"),3);2!=b.nextAddress&&(a=b.yieldResult);return b.return(a)})}}();
PlantUmlLanguageFeatures.prototype.registerThemeCompletion=function(){var a=this,b=function(d,c){var e,g;return $jscomp.asyncExecutePromiseGeneratorProgram(function(h){if(1==h.nextAddress)return h.yield(a.getThemes(),2);e=h.yieldResult;return h.return((null==(g=e)?void 0:g.filter(function(f){return c?f.includes(c):!0}).map(function(f){return{label:f,kind:monaco.languages.CompletionItemKind.Text,documentation:"PlantUML "+f+" theme",insertText:f,range:d}}))||[])})};monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector,
{triggerCharacters:[" "],provideCompletionItems:function(d,c){var e,g,h;return $jscomp.asyncExecutePromiseGeneratorProgram(function(f){return 1==f.nextAddress?(e=d.getValueInRange({startLineNumber:c.lineNumber,startColumn:1,endLineNumber:c.lineNumber,endColumn:c.column}),e.match(/^\s*!(t(h(e(m(e)?)?)?)?)?$/)?f.return({suggestions:[{label:"theme",kind:monaco.languages.CompletionItemKind.Keyword,documentation:"PlantUML theme command",insertText:"theme",range:a.getWordRange(d,c)}]}):(g=e.match(/^\s*!theme\s+([^\s]*)$/))?
f.yield(b(a.getWordRange(d,c),g[1]),3):f.jumpTo(2)):2!=f.nextAddress?(h=f.yieldResult,f.return({suggestions:h})):f.return({suggestions:[]})})}})};PlantUmlLanguageFeatures.prototype.getWordRange=function(a,b){a=a.getWordUntilPosition(b);return{startLineNumber:b.lineNumber,endLineNumber:b.lineNumber,startColumn:a.startColumn,endColumn:a.endColumn}};
PlantUmlLanguageFeatures.prototype.addStartEndValidationListeners=function(){var a=void 0,b=0,d=0;this.addValidationEventListener("before",function(){a=void 0;d=b=0});this.addValidationEventListener("code",function(c){var e=c.model;if(c=c.code.match(/^(?:(?:'.*)|\s)*@start(\w+)/))a=c[1];else return{message:"PlantUML diagrams should begin with the `@start` command and `@start` should also be the first command.",severity:monaco.MarkerSeverity.Warning,startLineNumber:1,startColumn:1,endLineNumber:1,
endColumn:e.getLineLength(1)+1}});this.addValidationEventListener("code",function(c){var e=c.model,g=c.code;c=e.getLineCount();return(g=g.match(/\s+@end(\w+)(?:(?:'.*)|\s)*$/))?a===g[1]?void 0:{message:"PlantUML diagrams should start and end with the type.\nExample: `@startjson ... @endjson`",severity:monaco.MarkerSeverity.Error,startLineNumber:c,startColumn:1,endLineNumber:c,endColumn:e.getLineLength(c)+1}:{message:"PlantUML diagrams should end with the `@end` command and `@end` should also be the last command.",
severity:monaco.MarkerSeverity.Warning,startLineNumber:c,startColumn:1,endLineNumber:c,endColumn:e.getLineLength(c)+1}});this.addValidationEventListener("line",function(c){var e=c.range;c=c.line;var g=c.match(/^\s*@start(\w+)(?:\s+.*)?$/);if(g&&(b+=1,1<b))return g="@start"+g[1],c=c.indexOf(g),{message:"Multiple @start commands detected.",severity:monaco.MarkerSeverity.Warning,startLineNumber:e.startLineNumber,startColumn:c+1,endLineNumber:e.endLineNumber,endColumn:c+g.length+1}});this.addValidationEventListener("line",
function(c){var e=c.range;c=c.line;var g=c.match(/^\s*@end(\w+)(?:\s+.*)?$/);if(g&&(d+=1,1<d))return g="@end"+g[1],c=c.indexOf(g),{message:"Multiple @end commands detected.",severity:monaco.MarkerSeverity.Warning,startLineNumber:e.startLineNumber,startColumn:c+1,endLineNumber:e.endLineNumber,endColumn:c+g.length+1}})};

12
src/main/webapp/min/plantuml.min.css vendored Normal file
View File

@ -0,0 +1,12 @@
:root{color-scheme:light dark;--font-color:black;--font-color-disabled:#888;--bg-color:white;--border-color:#ccc;--border-color-2:#aaa;--footer-font-color:#666;--footer-bg-color:#eee;--modal-bg-color:#fefefe;--file-drop-color:#eee}[data-theme="dark"]{--font-color:#ccc;--font-color-disabled:#777;--bg-color:#212121;--border-color:#848484;--border-color-2:#aaa;--footer-font-color:#ccc;--footer-bg-color:black;--modal-bg-color:#424242;--file-drop-color:#212121}html,body{margin:0;padding:0}
html{font-family:arial,helvetica,sans-serif}body{background-color:var(--bg-color);color:var(--font-color);overflow:auto}@media screen and (min-width:900px){body{height:100vh;overflow:hidden}.app{height:100%}}input:not([type="image"]){background-color:var(--bg-color);color:var(--font-color)}input[type="file"]::file-selector-button{background-color:var(--bg-color);color:var(--font-color)}select{background-color:var(--bg-color);color:var(--font-color)}
.hr{padding:1rem 0;width:100%}.flex-columns>.hr{padding:0 1rem;width:initial;height:100%}.hr:after{content:"";display:block;background-color:var(--border-color);height:100%;width:100%;min-height:3px;min-width:3px}.wait{cursor:wait}.wait>*{pointer-events:none}.flex-columns{display:flex;flex-direction:row;flex-wrap:wrap}.flex-rows{display:flex;flex-direction:column}.flex-main{flex:1 1 1px;overflow:auto}.flex-columns>*,.flex-rows>*{flex-shrink:0}.header{margin-left:auto;margin-right:auto;text-align:center}
.main{margin:1% 5%;z-index:1}.main>div{margin:0 1.75%}.main>div:first-child{margin-left:0}.main>div:last-child{margin-right:0}@media screen and (max-width:900px){.main{display:block;overflow:inherit}.main>div{margin:1.75% 0}.main>div:first-child{margin-top:0}.main>div:last-child{margin-bottom:0}}.footer p{background-color:var(--footer-bg-color);color:var(--footer-font-color);font-size:.7em;margin:0;padding:.5em;text-align:center}[data-theme="dark"] img:not(#diagram-png):not(.no-filter){filter:invert() contrast(30%)}
[data-theme="dark"] input[type="image"]{filter:invert() contrast(30%)}[data-theme="dark"] a{color:white}.editor{border:3px solid var(--border-color);box-sizing:border-box;overflow:hidden}@media screen and (max-width:900px){.editor{height:20em}}.editor .monaco-editor-container{overflow:hidden;position:relative}#monaco-editor{height:100%}#monaco-editor .overlayWidgets .suggest-details p img[alt="icon"],#monaco-editor .overlayWidgets .suggest-details p img[alt="emoji"]{height:1.2rem}.monaco-editor-container .editor-menu{position:absolute;right:0;top:0;display:flex;flex-direction:column;justify-content:center;align-items:center;flex:1}.monaco-editor-container .editor-menu>div.menu-kebab{width:60px;height:60px;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;cursor:pointer;scale:.5}.monaco-editor-container .editor-menu:hover>div.menu-kebab,.monaco-editor-container .editor-menu:focus>div.menu-kebab{outline:0;scale:.65}.monaco-editor-container .menu-kebab .kebab-circle{width:12px;height:12px;margin:3px;background:var(--font-color);border-radius:50%;display:block;opacity:.8}
.monaco-editor-container .menu-kebab{flex-direction:column;position:relative;transition:all 300ms cubic-bezier(0.175,0.885,0.32,1.275)}.monaco-editor-container .menu-kebab .kebab-circle:nth-child(4),.monaco-editor-container .menu-kebab .kebab-circle:nth-child(5){position:absolute;opacity:0;top:50%;margin-top:-6px;left:50%}.monaco-editor-container .menu-kebab .kebab-circle:nth-child(4){margin-left:-25px}.monaco-editor-container .menu-kebab .kebab-circle:nth-child(5){margin-left:13px}.monaco-editor-container .editor-menu:hover .menu-kebab,.monaco-editor-container .editor-menu:focus .menu-kebab{transform:rotate(45deg)}
.monaco-editor-container .editor-menu:hover .menu-kebab .kebab-circle,.monaco-editor-container .editor-menu:focus .menu-kebab .kebab-circle{opacity:1}.monaco-editor-container .editor-menu .menu-item{display:none;margin:1rem 0;height:1.75rem;opacity:.5;position:relative;-webkit-animation-name:editor-menu-animateitem;-webkit-animation-duration:.4s;animation-name:editor-menu-animateitem;animation-duration:.4s}@-webkit-keyframes editor-menu-animateitem{from{top:-50%;opacity:0}to{top:0;opacity:.5}}@keyframes editor-menu-animateitem{from{top:-50%;opacity:0}
to{top:0;opacity:.5}}.monaco-editor-container .editor-menu .menu-item:hover{opacity:1}.monaco-editor-container .editor-menu:hover .menu-item,.monaco-editor-container .editor-menu:focus .menu-item{display:block}.editor .btn-input{align-items:center;border-bottom:3px solid var(--border-color);box-sizing:border-box;display:flex;justify-content:center}.editor .btn-input input[type=text]{border:0;flex:1 1 1px;font-family:monospace;font-size:medium;padding:.2em;text-overflow:ellipsis}.editor .btn-input input[type=text]:focus{border:0;box-shadow:none;outline:0}.editor .btn-input input[type="image"]{height:1rem;margin-left:.7em;padding:0 .3em}#diagram-export.modal .label-input-pair label{min-width:8rem}#diagram-import p.error-message{color:darkred;padding-left:1rem;padding-right:1rem}#diagram-import input[type="file"]{display:block;width:100%;border:.2rem dashed var(--border-color);border-radius:.4rem;box-sizing:border-box;padding:5rem 2rem}#diagram-import input[type="file"],#diagram-import input[type="file"]::file-selector-button{background-color:var(--modal-bg-color)}#diagram-import input[type="file"]:hover,#diagram-import input[type="file"].drop-able{border-color:var(--border-color-2);background-color:var(--file-drop-color)}
#diagram-import input[type="file"]:hover::file-selector-button,#diagram-import input[type="file"].drop-able::file-selector-button{background-color:var(--file-drop-color)}.modal{display:block;position:fixed;z-index:1;padding:5%;left:0;top:0;bottom:0;right:0;overflow:auto;background-color:#000;background-color:rgba(0,0,0,0.4)}.modal .modal-content{background-color:var(--modal-bg-color);margin:auto;padding:2rem;border:3px solid var(--border-color);max-width:30rem;box-shadow:0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);-webkit-animation-name:modal-animatetop;-webkit-animation-duration:.4s;animation-name:modal-animatetop;animation-duration:.4s;position:relative;top:50%;transform:translateY(-50%)}
@-webkit-keyframes modal-animatetop{from{top:-50%;opacity:0}to{top:50%;opacity:1}}@keyframes modal-animatetop{from{top:-50%;opacity:0}to{top:50%;opacity:1}}.modal .modal-header h2{margin:0}.modal .modal-main{flex:1}.modal .modal-footer{margin-top:1rem;text-align:right}.modal input,.modal select{border:1px solid var(--border-color)}.modal input:not(:focus):invalid{border-bottom-color:red}.modal input[type="file"]::file-selector-button{border:1px solid var(--border-color)}
.modal input.ok,.modal input.cancel{min-width:5rem}.modal input.ok[disabled],.modal input.cancel[disabled]{color:var(--font-color-disabled)}.modal input.ok:not([disabled]):hover{border-bottom-color:green}.modal input.cancel:not([disabled]):hover{border-bottom-color:darkred}.modal .label-input-pair{margin:1rem 0;overflow:hidden}.modal .label-input-pair:first-child{margin-top:0}.modal .label-input-pair:last-child{margin-bottom:0}.modal .label-input-pair label{display:inline-block;min-width:15rem}.modal .label-input-pair label+input,.modal .label-input-pair label+select{box-sizing:border-box;display:inline-block;min-width:10rem}#settings #settings-monaco-editor{height:17rem;border:1px solid var(--border-color)}.diagram{height:100%;overflow:auto}.diagram[data-diagram-type="pdf"]{overflow:hidden}.diagram>div{margin:1rem 0;text-align:center}.diagram[data-diagram-type="pdf"]>div{height:20em;width:100%}.diagram img,.diagram svg,.diagram pre{border:3px solid var(--border-color);box-sizing:border-box;padding:10px}@media screen and (min-width:900px){.diagram{position:relative}.diagram>div{margin:0}.diagram:not([data-diagram-type="pdf"])>div{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);max-height:100%;max-width:100%}
.diagram[data-diagram-type="pdf"]>div{height:100%}}.preview-menu{margin-left:5%;margin-right:5%}.diagram-link img,.btn-dock{width:2.5rem}.btn-settings{width:2.2rem;margin-left:auto;margin-right:.25rem}.menu-r{min-width:3rem}.menu-r .btn-float-r{float:right;margin-left:.25rem;text-align:right}.diagram-links{align-items:center;display:flex}.diagram-link{margin-left:.25rem;margin-right:.25rem}.diagram-links .diagram-link:first-of-type{margin-left:.5rem}.diagram-links .diagram-link:last-of-type{margin-right:0}#paginator{text-align:center;margin-bottom:1rem}.previewer-container{height:100%}@media screen and (max-width:900px){.previewer-container{height:initial}.previewer-main{flex:none}}

77
src/main/webapp/min/plantuml.min.js vendored Normal file
View File

@ -0,0 +1,77 @@
'use strict';var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.makeIterator=function(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];if(b)return b.call(a);if("number"==typeof a.length)return $jscomp.arrayIterator(a);throw Error(String(a)+" is not an iterable or ArrayLike");};
$jscomp.arrayFromIterator=function(a){for(var b,c=[];!(b=a.next()).done;)c.push(b.value);return c};$jscomp.arrayFromIterable=function(a){return a instanceof Array?a:$jscomp.arrayFromIterator($jscomp.makeIterator(a))};$jscomp.owns=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;
$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};
$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};
$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(a,b,c){if(!c||null!=a){c=$jscomp.propertyToPolyfillSymbol[b];if(null==c)return a[b];c=a[c];return void 0!==c?c:a[b]}};$jscomp.polyfill=function(a,b,c,d){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,c,d):$jscomp.polyfillUnisolated(a,b,c,d))};
$jscomp.polyfillUnisolated=function(a,b,c,d){c=$jscomp.global;a=a.split(".");for(d=0;d<a.length-1;d++){var e=a[d];if(!(e in c))return;c=c[e]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})};
$jscomp.polyfillIsolated=function(a,b,c,d){var e=a.split(".");a=1===e.length;d=e[0];d=!a&&d in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var f=0;f<e.length-1;f++){var g=e[f];if(!(g in d))return;d=d[g]}e=e[e.length-1];c=$jscomp.IS_SYMBOL_NATIVE&&"es6"===c?d[e]:null;b=b(c);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,e,{configurable:!0,writable:!0,value:b}):b!==c&&(void 0===$jscomp.propertyToPolyfillSymbol[e]&&(c=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[e]=$jscomp.IS_SYMBOL_NATIVE?
$jscomp.global.Symbol(e):$jscomp.POLYFILL_PREFIX+c+"$"+e),$jscomp.defineProperty(d,$jscomp.propertyToPolyfillSymbol[e],{configurable:!0,writable:!0,value:b})))};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(a,b){for(var c=1;c<arguments.length;c++){var d=arguments[c];if(d)for(var e in d)$jscomp.owns(d,e)&&(a[e]=d[e])}return a};$jscomp.polyfill("Object.assign",function(a){return a||$jscomp.assign},"es6","es3");
$jscomp.underscoreProtoCanBeSet=function(){var a={a:!0},b={};try{return b.__proto__=a,b.a}catch(c){}return!1};$jscomp.setPrototypeOf=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.setPrototypeOf?Object.setPrototypeOf:$jscomp.underscoreProtoCanBeSet()?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null;$jscomp.generator={};
$jscomp.generator.ensureIteratorResultIsObject_=function(a){if(!(a instanceof Object))throw new TypeError("Iterator result "+a+" is not an object");};$jscomp.generator.Context=function(){this.isRunning_=!1;this.yieldAllIterator_=null;this.yieldResult=void 0;this.nextAddress=1;this.finallyAddress_=this.catchAddress_=0;this.finallyContexts_=this.abruptCompletion_=null};
$jscomp.generator.Context.prototype.start_=function(){if(this.isRunning_)throw new TypeError("Generator is already running");this.isRunning_=!0};$jscomp.generator.Context.prototype.stop_=function(){this.isRunning_=!1};$jscomp.generator.Context.prototype.jumpToErrorHandler_=function(){this.nextAddress=this.catchAddress_||this.finallyAddress_};$jscomp.generator.Context.prototype.next_=function(a){this.yieldResult=a};
$jscomp.generator.Context.prototype.throw_=function(a){this.abruptCompletion_={exception:a,isException:!0};this.jumpToErrorHandler_()};$jscomp.generator.Context.prototype.return=function(a){this.abruptCompletion_={return:a};this.nextAddress=this.finallyAddress_};$jscomp.generator.Context.prototype.jumpThroughFinallyBlocks=function(a){this.abruptCompletion_={jumpTo:a};this.nextAddress=this.finallyAddress_};$jscomp.generator.Context.prototype.yield=function(a,b){this.nextAddress=b;return{value:a}};
$jscomp.generator.Context.prototype.yieldAll=function(a,b){a=$jscomp.makeIterator(a);var c=a.next();$jscomp.generator.ensureIteratorResultIsObject_(c);if(c.done)this.yieldResult=c.value,this.nextAddress=b;else return this.yieldAllIterator_=a,this.yield(c.value,b)};$jscomp.generator.Context.prototype.jumpTo=function(a){this.nextAddress=a};$jscomp.generator.Context.prototype.jumpToEnd=function(){this.nextAddress=0};
$jscomp.generator.Context.prototype.setCatchFinallyBlocks=function(a,b){this.catchAddress_=a;void 0!=b&&(this.finallyAddress_=b)};$jscomp.generator.Context.prototype.setFinallyBlock=function(a){this.catchAddress_=0;this.finallyAddress_=a||0};$jscomp.generator.Context.prototype.leaveTryBlock=function(a,b){this.nextAddress=a;this.catchAddress_=b||0};
$jscomp.generator.Context.prototype.enterCatchBlock=function(a){this.catchAddress_=a||0;a=this.abruptCompletion_.exception;this.abruptCompletion_=null;return a};$jscomp.generator.Context.prototype.enterFinallyBlock=function(a,b,c){c?this.finallyContexts_[c]=this.abruptCompletion_:this.finallyContexts_=[this.abruptCompletion_];this.catchAddress_=a||0;this.finallyAddress_=b||0};
$jscomp.generator.Context.prototype.leaveFinallyBlock=function(a,b){b=this.finallyContexts_.splice(b||0)[0];if(b=this.abruptCompletion_=this.abruptCompletion_||b){if(b.isException)return this.jumpToErrorHandler_();void 0!=b.jumpTo&&this.finallyAddress_<b.jumpTo?(this.nextAddress=b.jumpTo,this.abruptCompletion_=null):this.nextAddress=this.finallyAddress_}else this.nextAddress=a};$jscomp.generator.Context.prototype.forIn=function(a){return new $jscomp.generator.Context.PropertyIterator(a)};
$jscomp.generator.Context.PropertyIterator=function(a){this.object_=a;this.properties_=[];for(var b in a)this.properties_.push(b);this.properties_.reverse()};$jscomp.generator.Context.PropertyIterator.prototype.getNext=function(){for(;0<this.properties_.length;){var a=this.properties_.pop();if(a in this.object_)return a}return null};$jscomp.generator.Engine_=function(a){this.context_=new $jscomp.generator.Context;this.program_=a};
$jscomp.generator.Engine_.prototype.next_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_.next,a,this.context_.next_);this.context_.next_(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.return_=function(a){this.context_.start_();var b=this.context_.yieldAllIterator_;if(b)return this.yieldAllStep_("return"in b?b["return"]:function(c){return{value:c,done:!0}},a,this.context_.return);this.context_.return(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.throw_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_["throw"],a,this.context_.next_);this.context_.throw_(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.yieldAllStep_=function(a,b,c){try{var d=a.call(this.context_.yieldAllIterator_,b);$jscomp.generator.ensureIteratorResultIsObject_(d);if(!d.done)return this.context_.stop_(),d;var e=d.value}catch(f){return this.context_.yieldAllIterator_=null,this.context_.throw_(f),this.nextStep_()}this.context_.yieldAllIterator_=null;c.call(this.context_,e);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.nextStep_=function(){for(;this.context_.nextAddress;)try{var a=this.program_(this.context_);if(a)return this.context_.stop_(),{value:a.value,done:!1}}catch(b){this.context_.yieldResult=void 0,this.context_.throw_(b)}this.context_.stop_();if(this.context_.abruptCompletion_){a=this.context_.abruptCompletion_;this.context_.abruptCompletion_=null;if(a.isException)throw a.exception;return{value:a.return,done:!0}}return{value:void 0,done:!0}};
$jscomp.generator.Generator_=function(a){this.next=function(b){return a.next_(b)};this.throw=function(b){return a.throw_(b)};this.return=function(b){return a.return_(b)};this[Symbol.iterator]=function(){return this}};$jscomp.generator.createGenerator=function(a,b){b=new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(b));$jscomp.setPrototypeOf&&a.prototype&&$jscomp.setPrototypeOf(b,a.prototype);return b};
$jscomp.asyncExecutePromiseGenerator=function(a){function b(d){return a.next(d)}function c(d){return a.throw(d)}return new Promise(function(d,e){function f(g){g.done?d(g.value):Promise.resolve(g.value).then(b,c).then(f,e)}f(a.next())})};$jscomp.asyncExecutePromiseGeneratorFunction=function(a){return $jscomp.asyncExecutePromiseGenerator(a())};$jscomp.asyncExecutePromiseGeneratorProgram=function(a){return $jscomp.asyncExecutePromiseGenerator(new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(a)))};
$jscomp.getRestArguments=function(){for(var a=Number(this),b=[],c=a;c<arguments.length;c++)b[c-a]=arguments[c];return b};
function initApp(){var a,b;return $jscomp.asyncExecutePromiseGeneratorProgram(function(c){if(1==c.nextAddress)return b=null==(a=(new URL(window.location.href)).searchParams.get("view"))?void 0:a.toLowerCase(),c.yield(initEditor(b),2);if(3!=c.nextAddress){var d=analyseUrl(window.location.href),e,f=null==(e=document.editor)?void 0:e.getValue(),g;document.appData=Object.assign({},null==(g=window.opener)?void 0:g.document.appData);0===Object.keys(document.appData).length&&(document.appData={encodedDiagram:d.encodedDiagram,
index:d.index,numberOfDiagramPages:f?getNumberOfDiagramPagesFromCode(f):1});initTheme();initAppCommunication();return c.yield(initPreview(b),3)}initModals(b);document.editor&&(document.editor.focus(),"SyfFKj2rKt3CoKnELR1Io4ZDoSa70000"==document.appData.encodedDiagram&&document.editor.setSelection({startLineNumber:2,endLineNumber:2,startColumn:1,endColumn:21}));document.appConfig.autoRefreshState="complete";c.jumpToEnd()})}window.onload=initApp;
var $jscomp$destructuring$var0=function(){return{setEditorValue:function(a,b,c){c=void 0===c?{}:c;var d=void 0===c.forceMoveMarkers?void 0:c.forceMoveMarkers;(void 0===c.suppressEditorChangedMessage?0:c.suppressEditorChangedMessage)&&a===document.editor&&suppressNextMessage("editor");a.executeEdits("",[{range:a.getModel().getFullModelRange(),text:b,forceMoveMarkers:d}])},initEditor:function(a){function b(){return new Promise(function(g,m){require.config({paths:{vs:"webjars/monaco-editor/0.36.1/min/vs"}});
require(["vs/editor/editor.main"],g)})}function c(){function g(t,p,k){function n(){document.appConfig.autoRefreshState="started";var r=getNumberOfDiagramPagesFromCode(t),u=document.appData.index;void 0===u||1===r?u=void 0:u>=r&&(u=r-1);makeRequest("POST","coder",{data:t}).then(function(v){sendMessage({sender:p,data:{encodedDiagram:v,numberOfDiagramPages:r,index:u},synchronize:!0})})}k=void 0===k?!0:k;var q=function(){return function(){var r=document.editor.getModel();m=m||new PlantUmlLanguageFeatures;
m.validateCode(r).then(function(u){return monaco.editor.setModelMarkers(r,"plantuml",u)})}}();p&&k&&n();q()}var m,l=monaco.editor.createModel(function(){var t=document.getElementById("initCode"),p=t.value;t.remove();return p}(),"apex",monaco.Uri.parse("inmemory://plantuml")),h=0;l.onDidChangeContent(function(){clearTimeout(h);document.appConfig.autoRefreshState="waiting";h=setTimeout(function(){return g(l.getValue(),"editor")},document.appConfig.editorWatcherTimeout)});return l}function d(){return{get:function(){},
getBoolean:function(g){return"expandSuggestionDocs"===g},getNumber:function(){return 0},remove:function(){},store:function(){},onWillSaveState:function(){},onDidChangeStorage:function(){},onDidChangeValue:function(){}}}var e,f;return $jscomp.asyncExecutePromiseGeneratorProgram(function(g){if(1==g.nextAddress)return g.yield(b(),2);"previewer"!==a&&(e=c(),f=d(),document.editor=monaco.editor.create(document.getElementById("monaco-editor"),Object.assign({},{model:e},document.appConfig.editorCreateOptions),
{storageService:f}),document.addEventListener("resize",function(){return document.editor.layout()}),initEditorUrlInput(),initEditorMenu());g.jumpToEnd()})}}}(),setEditorValue=$jscomp$destructuring$var0.setEditorValue,initEditor=$jscomp$destructuring$var0.initEditor;
function initEditorMenu(){document.getElementById("menu-item-editor-code-copy").addEventListener("click",function(){var a=document.editor.getModel().getFullModelRange();document.editor.focus();document.editor.setSelection(a);a=document.editor.getValue();var b;null==(b=navigator.clipboard)||b.writeText(a).catch(function(){})})}
var $jscomp$destructuring$var3=function(){function a(b,c,d){var e=void 0===c?{}:c;c=void 0===e.encodedDiagram?void 0:e.encodedDiagram;e=void 0===e.index?void 0:e.index;d=void 0===d?{}:d;if(b||c)(void 0===d.suppressEditorChangedMessage?0:d.suppressEditorChangedMessage)&&suppressNextMessage("url"),document.getElementById("url").value=b?b:resolvePath(buildUrl("png",c,e))}return{setUrlValue:a,initEditorUrlInput:function(){var b=document.getElementById("url");a(resolvePath(b.value));b.addEventListener("change",
function(c){var d,e;return $jscomp.asyncExecutePromiseGeneratorProgram(function(f){if(1==f.nextAddress)return document.appConfig.autoRefreshState="started",c.target.title=c.target.value,d=analyseUrl(c.target.value),f.yield(makeRequest("GET","coder/"+d.encodedDiagram),2);e=f.yieldResult;setEditorValue(document.editor,e,{suppressEditorChangedMessage:!0});sendMessage({sender:"url",data:{encodedDiagram:d.encodedDiagram,index:d.index},synchronize:!0});f.jumpToEnd()})});document.getElementById("url-copy-btn").addEventListener("click",
function(){b.focus();b.select();var c;null==(c=navigator.clipboard)||c.writeText(b.value).catch(function(){})})}}}(),setUrlValue=$jscomp$destructuring$var3.setUrlValue,initEditorUrlInput=$jscomp$destructuring$var3.initEditorUrlInput;
function initDiagramExport(){function a(){setVisibility(document.getElementById("diagram-export"),!0,!0);var f=document.editor.getValue();f=Array.from(f.matchAll(/^\s*@start[a-zA-Z]*\s+([a-zA-Z-_\u00e4\u00f6\u00fc\u00c4\u00d6\u00dc\u00df ]+)\s*$/gm),function(g){return g[1]})[0]||"diagram";d.value=f+".puml";e.value="code";d.focus()}function b(f){var g=f.lastIndexOf(".");return 1>g?{name:f,ext:null}:g===f.length-1?{name:f.slice(0,-1),ext:null}:{name:f.substring(0,g),ext:f.substring(g+1)}}function c(f){if(!f)return f;
f=f.toLowerCase();switch(f){case "puml":case "plantuml":case "code":return"code";case "ascii":return"txt";default:return f}}var d=document.getElementById("download-name"),e=document.getElementById("download-type");registerModalListener("diagram-export",a);d.addEventListener("change",function(f){f=b(f.target.value).ext;if(f=c(f))e.value=f});e.addEventListener("change",function(f){f=f.target.value;a:switch(f){case "epstext":f="eps";break a;case "code":f="puml"}var g=b(d.value).name;d.value=g+"."+f});
document.getElementById("diagram-export-ok-btn").addEventListener("click",function(){var f=d.value,g=e.value,m=document.createElement("a");m.download=f;"code"===g?(f=document.editor.getValue(),m.href="data:,"+encodeURIComponent(f)):m.href=void 0!==document.appData.index?g+"/"+document.appData.index+"/"+document.appData.encodedDiagram:g+"/"+document.appData.encodedDiagram;m.click()});window.addEventListener("keydown",function(f){"s"===f.key&&(isMac?f.metaKey:f.ctrlKey)&&(f.preventDefault(),isModalOpen("diagram-export")||
a())},!1)}
function initDiagramImport(){function a(h){h=void 0===h?!0:h;setVisibility(f,!0,!0);f.dataset.isOpenManually=h.toString();g.value="";c(g)}function b(){g.value="";c(g);f.removeAttribute("data-is-open-manually");setVisibility(f,!1)}function c(h){l.innerText="";var t;m.disabled=1>(null==(t=h.files)?void 0:t.length)}function d(h){function t(k){var n=k.name,q=k.type;k=["plain","text","plantuml","puml"];if(0<k.filter(function(u){return-1!==q.toLowerCase().indexOf(u)}).length)return!0;if(-1===n.indexOf("."))return!1;
var r=n.substring(n.lastIndexOf(".")+1).toLowerCase();k=["txt","puml","plantuml"];return 0<k.filter(function(u){return r===u}).length}var p=function(k){var n=k.name,q=k.type;k=["png","svg"];var r=k.filter(function(v){return-1!==q.toLowerCase().indexOf(v)})[0];if(r)return r;if(-1!==n.indexOf(".")){var u=n.substring(n.lastIndexOf(".")+1).toLowerCase();return k.filter(function(v){return u===v})[0]}}(h);h=void 0===p?t(h):!1;p||h||(l.innerText="File not supported. Only PNG and SVG diagram images as well as PlantUML code text files are supported.");
return{type:p,isDiagramCode:h,valid:p||h}}function e(h,t){function p(k){var n=new FormData;n.append("diagram",k,k.name);return makeRequest("POST","metadata",{data:n,responseType:"json",headers:{Accept:"application/json"}})}f.classList.add("wait");return(new Promise(function(k,n){if(t.type)p(h).then(function(r){setEditorValue(document.editor,r.decoded);k()},function(r){r=r.response;l.innerText=r.message||r;n()});else if(t.isDiagramCode){var q=new FileReader;q.onload=function(r){setEditorValue(document.editor,
r.target.result)};q.readAsText(h);k()}else l.innerText="File not supported. Only PNG and SVG diagram images as well as PlantUML code text files are supported.",n()})).then(function(){return b()},function(){}).finally(function(){return f.classList.remove("wait")})}var f=document.getElementById("diagram-import"),g=document.getElementById("diagram-import-input"),m=document.getElementById("diagram-import-ok-btn"),l=document.getElementById("diagram-import-error-message");window.addEventListener("dragenter",
function(h){h.stopPropagation();h.preventDefault();isVisible(f)||a(!1)},!1);g.addEventListener("dragenter",function(h){return h.target.classList.add("drop-able")},!1);g.addEventListener("dragover",function(h){h.stopPropagation();h.preventDefault();null!==h.dataTransfer&&(h.dataTransfer.dropEffect="copy")},!1);g.addEventListener("dragexit",function(h){return h.target.classList.remove("drop-able")},!1);g.addEventListener("drop",function(h){function t(){h.stopPropagation();h.preventDefault()}var p=h.dataTransfer.files||
h.target.files;if(!p||1>p.length)return t();p=p[0];var k=d(p);if(!k.valid)return t();"true"!==f.dataset.isOpenManually&&(t(),e(p,k))},!1);m.addEventListener("click",function(){var h=g.files[0];e(h,d(h))});registerModalListener("diagram-import",a,b)}
var $jscomp$destructuring$var16=function(){var a={};return{registerModalListener:function(b,c,d){a[b]={fnOpen:c,fnClose:d}},openModal:function(b){var c=$jscomp.getRestArguments.apply(1,arguments),d,e=null==(d=a[b])?void 0:d.fnOpen;e?e.apply(null,$jscomp.arrayFromIterable(c)):setVisibility(document.getElementById(b),!0,!0)},closeModal:function(b){var c=$jscomp.getRestArguments.apply(1,arguments),d,e=null==(d=a[b])?void 0:d.fnClose;e?e.apply(null,$jscomp.arrayFromIterable(c)):setVisibility(document.getElementById(b),
!1)}}}(),registerModalListener=$jscomp$destructuring$var16.registerModalListener,openModal=$jscomp$destructuring$var16.openModal,closeModal=$jscomp$destructuring$var16.closeModal;
function initModals(a){function b(c){"Escape"===c.key||"Esc"===c.key?(c.preventDefault(),closeModal(c.target.closest(".modal").id)):"Enter"===c.key&&(c.preventDefault(),(c=modal.querySelector('input.ok[type\x3d"button"]'))&&!c.disabled&&c.click())}document.querySelectorAll(".modal").forEach(function(c){c.addEventListener("keydown",b,!1)});initSettings();"previewer"!==a&&(initDiagramExport(),initDiagramImport())}function isModalOpen(a){return isVisible(document.getElementById(a))}
function closeAllModals(){document.querySelectorAll(".modal").forEach(function(a){return closeModal(a.id)})}
function initSettings(){function a(){setVisibility(document.getElementById("settings"),!0,!0);b.value=document.appConfig.theme;c.value=document.appConfig.diagramPreviewType;d.value=document.appConfig.editorWatcherTimeout;setEditorValue(document.settingsEditor,JSON.stringify(document.appConfig.editorCreateOptions,null," "))}var b=document.getElementById("theme"),c=document.getElementById("diagramPreviewType"),d=document.getElementById("editorWatcherTimeout");document.settingsEditor=monaco.editor.create(document.getElementById("settings-monaco-editor"),
Object.assign({},{language:"json"},document.appConfig.editorCreateOptions));b.addEventListener("change",function(e){e=e.target.value;var f=document.settingsEditor.getValue();setEditorValue(document.settingsEditor,f.replace(new RegExp('("theme"\\s*:\\s*)"'+("dark"===e?"vs":"vs-dark")+'"',"gm"),'$1"'+("dark"===e?"vs-dark":"vs")+'"'))});document.getElementById("settings-ok-btn").addEventListener("click",function(){var e=Object.assign({},document.appConfig);e.theme=b.value;e.editorWatcherTimeout=d.value;
e.diagramPreviewType=c.value;e.editorCreateOptions=JSON.parse(document.settingsEditor.getValue());updateConfig(e);closeModal("settings")});window.addEventListener("keydown",function(e){","===e.key&&(isMac?e.metaKey:e.ctrlKey)&&(e.preventDefault(),isModalOpen("settings")||a())},!1);registerModalListener("settings",a)}
function initializeDiagram(){return $jscomp.asyncExecutePromiseGeneratorProgram(function(a){if("png"!==document.appConfig.diagramPreviewType)return a.return(setDiagram(document.appConfig.diagramPreviewType,document.appData.encodedDiagram,document.appData.index));a.jumpToEnd()})}
function setDiagram(a,b,c){function d(k,n,q){return $jscomp.asyncExecutePromiseGeneratorProgram(function(r){return r.return(makeRequest("GET",buildUrl(k,n,q)))})}var e,f,g,m,l,h,t,p;return $jscomp.asyncExecutePromiseGeneratorProgram(function(k){switch(k.nextAddress){case 1:e=document.getElementById("diagram");f=document.getElementById("diagram-png");g=document.getElementById("diagram-txt");m=document.getElementById("diagram-pdf");if("png"===a)return f.src=buildUrl("png",b,c),k.yield(d("map",b,c),
9);if("svg"===a)return k.yield(d("svg",b,c),8);if("txt"!==a){if("pdf"===a)m.data=buildUrl("pdf",b,c);else return l="unknown diagram type: "+a,(console.error||console.log)(l),k.return(Promise.reject(l));k.jumpTo(3);break}h=g;return k.yield(d("txt",b,c),7);case 7:h.innerHTML=k.yieldResult;k.jumpTo(3);break;case 8:t=k.yieldResult;var n=document.getElementById("diagram-svg"),q=document.createElement("div");q.innerHTML=t;q=q.querySelector("svg");q.id="diagram-svg";q.classList=n.classList;q.style.cssText=
n.style.cssText;n.parentNode.replaceChild(q,n);k.jumpTo(3);break;case 9:if(p=k.yieldResult,n=document.getElementById("plantuml_map"),q=document.getElementById("map-diagram-link"),p){var r=document.createElement("div");r.innerHTML=p;n.parentNode.replaceChild(r.firstChild,n);setVisibility(q,!0)}else removeChildren(n),setVisibility(q,!1);case 3:n=document.getElementById("plantuml_map"),q=document.getElementById("diagram-svg"),e.setAttribute("data-diagram-type",a),setVisibility(f,"png"===a),setVisibility(n,
"png"===a),setVisibility(q,"svg"===a),setVisibility(g,"txt"===a),setVisibility(m,"pdf"===a),k.jumpToEnd()}})}function getNumberOfDiagramPagesFromCode(a){var b;return(null==(b=a.match(/^\s*newpage\s?.*$/gm))?void 0:b.length)+1||1}function updatePaginatorSelection(){var a=document.getElementById("paginator"),b=document.appData.index;if(void 0===b||a.childNodes.length<=b)for(a=$jscomp.makeIterator(a.childNodes),b=a.next();!b.done;b=a.next())b.value.checked=!1;else a.childNodes[b].checked=!0}
var updatePaginator=function(){function a(b,c){for(;b.childElementCount>c;)b.removeChild(b.lastChild);for(;b.childElementCount<c;){var d=document.createElement("input");d.name="paginator";d.type="radio";d.value=b.childElementCount;d.addEventListener("click",function(e){sendMessage({sender:"paginator",data:{index:e.target.value},synchronize:!0})});b.appendChild(d)}}return function(){var b=document.getElementById("paginator"),c=document.appData.numberOfDiagramPages;1<c?(a(b,c),setVisibility(b,!0)):
setVisibility(b,!1)}}();function initializePaginator(){1<document.appData.numberOfDiagramPages&&(updatePaginator(),updatePaginatorSelection())}
function initPreview(a){function b(){setVisibility(e,!1);setVisibility(f,window.opener);g&&(g.style.width="100%");m&&setVisibility(m,!1)}function c(){setVisibility(e,!0);setVisibility(f,!1);g&&g.style.removeProperty("width");m&&setVisibility(m,!0)}function d(){var l=new URL(window.location.href);l.searchParams.set("view","previewer");if(l=window.open(l,"PlantUML Diagram Previewer","popup"))l.onbeforeunload=c,b()}var e,f,g,m;return $jscomp.asyncExecutePromiseGeneratorProgram(function(l){if(1==l.nextAddress)return e=
document.getElementById("btn-undock"),f=document.getElementById("btn-dock"),g=document.getElementById("editor-main-container"),m=document.getElementById("previewer-main-container"),e.addEventListener("click",d),l.yield(initializeDiagram(),2);initializePaginator();["previewer","editor"].includes(a)&&b();l.jumpToEnd()})}
var $jscomp$destructuring$var17=function(){var a=function(){var c=[];return{suppressNextMessage:function(d,e){c.push({sender:d,condition:e})},isMessageSuppressed:function(d){for(var e=0;e<c.length;e++){var f=c[e];if(!f.sender||f.sender===d.sender)if(!f.condition||f.condition(d))return c.splice(e,1),!0}return!1}}}(),b=a.isMessageSuppressed;return{sendMessage:function(c){b(c)||(new BroadcastChannel("plantuml-server")).postMessage(c)},suppressNextMessage:a.suppressNextMessage,initAppCommunication:function(){(new BroadcastChannel("plantuml-server")).onmessage=
function(c){function d(m){var l,h,t,p,k,n;return $jscomp.asyncExecutePromiseGeneratorProgram(function(q){if(1==q.nextAddress)return document.appConfig.autoRefreshState="syncing",l=document.appData.encodedDiagram,h=document.appData.index,"url"!==m&&document.getElementById("url")&&setUrlValue(void 0,{encodedDiagram:l,index:h},{suppressEditorChangedMessage:!0}),q.yield(setDiagram(document.appConfig.diagramPreviewType,l,h),2);t=$jscomp.makeIterator(document.getElementsByClassName("diagram-link"));for(p=
t.next();!p.done;p=t.next())k=p.value,k.href=buildUrl(k.dataset.imgType,l,h);n=replaceUrl(window.location.href,l,h).url;history.replaceState(history.stat,document.title,n);document.appConfig.autoRefreshState="complete";q.jumpToEnd()})}var e,f,g;return $jscomp.asyncExecutePromiseGeneratorProgram(function(m){if(1==m.nextAddress){e=c.data.data;f=c.data.force||!1;if(e&&0!==Object.keys(e).length){var l={};"encodedDiagram"in e&&e.encodedDiagram!==document.appData.encodedDiagram&&(document.appData.encodedDiagram=
e.encodedDiagram,l.diagram=!0);"index"in e&&e.index!==document.appData.index&&(document.appData.index=e.index,l.index=!0);"numberOfDiagramPages"in e&&e.numberOfDiagramPages!==document.appData.numberOfDiagramPages&&(document.appData.numberOfDiagramPages=e.numberOfDiagramPages,l.numberOfDiagramPages=!0);"appConfig"in e&&e.appConfig!==document.appConfig&&(document.appConfig=e.appConfig,l.appConfig=!0)}else l={};g=l;return!0!==c.data.synchronize?m.jumpTo(2):f||g.diagram||g.index||g.appConfig?m.yield(d(c.data.sender),
3):m.jumpTo(3)}2!=m.nextAddress&&((f||g.numberOfDiagramPages)&&updatePaginator(),(f||g.numberOfDiagramPages||g.index)&&updatePaginatorSelection(),g.appConfig&&applyConfig());!0===c.data.reload&&window.location.reload();m.jumpToEnd()})}}}}(),sendMessage=$jscomp$destructuring$var17.sendMessage,suppressNextMessage=$jscomp$destructuring$var17.suppressNextMessage,initAppCommunication=$jscomp$destructuring$var17.initAppCommunication;
function makeRequest(a,b,c){c=void 0===c?{}:c;return PlantUmlLanguageFeatures.makeRequest(a,b,{data:void 0===c.data?null:c.data,headers:void 0===c.headers?{"Content-Type":"text/plain"}:c.headers,responseType:void 0===c.responseType?"text":c.responseType,baseUrl:void 0===c.baseUrl?"":c.baseUrl})}
var $jscomp$destructuring$var21=function(){var a={changeEventsEnabled:!0,autoRefreshState:"disabled",theme:void 0,diagramPreviewType:"png",editorWatcherTimeout:500,editorCreateOptions:{automaticLayout:!0,fixedOverflowWidgets:!0,minimap:{enabled:!1},scrollbar:{alwaysConsumeMouseWheel:!1},scrollBeyondLastLine:!1,tabSize:2,theme:"vs"}},b;document.appConfig=Object.assign({},null==(b=window.opener)?void 0:b.document.appConfig);0===Object.keys(document.appConfig).length&&(document.appConfig=JSON.parse(localStorage.getItem("document.appConfig"))||
a);return{applyConfig:function(){setTheme(document.appConfig.theme);var c;null==(c=document.editor)||c.updateOptions(document.appConfig.editorCreateOptions);var d;null==(d=document.settingsEditor)||d.updateOptions(document.appConfig.editorCreateOptions)},updateConfig:function(c){localStorage.setItem("document.appConfig",JSON.stringify(c));sendMessage({sender:"config",data:{appConfig:c},synchronize:!0})}}}(),applyConfig=$jscomp$destructuring$var21.applyConfig,updateConfig=$jscomp$destructuring$var21.updateConfig;
function removeChildren(a){a.replaceChildren?a.replaceChildren():a.innerHTML=""}function isVisible(a){return null!==a.offsetParent}function setVisibility(a,b,c){c=void 0===c?!1:c;b?(a.style.removeProperty("display"),c&&a.focus()):a.style.display="none"}var isMac=function(){var a,b,c;return((null==(a=navigator)?void 0:null==(b=a.userAgentData)?void 0:b.platform)||(null==(c=navigator)?void 0:c.platform)||"unknown").match("Mac")}();
function setTheme(a){document.documentElement.setAttribute("data-theme",a)}
function initTheme(){function a(d){"dark"===d&&"vs"===document.appConfig.editorCreateOptions.theme&&(document.appConfig.editorCreateOptions.theme="vs-dark");"light"===d&&"vs-dark"===document.appConfig.editorCreateOptions.theme&&(document.appConfig.editorCreateOptions.theme="vs")}var b=document.appConfig,c;(c=document.appConfig.theme)||(c=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":window.matchMedia("(prefers-color-scheme: light)").matches?"light":void 0);b.theme=c||"light";setTheme(document.appConfig.theme);
a(document.appConfig.theme);window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(d){d=d.matches?"dark":"light";document.appConfig.theme=d;a(d);updateConfig(document.appConfig)})}function resolvePath(a){return PlantUmlLanguageFeatures.absolutePath(a)}
function prepareUrl(a){a instanceof URL||(a=new URL(resolvePath(a)));var b=(new URL((document.querySelector("base")||{}).href||window.location.origin)).pathname;"/"===b.slice(-1)&&(b=b.slice(0,-1));b=a.pathname.startsWith(b)?a.pathname.slice(b.length):a.pathname;var c=/\/\w+(?:\/(\d+))?(?:\/([^/]+))?\/?$/gm.exec(b);return[a,b,{idx:c[1],encoded:c[2]}]}
function analyseUrl(a){var b=$jscomp.makeIterator(prepareUrl(a));a=b.next().value;b.next();b=b.next().value;return{index:b.idx,encodedDiagram:b.encoded||a.searchParams.get("url")}}
function replaceUrl(a,b,c){var d=$jscomp.makeIterator(prepareUrl(a));a=d.next().value;var e=d.next().value;d=d.next().value.encoded;var f=e.slice(1);f=f.slice(0,f.indexOf("/"));c&&0<=c&&(f+="/"+c);d&&(f+="/"+b);"/"===e.slice(-1)&&(f+="/");a.pathname=(new URL(resolvePath(f))).pathname;a.searchParams.get("url")&&a.searchParams.set("url",b);return{url:a,pathname:f}}function buildUrl(a,b,c){c&&0<=c&&(a+="/"+c);return a+("/"+b)};

View File

@ -1,539 +0,0 @@
/******************************
* PlantUMLServlet style sheet *
******************************/
/************* variables *************/
:root {
color-scheme: light dark;
--font-color: black;
--font-color-disabled: #888;
--bg-color: white;
--border-color: #ccc;
--border-color-2: #aaa;
--footer-font-color: #666;
--footer-bg-color: #eee;
--modal-bg-color: #fefefe;
--file-drop-color: #eee;
}
[data-theme="dark"] {
--font-color: #ccc;
--font-color-disabled: #777;
--bg-color: #212121;
--border-color: #848484;
--border-color-2: #aaa;
--footer-font-color: #ccc;
--footer-bg-color: black;
--modal-bg-color: #424242;
--file-drop-color: #212121;
}
/************* default settings *************/
html, body {
margin: 0;
padding: 0;
}
html {
font-family: arial,helvetica,sans-serif;
}
body {
background-color: var(--bg-color);
color: var(--font-color);
overflow: auto;
}
@media screen and (min-width: 900px) {
body {
height: 100vh;
overflow: hidden;
}
.app {
height: 100%;
}
}
input:not([type="image"]) {
background-color: var(--bg-color);
color: var(--font-color);
}
input[type="file"]::file-selector-button {
background-color: var(--bg-color);
color: var(--font-color);
}
select {
background-color: var(--bg-color);
color: var(--font-color);
}
/************* ruler *************/
.hr {
padding: 1rem 0;
width: 100%;
}
.flex-columns > .hr {
padding: 0 1rem;
width: initial;
height: 100%;
}
.hr:after {
content: "";
display: block;
background-color: var(--border-color);
height: 100%;
width: 100%;
min-height: 3px;
min-width: 3px;
}
/************* wait cursor *************/
.wait {
cursor: wait;
}
.wait > * {
pointer-events: none;
}
/************* flex rows and columns *************/
.flex-columns {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.flex-rows {
display: flex;
flex-direction: column;
}
.flex-main {
flex: 1 1 1px;
overflow: auto;
}
.flex-columns > *, .flex-rows > * {
flex-shrink: 0;
}
/*******************************************************************/
/************* header, main, footer *************/
.header {
margin-left: auto;
margin-right: auto;
text-align: center;
}
.main {
margin: 1% 5%;
z-index: 1;
}
.main > div {
margin: 0 1.75%;
}
.main > div:first-child {
margin-left: 0;
}
.main > div:last-child {
margin-right: 0;
}
@media screen and (max-width: 900px) {
.main {
display: block;
overflow: inherit;
}
.main > div {
margin: 1.75% 0;
}
.main > div:first-child {
margin-top: 0;
}
.main > div:last-child {
margin-bottom: 0;
}
}
.footer p {
background-color: var(--footer-bg-color);
color: var(--footer-font-color);
font-size: 0.7em;
margin: 0;
padding: 0.5em;
text-align: center;
}
/*******************************************************************/
/************* editor *************/
.editor {
border: 3px solid var(--border-color);
box-sizing: border-box;
overflow: hidden;
}
@media screen and (max-width: 900px) {
.editor {
height: 20em;
}
}
.monaco-editor-container {
overflow: hidden;
position: relative;
}
#monaco-editor {
height: 100%;
}
/* Hack to display the icons and emojis in the auto completion documentation in a visible size.
* (see PlantUmlLanguageFeatures.register{Icon,Emoji}Completion) */
#monaco-editor .overlayWidgets .suggest-details p img[alt="icon"],
#monaco-editor .overlayWidgets .suggest-details p img[alt="emoji"] {
height: 1.2rem;
}
/************* URL input + copy button *************/
.btn-input {
align-items: center;
border-bottom: 3px solid var(--border-color);
box-sizing: border-box;
display: flex;
justify-content: center;
}
.btn-input input[type=text] {
border: 0;
flex: 1 1 1px;
font-family: monospace;
font-size: medium;
padding: 0.2em;
text-overflow: ellipsis;
}
.btn-input input[type=text]:focus {
border: 0;
box-shadow: none;
outline: none;
}
.btn-input input[type="image"] {
height: 1rem;
margin-left: 0.7em;
padding: 0 0.3em;
}
/************* Monaco editor action menu *************/
.monaco-editor-container .editor-menu {
position: absolute;
right: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
}
.monaco-editor-container .editor-menu > div.menu-kebab {
width: 60px;
height: 60px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
cursor: pointer;
scale: 0.5;
}
.monaco-editor-container .editor-menu:hover > div.menu-kebab,
.monaco-editor-container .editor-menu:focus > div.menu-kebab {
outline: none;
scale: 0.65;
}
.monaco-editor-container .menu-kebab .kebab-circle {
width: 12px;
height: 12px;
margin: 3px;
background: var(--font-color);
border-radius: 50%;
display: block;
opacity: 0.8;
}
.monaco-editor-container .menu-kebab {
flex-direction: column;
position: relative;
transition: all 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(4),
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(5) {
position: absolute;
opacity: 0;
top: 50%;
margin-top: -6px;
left: 50%;
}
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(4) {
margin-left: -25px;
}
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(5) {
margin-left: 13px;
}
.monaco-editor-container .editor-menu:hover .menu-kebab,
.monaco-editor-container .editor-menu:focus .menu-kebab {
transform: rotate(45deg);
}
.monaco-editor-container .editor-menu:hover .menu-kebab .kebab-circle,
.monaco-editor-container .editor-menu:focus .menu-kebab .kebab-circle {
opacity: 1;
}
.monaco-editor-container .editor-menu .menu-item {
display: none;
margin: 1rem 0;
height: 1.75rem;
opacity: 0.5;
position: relative;
-webkit-animation-name: animateitem;
-webkit-animation-duration: 0.4s;
animation-name: animateitem;
animation-duration: 0.4s;
}
@-webkit-keyframes animateitem {
from { top: -50%; opacity: 0; }
to { top: 0; opacity: 0.5; }
}
@keyframes animateitem {
from { top: -50%; opacity: 0; }
to { top: 0; opacity: 0.5; }
}
.monaco-editor-container .editor-menu .menu-item:hover {
opacity: 1;
}
.monaco-editor-container .editor-menu:hover .menu-item,
.monaco-editor-container .editor-menu:focus .menu-item {
display: block;
}
/*******************************************************************/
/************* previewer *************/
.content.viewer-content {
margin: 5%;
}
.content.viewer-content, .previewer-container {
height: 100%;
}
@media screen and (max-width: 900px) {
.previewer-container {
height: initial;
}
.previewer-main {
flex: none;
}
}
/************* menu *************/
.preview-menu {
margin-left: 5%;
margin-right: 5%;
}
.diagram-link img, .btn-dock {
width: 2.5rem;
}
.btn-settings {
width: 2.2rem;
margin-left: auto;
margin-right: 0.25rem;
}
.menu-r {
min-width: 3rem;
}
.menu-r .btn-float-r {
float: right;
margin-left: 0.25rem;
text-align: right;
}
.diagram-links {
align-items: center;
display: flex;
}
.diagram-link {
margin-left: 0.25rem;
margin-right: 0.25rem;
}
.diagram-links .diagram-link:first-of-type {
margin-left: 0.5rem;
}
.diagram-links .diagram-link:last-of-type {
margin-right: 0;
}
/************* paginator *************/
#paginator {
text-align: center;
margin-bottom: 1rem;
}
/************* diagram *************/
.diagram {
height: 100%;
overflow: auto;
}
.diagram[data-diagram-type="pdf"] {
overflow: hidden;
}
.diagram > div {
margin: 1rem 0;
text-align: center;
}
.diagram[data-diagram-type="pdf"] > div {
height: 20em;
width: 100%;
}
.diagram img, .diagram svg, .diagram pre {
border: 3px solid var(--border-color);
box-sizing: border-box;
padding: 10px;
}
@media screen and (min-width: 900px) {
.diagram {
position: relative;
}
.diagram > div {
margin: 0;
}
.diagram:not([data-diagram-type="pdf"]) > div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-height: 100%;
max-width: 100%;
}
.diagram[data-diagram-type="pdf"] > div {
height: 100%;
}
}
/*******************************************************************/
/************* modal *************/
.modal {
display: block;
position: fixed;
z-index: 1;
padding: 5%;
left: 0;
top: 0;
bottom: 0;
right: 0;
overflow: auto;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
}
.modal .modal-content {
background-color: var(--modal-bg-color);
margin: auto;
padding: 2rem;
border: 3px solid var(--border-color);
max-width: 30rem;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;
animation-duration: 0.4s;
position: relative;
top: 50%;
transform: translateY(-50%);
}
@-webkit-keyframes animatetop {
from { top: -50%; opacity: 0; }
to { top: 50%; opacity: 1; }
}
@keyframes animatetop {
from { top: -50%; opacity: 0; }
to { top: 50%; opacity: 1; }
}
/************* header, main, footer *************/
.modal .modal-header h2 {
margin: 0;
}
.modal .modal-main {
flex: 1;
}
.modal .modal-footer {
margin-top: 1rem;
text-align: right;
}
/************* inputs *************/
.modal input, .modal select {
border: 1px solid var(--border-color);
}
.modal input:not(:focus):invalid {
border-bottom-color: red;
}
.modal input[type="file"]::file-selector-button {
border: 1px solid var(--border-color);
}
/************* ok + cancel buttons *************/
.modal input.ok, .modal input.cancel {
min-width: 5rem;
}
.modal input.ok[disabled], .modal input.cancel[disabled] {
color: var(--font-color-disabled);
}
.modal input.ok:not([disabled]):hover {
border-bottom-color: green;
}
.modal input.cancel:not([disabled]):hover {
border-bottom-color: darkred;
}
/************* label + input pair *************/
.modal .label-input-pair {
margin: 1rem 0;
overflow: hidden;
}
.modal .label-input-pair:first-child {
margin-top: 0;
}
.modal .label-input-pair:last-child {
margin-bottom: 0;
}
.modal .label-input-pair label {
display: inline-block;
min-width: 15rem;
}
.modal .label-input-pair label + input,
.modal .label-input-pair label + select {
box-sizing: border-box;
display: inline-block;
min-width: 10rem;
}
/************* settings *************/
#settings #settings-monaco-editor {
height: 17rem;
border: 1px solid var(--border-color);
}
/************* diagram import *************/
#diagram-import p.error-message {
color: darkred;
padding-left: 1rem;
padding-right: 1rem;
}
#diagram-import input[type="file"] {
display: block;
width: 100%;
border: 0.2rem dashed var(--border-color);
border-radius: 0.4rem;
box-sizing: border-box;
padding: 5rem 2rem;
}
#diagram-import input[type="file"],
#diagram-import input[type="file"]::file-selector-button {
background-color: var(--modal-bg-color);
}
#diagram-import input[type="file"]:hover,
#diagram-import input[type="file"].drop-able {
border-color: var(--border-color-2);
background-color: var(--file-drop-color);
}
#diagram-import input[type="file"]:hover::file-selector-button,
#diagram-import input[type="file"].drop-able::file-selector-button {
background-color: var(--file-drop-color);
}
/************* diagram export *************/
#diagram-export.modal .label-input-pair label {
min-width: 8rem;
}
/*******************************************************************/
/************* color themes *************/
[data-theme="dark"] img:not(#diagram-png):not(.no-filter) {
filter: invert() contrast(30%);
}
[data-theme="dark"] input[type="image"] {
filter: invert() contrast(30%);
}
[data-theme="dark"] a {
color: white;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,437 +0,0 @@
/*******************************************
* Monaco Editor PlantUML language features *
********************************************/
/**
* Monaco Editor PlantUML Language Features.
*
* @example
* ```js
* plantumlFeatures = new PlantUmlLanguageFeatures();
* const model = monaco.editor.createModel(initCode, "apex", uri);
* model.onDidChangeContent(() => plantumlFeatures.validateCode(model));
* ```
*/
const PlantUmlLanguageFeatures = (function() {
'use strict';
/**
* Create Monaco Editor PlantUML Language Features instance.
*
* @param {object} [options] global instance options
*/
function PlantUmlLanguageFeatures({
baseUrl = "",
languageSelector = ["apex", "plantuml"],
initialize = true
} = {}) {
const validationEventListeners = {};
// ==========================================================================================================
// == PlantUML valdation methods ==
/**
* Add validation event listener.
*
* Validation Event Order:
* before -> code -> line -> after
*
* @param {("before"|"code"|"line"|"after")} type before|code|line|after event type
* @param {(event: any) => Promise<editor.IMarkerData>|editor.IMarkerData|Promise<editor.IMarkerData[]>|editor.IMarkerData[]|Promise<void>|void} listener event listener
*/
this.addValidationEventListener = (type, listener) => {
if (!["before", "code", "line", "after"].includes(type)) {
throw Error("Unknown validation event type: " + type);
}
validationEventListeners[type] = validationEventListeners[type] || [];
validationEventListeners[type].push(listener);
};
/**
* Validate PlantUML language of monaco editor model.
*
* @param {editor.ITextModel} model editor model to validate
*
* @returns editor markers as promise
*
* @example
* ```js
* validateCode(editor.getModel())
* .then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers));
* ```
*/
this.validateCode = async (model) => {
const promises = [];
// raise before events
promises.push(validationEventListeners.before?.map(listener => listener({ model })));
// raise code events
promises.push(validationEventListeners.code?.map(listener => listener({ model, code: model.getValue() })));
if (validationEventListeners.line && validationEventListeners.line.length > 0) {
// NOTE: lines and columns start at 1
const lineCount = model.getLineCount();
for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
const range = {
startLineNumber: lineNumber,
startColumn: 1,
endLineNumber: lineNumber,
endColumn: model.getLineLength(lineNumber) + 1,
};
const line = model.getValueInRange(range);
// raise line events
promises.push(validationEventListeners.line?.map(listener => listener({ model, range, line, lineNumber, lineCount })));
}
}
// raise after events
promises.push(validationEventListeners.after?.map(listener => listener({ model })));
// collect all markers and ...
// - since each event can results in an array of markers -> `flat(1)`
// - since not each event has to results in markers and can be `undef
return Promise.all(promises).then(results => results.flat(1).filter(marker => marker));
};
/**
* Add PlantUML `@start` and `@end` command validation.
*/
this.addStartEndValidationListeners = () => {
let diagramType = undefined;
let startCounter = 0;
let endCounter = 0;
// reset validation cache
this.addValidationEventListener("before", () => {
diagramType = undefined;
startCounter = 0;
endCounter = 0;
});
// @start should be the first command
this.addValidationEventListener("code", ({ model, code }) => {
const match = code.match(/^(('.*)|\s)*@start(?<type>\w+)/);
if (match) {
diagramType = match.groups.type;
return; // diagram code starts with a `@start`
}
return {
message: "PlantUML diagrams should begin with the `@start` command and `@start` should also be the first command.",
severity: monaco.MarkerSeverity.Warning,
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: model.getLineLength(1) + 1,
};
});
// @end should be the last command and should be of the same type (e.g. @startjson ... @endjson)
this.addValidationEventListener("code", ({ model, code }) => {
const lineCount = model.getLineCount();
const match = code.match(/\s+@end(?<type>\w+)(('.*)|\s)*$/);
if (match) {
if (diagramType === match.groups.type) {
return; // diagram code ends with a `@end` of the same type as the `@start`
}
return {
message: "PlantUML diagrams should start and end with the type.\nExample: `@startjson ... @endjson`",
severity: monaco.MarkerSeverity.Error,
startLineNumber: lineCount,
startColumn: 1,
endLineNumber: lineCount,
endColumn: model.getLineLength(lineCount) + 1,
};
}
return {
message: "PlantUML diagrams should end with the `@end` command and `@end` should also be the last command.",
severity: monaco.MarkerSeverity.Warning,
startLineNumber: lineCount,
startColumn: 1,
endLineNumber: lineCount,
endColumn: model.getLineLength(lineCount) + 1,
};
});
// @start should only be used once
this.addValidationEventListener("line", ({ range, line }) => {
const match = line.match(/^\s*@start(?<type>\w+)(\s+.*)?$/);
if (!match) return;
startCounter += 1;
if (startCounter > 1) {
const word = "@start" + match.groups.type;
const wordIndex = line.indexOf(word);
return {
message: "Multiple @start commands detected.",
severity: monaco.MarkerSeverity.Warning,
startLineNumber: range.startLineNumber,
startColumn: wordIndex + 1,
endLineNumber: range.endLineNumber,
endColumn: wordIndex + word.length + 1,
};
}
});
// @end should only be used once
this.addValidationEventListener("line", ({ range, line }) => {
const match = line.match(/^\s*@end(?<type>\w+)(\s+.*)?$/);
if (!match) return;
endCounter += 1;
if (endCounter > 1) {
const word = "@end" + match.groups.type;
const wordIndex = line.indexOf(word);
return {
message: "Multiple @end commands detected.",
severity: monaco.MarkerSeverity.Warning,
startLineNumber: range.startLineNumber,
startColumn: wordIndex + 1,
endLineNumber: range.endLineNumber,
endColumn: wordIndex + word.length + 1,
};
}
});
};
// ==========================================================================================================
// == PlantUML code completion methods ==
this.registerThemeCompletion = () => {
const createThemeProposals = async (range, filter = undefined) => {
const themes = await this.getThemes();
return themes?.filter(theme => filter ? theme.includes(filter) : true)
.map(theme => ({
label: theme,
kind: monaco.languages.CompletionItemKind.Text,
documentation: "PlantUML " + theme + " theme",
insertText: theme,
range: range,
})) || [];
};
monaco.languages.registerCompletionItemProvider(languageSelector, {
triggerCharacters: [" "],
provideCompletionItems: async (model, position) => {
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
if (textUntilPosition.match(/^\s*!(t(h(e(m(e)?)?)?)?)?$/)) {
return {
suggestions: [
{
label: 'theme',
kind: monaco.languages.CompletionItemKind.Keyword,
documentation: "PlantUML theme command",
insertText: 'theme',
range: getWordRange(model, position),
}
]
};
}
const match = textUntilPosition.match(/^\s*!theme\s+(?<theme>[^\s]*)$/);
if (match) {
const suggestions = await createThemeProposals(getWordRange(model, position), match.groups.theme);
return { suggestions };
}
return { suggestions: [] };
}
});
};
this.registerIconCompletion = () => {
const createIconProposals = async (range, filter = undefined) => {
const icons = await this.getIcons();
return icons?.filter(icon => filter ? icon.includes(filter) : true)
.map(icon => {
// NOTE: markdown image path inside suggestions seems to have rendering issues while using relative paths
const iconUrl = this.resolvePath(baseUrl + "ui-helper?request=icons.svg#" + icon);
return {
label: icon,
kind: monaco.languages.CompletionItemKind.Constant,
documentation: {
//supportHtml: true, // also a possibility but quite limited html
value: "![icon](" + iconUrl + ") &nbsp; " + icon
},
insertText: icon + ">",
range: range
};
}) || [];
};
monaco.languages.registerCompletionItemProvider(languageSelector, {
triggerCharacters: ["&"],
provideCompletionItems: async (model, position) => {
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
const match = textUntilPosition.match(/<&(?<icon>[^\s>]*)$/);
if (match) {
const suggestions = await createIconProposals(getWordRange(model, position), match.groups.icon);
return { suggestions };
}
return { suggestions: [] };
}
});
};
this.registerEmojiCompletion = () => {
const createEmojiProposals = async (range, filter = undefined) => {
const emojis = await this.getEmojis();
return emojis?.filter(([unicode, name]) => filter ? unicode.includes(filter) || name?.includes(filter) : true)
.map(([unicode, name]) => {
// NOTE: load images direct from GitHub source: https://github.com/twitter/twemoji#download
const emojiUrl = "https://raw.githubusercontent.com/twitter/twemoji/gh-pages/v/13.1.0/svg/" + unicode + ".svg";
const docHint = (name) ? name + " (" + unicode + ")" : unicode;
const isUnicode = !name || (filter && unicode.includes(filter));
const label = isUnicode ? unicode : name;
return {
label: label,
kind: monaco.languages.CompletionItemKind.Constant,
documentation: {
//supportHtml: true, // also a possibility but quite limited html
value: "![emoji](" + emojiUrl + ") &nbsp; " + docHint
},
insertText: label + ":>",
range: range
};
}) || [];
};
monaco.languages.registerCompletionItemProvider(languageSelector, {
triggerCharacters: [":"],
provideCompletionItems: async (model, position) => {
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
const match = textUntilPosition.match(/<:(?<emoji>[^\s>]*)$/);
if (match) {
const suggestions = await createEmojiProposals(getWordRange(model, position), match.groups.emoji);
return { suggestions };
}
return { suggestions: [] };
}
});
};
// ==========================================================================================================
// == helper methods ==
this.resolvePath = (path) => {
if (path.startsWith("http")) return path;
if (path.startsWith("/")) return window.location.origin + path;
if (path.slice(0, 2) == "./") path = path.slice(2);
let base = (document.querySelector("base") || {}).href || window.location.origin;
if (base.slice(-1) == "/") base = base.slice(0, -1);
return base + "/" + path;
};
this.getIcons = (function(){
let icons = undefined;
return async () => {
if (icons === undefined) {
icons = await makeRequest("GET", "ui-helper?request=icons", { responseType: "json" });
}
return icons;
}
})();
this.getEmojis = (function(){
let emojis = undefined;
return async () => {
if (emojis === undefined) {
emojis = await makeRequest("GET", "ui-helper?request=emojis", { responseType: "json" });
}
return emojis;
}
})();
this.getThemes = (function(){
let themes = undefined;
return async () => {
if (themes === undefined) {
themes = await makeRequest("GET", "ui-helper?request=themes", { responseType: "json" });
}
return themes;
}
})();
const makeRequest = (
method,
url,
{ data = null, headers = { "Content-Type": "text/plain" }, responseType = "text", ignoreBaseUrl = false } = {}
) => {
const targetUrl = (ignoreBaseUrl === true) ? url : baseUrl + url;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status <= 300) {
if (responseType === "json") {
resolve(xhr.response);
} else {
resolve(xhr.responseText);
}
} else {
if (responseType === "json") {
reject({ status: xhr.status, response: xhr.response });
} else {
reject({ status: xhr.status, responseText: xhr.responseText });
}
}
}
}
xhr.open(method, targetUrl, true);
xhr.responseType = responseType;
headers && Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]));
xhr.send(data);
});
};
const getWordRange = (model, position) => {
const word = model.getWordUntilPosition(position);
return {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
}
// ==========================================================================================================
// == constructor running code ==
// prepare base URL
if (baseUrl === null || baseUrl === undefined) {
baseUrl = "";
} else if (baseUrl !== "") {
// add tailing "/"
if (baseUrl.slice(-1) !== "/") baseUrl = baseUrl + "/";
}
// initialize default validation and code completion
if (initialize) {
this.addStartEndValidationListeners();
this.registerThemeCompletion();
this.registerIconCompletion();
this.registerEmojiCompletion();
}
}
return PlantUmlLanguageFeatures;
})();

View File

@ -14,13 +14,19 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<%@ include file="resource/htmlheadbase.jsp" %>
<%@ include file="/components/app-head.jsp" %>
<title>PlantUML Server</title>
<style>
.content.viewer-content {
height: 100%;
margin: 5%;
}
</style>
</head>
<body>
<div class="content viewer-content">
<%-- Preview --%>
<%@ include file="resource/preview.jsp" %>
<%@ include file="/components/preview/preview.jsp" %>
</div>
</body>
</html>

View File

@ -1,17 +0,0 @@
<div>
<img
style="display: inline; position: absolute; top: 0; right: 0; border: 0; max-width: 35%;"
class="no-filter"
src="assets/github-fork-me.png"
alt="Fork me on GitHub"
usemap="#github-banner"
/>
<map id="github-banner" name="github-banner" style="cursor: pointer;">
<area
shape="poly"
coords="15,0 59,0 149,90 149,134"
href="https://github.com/plantuml/plantuml-server"
alt="Fork me on GitHub"
/>
</map>
</div>

View File

@ -1,89 +0,0 @@
<div class="previewer-container flex-rows">
<div class="preview-menu">
<div class="diagram-links flex-columns">
<span>View as:</span>
<a class="diagram-link" data-img-type="png" href="png/<%= diagramUrl %>" title="View diagram as PNG">
<img src="assets/file-types/png.svg" alt="PNG" />
</a>
<a class="diagram-link" data-img-type="svg" href="svg/<%= diagramUrl %>" title="View diagram as SVG">
<img src="assets/file-types/svg.svg" alt="SVG" />
</a>
<a class="diagram-link" data-img-type="txt" href="txt/<%= diagramUrl %>" title="View diagram as ASCII Art">
<img src="assets/file-types/ascii.svg" alt="ASCII Art" />
</a>
<a class="diagram-link" data-img-type="pdf" href="pdf/<%= diagramUrl %>" title="View diagram as PDF">
<img src="assets/file-types/pdf.svg" alt="PDF" />
</a>
<a
id="map-diagram-link"
class="diagram-link"
data-img-type="map"
href="map/<%= diagramUrl %>"
title="View diagram as Map Data"
<% if (!hasMap) { %>
style="display: none;"
<% } %>
>
<img src="assets/file-types/map.svg" alt="MAP" />
</a>
<div class="flex-main menu-r">
<div class="btn-float-r">
<input
id="btn-settings"
class="btn-settings"
type="image"
src="assets/actions/settings.svg"
alt="settings"
onclick="openSettings();"
/>
<input
id="btn-undock"
class="btn-dock"
type="image"
src="assets/actions/undock.svg"
alt="undock"
onclick="undock();"
/>
<input
id="btn-dock"
class="btn-dock"
type="image"
src="assets/actions/dock.svg"
alt="dock"
onclick="window.close();"
style="display: none;"
/>
</div>
</div>
</div>
</div>
<div class="hr"></div>
<div id="paginator" data-number-of-diagram-pages="1" style="display: none;"></div>
<div class="previewer-main flex-main">
<div id="diagram" class="diagram">
<div>
<!-- PNG -->
<img id="diagram-png" src="png/<%= diagramUrl %>" alt="PlantUML diagram" usemap="#plantuml_map" />
<% if (hasMap) { %>
<%= map %>
<% } else { %>
<map id="plantuml_map" name="plantuml_map"></map>
<% } %>
<!-- SVG -->
<svg id="diagram-svg" style="display: none;"></svg>
<!-- ASCII Art -->
<pre id="diagram-txt" style="display: none;"></pre>
<!-- PDF -->
<object id="diagram-pdf" data="" type="application/pdf" width="100%" height="100%" style="display: none;">
<p>Unable to display PDF file.</p>
</object>
</div>
</div>
</div>
<% if (showSocialButtons) { %>
<div>
<%@ include file="socialbuttons2.jsp" %>
</div>
<% } %>
<%@ include file="settings.jsp" %>
</div>

View File

@ -1 +0,0 @@
<!-- Insert here the html code of your social buttons -->