Skip to content

Commit 88b0dfa

Browse files
authored
Merge pull request #10 from openjavaformat/abashev/playground
Add a Playground page that formats Java in the browser
2 parents e0d3bef + 6fcba07 commit 88b0dfa

9 files changed

Lines changed: 420 additions & 4 deletions

File tree

‎.github/workflows/docs.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ jobs:
3030
- name: Install Zensical
3131
run: pip install -r requirements.txt
3232

33+
# The playground's formatter: the WebAssembly module of the release that ojf_version names. A release
34+
# without one leaves the page saying that the playground comes with the next release.
35+
- name: Fetch the playground's formatter
36+
run: ./scripts/fetch-playground.sh
37+
3338
# Strict mode fails the build on a broken link or anchor. That check is what makes a pull
3439
# request opened from the "Edit this page" button safe to merge from the browser.
3540
- name: Build the site

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@
66
/.venv/
77

88
.DS_Store
9+
10+
# The playground's formatter, which scripts/fetch-playground.sh downloads from the release
11+
/docs/playground/open-java-format.js
12+
/docs/playground/open-java-format.js.wasm

‎README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ zensical serve
3535
The preview runs on <http://localhost:8000> and reloads on save. `mise run build` runs the same
3636
clean, strict build as CI.
3737

38+
## The playground
39+
40+
The Playground page formats code in the browser with the formatter compiled to WebAssembly. The module
41+
is not in this repository: `scripts/fetch-playground.sh` downloads it from the GitHub release that
42+
`ojf_version` names, checks it against that release's checksums, and unpacks it next to the page.
43+
`docs.yml` runs it before every build, and `mise run serve` and `mise run build` run it through
44+
`mise run playground`. A release without the module is a warning, not an error: the page then says
45+
that the playground comes with the next release.
46+
47+
To try a module built locally, point `OJF_WASM_ZIP` at its zip:
48+
49+
```bash
50+
OJF_WASM_ZIP=../open-java-format/open-java-format-wasm/build/distributions/open-java-format-wasm-<version>.zip mise run serve
51+
```
52+
3853
## Publishing
3954

4055
A push to `main` runs `.github/workflows/docs.yml`, which builds the site and deploys it to GitHub

‎docs/javascripts/playground.js‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// The Playground page, docs/playground/index.md: formats the code on the left into the pane on the right, in the
2+
// browser. The formatter is a WebAssembly module, open-java-format.js and open-java-format.js.wasm, which the build
3+
// puts next to the page; it is the one the release named by ojf_version carries.
4+
//
5+
// Loaded on every page through extra_javascript, and it does nothing on a page without #playground. The site
6+
// navigates without reloading (navigation.instant), so the page is set up on every arrival through document$,
7+
// while the module, which defines globals, is loaded once per visit.
8+
9+
(function () {
10+
"use strict";
11+
12+
// The smallest module with a WebAssembly GC type, a struct of one i8: what browsers without GC reject.
13+
const WASM_GC_PROBE = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0]);
14+
15+
// Settles with globalThis.openJavaFormat, or rejects with "missing" when the site has no module, or the error.
16+
let module = null;
17+
18+
function supportsWasmGc() {
19+
try {
20+
return typeof WebAssembly === "object" && WebAssembly.validate(WASM_GC_PROBE);
21+
} catch {
22+
return false;
23+
}
24+
}
25+
26+
function loadModule(wrapperUrl) {
27+
if (module === null) {
28+
module = new Promise((resolve, reject) => {
29+
const script = document.createElement("script");
30+
script.src = wrapperUrl;
31+
script.onload = resolve;
32+
script.onerror = () => {
33+
// Not there at all is the release without a module; anything else is a failed download.
34+
fetch(wrapperUrl, { method: "HEAD" }).then(
35+
(response) => reject(response.status === 404 ? "missing" : new Error(`HTTP ${response.status}`)),
36+
reject,
37+
);
38+
};
39+
document.head.append(script);
40+
})
41+
.then(() => {
42+
// The wrapper does not start the module by itself (-H:-AutoRunVM): GraalVM.run settles once it has.
43+
const config = new GraalVM.Config();
44+
config.wasm_path = `${wrapperUrl}.wasm`;
45+
return GraalVM.run([], config);
46+
})
47+
.then(() => globalThis.openJavaFormat);
48+
// A failed load is tried again on the next visit to the page; "missing" stays missing until a reload.
49+
module.catch((error) => {
50+
if (error !== "missing") {
51+
module = null;
52+
}
53+
});
54+
}
55+
return module;
56+
}
57+
58+
function setUp(root) {
59+
const source = root.querySelector("#ojf-source");
60+
const output = root.querySelector("#ojf-output");
61+
const button = root.querySelector("#ojf-format");
62+
const copy = root.querySelector("#ojf-copy");
63+
64+
const show = (state) => {
65+
root.dataset.state = state;
66+
for (const message of root.querySelectorAll(".ojf-playground__status [data-state]")) {
67+
message.hidden = message.dataset.state !== state;
68+
}
69+
};
70+
71+
// "macOS" from userAgentData, "MacIntel" or "iPhone" from the older navigator.platform.
72+
if (/mac|iphone|ipad/i.test(navigator.userAgentData?.platform ?? navigator.platform)) {
73+
root.querySelector("#ojf-modifier").textContent = "⌘";
74+
}
75+
76+
if (!supportsWasmGc()) {
77+
show("unsupported");
78+
return;
79+
}
80+
81+
show("loading");
82+
loadModule(new URL("open-java-format.js", document.location.href).href).then(
83+
(openJavaFormat) => {
84+
const format = () => {
85+
const started = performance.now();
86+
const result = openJavaFormat.format(source.value);
87+
const millis = performance.now() - started;
88+
output.value = result.ok ? result.output : result.error;
89+
output.classList.toggle("ojf-playground__output--error", !result.ok);
90+
copy.hidden = !result.ok;
91+
if (result.ok) {
92+
root.querySelector("[data-millis]").textContent = Math.max(1, Math.round(millis)).toString();
93+
show("formatted");
94+
} else {
95+
// A diagnostic starts with line:column; anything else is an exception from the formatter.
96+
show(/^\d+:\d+: /.test(result.error) ? "error" : "bug");
97+
}
98+
};
99+
button.addEventListener("click", format);
100+
source.addEventListener("keydown", (event) => {
101+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
102+
event.preventDefault();
103+
format();
104+
}
105+
});
106+
copy.addEventListener("click", () => {
107+
navigator.clipboard.writeText(output.value).then(() => {
108+
copy.textContent = "Copied";
109+
setTimeout(() => (copy.textContent = "Copy"), 1500);
110+
});
111+
});
112+
button.disabled = false;
113+
show("ready");
114+
},
115+
(error) => {
116+
if (error !== "missing") {
117+
console.error("open-java-format did not load", error);
118+
}
119+
show(error === "missing" ? "missing" : "failed");
120+
},
121+
);
122+
}
123+
124+
document$.subscribe(() => {
125+
const root = document.getElementById("playground");
126+
if (root !== null) {
127+
setUp(root);
128+
}
129+
});
130+
})();

