Picto: add Kroki integration
diff --git a/plugins/org.eclipse.epsilon.picto/plugin.xml b/plugins/org.eclipse.epsilon.picto/plugin.xml
index 07ad0e4..66ca23f 100644
--- a/plugins/org.eclipse.epsilon.picto/plugin.xml
+++ b/plugins/org.eclipse.epsilon.picto/plugin.xml
@@ -108,6 +108,9 @@
       <viewContentTransformer
             class="org.eclipse.epsilon.picto.transformers.CsvContentTransformer">
       </viewContentTransformer>
+      <viewContentTransformer
+            class="org.eclipse.epsilon.picto.transformers.KrokiContentTransformer">
+      </viewContentTransformer>
    </extension>
    <extension
          point="org.eclipse.epsilon.picto.browserFunction">
diff --git a/plugins/org.eclipse.epsilon.picto/src/org/eclipse/epsilon/picto/preferences/PictoPreferencePage.java b/plugins/org.eclipse.epsilon.picto/src/org/eclipse/epsilon/picto/preferences/PictoPreferencePage.java
index b4e9dbc..a6dad39 100644
--- a/plugins/org.eclipse.epsilon.picto/src/org/eclipse/epsilon/picto/preferences/PictoPreferencePage.java
+++ b/plugins/org.eclipse.epsilon.picto/src/org/eclipse/epsilon/picto/preferences/PictoPreferencePage.java
@@ -17,36 +17,59 @@
 import org.eclipse.jface.preference.IPreferenceStore;
 import org.eclipse.jface.preference.IntegerFieldEditor;
 import org.eclipse.jface.preference.PreferencePage;
+import org.eclipse.jface.preference.StringFieldEditor;
 import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
 import org.eclipse.ui.IWorkbench;
 import org.eclipse.ui.IWorkbenchPreferencePage;
 
 public class PictoPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
 	
 	public static final String ID = "org.eclipse.epsilon.picto.preferences.PictoPreferencePage";
+	
 	public static final String PROPERTY_RENDER_VERBATIM = "verbatim.sources";
+	public static final boolean DEFAULT_RENDER_VERBATIM = false;
+
 	public static final String TIMEOUT = "external.timeout";
 	public static final int DEFAULT_TIMEOUT = 60;
+
+	public static final String KROKI_URL = "kroki.url";
+	public static final String DEFAULT_KROKI_URL = "https://kroki.io";
 	
 	protected final ArrayList<FieldEditor> fieldEditors = new ArrayList<>();
-	protected IPreferenceStore preferences = EpsilonCommonsPlugin.getDefault().getPreferenceStore();
+	protected final IPreferenceStore preferences = EpsilonCommonsPlugin.getDefault().getPreferenceStore();
+
+	private IntegerFieldEditor timeoutEditor;
+	private BooleanFieldEditor verbatimBooleanEditor;
+	private StringFieldEditor krokiURLEditor;
 	
 	public PictoPreferencePage() {
+		preferences.setDefault(PROPERTY_RENDER_VERBATIM, DEFAULT_RENDER_VERBATIM);
 		preferences.setDefault(TIMEOUT, DEFAULT_TIMEOUT);
+		preferences.setDefault(KROKI_URL, DEFAULT_KROKI_URL);
 	}
-	
+
 	@Override
 	protected Control createContents(Composite parent) {
 		final Composite composite = new Composite(parent, SWT.FILL);
 		
-		final IntegerFieldEditor timeoutEditor = new IntegerFieldEditor(TIMEOUT, "Rendering timeout (seconds)", composite);
-		final BooleanFieldEditor verbatimBooleanEditor = new BooleanFieldEditor(PROPERTY_RENDER_VERBATIM, "Render verbatim sources", composite);
-		
+		timeoutEditor = new IntegerFieldEditor(TIMEOUT, "Rendering timeout (seconds)", composite);
+
+		verbatimBooleanEditor = new BooleanFieldEditor(PROPERTY_RENDER_VERBATIM, "Render verbatim sources", composite);
+		// Fills in the second column in the field editor
+		new Label(parent, SWT.NONE);
+
+		final Label lblSeparator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
+		lblSeparator.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 2, 1));
+		krokiURLEditor = new StringFieldEditor(KROKI_URL, "Kroki server base URL", composite);
+
 		fieldEditors.add(verbatimBooleanEditor);
 		fieldEditors.add(timeoutEditor);
