diff --git a/.github/workflows/build-web-demo/action.yaml b/.github/workflows/build-web-demo/action.yaml
index 9e15bf15..ab467b87 100644
--- a/.github/workflows/build-web-demo/action.yaml
+++ b/.github/workflows/build-web-demo/action.yaml
@@ -1,10 +1,24 @@
name: Build Web Demo
description: Build the web demo
+on:
+ workflow_call:
+ inputs:
+ git_ref:
+ description: 'The git ref to checkout and build'
+ required: false
+ default: ''
runs:
using: "composite"
steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ if: ${{ inputs.git_ref != '' }}
+ with:
+ fetch-depth: 0
+ ref: ${{ inputs.git_ref }}
+
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
diff --git a/.github/workflows/deploy-web-demo.yaml b/.github/workflows/deploy-web-demo.yaml
index 20e8fc87..d93a5a72 100644
--- a/.github/workflows/deploy-web-demo.yaml
+++ b/.github/workflows/deploy-web-demo.yaml
@@ -3,10 +3,16 @@ name: Build and Deploy Web Demo to GitHub Pages
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
+ inputs:
+ git_ref:
+ description: 'The git ref to checkout and build'
+ required: false
push:
branches:
- master
+ tags:
+ - v*
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
@@ -22,27 +28,94 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.set-version.outputs.VERSION }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
+ - name: Determine version
+ id: set-version
+ run: |
+ VERSION="${{ github.event.inputs.git_ref || github.ref_name }}"
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
+ echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
+
- name: Build Web Demo
uses: ./.github/workflows/build-web-demo/
+ with:
+ git_ref: ${{ env.VERSION }}
- name: Setup Pages
uses: actions/configure-pages@v5
- - name: Upload artifact
+ - name: Upload wasm-pack artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: 'web/pkg/'
+ name: 'wasm'
+
+ - name: Upload Vite artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'web/demo/dist/'
+ name: 'vite'
deploy:
runs-on: ubuntu-latest
needs: build
+ permissions:
+ contents: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
+ - name: Set VERSION environment variable
+ run: echo "VERSION=${{ needs.build.outputs.VERSION }}" >> $GITHUB_ENV
+
+ - name: Checkout gh-pages branch
+ uses: actions/checkout@v4
+ with:
+ ref: gh-pages
+ fetch-depth: 0
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: ./
+
+ - name: Extract artifact
+ run: |
+ set -e
+
+ PKG_DIR=pkg/${VERSION:?}
+ rm -rf "$PKG_DIR"
+ mkdir -p "$PKG_DIR"
+ tar -xvf wasm/artifact.tar -C "$PKG_DIR" --wildcards "*.js" "*.wasm"
+ rm wasm/artifact.tar
+
+ tar -xvf vite/artifact.tar
+ rm vite/artifact.tar
+
+ - name: Update versions.json
+ run: |
+ set -e
+
+ readarray -t versions < <(find pkg -mindepth 1 -maxdepth 1 -type d -not -name '.*' -printf '%P\n' | sort -Vr)
+ echo "[$(printf '"%s",' "${versions[@]}" | sed 's/,$//')]" > versions.json
+
+ - name: Create .nojekyll file
+ run: |
+ touch .nojekyll
+
+ - name: Commit and push changes
+ run: |
+ set -e
+
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add -A
+ git commit -m "Deploy ${VERSION:?}" || echo "No changes to commit"
+ git push origin gh-pages
\ No newline at end of file
diff --git a/web/demo/index.html b/web/demo/index.html
index 62662b2f..1b934cb0 100644
--- a/web/demo/index.html
+++ b/web/demo/index.html
@@ -4,7 +4,7 @@
pasfmt demo
-
+
@@ -124,7 +152,11 @@ Settings
-
+
+
diff --git a/web/demo/public/versions.json b/web/demo/public/versions.json
new file mode 100644
index 00000000..0637a088
--- /dev/null
+++ b/web/demo/public/versions.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/web/demo/src/index.ts b/web/demo/src/index.ts
index 63939b60..17e42f4b 100644
--- a/web/demo/src/index.ts
+++ b/web/demo/src/index.ts
@@ -1,5 +1,3 @@
-import init, { fmt, default_settings_toml, SettingsWrapper } from "../../pkg";
-
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
// support for ini, good enough for our TOML config
@@ -21,7 +19,106 @@ import "monaco-editor/esm/vs/editor/contrib/wordOperations/browser/wordOperation
// custom delphi tokenizer
import * as delphi from "./delphi";
-await init();
+const BASE_URL = import.meta.env.BASE_URL;
+
+const url = new URL(window.location.href);
+const version_param = url.searchParams.get("version");
+const source_param = url.searchParams.get("source");
+const settings_param = url.searchParams.get("settings");
+
+// All use of this needs to be compatible with all supported versions of the WASM module.
+let pasfmt: any;
+
+// Load a specific version
+const loadWasmVersion = async (version: string) => {
+ // Dynamically import the JS glue code
+ const mod = import.meta.env.DEV
+ ? await import("../../pkg/web.js")
+ : await import(/* @vite-ignore */ `${BASE_URL}pkg/${version}/web.js`);
+
+ await mod.default();
+
+ console.log(`Loaded WASM module version: ${version}`);
+
+ pasfmt = mod;
+};
+
+const loadVersion = async () => {
+ await loadWasmVersion(versionPicker.value);
+};
+
+const versionPicker = document.getElementById(
+ "version-picker"
+)! as HTMLSelectElement;
+
+await fetch(`${BASE_URL}versions.json`)
+ .then((res) => res.json())
+ .then((versions: Array) => {
+ versions.forEach((v) => {
+ const opt = document.createElement("option");
+ opt.value = v;
+ opt.textContent = v;
+ versionPicker.appendChild(opt);
+ });
+
+ if (version_param !== null) {
+ const decoded = atob(version_param);
+ versionPicker.value = decoded;
+ if (!versionPicker.value) {
+ console.log(`Invalid version from search parameters: ${decoded}`);
+ }
+ } else {
+ versionPicker.value = versions[0];
+ }
+ })
+ .then(loadVersion);
+
+const showLoadingSpinner = () => {
+ versionPicker.disabled = true;
+ versionPicker.classList.add('loading')
+}
+
+const hideLoadingSpinner = () => {
+ versionPicker.disabled = false;
+ versionPicker.classList.remove('loading')
+}
+
+versionPicker.addEventListener("change", async () => {
+ showLoadingSpinner();
+ await loadVersion();
+ hideLoadingSpinner();
+ updateSetingsOnVersionChange();
+ formatEditors();
+});
+
+const updateSetingsOnVersionChange = () => {
+ const new_valid_keys = defaultSettings()
+ .split("\n")
+ .map((line) => line.split("=")[0].trim());
+ const new_settings = settingsEditor
+ .getValue()
+ .split("\n")
+ .map((line) => {
+ if (!line.includes("=")) {
+ return line;
+ }
+
+ const commented_out = "#(unavailable)";
+ if (line.startsWith(commented_out)) {
+ line = line.substring(commented_out.length);
+ }
+
+ let key = line.split("=")[0].trim();
+ if (new_valid_keys.includes(key)) {
+ return line;
+ } else {
+ return commented_out + line;
+ }
+ })
+ .join("\n");
+
+ settingsEditor.setValue(new_settings);
+};
const diffEditorContainer = document.getElementById("diffpane")!;
const sideBySideContainer = document.getElementById("editpane")!;
@@ -52,13 +149,14 @@ const settingsEditor = monaco.editor.create(settingsDiv, {
const resetDefaultSettingsButton = document.getElementById(
"resetToDefaultSettings"
)!;
-const resetSettings = () => settingsEditor.setValue(default_settings_toml());
+const defaultSettings = (): string => pasfmt.default_settings_toml();
+const resetSettings = () => settingsEditor.setValue(defaultSettings());
resetSettings();
resetDefaultSettingsButton.onclick = resetSettings;
const parseSettings = () => {
try {
- return new SettingsWrapper(settingsEditor.getValue());
+ return new pasfmt.SettingsWrapper(settingsEditor.getValue());
} catch (error) {
throw new Error("Failed to parse settings", {
cause: error,
@@ -70,10 +168,10 @@ const closeModalButton = document.getElementById(
"closeModal"
) as HTMLButtonElement;
-const setSettingsBorderCol = (col) =>
+const setSettingsBorderCol = (col: string) =>
document.documentElement.style.setProperty("--settings-border", col);
-let settingsErrorTimeout;
+let settingsErrorTimeout: number;
let settingsValid = true;
settingsModel.onDidChangeContent(() => {
clearTimeout(settingsErrorTimeout);
@@ -175,7 +273,7 @@ document
.getElementById("toggle-view")!
.addEventListener("click", toggleDiffView);
-const renderErrorInModel = (error, model) => {
+const renderErrorInModel = (error: any, model: monaco.editor.ITextModel) => {
const errorMessage =
error + (error.cause ? `\nCaused by: ${error.cause}` : "");
const markers = [
@@ -192,11 +290,11 @@ const renderErrorInModel = (error, model) => {
monaco.editor.setModelMarkers(model, "", markers);
};
-const clearErrorsInModel = (model) => {
+const clearErrorsInModel = (model: monaco.editor.ITextModel) => {
monaco.editor.setModelMarkers(model, "", []);
};
-const updateRulers = (maxLineLen) => {
+const updateRulers = (maxLineLen: number) => {
originalEditor.updateOptions({ rulers: [makeRuler(maxLineLen)] });
formattedEditor.updateOptions({ rulers: [makeRuler(maxLineLen)] });
if (diffEditor !== undefined) {
@@ -208,7 +306,7 @@ const formatEditors = () => {
try {
let settingsObj = parseSettings();
updateRulers(settingsObj.max_line_len());
- formattedModel.setValue(fmt(originalModel.getValue(), settingsObj));
+ formattedModel.setValue(pasfmt.fmt(originalModel.getValue(), settingsObj));
} catch (error) {
console.log(error);
renderErrorInModel(error, formattedModel);
@@ -245,19 +343,15 @@ document
.getElementById("sample-picker")!
.addEventListener("change", loadSample);
-const url = new URL(window.location.href);
-let source = url.searchParams.get("source");
-let settings = url.searchParams.get("settings");
-
-if (source !== null) {
- let decoded = atob(source);
+if (source_param !== null) {
+ let decoded = atob(source_param);
originalEditor.setValue(decoded);
} else {
- loadSampleFile("/pasfmt/examples/simple.pas");
+ loadSampleFile(`${BASE_URL}examples/simple.pas`);
}
-if (settings !== null) {
- let decoded = atob(settings);
+if (settings_param !== null) {
+ let decoded = atob(settings_param);
settingsEditor.setValue(decoded);
}
@@ -267,6 +361,7 @@ const shareExample = document.getElementById(
shareExample.onclick = () => {
url.searchParams.set("source", btoa(originalEditor.getValue()));
url.searchParams.set("settings", btoa(settingsEditor.getValue()));
+ url.searchParams.set("version", btoa(versionPicker.value));
window.history.replaceState(null, "", url);
navigator.clipboard.writeText(window.location.href);
};
diff --git a/web/demo/src/vite-env.d.ts b/web/demo/src/vite-env.d.ts
new file mode 100644
index 00000000..151aa685
--- /dev/null
+++ b/web/demo/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/web/src/lib.rs b/web/src/lib.rs
index ff1030ab..d6202b7e 100644
--- a/web/src/lib.rs
+++ b/web/src/lib.rs
@@ -2,6 +2,9 @@ use pasfmt::{make_formatter, FormattingConfig};
use pasfmt_core::prelude::FileOptions;
use wasm_bindgen::prelude::*;
+// Everything here needs to be kept backwards-compatible, because the web demo
+// uses the same JavaScript to interface with many versions of the WASM module.
+
#[wasm_bindgen]
pub struct SettingsWrapper {
config: FormattingConfig,