Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ jobs:
- name: Install Zensical
run: pip install -r requirements.txt

# The playground's formatter: the WebAssembly module of the release that ojf_version names. A release
# without one leaves the page saying that the playground comes with the next release.
- name: Fetch the playground's formatter
run: ./scripts/fetch-playground.sh

# Strict mode fails the build on a broken link or anchor. That check is what makes a pull
# request opened from the "Edit this page" button safe to merge from the browser.
- name: Build the site
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
/.venv/

.DS_Store

# The playground's formatter, which scripts/fetch-playground.sh downloads from the release
/docs/playground/open-java-format.js
/docs/playground/open-java-format.js.wasm
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ zensical serve
The preview runs on <http://localhost:8000> and reloads on save. `mise run build` runs the same
clean, strict build as CI.

## The playground

The Playground page formats code in the browser with the formatter compiled to WebAssembly. The module
is not in this repository: `scripts/fetch-playground.sh` downloads it from the GitHub release that
`ojf_version` names, checks it against that release's checksums, and unpacks it next to the page.
`docs.yml` runs it before every build, and `mise run serve` and `mise run build` run it through
`mise run playground`. A release without the module is a warning, not an error: the page then says
that the playground comes with the next release.

To try a module built locally, point `OJF_WASM_ZIP` at its zip:

```bash
OJF_WASM_ZIP=../open-java-format/open-java-format-wasm/build/distributions/open-java-format-wasm-<version>.zip mise run serve
```

## Publishing

A push to `main` runs `.github/workflows/docs.yml`, which builds the site and deploys it to GitHub
Expand Down
130 changes: 130 additions & 0 deletions docs/javascripts/playground.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// The Playground page, docs/playground/index.md: formats the code on the left into the pane on the right, in the
// browser. The formatter is a WebAssembly module, open-java-format.js and open-java-format.js.wasm, which the build
// puts next to the page; it is the one the release named by ojf_version carries.
//
// Loaded on every page through extra_javascript, and it does nothing on a page without #playground. The site
// navigates without reloading (navigation.instant), so the page is set up on every arrival through document$,
// while the module, which defines globals, is loaded once per visit.

(function () {
"use strict";

// The smallest module with a WebAssembly GC type, a struct of one i8: what browsers without GC reject.
const WASM_GC_PROBE = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0]);

// Settles with globalThis.openJavaFormat, or rejects with "missing" when the site has no module, or the error.
let module = null;

function supportsWasmGc() {
try {
return typeof WebAssembly === "object" && WebAssembly.validate(WASM_GC_PROBE);
} catch {
return false;
}
}

function loadModule(wrapperUrl) {
if (module === null) {
module = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = wrapperUrl;
script.onload = resolve;
script.onerror = () => {
// Not there at all is the release without a module; anything else is a failed download.
fetch(wrapperUrl, { method: "HEAD" }).then(
(response) => reject(response.status === 404 ? "missing" : new Error(`HTTP ${response.status}`)),
reject,
);
};
document.head.append(script);
})
.then(() => {
// The wrapper does not start the module by itself (-H:-AutoRunVM): GraalVM.run settles once it has.
const config = new GraalVM.Config();
config.wasm_path = `${wrapperUrl}.wasm`;
return GraalVM.run([], config);
})
.then(() => globalThis.openJavaFormat);
// A failed load is tried again on the next visit to the page; "missing" stays missing until a reload.
module.catch((error) => {
if (error !== "missing") {
module = null;
}
});
}
return module;
}