-		
+		fieldEditors.add(krokiURLEditor);
+
 		for (FieldEditor fieldEditor : fieldEditors) {
 			fieldEditor.setPreferenceStore(preferences);
 			fieldEditor.load();
@@ -70,4 +93,13 @@
 		return true;
 	}
 
+	@Override
+	protected void performDefaults() {
+		timeoutEditor.loadDefault();
+		verbatimBooleanEditor.loadDefault();
+		krokiURLEditor.loadDefault();
+
+		super.performDefaults();
+	}
+
 }
diff --git a/plugins/org.eclipse.epsilon.picto/src/org/eclipse/epsilon/picto/transformers/KrokiContentTransformer.java b/plugins/org.eclipse.epsilon.picto/src/org/eclipse/epsilon/picto/transformers/KrokiContentTransformer.java
new file mode 100644
index 0000000..67f748e
--- /dev/null
+++ b/plugins/org.eclipse.epsilon.picto/src/org/eclipse/epsilon/picto/transformers/KrokiContentTransformer.java
@@ -0,0 +1,82 @@
+/*********************************************************************
+* Copyright (c) 2022 The University of York.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipse.epsilon.picto.transformers;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.eclipse.epsilon.common.dt.EpsilonCommonsPlugin;
+import org.eclipse.epsilon.picto.PictoView;
+import org.eclipse.epsilon.picto.ViewContent;
+import org.eclipse.epsilon.picto.preferences.PictoPreferencePage;
+import org.eclipse.jface.preference.IPreferenceStore;
+
+public class KrokiContentTransformer implements ViewContentTransformer {
+
+	private static final Pattern KROKI_FORMAT_REGEX = Pattern.compile("kroki-(?<format>[a-zA-Z]+)", Pattern.CASE_INSENSITIVE);
+
+	@Override
+	public boolean canTransform(ViewContent content) {
+		return KROKI_FORMAT_REGEX.matcher(content.getFormat()).matches();
+	}
+
+	@Override
+	public String getLabel(ViewContent content) {
+		return "Kroki";
+	}
+
+	@Override
+	public ViewContent transform(ViewContent content, PictoView pictoView) throws Exception {
+		final Matcher matcher = KROKI_FORMAT_REGEX.matcher(content.getFormat());
+
+		if (matcher.matches()) {
+			final String format = matcher.group("format");
+			return new ViewContent("svg", krokiToRawSvg(format, content.getText()), content);
+		} else {
+			return new ViewContent("text", String.format("BUG: Kroki format could not be extracted from '%s'", content.getFormat()), content);
+		}
+	}
+
+	private String krokiToRawSvg(String format, String text) throws IOException {
+		final URL url = new URL(String.format("%s/%s/svg", getKrokiBaseURL(), format));
+
+		final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+		conn.setDoOutput(true);
+		conn.setRequestMethod("POST");
+		conn.setRequestProperty("Content-Type", "text-plain");
+		try (OutputStream os = conn.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8")) {
+			osw.write(text);
+		}
+		conn.connect();
+
+		String svgOutput = new BufferedReader(
+			new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))
+			.lines()
+			.collect(Collectors.joining("\n"));
+		
+		return svgOutput;
+	}
+
+	private String getKrokiBaseURL() {
+		IPreferenceStore preferenceStore = EpsilonCommonsPlugin.getDefault().getPreferenceStore();
+		return preferenceStore.isDefault(PictoPreferencePage.KROKI_URL)
+			? PictoPreferencePage.DEFAULT_KROKI_URL : preferenceStore.getString(PictoPreferencePage.KROKI_URL);
+	}
+
+}