feature/undertow #4

Merged
manuel merged 15 commits from feature/undertow into master 2023-04-08 22:40:46 +02:00
8 changed files with 281 additions and 270 deletions
Showing only changes of commit 0bc1114aec - Show all commits

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" <settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/SETTINGS/1.1.0" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> xmlns="http://maven.apache.org/SETTINGS/1.1.0">
<servers> <servers>
<server> <server>
<id>repo.gittr.ch</id> <id>repo.gittr.ch</id>
<username>ci</username> <username>ci</username>
<password>${env.REPO_TOKEN}</password> <password>${env.REPO_TOKEN}</password>
</server> </server>
</servers> </servers>
</settings> </settings>

248
pom.xml
View file

@ -1,130 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
<modelVersion>4.0.0</modelVersion> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>ch.fritteli</groupId> <groupId>ch.fritteli</groupId>
<artifactId>fritteli-build-parent</artifactId> <artifactId>fritteli-build-parent</artifactId>
<version>2.0.4</version> <version>2.0.4</version>
</parent> </parent>
<groupId>ch.fritteli.labyrinth</groupId> <groupId>ch.fritteli.labyrinth</groupId>
<artifactId>labyrinth-server</artifactId> <artifactId>labyrinth-server</artifactId>
<version>0.0.2-SNAPSHOT</version> <version>0.0.2-SNAPSHOT</version>
<properties> <properties>
<logback.version>1.4.6</logback.version> <logback.version>1.4.6</logback.version>
<lombok.version>1.18.26</lombok.version> <lombok.version>1.18.26</lombok.version>
<slf4j.version>2.0.5</slf4j.version> <slf4j.version>2.0.5</slf4j.version>
<java.source.version>17</java.source.version> <java.source.version>17</java.source.version>
<java.target.version>17</java.target.version> <java.target.version>17</java.target.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>ch.fritteli.labyrinth</groupId> <groupId>ch.fritteli.labyrinth</groupId>
<artifactId>labyrinth-generator</artifactId> <artifactId>labyrinth-generator</artifactId>
<version>0.0.2</version> <version>0.0.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.vavr</groupId> <groupId>io.vavr</groupId>
<artifactId>vavr</artifactId> <artifactId>vavr</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains</groupId> <groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId> <artifactId>annotations</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version> <version>${slf4j.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>
<version>${logback.version}</version> <version>${logback.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.undertow</groupId> <groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId> <artifactId>undertow-core</artifactId>
<version>2.2.22.Final</version> <version>2.2.22.Final</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>junit-jupiter-api</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version> <version>3.2.4</version>
<configuration> <configuration>
<transformers> <transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <transformer
<mainClass>ch.fritteli.labyrinth.server.Main</mainClass> implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
</transformer> <mainClass>ch.fritteli.labyrinth.server.Main</mainClass>
</transformers> </transformer>
</configuration> </transformers>
<executions> </configuration>
<execution> <executions>
<phase>package</phase> <execution>
<goals> <phase>package</phase>
<goal>shade</goal> <goals>
</goals> <goal>shade</goal>
</execution> </goals>
</executions> </execution>
</plugin> </executions>
</plugins> </plugin>
</build> </plugins>
<scm> </build>
<connection>scm:git:git://gittr.ch/java/labyrinth-server.git</connection> <scm>
<developerConnection>scm:git:ssh://git@gittr.ch/java/labyrinth-server.git</developerConnection> <connection>scm:git:git://gittr.ch/java/labyrinth-server.git</connection>
<url>https://gittr.ch/java/labyrinth-server</url> <developerConnection>scm:git:ssh://git@gittr.ch/java/labyrinth-server.git</developerConnection>
<tag>HEAD</tag> <url>https://gittr.ch/java/labyrinth-server</url>
</scm> <tag>HEAD</tag>
<distributionManagement> </scm>
<repository> <distributionManagement>
<id>repo.gittr.ch</id> <repository>
<name>gittr.ch</name> <id>repo.gittr.ch</id>
<url>https://repo.gittr.ch/releases/</url> <name>gittr.ch</name>
</repository> <url>https://repo.gittr.ch/releases/</url>
<snapshotRepository> </repository>
<id>repo.gittr.ch</id> <snapshotRepository>
<name>gittr.ch</name> <id>repo.gittr.ch</id>
<url>https://repo.gittr.ch/snapshots/</url> <name>gittr.ch</name>
</snapshotRepository> <url>https://repo.gittr.ch/snapshots/</url>
</distributionManagement> </snapshotRepository>
<repositories> </distributionManagement>
<repository> <repositories>
<id>repo.gittr.ch.releases</id> <repository>
<url>https://repo.gittr.ch/releases/</url> <id>repo.gittr.ch.releases</id>
<releases> <url>https://repo.gittr.ch/releases/</url>
<enabled>true</enabled> <releases>
<updatePolicy>never</updatePolicy> <enabled>true</enabled>
</releases> <updatePolicy>never</updatePolicy>
<snapshots> </releases>
<enabled>false</enabled> <snapshots>
<updatePolicy>never</updatePolicy> <enabled>false</enabled>
</snapshots> <updatePolicy>never</updatePolicy>
</repository> </snapshots>
<repository> </repository>
<id>repo.gittr.ch.snapshots</id> <repository>
<url>https://repo.gittr.ch/snapshots/</url> <id>repo.gittr.ch.snapshots</id>
<releases> <url>https://repo.gittr.ch/snapshots/</url>
<enabled>false</enabled> <releases>
<updatePolicy>never</updatePolicy> <enabled>false</enabled>
</releases> <updatePolicy>never</updatePolicy>
<snapshots> </releases>
<enabled>true</enabled> <snapshots>
<updatePolicy>always</updatePolicy> <enabled>true</enabled>
</snapshots> <updatePolicy>always</updatePolicy>
</repository> </snapshots>
</repositories> </repository>
</repositories>
</project> </project>

View file

@ -119,15 +119,13 @@ public class LabyrinthServer {
} }
responseHeaders.add("Content-type", output.getContentType()); responseHeaders.add("Content-type", output.getContentType());
if (output.isAttachment()) { if (output.isAttachment()) {
responseHeaders.add( responseHeaders.add("Content-disposition", String.format(
"Content-disposition", "attachment; filename=\"labyrinth-%dx%d-%d.%s\"",
String.format("attachment; filename=\"labyrinth-%dx%d-%d.%s\"", width,
width, height,
height, id,
id, output.getFileExtension()
output.getFileExtension() ));
)
);
} }
exchange.sendResponseHeaders(200, 0); exchange.sendResponseHeaders(200, 0);
final OutputStream responseBody = exchange.getResponseBody(); final OutputStream responseBody = exchange.getResponseBody();
@ -254,7 +252,7 @@ public class LabyrinthServer {
} }
} }
private enum OutputType { public enum OutputType {
TEXT_PLAIN("text/plain; charset=UTF-8", TEXT_PLAIN("text/plain; charset=UTF-8",
labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
"txt", "txt",
@ -303,7 +301,7 @@ public class LabyrinthServer {
this.names = List.of(names); this.names = List.of(names);
} }
static Option<OutputType> ofString(@Nullable final String name) { public static Option<OutputType> ofString(@Nullable final String name) {
if (name == null) { if (name == null) {
return Option.none(); return Option.none();
} }
@ -316,7 +314,7 @@ public class LabyrinthServer {
return this.names.last(); return this.names.last();
} }
byte[] render(@NonNull final Labyrinth labyrinth) { public byte[] render(@NonNull final Labyrinth labyrinth) {
return this.render.apply(labyrinth); return this.render.apply(labyrinth);
} }
} }

View file

@ -10,7 +10,6 @@ import org.jetbrains.annotations.Nullable;
import java.net.InetAddress; import java.net.InetAddress;
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
@Slf4j @Slf4j
@Value @Value
@ -18,8 +17,7 @@ public class ServerConfig {
public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host"; public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host";
public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port"; public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port";
@NonNull @NonNull InetAddress address;
InetAddress address;
int port; int port;
public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException { public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException {
@ -28,6 +26,19 @@ public class ServerConfig {
log.debug("host={}, port={}", this.address, this.port); log.debug("host={}, port={}", this.address, this.port);
} }
@NonNull
private static InetAddress validateAddress(@Nullable final String address) {
return Try.of(() -> InetAddress.getByName(address))
.getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause));
}
private static int validatePort(final int port) {
if (port < 0 || port > 0xFFFF) {
throw new ConfigurationException("Port out of range (0..65535): " + port);
}
return port;
}
@NonNull @NonNull
public static ServerConfig init() throws ConfigurationException { public static ServerConfig init() throws ConfigurationException {
final String host = System.getProperty(SYSPROP_HOST); final String host = System.getProperty(SYSPROP_HOST);
@ -36,26 +47,16 @@ public class ServerConfig {
return new ServerConfig(host, port); return new ServerConfig(host, port);
} }
@NonNull
private static InetAddress validateAddress(@Nullable final String address) {
return Try.of(() -> InetAddress.getByName(address))
.getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause));
}
private static int validatePort(@Nullable final String portString) { private static int validatePort(@Nullable final String portString) {
if (portString == null) { if (portString == null) {
log.info("No port configured; using default."); log.info("No port configured; using default.");
return 0; return 0;
} }
return Try.of(() -> Integer.valueOf(portString)) return Try.of(() -> Integer.valueOf(portString))
.map(ServerConfig::validatePort) .map(ServerConfig::validatePort)
.getOrElseThrow(cause -> new ConfigurationException("Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, cause)); .getOrElseThrow(cause -> new ConfigurationException(
} "Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString,
cause
private static int validatePort(final int port) { ));
if (port < 0 || port > 0xFFFF) {
throw new ConfigurationException("Port out of range (0..65535): " + port);
}
return port;
} }
} }

