- XSLT Playground is the most powerful free online XSLT tool available. - Unlike every other online XSLT tester, it supports XSLT 1.0, XSLT 2.0, and XSLT 3.0 - via Saxon HE 12.5 — the same enterprise-grade processor used in production - applications worldwide. No installation, no account, no limits. + XSLT Playground is the most powerful free online XSLT tool available — an + editor, tester, viewer and validator in one. Unlike every other online XSLT tester, it supports + XSLT 1.0, XSLT 2.0, and XSLT 3.0 via Saxon HE 12.5 — the same + enterprise-grade processor used in production applications worldwide. Run any XSLT transformation + online and see the result instantly. No installation, no account, no limits.
Open the XSLT Editor →
+ XSLT Playground works as an online XSLT viewer: paste a stylesheet and an XML
+ document to view the formatted transformation output, with syntax highlighting and a live HTML
+ preview when your stylesheet uses method="html". It is also a full
+ XSLT validator — invalid stylesheets are flagged with the exact line number and
+ the original Saxon error message, so you can validate XSLT online before deploying it to production.
+
+ Every XSLT transformation online runs against a real Saxon backend in + milliseconds. There is nothing to install and nothing to configure: open the editor, paste your + code, and the result appears automatically as you type. +
+xsl:for-each-group), regular expressions, multiple output documents, XPath 2.0.+ This stylesheet groups a flat list of sales by region and totals each group — a task that needs + verbose Muenchian keys in XSLT 1.0 but is trivial in XSLT 2.0. Paste it into the + XSLT 2.0 editor to run it. +
+XML input:
+<sales>
+ <sale region="EU" amount="120"/>
+ <sale region="US" amount="200"/>
+ <sale region="EU" amount="80"/>
+ <sale region="US" amount="50"/>
+</sales>
+ XSLT 2.0 stylesheet:
+<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:output method="xml" indent="yes"/>
+ <xsl:template match="/sales">
+ <totals>
+ <xsl:for-each-group select="sale" group-by="@region">
+ <region name="{current-grouping-key()}"
+ total="{sum(current-group()/@amount)}"/>
+ </xsl:for-each-group>
+ </totals>
+ </xsl:template>
+</xsl:stylesheet>
+ Output:
+<totals>
+ <region name="EU" total="200"/>
+ <region name="US" total="250"/>
+</totals>
+
+ Looking for XSLT 3.0 with maps, arrays and higher-order functions? Try the XSLT 3.0 Online Tester →
diff --git a/frontend/public/xslt-3-0/index.html b/frontend/public/xslt-3-0/index.html index 17a4037b..492cfe5e 100644 --- a/frontend/public/xslt-3-0/index.html +++ b/frontend/public/xslt-3-0/index.html @@ -131,6 +131,8 @@ ul, ol { color: #374151; line-height: 1.75; padding-left: 1.5rem; } li { margin-bottom: 0.4rem; } code { background: #eef2ff; padding: 0.1em 0.35em; border-radius: 3px; font-size: 0.9em; } + pre { background: #1e293b; color: #e2e8f0; padding: 1rem 1.25rem; border-radius: 8px; overflow-x: auto; line-height: 1.5; } + pre code { background: none; padding: 0; color: inherit; font-size: 0.86rem; } .feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); @@ -284,6 +286,39 @@
+ This stylesheet builds a map of currency rates and uses the higher-order function
+ fn:fold-left() to sum a converted total — both impossible in XSLT 2.0. Paste it into
+ the XSLT 3.0 editor to run it.
+
XSLT 3.0 stylesheet:
+<xsl:stylesheet version="3.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xsl:output method="text"/>
+ <xsl:template name="xsl:initial-template">
+ <xsl:variable name="rates" select="map{'EUR': 1.0, 'USD': 0.92, 'GBP': 1.17}"/>
+ <xsl:variable name="amounts" select="100, 200, 50"/>
+ <xsl:variable name="total" select="
+ fold-left($amounts, 0.0, function($acc, $n) { $acc + $n * $rates('USD') })"/>
+ <xsl:value-of select="format-number($total, '0.00')"/>
+ </xsl:template>
+</xsl:stylesheet>
+ Output:
+322.00
+
+ Run it with no XML input — the named xsl:initial-template is an XSLT 3.0 entry point
+ that needs no source document.
+
Need XSLT 2.0 with grouping and regular expressions? Try the XSLT 2.0 Online Tester →
diff --git a/scripts/seo_report.py b/scripts/seo_report.py new file mode 100644 index 00000000..74a511b0 --- /dev/null +++ b/scripts/seo_report.py @@ -0,0 +1,198 @@ +"""GSC + GA4 report for xsltplayground.com — last 28 days vs prior 28 days.""" +import json +from datetime import date, timedelta +from google.oauth2 import service_account +import google.auth.transport.requests +import requests as req_lib + +SA_KEY = json.load(open("/tmp/sa_key.json")) +SITE = "sc-domain:xsltplayground.com" +GA4_PROPERTY = "495227679" # xslt-playground (verified via admin list) + +today = date.today() +end = today - timedelta(days=1) +start = end - timedelta(days=27) +prev_end = start - timedelta(days=1) +prev_start = prev_end - timedelta(days=27) + + +def gsc_token(): + creds = service_account.Credentials.from_service_account_info( + SA_KEY, scopes=["https://www.googleapis.com/auth/webmasters.readonly"]) + creds.refresh(google.auth.transport.requests.Request(session=req_lib.Session())) + return creds.token + + +def gsc_query(token, dims, start_d, end_d, row_limit=25, filters=None): + import urllib.parse + site = urllib.parse.quote(SITE, safe="") + url = f"https://www.googleapis.com/webmasters/v3/sites/{site}/searchAnalytics/query" + body = { + "startDate": str(start_d), "endDate": str(end_d), + "dimensions": dims, "rowLimit": row_limit, + } + if filters: + body["dimensionFilterGroups"] = filters + r = req_lib.post(url, headers={"Authorization": "Bearer " + token, + "Content-Type": "application/json"}, json=body) + if r.status_code != 200: + return {"error": f"HTTP {r.status_code}: {r.text[:200]}"} + return r.json() + + +def totals(rows): + c = sum(r["clicks"] for r in rows) + i = sum(r["impressions"] for r in rows) + ctr = (c / i * 100) if i else 0 + pos = (sum(r["position"] * r["impressions"] for r in rows) / i) if i else 0 + return c, i, ctr, pos + + +print("=" * 70) +print(f" GSC REPORT — {SITE}") +print(f" Current: {start} → {end}") +print(f" Previous: {prev_start} → {prev_end}") +print("=" * 70) + +token = gsc_token() + +# Site totals current vs previous (use date dimension to get all rows) +cur = gsc_query(token, ["date"], start, end, row_limit=100) +prev = gsc_query(token, ["date"], prev_start, prev_end, row_limit=100) +if "error" in cur: + print("GSC ERROR:", cur["error"]) +else: + cc, ci, cctr, cpos = totals(cur.get("rows", [])) + pc, pi, pctr, ppos = totals(prev.get("rows", [])) + print(f"\n TOTALS (28d) current previous delta") + print(f" Clicks {cc:>10} {pc:>10} {cc-pc:+}") + print(f" Impressions {ci:>10} {pi:>10} {ci-pi:+}") + print(f" CTR {cctr:>9.2f}% {pctr:>9.2f}% {cctr-pctr:+.2f}pp") + print(f" Avg position {cpos:>10.1f} {ppos:>10.1f} {cpos-ppos:+.1f}") + +# Top queries +print("\n TOP QUERIES (by impressions, current 28d)") +q = gsc_query(token, ["query"], start, end, row_limit=30) +print(f" {'query':<42} {'clk':>5} {'impr':>7} {'ctr':>6} {'pos':>5}") +for r in sorted(q.get("rows", []), key=lambda x: -x["impressions"])[:25]: + kw = r["keys"][0][:40] + print(f" {kw:<42} {r['clicks']:>5} {r['impressions']:>7} " + f"{r['ctr']*100:>5.1f}% {r['position']:>5.1f}") + +# Top pages +print("\n TOP PAGES (by clicks, current 28d)") +p = gsc_query(token, ["page"], start, end, row_limit=30) +print(f" {'page':<52} {'clk':>5} {'impr':>7} {'pos':>5}") +for r in sorted(p.get("rows", []), key=lambda x: -x["clicks"])[:20]: + pg = r["keys"][0].replace("https://xsltplayground.com", "").replace( + "https://blog.xsltplayground.com", "[blog]")[:50] + print(f" {pg:<52} {r['clicks']:>5} {r['impressions']:>7} {r['position']:>5.1f}") + +# High-impression low-CTR queries (opportunity) +print("\n OPPORTUNITY: high impressions, position 5-20, low CTR") +print(f" {'query':<42} {'impr':>7} {'ctr':>6} {'pos':>5}") +opps = [r for r in q.get("rows", []) + if r["impressions"] >= 30 and 4 < r["position"] <= 20 and r["ctr"] < 0.03] +for r in sorted(opps, key=lambda x: -x["impressions"])[:15]: + kw = r["keys"][0][:40] + print(f" {kw:<42} {r['impressions']:>7} {r['ctr']*100:>5.1f}% {r['position']:>5.1f}") + +# Striking distance: position 8-20 (close to page 1 / top of page 1) +print("\n STRIKING DISTANCE: position 8-20 (push to top with content/links)") +print(f" {'query':<42} {'impr':>7} {'pos':>5}") +strike = [r for r in q.get("rows", []) + if r["impressions"] >= 15 and 8 <= r["position"] <= 20] +for r in sorted(strike, key=lambda x: x["position"])[:15]: + kw = r["keys"][0][:40] + print(f" {kw:<42} {r['impressions']:>7} {r['position']:>5.1f}") + +# ---- GA4 ---- +print("\n" + "=" * 70) +print(f" GA4 REPORT — property {GA4_PROPERTY} (xslt-playground)") +print("=" * 70) +try: + from google.analytics.data_v1beta import BetaAnalyticsDataClient + from google.analytics.data_v1beta.types import ( + DateRange, Dimension, Metric, RunReportRequest, OrderBy, Filter, + FilterExpression) + ga_creds = service_account.Credentials.from_service_account_info( + SA_KEY, scopes=["https://www.googleapis.com/auth/analytics.readonly"]) + client = BetaAnalyticsDataClient(credentials=ga_creds) + prop = f"properties/{GA4_PROPERTY}" + + # Totals current vs previous (dateRange dimension is implicit with 2 ranges) + r = client.run_report(RunReportRequest( + property=prop, + date_ranges=[DateRange(start_date=str(start), end_date=str(end), name="cur"), + DateRange(start_date=str(prev_start), end_date=str(prev_end), name="prev")], + metrics=[Metric(name="sessions"), Metric(name="totalUsers"), + Metric(name="engagementRate"), Metric(name="averageSessionDuration"), + Metric(name="screenPageViews")], + )) + vals = {} + for row in r.rows: + dr = row.dimension_values[0].value + vals[dr] = [m.value for m in row.metric_values] + cur_v = vals.get("cur", ["0"]*5) + prev_v = vals.get("prev", ["0"]*5) + labels = ["Sessions", "Users", "Engagement%", "AvgSessDur(s)", "Pageviews"] + print(f"\n {'metric':<16} {'current':>12} {'previous':>12}") + for i, lab in enumerate(labels): + cv = float(cur_v[i]); pv = float(prev_v[i]) + if "%" in lab: cv*=100; pv*=100 + print(f" {lab:<16} {cv:>12.1f} {pv:>12.1f}") + + # Top landing pages from organic + print("\n TOP LANDING PAGES (current 28d, by sessions)") + r2 = client.run_report(RunReportRequest( + property=prop, + date_ranges=[DateRange(start_date=str(start), end_date=str(end))], + dimensions=[Dimension(name="landingPage")], + metrics=[Metric(name="sessions"), Metric(name="engagementRate"), + Metric(name="averageSessionDuration")], + order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="sessions"), desc=True)], + limit=15, + )) + print(f" {'landing page':<40} {'sess':>6} {'eng%':>6} {'dur(s)':>7}") + for row in r2.rows: + lp = row.dimension_values[0].value[:38] + s = row.metric_values[0].value + e = float(row.metric_values[1].value)*100 + d = float(row.metric_values[2].value) + print(f" {lp:<40} {s:>6} {e:>5.0f}% {d:>7.0f}") + + # Channel breakdown + print("\n TRAFFIC BY CHANNEL (current 28d)") + r3 = client.run_report(RunReportRequest( + property=prop, + date_ranges=[DateRange(start_date=str(start), end_date=str(end))], + dimensions=[Dimension(name="sessionDefaultChannelGroup")], + metrics=[Metric(name="sessions"), Metric(name="engagementRate")], + order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="sessions"), desc=True)], + )) + print(f" {'channel':<26} {'sess':>6} {'eng%':>6}") + for row in r3.rows: + ch = row.dimension_values[0].value[:24] + s = row.metric_values[0].value + e = float(row.metric_values[1].value)*100 + print(f" {ch:<26} {s:>6} {e:>5.0f}%") + + # Key events / conversions + print("\n TOP EVENTS (current 28d)") + r4 = client.run_report(RunReportRequest( + property=prop, + date_ranges=[DateRange(start_date=str(start), end_date=str(end))], + dimensions=[Dimension(name="eventName")], + metrics=[Metric(name="eventCount")], + order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="eventCount"), desc=True)], + limit=15, + )) + print(f" {'event':<32} {'count':>8}") + for row in r4.rows: + ev = row.dimension_values[0].value[:30] + c = row.metric_values[0].value + print(f" {ev:<32} {c:>8}") +except Exception as e: + import traceback + print(" GA4 error:", str(e)[:300]) + traceback.print_exc() diff --git a/site/content/posts/xsl-online-tester.md b/site/content/posts/xsl-online-tester.md index cfc67ffc..69e9bf58 100644 --- a/site/content/posts/xsl-online-tester.md +++ b/site/content/posts/xsl-online-tester.md @@ -27,6 +27,8 @@ In practice, when people say "XSL" in an integration or development context, the The output appears immediately. If the stylesheet has errors, the error panel shows the exact line and message from Saxon. +If you already know which version you need, there are dedicated testers: the [XSLT 2.0 online tester](https://xsltplayground.com/xslt-2-0/) for grouping and regular expressions, and the [XSLT 3.0 online tester](https://xsltplayground.com/xslt-3-0/) for maps, arrays and streaming. Both run the same Saxon HE backend. + ## Common XSL use cases **XML to HTML** — the most common use. An XSL stylesheet walks an XML document tree and emits HTML tags: @@ -96,3 +98,23 @@ For most development and debugging tasks, the online tester is faster than runni - You need to integrate the transform into a build pipeline For everything else — prototyping, debugging, sharing test cases — the online XSL tester is quicker. + +## Frequently asked questions + +**Is there a free XSL online tester?** +Yes. [XSLT Playground](https://xsltplayground.com) is a completely free online XSL tester. It runs XSLT 1.0, 2.0 and 3.0 on a real Saxon HE backend, with no account, signup or installation required. + +**Can I run XSL transformations in the browser?** +The editor runs in your browser, but the transformation itself executes on a Saxon server so the results match production exactly — unlike the browser's built-in XSLT 1.0 engine, which is limited and inconsistent across browsers. + +**What is the difference between .xsl and .xslt files?** +Both extensions are valid and interchangeable. `.xsl` is older and common in enterprise systems; `.xslt` is more explicit. Saxon and XSLT Playground accept either. + +**Can I validate an XSL stylesheet online?** +Yes. If your stylesheet is malformed or has a runtime error, the [XSLT validator](https://blog.xsltplayground.com/posts/xslt-validator-online/) reports the exact line number and the original Saxon error message so you can fix it before deploying. + +## Related guides + +- [XSLT online editor: how to test transformations without installing anything](https://blog.xsltplayground.com/posts/xslt-online-editor-guide/) +- [XSLT for beginners: your first transformation](https://blog.xsltplayground.com/posts/xslt-for-beginners/) +- [XSLT validator online: catch errors before running your transform](https://blog.xsltplayground.com/posts/xslt-validator-online/) diff --git a/site/content/posts/xslt-string-functions.md b/site/content/posts/xslt-string-functions.md index 8308c607..86cf317d 100644 --- a/site/content/posts/xslt-string-functions.md +++ b/site/content/posts/xslt-string-functions.md @@ -7,6 +7,28 @@ tags: ["xslt", "xpath", "strings", "functions"] String manipulation is one of the most common tasks in XSLT. Whether you are formatting output, parsing codes, or normalising values from external systems, XPath provides a rich set of string functions. This reference covers the most useful ones with examples you can run in [XSLT Playground](https://xsltplayground.com). +## XSLT string functions at a glance + +| Function | Purpose | Minimum version | +|---|---|---| +| `string-length` | Number of characters in a string | XSLT 1.0 | +| `substring` | Extract part of a string by position | XSLT 1.0 | +| `substring-before` / `substring-after` | Split a string on a delimiter | XSLT 1.0 | +| `contains` / `starts-with` | Test for a substring or prefix | XSLT 1.0 | +| `concat` | Join strings together | XSLT 1.0 | +| `normalize-space` | Trim and collapse whitespace | XSLT 1.0 | +| `translate` | Replace characters one-for-one | XSLT 1.0 | +| `upper-case` / `lower-case` | Change case | XSLT 2.0 | +| `ends-with` | Test for a suffix | XSLT 2.0 | +| `replace` | Regex-based substitution | XSLT 2.0 | +| `matches` | Test a string against a regex | XSLT 2.0 | +| `tokenize` | Split a string into a sequence by regex | XSLT 2.0 | +| `string-join` | Join a sequence with a separator | XSLT 2.0 | +| `format-number` | Format a number with a picture pattern | XSLT 1.0 | +| `format-date` / `format-dateTime` | Format dates with a picture string | XSLT 2.0 | + +Each function is explained with runnable examples below. Try them in the [XSLT 2.0 online tester](https://xsltplayground.com/xslt-2-0/) for the 2.0 functions, or the main [online XSLT editor](https://xsltplayground.com) for the 1.0 ones. + ## Basic string functions (XSLT 1.0+) ### string-length @@ -179,3 +201,9 @@ Format xs:date and xs:dateTime values using picture strings. All of these work in [XSLT Playground](https://xsltplayground.com). Set the version to 2.0 or 3.0 for the functions that require it. +## Related guides + +- [XSLT grouping with xsl:for-each-group](https://blog.xsltplayground.com/posts/xslt-grouping-for-each-group/) — group and aggregate string values +- [Transforming XML to JSON and CSV with XSLT](https://blog.xsltplayground.com/posts/xslt-xml-to-json-csv/) — `string-join` and `tokenize` in practice +- [XSLT template matching explained](https://blog.xsltplayground.com/posts/xslt-template-matching-explained/) — apply string logic inside template rules +