function setUp(root) {
const source = root.querySelector("#ojf-source");
const output = root.querySelector("#ojf-output");
const button = root.querySelector("#ojf-format");
const copy = root.querySelector("#ojf-copy");

const show = (state) => {
root.dataset.state = state;
for (const message of root.querySelectorAll(".ojf-playground__status [data-state]")) {
message.hidden = message.dataset.state !== state;
}
};

// "macOS" from userAgentData, "MacIntel" or "iPhone" from the older navigator.platform.
if (/mac|iphone|ipad/i.test(navigator.userAgentData?.platform ?? navigator.platform)) {
root.querySelector("#ojf-modifier").textContent = "⌘";
}

if (!supportsWasmGc()) {
show("unsupported");
return;
}

show("loading");
loadModule(new URL("open-java-format.js", document.location.href).href).then(
(openJavaFormat) => {
const format = () => {
const started = performance.now();
const result = openJavaFormat.format(source.value);
const millis = performance.now() - started;
output.value = result.ok ? result.output : result.error;
output.classList.toggle("ojf-playground__output--error", !result.ok);
copy.hidden = !result.ok;
if (result.ok) {
root.querySelector("[data-millis]").textContent = Math.max(1, Math.round(millis)).toString();
show("formatted");
} else {
// A diagnostic starts with line:column; anything else is an exception from the formatter.
show(/^\d+:\d+: /.test(result.error) ? "error" : "bug");
}
};
button.addEventListener("click", format);
source.addEventListener("keydown", (event) => {
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
format();
}
});
copy.addEventListener("click", () => {
navigator.clipboard.writeText(output.value).then(() => {
copy.textContent = "Copied";
setTimeout(() => (copy.textContent = "Copy"), 1500);
});
});
button.disabled = false;
show("ready");
},
(error) => {
if (error !== "missing") {
console.error("open-java-format did not load", error);
}
show(error === "missing" ? "missing" : "failed");
},
);
}

document$.subscribe(() => {
const root = document.getElementById("playground");
if (root !== null) {
setUp(root);
}
});
})();
89 changes: 89 additions & 0 deletions docs/playground/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: Playground
description: Paste Java code and see how open-java-format lays it out. It runs in your browser, nothing is sent anywhere.
hide:
- navigation
- toc
---

# Playground

Paste Java code on the left and press **Format**. The formatter runs in your browser, compiled to
WebAssembly from the same release that [the other pages](../get-started/index.md) install: the code is
not sent anywhere, and the result is what the command line would write.

<div id="playground" class="ojf-playground" markdown>

<div class="ojf-playground__pane">
<label for="ojf-source">Your code</label>
<textarea id="ojf-source" spellcheck="false" autocapitalize="off" autocomplete="off" wrap="off">
class Example {
private static void configureResolvedVersionsWithVersionMapping(Project project) {
project.getPluginManager()
.withPlugin(
"maven-publish",
plugin -> {
project.getExtensions()
.getByType(PublishingExtension.class)
.getPublications()
.withType(MavenPublication.class)
.configureEach(
publication ->
publication.versionMapping(
mapping -> {
mapping.allVariants(
VariantVersionMappingStrategy
::fromResolutionResult);
}));
});
}
}
</textarea>
</div>

<div class="ojf-playground__pane">
<label for="ojf-output">open-java-format {{ ojf_version }}</label>
<textarea id="ojf-output" spellcheck="false" wrap="off" readonly></textarea>
<button id="ojf-copy" class="ojf-playground__copy" type="button" hidden>Copy</button>
</div>

<div class="ojf-playground__bar" markdown>

<button id="ojf-format" class="md-button md-button--primary" type="button" disabled>Format</button>
<span class="ojf-playground__keys"><kbd id="ojf-modifier">Ctrl</kbd>+<kbd>Enter</kbd></span>

<div class="ojf-playground__status" role="status" aria-live="polite" markdown>

<p data-state="loading">Loading the formatter…</p>
<p data-state="ready" hidden>Ready.</p>
<p data-state="formatted" hidden>Formatted in <span data-millis></span> ms.</p>
<p data-state="error" hidden>The code does not parse; the result pane says where.</p>
<p data-state="bug" hidden>The formatter failed on this code. That is a bug: please
<a href="https://github.com/openjavaformat/open-java-format/issues/new">report it</a> with the code.</p>