‎docs/playground/index.md‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
title: Playground
3+
description: Paste Java code and see how open-java-format lays it out. It runs in your browser, nothing is sent anywhere.
4+
hide:
5+
- navigation
6+
- toc
7+
---
8+
9+
# Playground
10+
11+
Paste Java code on the left and press **Format**. The formatter runs in your browser, compiled to
12+
WebAssembly from the same release that [the other pages](../get-started/index.md) install: the code is
13+
not sent anywhere, and the result is what the command line would write.
14+
15+
<div id="playground" class="ojf-playground" markdown>
16+
17+
<div class="ojf-playground__pane">
18+
<label for="ojf-source">Your code</label>
19+
<textarea id="ojf-source" spellcheck="false" autocapitalize="off" autocomplete="off" wrap="off">
20+
class Example {
21+
private static void configureResolvedVersionsWithVersionMapping(Project project) {
22+
project.getPluginManager()
23+
.withPlugin(
24+
"maven-publish",
25+
plugin -> {
26+
project.getExtensions()
27+
.getByType(PublishingExtension.class)
28+
.getPublications()
29+
.withType(MavenPublication.class)
30+
.configureEach(
31+
publication ->
32+
publication.versionMapping(
33+
mapping -> {
34+
mapping.allVariants(
35+
VariantVersionMappingStrategy
36+
::fromResolutionResult);
37+
}));
38+
});
39+
}
40+
}
41+
</textarea>
42+
</div>
43+
44+
<div class="ojf-playground__pane">
45+
<label for="ojf-output">open-java-format {{ ojf_version }}</label>
46+
<textarea id="ojf-output" spellcheck="false" wrap="off" readonly></textarea>
47+
<button id="ojf-copy" class="ojf-playground__copy" type="button" hidden>Copy</button>
48+
</div>
49+
50+
<div class="ojf-playground__bar" markdown>
51+
52+
<button id="ojf-format" class="md-button md-button--primary" type="button" disabled>Format</button>
53+
<span class="ojf-playground__keys"><kbd id="ojf-modifier">Ctrl</kbd>+<kbd>Enter</kbd></span>
54+
55+
<div class="ojf-playground__status" role="status" aria-live="polite" markdown>
56+
57+
<p data-state="loading">Loading the formatter…</p>
58+
<p data-state="ready" hidden>Ready.</p>
59+
<p data-state="formatted" hidden>Formatted in <span data-millis></span> ms.</p>
60+
<p data-state="error" hidden>The code does not parse; the result pane says where.</p>
61+
<p data-state="bug" hidden>The formatter failed on this code. That is a bug: please
62+
<a href="https://github.com/openjavaformat/open-java-format/issues/new">report it</a> with the code.</p>
63+
64+
<div data-state="missing" hidden markdown>
65+
The playground comes with the next release of open-java-format: {{ ojf_version }}, the release this
66+
site installs, has no WebAssembly build yet. Until then the [command line](../get-started/command-line.md)
67+
gives the same result.
68+
</div>
69+
70+
<div data-state="unsupported" hidden markdown>
71+
This browser cannot run the formatter: it needs WebAssembly garbage collection, which current Chrome,
72+
Edge, Firefox and Safari have. The [command line](../get-started/command-line.md) gives the same result.
73+
</div>
74+
75+
<div data-state="failed" hidden markdown>
76+
The formatter did not load. Reload the page to try again, or use the
77+
[command line](../get-started/command-line.md), which gives the same result.
78+
</div>
79+
80+
</div>
81+
82+
</div>
83+
84+
</div>
85+
86+
Thank you to Denis Pavlyuchenko ([@pavlyuchenko_denis](https://t.me/pavlyuchenko_denis) on Telegram),
87+
who suggested a playground that runs in the browser, and to [cobr123](https://github.com/cobr123), whose
88+
[WebAssembly build](https://github.com/cobr123/open-java-format-wasm-demo) of the formatter this one
89+
started from.

‎docs/stylesheets/playground.css‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/* The Playground page, docs/playground/index.md: the code and the result side by side, stacked on a narrow screen.
2+
Colours and fonts are the theme's, so both palettes work. */
3+
4+
/* The Playground tab in the header stands out on every page: white on the theme's blue, like the Format button.
5+
Its href is relative, ./playground/ or ../playground/, so the selector matches the end. */
6+
.md-tabs__link[href$="playground/"] {
7+
margin-top: 0.5rem;
8+
padding: 0.25rem 0.6rem;
9+
border-radius: 0.2rem;
10+
background-color: var(--md-primary-fg-color);
11+
color: var(--md-primary-bg-color);
12+
opacity: 1;
13+
}
14+
15+
.md-tabs__link[href$="playground/"]:is(:hover, :focus) {
16+
background-color: var(--md-accent-fg-color);
17+
color: var(--md-accent-bg-color);
18+
}
19+
20+
.ojf-playground {
21+
display: grid;
22+
grid-template-columns: 1fr;
23+
gap: 1rem;
24+
}
25+
26+
/* The theme's own breakpoint for a wide screen. */
27+
@media screen and (min-width: 60em) {
28+
.ojf-playground {
29+
grid-template-columns: 1fr 1fr;
30+
}
31+
}
32+
33+
.ojf-playground__pane {
34+
position: relative;
35+
display: flex;
36+
flex-direction: column;
37+
min-width: 0;
38+
}
39+
40+
.ojf-playground__pane label {
41+
font-weight: 700;
42+
margin-bottom: 0.4rem;
43+
}
44+
45+
.ojf-playground textarea {
46+
box-sizing: border-box;
47+
width: 100%;
48+
height: 45vh;
49+
min-height: 16rem;
50+
padding: 0.8em 1.2em;
51+
border: 1px solid var(--md-default-fg-color--lightest);
52+
border-radius: 0.1rem;
53+
background-color: var(--md-code-bg-color);
54+
color: var(--md-code-fg-color);
55+
font-family: var(--md-code-font-family);
56+
font-size: 0.75rem;
57+
line-height: 1.4;
58+
tab-size: 4;
59+
white-space: pre;
60+
overflow: auto;
61+
resize: none;
62+
}
63+
64+
/* Side by side, the panes can take most of the window; stacked, two of them have to fit. */
65+
@media screen and (min-width: 60em) {
66+
.ojf-playground textarea {
67+
height: min(70vh, 40rem);
68+
}
69+
}
70+
71+
.ojf-playground textarea:focus {
72+
outline: 2px solid var(--md-accent-fg-color);
73+
outline-offset: -1px;
74+
}
75+
76+
.ojf-playground .ojf-playground__output--error {
77+
color: var(--md-typeset-del-color, #d32f2f);
78+
}
79+
80+
.ojf-playground__copy {
81+
position: absolute;
82+
top: 2.2rem;
83+
right: 0.6rem;
84+
padding: 0.1rem 0.5rem;
85+
border: 1px solid var(--md-default-fg-color--lightest);
86+
border-radius: 0.1rem;
87+
background-color: var(--md-default-bg-color);
88+
color: var(--md-default-fg-color--light);
89+
font-size: 0.7rem;
90+
cursor: pointer;
91+
}
92+
93+
.ojf-playground__bar {
94+
grid-column: 1 / -1;
95+
display: flex;
96+
flex-wrap: wrap;
97+
align-items: center;
98+
gap: 0.6rem 1rem;
99+
}
100+
101+
.ojf-playground__bar .md-button {
102+
margin: 0;
103+
}
104+
105+
.ojf-playground__bar .md-button:disabled {
106+
opacity: 0.5;
107+
cursor: progress;
108+
}
109+
110+
.ojf-playground__keys {
111+
color: var(--md-default-fg-color--light);
112+
font-size: 0.75rem;
113+
}
114+
115+
.ojf-playground__status {
116+
flex: 1 1 20rem;
117+
}
118+
119+
.ojf-playground__status > * {
120+
margin: 0;
121+
}

0 commit comments

Comments
 (0)