mirror of
https://github.com/alexandrev/xslt-lab.git
synced 2026-09-13 08:43:16 +00:00
Feat: runnable examples on the reference pages via an embedded mode
The 230 function pages linked out to the playground; now they can run their own example in place. ?embed=1 renders the app without ads or workspace chrome, plus a link out to the full editor. The iframe is only created on click: embedding it eagerly on every reference page would pull the editor bundle into 230 page loads and undo the LCP work. Also adds ?template=<id> so a URL can open a starter workspace directly, and an XPath tester template. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
This commit is contained in:
+38
-2
@@ -13,7 +13,7 @@ import {
|
||||
detectVersionUpgradeHint,
|
||||
findErrorReference,
|
||||
} from "./lib/workspaceUtils";
|
||||
import { templateToWorkspace } from "./lib/templates";
|
||||
import { templateToWorkspace, findTemplate } from "./lib/templates";
|
||||
|
||||
/* global __APP_VERSION__, __GIT_COMMIT__ */
|
||||
|
||||
@@ -416,6 +416,17 @@ function defaultTab(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Embedded mode (?embed=1): the app is rendered inside an iframe on the blog's
|
||||
// reference pages, so it drops the ads and workspace chrome and shows just the
|
||||
// editor plus a way out to the full app.
|
||||
const IS_EMBED = (() => {
|
||||
try {
|
||||
return new URLSearchParams(window.location.search).get("embed") === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const MAX_WORKSPACES = 3;
|
||||
const WORKSPACE_EXPORT_VERSION = 1;
|
||||
const RESULT_HEIGHT_KEY = "resultPaneHeight";
|
||||
@@ -675,6 +686,16 @@ export default function App() {
|
||||
);
|
||||
}
|
||||
}
|
||||
// ?template=<id> opens a starter workspace directly — this is what the
|
||||
// /xpath-tester/ and /xml-to-json/ landing pages link to.
|
||||
try {
|
||||
const templateId = new URLSearchParams(window.location.search).get("template");
|
||||
const template = templateId ? findTemplate(templateId) : null;
|
||||
if (template) {
|
||||
initialTabs = [defaultTab(templateToWorkspace(template))];
|
||||
}
|
||||
} catch {}
|
||||
|
||||
let initialActive = initialTabs[0]?.id;
|
||||
try {
|
||||
const sAct = localStorage.getItem("active");
|
||||
@@ -821,6 +842,7 @@ export default function App() {
|
||||
typeof window !== "undefined" &&
|
||||
/^(localhost|127(?:\\.[0-9]+){3}|mac)$/i.test(window.location.hostname);
|
||||
const ethicalAdsEnabled =
|
||||
!IS_EMBED &&
|
||||
Boolean(ethicalAdsPublisher) &&
|
||||
(!isLocalhost || env.VITE_ETHICALADS_DEV === "true");
|
||||
const ethicalAdVariant = "stickybox";
|
||||
@@ -1902,7 +1924,7 @@ export default function App() {
|
||||
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className={`app-container${IS_EMBED ? " app-container--embed" : ""}`}>
|
||||
<h1 className="sr-only">XSLT Playground - Online XSLT Editor and Tester</h1>
|
||||
{ethicalAdsEnabled && (
|
||||
<div
|
||||
@@ -1915,6 +1937,19 @@ export default function App() {
|
||||
aria-label="Advertisement"
|
||||
/>
|
||||
)}
|
||||
{IS_EMBED && (
|
||||
<div className="embed-bar">
|
||||
<span>XSLT Playground</span>
|
||||
<a
|
||||
href={typeof window !== "undefined" ? buildShareUrl(activeTab) : "https://xsltplayground.com/"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Open in the full editor →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!IS_EMBED && (
|
||||
<div className="tabs">
|
||||
<TabsNav
|
||||
tabs={tabs}
|
||||
@@ -1967,6 +2002,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="main">
|
||||
{paramsCollapsed ? (
|
||||
<div className="params-collapsed">
|
||||
|
||||
@@ -156,8 +156,35 @@ export const TEMPLATES = [
|
||||
{ name: "country", value: "ES", open: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "xpath-tester",
|
||||
title: "XPath tester",
|
||||
description: "Evaluate an XPath expression against XML and list every match.",
|
||||
version: "3.0",
|
||||
xslt: `<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="xml" indent="yes"/>
|
||||
|
||||
<!-- Put the expression you want to test here. -->
|
||||
<xsl:variable name="expression" select="//order[@country = 'ES']"/>
|
||||
|
||||
<xsl:template match="/">
|
||||
<matches count="{count($expression)}">
|
||||
<xsl:for-each select="$expression">
|
||||
<match position="{position()}">
|
||||
<xsl:copy-of select="."/>
|
||||
</match>
|
||||
</xsl:for-each>
|
||||
</matches>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`,
|
||||
params: [{ name: "input", value: ORDERS_XML, open: true }],
|
||||
},
|
||||
];
|
||||
|
||||
export function findTemplate(id) {
|
||||
return TEMPLATES.find((t) => t.id === id) || null;
|
||||
}
|
||||
|
||||
// Strip the presentation-only fields before handing a template to defaultTab().
|
||||
export function templateToWorkspace(template) {
|
||||
return {
|
||||
|
||||
@@ -2205,3 +2205,36 @@ a:focus-visible {
|
||||
border-color: #2b3645;
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
/* Embedded mode (?embed=1) — the app inside an iframe on the reference pages. */
|
||||
.app-container--embed {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.embed-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
background: #f0f6ff;
|
||||
border-bottom: 1px solid #cfdcf4;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: #21426c;
|
||||
}
|
||||
|
||||
.embed-bar a {
|
||||
color: #007acc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.embed-bar a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .embed-bar {
|
||||
background: #161c24;
|
||||
border-color: #2b3645;
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
@@ -54,5 +54,6 @@
|
||||
<p class="muted">Repo: <a href="https://github.com/alexandrev/xslt-lab">alexandrev/xslt-lab</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
{{ block "scripts" . }}{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -30,7 +30,12 @@
|
||||
{{- end }}
|
||||
<div class="ref-try">
|
||||
{{ if .Params.tryUrl }}
|
||||
<a href="{{ .Params.tryUrl }}" class="button" target="_blank" rel="noopener noreferrer">Open live example in XSLT Playground →</a>
|
||||
{{- /* The playground runs inline in an iframe, but only once the reader asks
|
||||
for it: embedding it eagerly on 230 reference pages would load the
|
||||
whole editor bundle on every page view and wreck the LCP work. */ -}}
|
||||
<button type="button" class="button ref-run" data-run-url="{{ .Params.tryUrl }}">▶ Run this example here</button>
|
||||
<a href="{{ .Params.tryUrl }}" class="button button--ghost" target="_blank" rel="noopener noreferrer">Open in the full editor →</a>
|
||||
<div class="ref-embed" hidden></div>
|
||||
{{ else }}
|
||||
<a href="https://xsltplayground.com/" class="button" target="_blank" rel="noopener noreferrer">Try it in XSLT Playground →</a>
|
||||
{{ end }}
|
||||
@@ -39,3 +44,26 @@
|
||||
{{ partial "related-posts.html" . }}
|
||||
</article>
|
||||
{{ end }}
|
||||
{{ define "scripts" }}
|
||||
<script>
|
||||
// Lazily swap the button for an embedded playground running this page's example.
|
||||
document.querySelectorAll(".ref-run").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var host = btn.parentElement.querySelector(".ref-embed");
|
||||
if (!host || host.dataset.loaded) return;
|
||||
var url = btn.dataset.runUrl;
|
||||
url += (url.indexOf("?") === -1 ? "?" : "&") + "embed=1";
|
||||
var frame = document.createElement("iframe");
|
||||
frame.src = url;
|
||||
frame.title = "XSLT Playground — live example";
|
||||
frame.loading = "lazy";
|
||||
frame.className = "ref-embed-frame";
|
||||
host.appendChild(frame);
|
||||
host.hidden = false;
|
||||
host.dataset.loaded = "1";
|
||||
btn.disabled = true;
|
||||
btn.textContent = "▶ Running below";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
@@ -494,3 +494,16 @@ code {
|
||||
.post-list li { flex-direction: column; align-items: flex-start; }
|
||||
.ref-index-list a { min-width: unset; }
|
||||
}
|
||||
|
||||
/* Runnable example embedded on reference pages (loaded on click only). */
|
||||
.ref-try { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
||||
.button--ghost { background: transparent; border: 1px solid currentColor; }
|
||||
.ref-run[disabled] { opacity: 0.6; cursor: default; }
|
||||
.ref-embed { width: 100%; margin-top: 0.75rem; }
|
||||
.ref-embed-frame {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
border: 1px solid #cfdcf4;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user