You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by pa...@apache.org on 2022/09/29 18:16:00 UTC

[beam] branch master updated: Send JavaScript messages to Playground iframes when switching the language in docs (#22361) (#22960)

This is an automated email from the ASF dual-hosted git repository.

pabloem pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 577953c5ac9 Send JavaScript messages to Playground iframes when switching the language in docs (#22361) (#22960)
577953c5ac9 is described below

commit 577953c5ac9dd3d022b664c21da846307ac45191
Author: alexeyinkin <al...@akvelon.com>
AuthorDate: Thu Sep 29 22:15:52 2022 +0400

    Send JavaScript messages to Playground iframes when switching the language in docs (#22361) (#22960)
    
    * Send JavaScript messages to Playground iframes when switching the language in docs (#22361)
    
    * Change to the production URL (#22361)
    
    * Fix after internal review (#22361)
    
    * Trigger a stand build (#22361)
    
    * Trigger build (#22361)
    
    * Remove !important from CSS (#22361)
---
 website/www/site/assets/js/language-switch-v2.js   | 268 ++++++++++++++-------
 .../www/site/assets/scss/_capability-matrix.scss   |   4 +-
 website/www/site/assets/scss/_global.sass          |   6 +-
 website/www/site/assets/scss/_playground.sass      |   8 +
 .../www/site/assets/scss/_syntax-highlighting.scss |   7 -
 .../site/content/en/documentation/runners/flink.md |   6 +-
 .../site/content/en/documentation/runners/jet.md   |   4 +-
 .../site/content/en/documentation/runners/spark.md |   4 +-
 .../content/en/documentation/runners/twister2.md   |   2 +-
 .../site/layouts/shortcodes/language-switcher.html |   8 +-
 .../www/site/layouts/shortcodes/playground.html    |  49 ++++
 .../layouts/shortcodes/playground_snippet.html     |  12 +
 12 files changed, 268 insertions(+), 110 deletions(-)

diff --git a/website/www/site/assets/js/language-switch-v2.js b/website/www/site/assets/js/language-switch-v2.js
index c251aaac07e..2a559fb57ec 100644
--- a/website/www/site/assets/js/language-switch-v2.js
+++ b/website/www/site/assets/js/language-switch-v2.js
@@ -11,8 +11,8 @@
 // the License.
 
 $(document).ready(function() {
-    function getElementData(element) {
-        const clickedLangSwitchEl = element.target;
+    function getElementData(event) {
+        const clickedLangSwitchEl = event.target;
         const elPreviousOffsetFromViewPort = clickedLangSwitchEl.getBoundingClientRect().top;
         return {
             elPreviousOffsetFromViewPort,
@@ -26,17 +26,17 @@ $(document).ready(function() {
         $('html, body').scrollTop(elCurrentHeight - elPreviousOffsetFromViewPort);
     }
 
+    const loadTime = new Date();
+
     function Switcher(conf) {
-        var id = conf["class-prefix"],
-            def = conf["default"],
-            langs = [];
-        var prefix = id + "-";
+        const name = conf["name"];
+
         return {
-            "id": id,
-            "selector": "[class^=" + prefix + "]:not(.no-toggle)",
-            "wrapper": prefix + "switcher", // Parent wrapper-class.
-            "default": prefix + def, // Default type to display.
-            "dbKey": id, // Local Storage Key
+            ...conf,
+            "uniqueValues": new Set(),
+            "selector": `[class^=${name}-]:not(.no-toggle)`, // Ex: [class^=language-]:not(.no-toggle)
+            "wrapper": `${name}-switcher`, // Parent wrapper-class.
+            "localStorageKey": name,
 
             /**
              * @desc Generate bootstrapped like nav template,
@@ -44,44 +44,52 @@ $(document).ready(function() {
              * @param array $types - list of supported types.
              * @return string - html template, which is bootstrapped nav tabs.
             */
-            "navHtml": function(types) {
-                var lists = "";
-                var selectors = "";
-
-                types.forEach(function(type) {
-                    var name = type.replace(prefix, "");
-                    name = (name === "py")? "python": name;
-                    name = name.charAt(0).toUpperCase() + name.slice(1);
-                    selectors += " " + type;
-                    lists += "<li class=\"langSwitch-content\" data-type=\"" + type + "\">";
-                    lists += name + "</li>";
-                });
-                return "<div class=\"" + this.wrapper + selectors + "\"> \
-                        <ul class=\"nav nav-tabs\">" + lists + "</ul> </div>";
+            "navHtml": function (values) {
+                let lists = "";
+                let classes = "";
+
+                for (const value of values) {
+                    const title = this.valueToTabTitle(value);
+                    classes += ` ${name}-${value}`;
+                    lists += `<li class="langSwitch-content" data-value="${value}">${title}</li>`;
+                }
+
+                // Ex: language-switcher language-java language-py language-go
+                return `<div class="${this.wrapper + classes}"><ul class="nav nav-tabs">${lists}</ul></div>`;
             },
-            /**
-             * @desc Extract language from provided text.
-             * @param string $text - string containing language, e.g language-python.
-             * @return string - cleaned name of language, e.g python.
-             */
-            "parseName": function(str) {
-                var re = new RegExp(prefix + "(\\w+)");
-                var parse = re.exec(str);
-                return (parse) ? parse[1] : "";
+
+            "valueToTabTitle": function (value) {
+                switch (value) {
+                    case 'py': return 'Python';
+                    case 'typescript': return 'TypeScript';
+                }
+
+                return value.charAt(0).toUpperCase() + value.slice(1);
             },
+
             /**
              * @desc Add Navigation tabs on top of parent code blocks.
              */
             "addTabs": function() {
                 var _self = this;
 
-                $("div"+_self.selector).each(function() {
-                    if ($(this).prev().is("div"+_self.selector)) {
-                        return;
+                // Iterate over all content blocks to insert tabs before.
+                // If multiple content blocks go in a row, only insert tabs before the first one.
+                $("div" + _self.selector).each(function() { // Ex: div[class^=language-]:not(.no-toggle)
+                    if ($(this).prev().is("div" + _self.selector)) {
+                        return; // Not the first one.
                     }
-                    $(this).before(_self.navHtml(_self.lookup($(this), [])));
+
+                    const values = _self.findNextSiblingsValues($(this));
+                    for (const value of values) {
+                        _self.uniqueValues.add(value);
+                    }
+
+                    const tabsHtml = _self.navHtml(values);
+                    $(this).before(tabsHtml);
                 });
             },
+
             /**
              * @desc Search next sibling and if it's also a code block, then store
                     its type and move on to the next element. It will keep
@@ -89,81 +97,167 @@ $(document).ready(function() {
              * @param object $el - jQuery object, from where to start searching.
              * @param array $lang - list to hold types, found while searching.
              * @return array - list of types found.
-            */
-            "lookup": function(el, lang) {
-                if (!el.is("div"+this.selector)) {
-                    langs = lang;
-                    return lang;
+             */
+            "findNextSiblingsValues": function(el, lang) {
+                if (!el.is("div" + this.selector)) {
+                    return [];
+                }
+
+                const prefix = `${name}-`;
+                for (const cls of el[0].classList) {
+                    if (cls.startsWith(prefix)) {
+                        return [
+                            cls.replace(prefix, ""),
+                            ...this.findNextSiblingsValues(el.next()),
+                        ];
+                    }
                 }
 
-                lang.push(el.attr("class").split(" ")[0])
-                return this.lookup(el.next(), lang)
+                return [];
             },
+
             "bindEvents": function() {
                 var _self = this;
-                $("." + _self.wrapper + " ul li").click(function(el) {
-                    // Making type preferences presistance, for user.
-                    localStorage.setItem(_self.dbKey, $(this).data("type"));
+                $(`.${_self.wrapper} li`).click(function(event) {
+                    localStorage.setItem(_self.localStorageKey, $(this).data("value"));
 
                     // Set scroll to new position because Safari and Firefox
                     // can't do it automatically, only Chrome under the hood
                     // detects the correct position of viewport
-                    const clickedLangSwitchData = getElementData(el);
-                    _self.toggle();
+                    const clickedLangSwitchData = getElementData(event);
+                    _self.toggle(false);
                     setScrollToNewPosition(clickedLangSwitchData);
                 });
             },
-            "toggle": function() {
-                var pref=localStorage.getItem(this.dbKey) || this.default;
-                var isPrefSelected = false;
-
-                // Adjusting active elements in navigation header.
-                $("." + this.wrapper + " li").removeClass("active").each(function() {
-                    if ($(this).data("type") === pref) {
-                        $(this).addClass("active");
-                        isPrefSelected = true;
+
+            "toggle": function(isInitial) {
+                let value = localStorage.getItem(this.localStorageKey) || this.default;
+                let hasTabForValue = $(`.${this.wrapper} li[data-value="${value}"]`).length > 0; // Ex: .language-switcher li[data-value="java"]
+
+                if (!hasTabForValue) {
+                    // if there's a code block for the default language,
+                    // set the preferred language to the default language
+                    if (this.uniqueValues.has(this.default)) {
+                        value = this.default;
+                    } else {
+                        // otherwise set the preferred language to the first available
+                        // language, so we don't have a page with no code blocks
+                        value = [...this.uniqueValues][0];
                     }
-                });
+                }
+
+                $(`.${this.wrapper} li[data-value="${value}"]`).addClass("active");
+                $(`.${this.wrapper} li[data-value!="${value}"]`).removeClass("active");
 
-                if (!isPrefSelected) {
-                  // if there's a code block for the default language,
-                  // set the preferred language to the default language
-                  if (langs.includes(this.default)) {
-                    pref = this.default;
-                  // otherwise set the preferred language to the first available
-                  // language, so we don't have a page with no code blocks
-                  } else {
-                    pref = langs[0];
-                  }
-
-                  $("." + this.wrapper + " li").each(function() {
-                      if ($(this).data("type") === pref) {
-                          $(this).addClass("active");
-                      }
-                  });
-               }
                 // Swapping visibility of code blocks.
-                $(this.selector).hide();
-                $("nav"+this.selector).show();
+                $(this.selector).hide(); // Ex: [class^=language-]:not(.no-toggle)
+                $("nav" + this.selector).show();
                 // make sure that runner and shell snippets are still visible after changing language
-                $("code"+this.selector).show();
-                $("." + pref).show();
+                $("code" + this.selector).show();
+                $(`.${name}-${value}`).show();
 
                 //add refresh method because html elements are added/deleted after changing language
                 $('[data-spy="scroll"]').each(function () {
                     $(this).scrollspy('refresh');
                 });
+
+                if (this.onChanged) {
+                    this.onChanged(value, isInitial);
+                }
             },
             "render": function(wrapper) {
                 this.addTabs();
                 this.bindEvents();
-                this.toggle();
-            }
+                this.toggle(true);
+            },
         };
     }
 
-    Switcher({"class-prefix":"language","default":"java"}).render();
-    Switcher({"class-prefix":"runner","default":"direct"}).render();
-    Switcher({"class-prefix":"shell","default":"unix"}).render();
-    Switcher({"class-prefix":"version"}).render();
+    Switcher({
+        "name": "language",
+        "default": "java",
+
+        "onChanged": function (lang, isInitial) {
+            if (isInitial) {
+                this.onInit(lang);
+            } else {
+                this.onChangedAfterLoaded(lang);
+            }
+        },
+
+        /**
+         * @desc Called after language is determined for the first time.
+         *       Modifies the iframes' URLs by adding the language so we don't need to
+         *       send messages to them. This is cheap when the page only started loading.
+         */
+        "onInit": function (lang) {
+            const playgroundIframes = $(".code-snippet-playground iframe").get();
+            const sdk = this.langToSdk(lang);
+
+            for (const iframe of playgroundIframes) {
+                const url = new URL(iframe.src);
+                const searchParams = new URLSearchParams(url.search);
+                searchParams.set("sdk", sdk);
+                url.search = searchParams.toString();
+                iframe.src = url.href;
+            }
+        },
+
+        /**
+         * @desc Called when the user switched the language tab manually.
+         */
+        "onChangedAfterLoaded": function (lang) {
+            const playgroundIframes = $(".code-snippet-playground iframe").get();
+            const message = {
+                type: "SetSdk",
+                sdk: this.langToSdk(lang),
+            };
+
+            const _self = this;
+            let attempts = 30;
+
+            // If another cycle of sending these messages is running, stop it.
+            clearInterval(this.interval);
+
+            const sendMessage = function () {
+                for (const iframe of playgroundIframes) {
+                    iframe.contentWindow.postMessage(message, '*');
+                }
+
+                if (attempts-- === 0) {
+                    clearInterval(_self.interval);
+                }
+            };
+
+            if (!this.areFramesLoaded()) {
+                // The guess is that the iframes may not have loaded yet.
+                // If we just send a message, Flutter may have not yet set its listener.
+                // So send the message with intervals in hope some of them are received.
+                // Playground ignores duplicate messages in a row.
+                this.interval = setInterval(sendMessage, 1000);
+            }
+
+            sendMessage();
+        },
+
+        "langToSdk": function (lang) {
+            switch (lang) {
+                case "py": return "python";
+            }
+            return lang;
+        },
+
+        /**
+         * @desc A rough guess if the embedded iframes are loaded, timer-based.
+         *       Experiments hint that ~10 seconds is sufficient on slowest devices.
+         */
+        "areFramesLoaded": function () {
+            const millisecondsAgo = new Date() - loadTime;
+            return millisecondsAgo >= 30000;
+        },
+    }).render();
+
+    Switcher({"name": "runner", "default": "direct"}).render();
+    Switcher({"name": "shell", "default": "unix"}).render();
+    Switcher({"name": "version"}).render();
 });
diff --git a/website/www/site/assets/scss/_capability-matrix.scss b/website/www/site/assets/scss/_capability-matrix.scss
index 4f0d16b6114..a3b59a84441 100644
--- a/website/www/site/assets/scss/_capability-matrix.scss
+++ b/website/www/site/assets/scss/_capability-matrix.scss
@@ -304,7 +304,9 @@ nav.runner-switcher {
         border-top-left-radius: 6px;
         border-top-right-radius: 6px;
         border-bottom: none;
-        padding-left: 0 !important;
+        &:not(:last-child) {
+            border-right: none;
+        }
         &:hover {
             cursor: pointer
         }
diff --git a/website/www/site/assets/scss/_global.sass b/website/www/site/assets/scss/_global.sass
index a8b20d87946..100965a8935 100644
--- a/website/www/site/assets/scss/_global.sass
+++ b/website/www/site/assets/scss/_global.sass
@@ -109,7 +109,7 @@ body
     display: block
 
 .code-snippet, pre
-  background: rgba(255, 109, 0, 0.03) !important
+  background: rgba(255, 109, 0, 0.03)
   border-radius: 8px
   border-top-left-radius: 0
   border: solid 0.6px #ff6d05
@@ -126,7 +126,7 @@ body
     border: none
     padding: 0
     margin-top: 36px
-    background: initial !important
+    background: initial
   a
     float: right
     margin-left: 12px
@@ -152,7 +152,7 @@ body
   .without_switcher
     border-top-left-radius: 8px
   pre
-    background: initial !important
+    background: initial
 
 table
   margin-top: 24px
diff --git a/website/www/site/assets/scss/_playground.sass b/website/www/site/assets/scss/_playground.sass
index c4a978949a1..0f1b507fc68 100644
--- a/website/www/site/assets/scss/_playground.sass
+++ b/website/www/site/assets/scss/_playground.sass
@@ -17,6 +17,14 @@
 
 @import "media"
 
+.code-snippet-playground
+  padding: 0
+  background: none
+
+  iframe
+    border: none
+    border-top-right-radius: 8px
+
 .playground-section
   padding: 15px 30px 15px
   h1
diff --git a/website/www/site/assets/scss/_syntax-highlighting.scss b/website/www/site/assets/scss/_syntax-highlighting.scss
index 42c6301a401..8a347d3f74f 100644
--- a/website/www/site/assets/scss/_syntax-highlighting.scss
+++ b/website/www/site/assets/scss/_syntax-highlighting.scss
@@ -16,12 +16,6 @@
  * Syntax highlighting styles
  */
 .highlight {
-  //background: #fff;
-
-  .chroma {
-    background: #eef;
-  }
-
   .c {
     color: #998;
     font-style: italic;
@@ -239,7 +233,6 @@ pre {
   ul {
     li {
       margin-bottom:0 !important;
-      padding-left: 0 !important;
       &:hover {
         cursor: pointer;
       }
diff --git a/website/www/site/content/en/documentation/runners/flink.md b/website/www/site/content/en/documentation/runners/flink.md
index b79fadc7a85..4188dc9e9ab 100644
--- a/website/www/site/content/en/documentation/runners/flink.md
+++ b/website/www/site/content/en/documentation/runners/flink.md
@@ -66,9 +66,9 @@ Please use the switcher below to select the appropriate mode for the Runner:
 <nav class="language-switcher">
   <strong>Adapt for:</strong>
   <ul>
-    <li data-type="language-java">Classic (Java)</li>
-    <li data-type="language-py">Portable (Python)</li>
-    <li data-type="language-portable">Portable (Java/Python/Go)</li>
+    <li data-value="java">Classic (Java)</li>
+    <li data-value="py">Portable (Python)</li>
+    <li data-value="portable">Portable (Java/Python/Go)</li>
   </ul>
 </nav>
 
diff --git a/website/www/site/content/en/documentation/runners/jet.md b/website/www/site/content/en/documentation/runners/jet.md
index 9f55771251d..3b110ad86e6 100644
--- a/website/www/site/content/en/documentation/runners/jet.md
+++ b/website/www/site/content/en/documentation/runners/jet.md
@@ -93,8 +93,8 @@ Download latest Hazelcast Jet version compatible with the Beam you are using fro
 <nav class="version-switcher">
   <strong>Adapt for:</strong>
   <ul>
-    <li data-type="version-jet3">Hazelcast Jet 3.x</li>
-    <li data-type="version-jet4">Hazelcast Jet 4.x</li>
+    <li data-value="jet3">Hazelcast Jet 3.x</li>
+    <li data-value="jet4">Hazelcast Jet 4.x</li>
   </ul>
 </nav>
 
diff --git a/website/www/site/content/en/documentation/runners/spark.md b/website/www/site/content/en/documentation/runners/spark.md
index db4b505d2d6..ff6fa3cc47a 100644
--- a/website/www/site/content/en/documentation/runners/spark.md
+++ b/website/www/site/content/en/documentation/runners/spark.md
@@ -59,8 +59,8 @@ the portable Runner. For more information on portability, please visit the
 <nav class="language-switcher">
   <strong>Adapt for:</strong>
   <ul>
-    <li data-type="language-java">Non portable (Java)</li>
-    <li data-type="language-py">Portable (Java/Python/Go)</li>
+    <li data-value="java">Non portable (Java)</li>
+    <li data-value="py">Portable (Java/Python/Go)</li>
   </ul>
 </nav>
 
diff --git a/website/www/site/content/en/documentation/runners/twister2.md b/website/www/site/content/en/documentation/runners/twister2.md
index e63762f2dfa..ba15c010fa7 100644
--- a/website/www/site/content/en/documentation/runners/twister2.md
+++ b/website/www/site/content/en/documentation/runners/twister2.md
@@ -88,7 +88,7 @@ deployments and how to get them setup visit [Twister2 Docs](https://twister2.org
 <nav class="version-switcher">
   <strong>Adapt for:</strong>
   <ul>
-    <li data-type="version-twister2-0.6.0">Twister2 0.6.0</li>
+    <li data-value="twister2-0.6.0">Twister2 0.6.0</li>
   </ul>
 </nav>
 
diff --git a/website/www/site/layouts/shortcodes/language-switcher.html b/website/www/site/layouts/shortcodes/language-switcher.html
index a6bfe115fef..855ee64972f 100644
--- a/website/www/site/layouts/shortcodes/language-switcher.html
+++ b/website/www/site/layouts/shortcodes/language-switcher.html
@@ -15,16 +15,16 @@
   <ul>
     {{ range $lang := .Params }}
       {{ if eq $lang "java" }}
-        <li data-type="language-java" class="active">Java SDK</li>
+        <li data-value="java" class="active">Java SDK</li>
       {{ end }}
       {{ if eq $lang "py" }}
-        <li data-type="language-py">Python SDK</li>
+        <li data-value="py">Python SDK</li>
       {{ end }}
       {{ if eq $lang "go" }}
-        <li data-type="language-go">Go SDK</li>
+        <li data-value="go">Go SDK</li>
       {{ end }}
       {{ if eq $lang "typescript" }}
-        <li data-type="language-typescript">Typescript SDK</li>
+        <li data-value="typescript">TypeScript SDK</li>
       {{ end }}
     {{ end }}
   </ul>
diff --git a/website/www/site/layouts/shortcodes/playground.html b/website/www/site/layouts/shortcodes/playground.html
new file mode 100644
index 00000000000..4dc3eaf09e7
--- /dev/null
+++ b/website/www/site/layouts/shortcodes/playground.html
@@ -0,0 +1,49 @@
+{{/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. See accompanying LICENSE file.
+*/}}
+{{/*
+Embedding example:
+
+{{< playground height="700px" >}}
+{{< playground_snippet language="java" example="SDK_JAVA/PRECOMPILED_OBJECT_TYPE_KATA/AggregationCount" >}}
+{{< playground_snippet language="py" example="SDK_PYTHON/PRECOMPILED_OBJECT_TYPE_KATA/AggregationCount" >}}
+{{< playground_snippet language="go" example="SDK_GO/PRECOMPILED_OBJECT_TYPE_EXAMPLE/MinimalWordCount" >}}
+{{< playground_snippet language="scio" example="SDK_SCIO/PRECOMPILED_OBJECT_TYPE_EXAMPLE/MinimalWordCount" >}}
+{{< /playground >}}
+
+*/}}
+<div class="playground-wrapper">
+    <div class="playground-snippets">
+        {{ .Inner }}
+    </div>
+    {{ $snippetsList := slice }}
+    {{ $divMatches := findRE "<div class=\"[^\"]+(playground-snippet)\"(.*)</div>" .Inner }}
+
+    {{ range $divMatches }}
+        {{ $attributeRegex := "data-sdk=\"(?P<sdk>\\w+)\" data-example=\"([^\"]+)\"" }}
+        {{ $sdk := replaceRE ".*data-sdk=\"(\\w+)\".*" "$1" . }}
+        {{ $example := replaceRE ".*example=\"([^\"]+)\".*" "$1" . }}
+        {{ $json := printf "%s%s%s%s%s" "{\"sdk\":\"" $sdk "\",\"example\":\"" $example "\"}" }}
+        {{ $snippetsList = append $json $snippetsList }}
+    {{ end}}
+
+    {{ $snippets := printf "%s%s%s" "[" (delimit $snippetsList ",") "]" }}
+    {{ $editable := 1 }}{{ if isset .Params "editable" }}{{ $editable = index .Params "editable" }}{{ end }}
+    <div class="code-snippet code-snippet-playground">
+        <iframe
+            src="https://play.beam.apache.org/embedded?editable={{ $editable }}&examples={{ $snippets }}"
+            width="100%"
+            height="{{ .Get "height" }}"
+            class="playground"
+            allow="clipboard-write"
+        ></iframe>
+    </div>
+</div>
diff --git a/website/www/site/layouts/shortcodes/playground_snippet.html b/website/www/site/layouts/shortcodes/playground_snippet.html
new file mode 100644
index 00000000000..4c4ce501168
--- /dev/null
+++ b/website/www/site/layouts/shortcodes/playground_snippet.html
@@ -0,0 +1,12 @@
+{{/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. See accompanying LICENSE file.
+*/}}
+{{ $sdk := .Get "language" }}{{ if eq $sdk "py" }}{{ $sdk = "python" }}{{ end }}<div class="language-{{ .Get "language" }} playground-snippet" data-sdk="{{ $sdk }}" data-example="{{ .Get "example" }}"></div>