From 757764b1782f7e808613b0a480c2993d5e1de4e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 18:46:35 +0000 Subject: [PATCH] Open a class the playground hands over as #playground= The playground's Open in Layout Editor carries its app class in the share fragment Open in Playground already writes; the editor now reads that format the other way and imports the first file as Open ABAP class does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PBPa75o3TYJo1ZCSndPFY7 --- README.md | 3 +++ src/app.js | 6 +++++- src/handoff.js | 21 +++++++++++++++++++++ src/ui/files.js | 17 ++++++++++++++++- test/handoff.test.mjs | 11 ++++++++++- 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ac83794..3b36f9c 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ Also in the editor: a `.clas.abap` file, or link it with `?src=` (the playground's parameter; raw.githubusercontent.com, a github.com file page, or a gist) or `?class=&repo=[&ref=][&path=]`. + The playground's **Open in Layout Editor** hands its app class over as + `#playground=`, the format **Open in Playground** + writes, read the other way; the first file is the class. The `z2ui5_cl_ui5_view_builder` chain becomes the design, the public attributes the data, `client->_event( )` the event names. Export writes the chain back into that class and leaves every other line as it was; diff --git a/src/app.js b/src/app.js index e6e04d2..955819d 100644 --- a/src/app.js +++ b/src/app.js @@ -22,7 +22,7 @@ import { renderCode, highlightCode, setCode, setView, copyText } from './ui/code import { loadPreview, postPreview, sendPreview } from './ui/preview.js'; import { handleDrop, copySelection, pasteSelection, addControlByName } from './ui/edit.js'; import { openPalette } from './ui/palette.js'; -import { newDialog, importDialog, openClassDialog, openClassFromUrl, recentDialog, shareLink, openFile, saveDesign, exportZip, openInPlayground, openInVscode } from './ui/files.js'; +import { newDialog, importDialog, openClassDialog, openClassFromUrl, openClassFromPlayground, recentDialog, shareLink, openFile, saveDesign, exportZip, openInPlayground, openInVscode } from './ui/files.js'; import { onKey } from './ui/keys.js'; @@ -239,5 +239,9 @@ function openLinkedDesign() { openLinkedDesign(); window.addEventListener('hashchange', openLinkedDesign); +// a class the playground handed over: #playground= +openClassFromPlayground(); +window.addEventListener('hashchange', openClassFromPlayground); + // for the browser tests and the curious: the state and the generators window.designer = { state, generateClass, generateClassSource, toXml, validate, select, commit, handleDrop, openPalette, copySelection, pasteSelection, addControlByName }; diff --git a/src/handoff.js b/src/handoff.js index 050680a..f323134 100644 --- a/src/handoff.js +++ b/src/handoff.js @@ -28,6 +28,27 @@ export async function deflate(text) { return new Uint8Array(await new Response(new Blob([bytes]).stream().pipeThrough(new CompressionStream('deflate-raw'))).arrayBuffer()); } +export function fromBase64Url(text) { + const bin = atob(text.replace(/-/g, '+').replace(/_/g, '/')); + return Uint8Array.from(bin, (c) => c.charCodeAt(0)); +} + +export async function inflate(bytes) { + return new TextDecoder().decode(await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer()); +} + +// The way back: the playground's "Open in Layout Editor" hands its app class +// over as #playground=, the format below read the other +// way. Refuses what is not that format rather than guessing at it. +export async function filesFromPlaygroundFragment(fragment) { + if (!fragment || fragment[0] !== '2') throw new Error('not a playground link of a version this editor reads'); + const files = JSON.parse(await inflate(fromBase64Url(fragment.slice(1)))); + if (!Array.isArray(files) || !files.length || files.some((f) => typeof f?.name !== 'string' || typeof f?.source !== 'string')) { + throw new Error('the link holds no files'); + } + return files; +} + // the playground's fragment for one or more files: version 2 export async function playgroundFragment(files) { const payload = JSON.stringify(files.map(({ name, source }) => ({ name, source }))); diff --git a/src/ui/files.js b/src/ui/files.js index ac19a38..d4ce11e 100644 --- a/src/ui/files.js +++ b/src/ui/files.js @@ -5,7 +5,7 @@ import { clone, walk, normalizeDesign, validate, checkAttributeName } from '../m import { copyText } from './code.js'; import { fileName } from '../abap.js'; import { fromClass, classReport, classFiles, generateClassSource, sourceUrl, repoClassUrl } from '../clas.js'; -import { playgroundLink, vscodeLink, PLAYGROUND_URL } from '../handoff.js'; +import { playgroundLink, vscodeLink, filesFromPlaygroundFragment, PLAYGROUND_URL } from '../handoff.js'; import { zip } from '../zip.js'; import { state, commit, replaceDesign, showPanel, recentDesigns, designToLink } from './store.js'; import { $, h, toast, download, dialog } from './dom.js'; @@ -185,6 +185,21 @@ export function openClassFromUrl(params) { fetchClass(url); } +// #playground=: a class the playground handed over (its "Open in +// Layout Editor"). The first file is the app; it replaces the design the way +// Open ABAP class does, and the address is cleaned either way. +export async function openClassFromPlayground() { + if (!location.hash.startsWith('#playground=')) return; + const fragment = location.hash.slice('#playground='.length); + history.replaceState(null, '', location.pathname + location.search); + try { + const [app] = await filesFromPlaygroundFragment(fragment); + importClassText(app.source); + } catch (err) { + toast(`Cannot open the class from the playground: ${err.message}`); + } +} + async function fetchClass(raw) { try { const href = sourceUrl(raw, location.href); diff --git a/test/handoff.test.mjs b/test/handoff.test.mjs index 8238308..4885573 100644 --- a/test/handoff.test.mjs +++ b/test/handoff.test.mjs @@ -8,7 +8,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { allDesigns } from './fixtures.mjs'; import { generateClass } from '../src/abap.js'; -import { playgroundLink, playgroundFragment, vscodeLink, PLAYGROUND_URL } from '../src/handoff.js'; +import { playgroundLink, playgroundFragment, filesFromPlaygroundFragment, vscodeLink, PLAYGROUND_URL } from '../src/handoff.js'; const fromBase64Url = (text) => Uint8Array.from(atob(text.replaceAll('-', '+').replaceAll('_', '/')), (c) => c.charCodeAt(0)); const inflate = async (bytes) => new TextDecoder().decode(await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer()); @@ -39,3 +39,12 @@ test('the VS Code link carries the class for the extension\'s URI handler', asyn assert.equal(params.get('name'), 'zcl_my_app.clas.abap'); assert.equal(await inflate(fromBase64Url(params.get('code'))), source); }); + +test('a class the playground hands over (#playground=) reads back as the files it wrote', async () => { + const files = [{ name: 'zcl_my_app.clas.abap', source }, { name: 'zcl_helper.clas.abap', source: 'CLASS zcl_helper DEFINITION.' }]; + assert.deepEqual(await filesFromPlaygroundFragment(await playgroundFragment(files)), files); + await assert.rejects(filesFromPlaygroundFragment('1abc'), /version/); + await assert.rejects(filesFromPlaygroundFragment(''), /version/); + const empty = await playgroundFragment([]); + await assert.rejects(filesFromPlaygroundFragment(empty), /no files/); +});