View file

@ -24,43 +24,6 @@ public class StaticResourcesFileHandler implements HttpHandler {
log.debug("Created {}", this.getClass().getSimpleName()); log.debug("Created {}", this.getClass().getSimpleName());
} }
private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws IOException {
log.debug("Sending redirect to {}", target);
exchange.getResponseHeaders().add("Location", target);
exchange.sendResponseHeaders(302, -1);
}
private static void notFound(@NonNull final HttpExchange exchange, @NonNull final String path) throws IOException {
log.debug("Resource '{}' not found, replying with HTTP 404", path);
exchange.getResponseHeaders().add("Content-type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(404, 0);
exchange.getResponseBody().write("404 - Not found".getBytes(StandardCharsets.UTF_8));
exchange.getResponseBody().flush();
}
@NonNull
private static byte[] getBytes(@NonNull final String path) throws IOException {
final InputStream stream = StaticResourcesFileHandler.class.getClassLoader().getResourceAsStream(WEBASSETS_DIRECTORY + path);
if (stream == null) {
log.debug("Resource '{}' not found in classpath.", path);
return new byte[0];
}
final byte[] response = IOUtils.toByteArray(stream);
log.debug("Sending reply; {} bytes", response.length);
return response;
}
@Nullable
private static String getMimeType(@NonNull final String path) {
if (path.endsWith(".html")) {
return "text/html";
}
if (path.endsWith(".css")) {
return "text/css";
}
return null;
}
@Override @Override
public void handle(@NonNull final HttpExchange exchange) throws IOException { public void handle(@NonNull final HttpExchange exchange) throws IOException {
this.executorService.submit(() -> { this.executorService.submit(() -> {
@ -86,7 +49,13 @@ public class StaticResourcesFileHandler implements HttpHandler {
notFound(exchange, path); notFound(exchange, path);
return; return;
} }
log.debug("Serving {}{} with mimetype {}: {} bytes", WEBASSETS_DIRECTORY, path, mimeType, responseBytes.length); log.debug(
"Serving {}{} with mimetype {}: {} bytes",
WEBASSETS_DIRECTORY,
path,
mimeType,
responseBytes.length
);
exchange.getResponseHeaders().add("Content-type", mimeType); exchange.getResponseHeaders().add("Content-type", mimeType);
exchange.sendResponseHeaders(200, 0); exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().write(responseBytes); exchange.getResponseBody().write(responseBytes);
@ -98,4 +67,43 @@ public class StaticResourcesFileHandler implements HttpHandler {
} }
}); });
} }
private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws
IOException {
log.debug("Sending redirect to {}", target);
exchange.getResponseHeaders().add("Location", target);
exchange.sendResponseHeaders(302, -1);
}
private static void notFound(@NonNull final HttpExchange exchange, @NonNull final String path) throws IOException {
log.debug("Resource '{}' not found, replying with HTTP 404", path);
exchange.getResponseHeaders().add("Content-type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(404, 0);
exchange.getResponseBody().write("404 - Not found".getBytes(StandardCharsets.UTF_8));
exchange.getResponseBody().flush();
}
@Nullable
private static String getMimeType(@NonNull final String path) {
if (path.endsWith(".html")) {
return "text/html";
}
if (path.endsWith(".css")) {
return "text/css";
}
return null;
}
@NonNull
private static byte[] getBytes(@NonNull final String path) throws IOException {
final InputStream stream = StaticResourcesFileHandler.class.getClassLoader()
.getResourceAsStream(WEBASSETS_DIRECTORY + path);
if (stream == null) {
log.debug("Resource '{}' not found in classpath.", path);
return new byte[0];
}
final byte[] response = IOUtils.toByteArray(stream);
log.debug("Sending reply; {} bytes", response.length);
return response;
}
} }