<div data-state="missing" hidden markdown>
The playground comes with the next release of open-java-format: {{ ojf_version }}, the release this
site installs, has no WebAssembly build yet. Until then the [command line](../get-started/command-line.md)
gives the same result.
</div>

<div data-state="unsupported" hidden markdown>
This browser cannot run the formatter: it needs WebAssembly garbage collection, which current Chrome,
Edge, Firefox and Safari have. The [command line](../get-started/command-line.md) gives the same result.
</div>

<div data-state="failed" hidden markdown>
The formatter did not load. Reload the page to try again, or use the
[command line](../get-started/command-line.md), which gives the same result.
</div>

</div>

</div>

</div>

Thank you to Denis Pavlyuchenko ([@pavlyuchenko_denis](https://t.me/pavlyuchenko_denis) on Telegram),
who suggested a playground that runs in the browser, and to [cobr123](https://github.com/cobr123), whose
[WebAssembly build](https://github.com/cobr123/open-java-format-wasm-demo) of the formatter this one
started from.
121 changes: 121 additions & 0 deletions docs/stylesheets/playground.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* The Playground page, docs/playground/index.md: the code and the result side by side, stacked on a narrow screen.
Colours and fonts are the theme's, so both palettes work. */

/* The Playground tab in the header stands out on every page: white on the theme's blue, like the Format button.
Its href is relative, ./playground/ or ../playground/, so the selector matches the end. */
.md-tabs__link[href$="playground/"] {
margin-top: 0.5rem;
padding: 0.25rem 0.6rem;
border-radius: 0.2rem;
background-color: var(--md-primary-fg-color);
color: var(--md-primary-bg-color);
opacity: 1;
}

.md-tabs__link[href$="playground/"]:is(:hover, :focus) {
background-color: var(--md-accent-fg-color);
color: var(--md-accent-bg-color);
}

.ojf-playground {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}

/* The theme's own breakpoint for a wide screen. */
@media screen and (min-width: 60em) {
.ojf-playground {
grid-template-columns: 1fr 1fr;
}
}

.ojf-playground__pane {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
}

.ojf-playground__pane label {
font-weight: 700;
margin-bottom: 0.4rem;
}

.ojf-playground textarea {
box-sizing: border-box;
width: 100%;
height: 45vh;
min-height: 16rem;
padding: 0.8em 1.2em;
border: 1px solid var(--md-default-fg-color--lightest);
border-radius: 0.1rem;
background-color: var(--md-code-bg-color);
color: var(--md-code-fg-color);
font-family: var(--md-code-font-family);
font-size: 0.75rem;
line-height: 1.4;
tab-size: 4;
white-space: pre;
overflow: auto;
resize: none;
}

/* Side by side, the panes can take most of the window; stacked, two of them have to fit. */
@media screen and (min-width: 60em) {
.ojf-playground textarea {
height: min(70vh, 40rem);
}
}

.ojf-playground textarea:focus {
outline: 2px solid var(--md-accent-fg-color);
outline-offset: -1px;
}

.ojf-playground .ojf-playground__output--error {
color: var(--md-typeset-del-color, #d32f2f);
}

.ojf-playground__copy {
position: absolute;
top: 2.2rem;
right: 0.6rem;
padding: 0.1rem 0.5rem;
border: 1px solid var(--md-default-fg-color--lightest);
border-radius: 0.1rem;
background-color: var(--md-default-bg-color);
color: var(--md-default-fg-color--light);
font-size: 0.7rem;
cursor: pointer;
}

.ojf-playground__bar {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem 1rem;
}

.ojf-playground__bar .md-button {
margin: 0;
}

.ojf-playground__bar .md-button:disabled {
opacity: 0.5;
cursor: progress;
}

.ojf-playground__keys {
color: var(--md-default-fg-color--light);
font-size: 0.75rem;
}

.ojf-playground__status {
flex: 1 1 20rem;
}

.ojf-playground__status > * {
margin: 0;
}
Loading
Loading