View file

@ -1,5 +1,7 @@
package ch.fritteli.labyrinth.server.undertow_playground; package ch.fritteli.labyrinth.server.undertow_playground;
import ch.fritteli.labyrinth.generator.model.Labyrinth;
import ch.fritteli.labyrinth.server.LabyrinthServer;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import io.undertow.server.RoutingHandler; import io.undertow.server.RoutingHandler;
@ -10,69 +12,69 @@ import io.vavr.control.Option;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer;
import java.util.Deque; import java.util.Deque;
import java.util.Map; import java.util.Map;
import java.util.Random;
@Slf4j @Slf4j
public class UndertowPlayground { public class UndertowPlayground {
public static final RoutingHandler r = new RoutingHandler() public static final RoutingHandler r = new RoutingHandler().get("/create/{output}", new HttpHandler() {
.get("/create/{output}", new HttpHandler() { @Override
@Override public void handleRequest(HttpServerExchange exchange) throws Exception {
public void handleRequest(HttpServerExchange exchange) throws Exception { if (exchange.isInIoThread()) {
if (exchange.isInIoThread()) { exchange.dispatch(this);
exchange.dispatch(this); return;
return; }
} final Map<String, Deque<String>> queryParameters = exchange.getQueryParameters();
final Map<String, Deque<String>> queryParameters = exchange.getQueryParameters(); final Option<String> output = getFirstOption(queryParameters, "output");
final Option<String> output = getFirstOption(queryParameters, "output"); final Option<Integer> width = getIntOption(queryParameters, "width");
final Option<Integer> width = getIntOption(queryParameters, "width"); final Option<Integer> height = getIntOption(queryParameters, "height");
final Option<Integer> height = getIntOption(queryParameters, "height"); final Option<Integer> id = getIntOption(queryParameters, "id");
final Option<Integer> id = getIntOption(queryParameters, "id");
log.info("Output: {}", output); log.info("Output: {}", output);
log.info("Width: {}", width); log.info("Width: {}", width);
log.info("Height: {}", height); log.info("Height: {}", height);
log.info("Id: {}", id); log.info("Id: {}", id);
exchange.getResponseSender().send( final Integer theId = id.getOrElse(() -> new Random().nextInt());
"Output: " + output + final Labyrinth labyrinth = new Labyrinth(width.get(), height.get(), theId);
", Width: " + width + final LabyrinthServer.OutputType outputType = output.flatMap(LabyrinthServer.OutputType::ofString).get();
", Height: " + height + final byte[] result = outputType.render(labyrinth);
", Id: " + id exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, outputType.getContentType());
); exchange.getResponseHeaders().put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(theId));
} exchange.getResponseHeaders()
}) .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(width.get()));
.post("/render", new HttpHandler() { exchange.getResponseHeaders()
@Override .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(height.get()));
public void handleRequest(final HttpServerExchange exchange) { exchange.getResponseSender().send(ByteBuffer.wrap(result));
if (exchange.isInIoThread()) { }
exchange.dispatch(this); }).post("/render", new HttpHandler() {
return; @Override
} public void handleRequest(final HttpServerExchange exchange) {
exchange.getResponseSender().send("TODO: read body, render stuff"); if (exchange.isInIoThread()) {
} exchange.dispatch(this);
}) return;
.setFallbackHandler(new HttpHandler() { }
@Override exchange.getResponseSender().send("TODO: read body, render stuff");
public void handleRequest(HttpServerExchange exchange) throws Exception { }
exchange.getResponseSender().send("Request: " + exchange.getRequestURI()); }).setFallbackHandler(new HttpHandler() {
final HeaderValues strings = exchange.getRequestHeaders().get(Headers.ACCEPT); @Override
strings.peekFirst(); public void handleRequest(HttpServerExchange exchange) throws Exception {
} exchange.getResponseSender().send("Request: " + exchange.getRequestURI());
} final HeaderValues strings = exchange.getRequestHeaders().get(Headers.ACCEPT);
); strings.peekFirst();
}
});
@NonNull @NonNull
private static Option<String> getFirstOption(@NonNull final Map<String, Deque<String>> queryParams, @NonNull final String paramName) { private static Option<Integer> getIntOption(@NonNull final Map<String, Deque<String>> queryParams,
return Option.of(queryParams.get(paramName)) @NonNull final String paramName) {
.map(Deque::peek) return getFirstOption(queryParams, paramName).toTry().map(Integer::parseInt).toOption();
.flatMap(Option::of);
} }
@NonNull @NonNull
private static Option<Integer> getIntOption(@NonNull final Map<String, Deque<String>> queryParams, @NonNull final String paramName) { private static Option<String> getFirstOption(@NonNull final Map<String, Deque<String>> queryParams,
return getFirstOption(queryParams, paramName) @NonNull final String paramName) {
.toTry() return Option.of(queryParams.get(paramName)).map(Deque::peek).flatMap(Option::of);
.map(Integer::parseInt)
.toOption();
} }
} }

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<configuration> <configuration>
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/> <shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type <!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder --> ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder> <encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="info"> <root level="info">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="ch.fritteli.labyrinth.server.StaticResourcesFileHandler" level="debug"/> <logger name="ch.fritteli.labyrinth.server.StaticResourcesFileHandler" level="debug"/>
</configuration> </configuration>

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Labyrinth Generator</title> <title>Labyrinth Generator</title>
<link rel="stylesheet" href="style.css"> <link href="style.css" rel="stylesheet">
</head> </head>
<body> <body>
<div class="content"> <div class="content">
@ -11,8 +11,8 @@
<p>Enter some values, click the "Create!" button and see what happens!</p> <p>Enter some values, click the "Create!" button and see what happens!</p>
<form action="/create" method="get"> <form action="/create" method="get">
<div class="inputs-wrapper"> <div class="inputs-wrapper">
<label for="width">Width:</label><input id="width" name="width" type="number" min="1" required> <label for="width">Width:</label><input id="width" min="1" name="width" required type="number">
<label for="height">Height:</label><input id="height" name="height" type="number" min="1" required> <label for="height">Height:</label><input id="height" min="1" name="height" required type="number">
<label for="output">Output format:</label> <label for="output">Output format:</label>
<select id="output" name="output" required> <select id="output" name="output" required>
<option label="HTML Document" value="html"></option> <option label="HTML Document" value="html"></option>
@ -24,7 +24,7 @@
<label for="id">Seed (optional):</label><input id="id" name="id" type="number"> <label for="id">Seed (optional):</label><input id="id" name="id" type="number">
</div> </div>
<div class="controls-wrapper"> <div class="controls-wrapper">
<button type="submit" class="primary">Create!</button> <button class="primary" type="submit">Create!</button>
<button type="reset">Reset form</button> <button type="reset">Reset form</button>
</div> </div>
</form> </form>