Skip to content

How to Build a Code Block Web Component

Businesses with APIs of all kinds need to be able to document them so they can be leveraged by both customers and partners. HubSpot is no different — we have a lot of API endpoints and a full CMS Hub, which is directly integrated into the HubSpot CRM.

There are a multitude of ways that we want to use formatted code blocks in our documentation and we have multiple custom modules for displaying them. 

completed web component example

Maintaining separate code for each module can cause issues, though. Duplicate code makes it harder to maintain. It means we need to update multiple modules at a time in order to ensure feature parity, and any updates that require HTML changes need to be made in all of the modules or we risk breaking live code blocks. We also have multiple teams that can benefit from the code blocks but may need to use them in different environments. In some environments, we may have JavaScript frameworks, in others we don't. We have teams that want to use them in blog posts and our developer documentation. 

To solve for the wide array of use–cases, we're turning to web components for this specific task. If you've never used web components before, be sure to read our primer for using web components on HubSpot.

Briefly, however, web components are like custom HTML elements. You build them using JS, HTML, and CSS. Adding a web component to the page is the same as adding a normal HTML element like a <div>. The web component acts almost like an iframe — CSS and JavaScript in the main page can't affect the code inside the web component unless specifically allowed. This prevents styling conflicts, issues with IDs and classes used for targeting JavaScript, as well as other kinds of conflicts. The shadow DOM is the main way web components are encapsulated (protected from conflicts). While the term “shadow DOM” sounds dark and mysterious, it’s really just the HTML of the web component itself.

The advantage here is we can use the same web component across all of our modules, in our blog posts, and in environments where we're using JavaScript frameworks. In all of these situations, the component will load quickly, and it will look and function the same way. This also consolidates most of the code for displaying the code block into the web component itself, leaving HubSpot modules to just handle fields and render the simple HTML. This means that improvements to the web component can improve all of the places that it gets used simultaneously.

We're technically going to build two web components that are designed to work together:

  1. A code-tab element into which we will place the code we want to visually display: This element will do the actual syntax highlighting and provide a button for site visitors to copy code from.
  2. A code-block element that will be an outer wrapper element into which we will place our code-tab elements: This code-block element will be able to hold multiple code-tab elements. It will generate a tabbed interface based on the code-tab elements added to it. For example, you could have a tab for HTML and a tab for CSS. We frequently have code-block elements on our site, for instance, showing HubL+HTML and the rendered output of the HubL+HTML.
<code-block> <code-tab data-label="HTML" data-language="html" data-escaped="true"> <!-- the HTML you want to display on the page--> </code-tab> <code-tab data-label="CSS" data-language="css" data-escaped="true"> // the CSS you want to display on the page. </code-tab> </code-block>

The finished code is available on GitHub if you just want to get right to using it. 

Step 1: Create a Component to Display Code

Let's first get a solid foundation for what the component should look like before adding the syntax highlighting by registering our code-block and code-tab elements. 

Then, we'll set the innerHTML for the shadow DOM. This works the same way as setting innerHTML on a normal HTML element, but instead, we just target the shadowRoot. Now, when the browser sees the web component, it will render some HTML.

// Register <code-tab> element class CodeTab extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback() { // The Shadow DOM HTML for a <code-tab> element. this.shadowRoot.innerHTML = ` <!-- we'll put styles here later --> <div class="code-snippet"> The web component is working if you can see this text. </div> `; } } window.customElements.define("code-tab", CodeTab); <code-tab></code-tab>

Screenshot of the output of the component which reads 'The web component is working if you see this text.'

Next, we'll create some basic HTML structure and then style it. We want our code to maintain its formatting, so we'll use a pre tag — and, to keep it semantic, we'll place a code tag inside. 

// Register <code-tab> element class CodeTab extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback() { // The Shadow DOM HTML for a <code-tab> element. this.shadowRoot.innerHTML = ` <!-- we'll put styles here later --> <div class="code-snippet"> <pre><code><slot></slot></code></pre> </div> `; } } window.customElements.define("code-tab", CodeTab); <code-tab> &lt;h1&gt; I'm a heading&lt;/h1&gt; </code-tab>

 

What may not be clear in the code-block element above is <slot>. The term “slot” may be familiar if you've built using a JS framework like React or Vue. It can be thought of as a placeholder that will contain anything you place inside of your web component. So, anything you put inside your newly created code-tab element will appear there in the shadow DOM.

There are several ways to style web components, but we're going to use a <style> tag inside the inner HTML to keep our web component code together.

// Register <code-tab> element class CodeTab extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback() { // The Shadow DOM HTML for a <code-tab> element. this.shadowRoot.innerHTML = ` <style> :host{display:block;} </style> <div class="code-snippet"> <pre><code><slot></slot></code></pre> </div> `; } } window.customElements.define("code-tab", CodeTab); <code-tab> &lt;h1&gt; I'm a heading&lt;/h1&gt; </code-tab>

Notice the :host selector above. This selects your new custom element itself so you can style it — it’s kind of like having a wrapper div around your shadow DOM. 

Nice work! This is already starting to come along.

Step 2: Add Syntax Highlighting to the Code Tabs

Syntax highlighting is complex and requires parsing the code to break it out into its individual parts. It doesn't make sense to do that from scratch, so what we're going to do is leverage Prism.js, a popular and well–made library for syntax highlighting code. 

The website for the project has a convenient build tool you can use to configure exactly what you want and download the code you need. We select the languages we need and then scroll to the bottom where the code is displayed. We're going to download prism.js and place it in our src/js/ directory. (The directories we're going to mention are based on the HubSpot boilerplate's structure; you can place the scripts in other folders, just keep in mind that you'll need to adjust the paths shown below.)

We're going to edit this newly downloaded JS file and add raw to the top of the code and endraw to the bottom.

{% raw %} /* PrismJS 1.28.0 https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+django+graphql+json+markup-templating+yaml&plugins=line-highlight+line-numbers+toolbar+copy-to-clipboard */ var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++t}),e.__id},clone:function e(n,t){var r,i;switch(t=t||{},a.util.type(n)){case"Object":if(i=a.util.objId(n),t[i])return t[i];for(var l in r={},t[i]=r,n)n.hasOwnProperty(l)&&(r[l]=e(n[l],t));return r;case"Array":return i=a.util.objId(n),t[i]?t[i]:(r=[],t[i]=r,n.forEach((function(n,a){r[a]=e(n,t)})),r);default:return n}},getLanguage:function(e){for(;e;){var t=n.exec(e.className);if(t)return t[1].toLowerCase();e=e.parentElement}return"none"},setLanguage:function(e,t){e.className=e.className.replace(RegExp(n,"gi"),""),e.classList.add("language-"+t)},currentScript:function(){if("undefined"==typeof document)return null;if("currentScript"in document)return document.currentScript;try{throw new Error}catch(r){var e=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(r.stack)||[])[1];if(e){var n=document.getElementsByTagName("script");for(var t in n)if(n[t].src==e)return n[t]}return null}},isActive:function(e,n,t){for(var r="no-"+n;e;){var a=e.classList;if(a.contains(n))return!0;if(a.contains(r))return!1;e=e.parentElement}return!!t}},languages:{plain:r,plaintext:r,text:r,txt:r,extend:function(e,n){var t=a.util.clone(a.languages[e]);for(var r in n)t[r]=n[r];return t},insertBefore:function(e,n,t,r){var i=(r=r||a.languages)[e],l={};for(var o in i)if(i.hasOwnProperty(o)){if(o==n)for(var s in t)t.hasOwnProperty(s)&&(l[s]=t[s]);t.hasOwnProperty(o)||(l[o]=i[o])}var u=r[e];return r[e]=l,a.languages.DFS(a.languages,(function(n,t){t===u&&n!=e&&(this[n]=l)})),l},DFS:function e(n,t,r,i){i=i||{};var l=a.util.objId;for(var o in n)if(n.hasOwnProperty(o)){t.call(n,o,n[o],r||o);var s=n[o],u=a.util.type(s);"Object"!==u||i[l(s)]?"Array"!==u||i[l(s)]||(i[l(s)]=!0,e(s,t,o,i)):(i[l(s)]=!0,e(s,t,null,i))}}},plugins:{},highlightAll:function(e,n){a.highlightAllUnder(document,e,n)},highlightAllUnder:function(e,n,t){var r={callback:t,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};a.hooks.run("before-highlightall",r),r.elements=Array.prototype.slice.apply(r.container.querySelectorAll(r.selector)),a.hooks.run("before-all-elements-highlight",r);for(var i,l=0;i=r.elements[l++];)a.highlightElement(i,!0===n,r.callback)},highlightElement:function(n,t,r){var i=a.util.getLanguage(n),l=a.languages[i];a.util.setLanguage(n,i);var o=n.parentElement;o&&"pre"===o.nodeName.toLowerCase()&&a.util.setLanguage(o,i);var s={element:n,language:i,grammar:l,code:n.textContent};function u(e){s.highlightedCode=e,a.hooks.run("before-insert",s),s.element.innerHTML=s.highlightedCode,a.hooks.run("after-highlight",s),a.hooks.run("complete",s),r&&r.call(s.element)}if(a.hooks.run("before-sanity-check",s),(o=s.element.parentElement)&&"pre"===o.nodeName.toLowerCase()&&!o.hasAttribute("tabindex")&&o.setAttribute("tabindex","0"),!s.code)return a.hooks.run("complete",s),void(r&&r.call(s.element));if(a.hooks.run("before-highlight",s),s.grammar)if(t&&e.Worker){var c=new Worker(a.filename);c.onmessage=function(e){u(e.data)},c.postMessage(JSON.stringify({language:s.language,code:s.code,immediateClose:!0}))}else u(a.highlight(s.code,s.grammar,s.language));else u(a.util.encode(s.code))},highlight:function(e,n,t){var r={code:e,grammar:n,language:t};if(a.hooks.run("before-tokenize",r),!r.grammar)throw new Error('The language "'+r.language+'" has no grammar.');return r.tokens=a.tokenize(r.code,r.grammar),a.hooks.run("after-tokenize",r),i.stringify(a.util.encode(r.tokens),r.language)},tokenize:function(e,n){var t=n.rest;if(t){for(var r in t)n[r]=t[r];delete n.rest}var a=new s;return u(a,a.head,e),o(e,a,n,a.head,0),function(e){for(var n=[],t=e.head.next;t!==e.tail;)n.push(t.value),t=t.next;return n}(a)},hooks:{all:{},add:function(e,n){var t=a.hooks.all;t[e]=t[e]||[],t[e].push(n)},run:function(e,n){var t=a.hooks.all[e];if(t&&t.length)for(var r,i=0;r=t[i++];)r(n)}},Token:i};function i(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=0|(r||"").length}function l(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function o(e,n,t,r,s,g){for(var f in t)if(t.hasOwnProperty(f)&&t[f]){var h=t[f];h=Array.isArray(h)?h:[h];for(var d=0;d<h.length;++d){if(g&&g.cause==f+","+d)return;var v=h[d],p=v.inside,m=!!v.lookbehind,y=!!v.greedy,k=v.alias;if(y&&!v.pattern.global){var x=v.pattern.toString().match(/[imsuy]*$/)[0];v.pattern=RegExp(v.pattern.source,x+"g")}for(var b=v.pattern||v,w=r.next,A=s;w!==n.tail&&!(g&&A>=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(j<O||"string"==typeof C.value);C=C.next)L++,j+=C.value.length;L--,E=e.slice(A,j),P.index-=A}else if(!(P=l(b,0,E,m)))continue;S=P.index;var N=P[0],_=E.slice(0,S),M=E.slice(S+N.length),W=A+E.length;g&&W>g.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a<t&&r!==e.tail;a++)r=r.next;n.next=r,r.prev=n,e.length-=a}if(e.Prism=a,i.stringify=function e(n,t){if("string"==typeof n)return n;if(Array.isArray(n)){var r="";return n.forEach((function(n){r+=e(n,t)})),r}var i={type:n.type,content:e(n.content,t),tag:"span",classes:["token",n.type],attributes:{},language:t},l=n.alias;l&&(Array.isArray(l)?Array.prototype.push.apply(i.classes,l):i.classes.push(l)),a.hooks.run("wrap",i);var o="";for(var s in i.attributes)o+=" "+s+'="'+(i.attributes[s]||"").replace(/"/g,"&quot;")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+o+">"+i.content+"</"+i.tag+">"},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); Prism.languages.markup={comment:{pattern:/<!--(?:(?!<!--)[\s\S])*?-->/,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/<!DOCTYPE(?:[^>"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^<!|>$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&amp;/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^<!\[CDATA\[|\]\]>$/i;var t={"included-cdata":{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:<!\\[CDATA\\[(?:[^\\]]|\\](?!\\]>))*\\]\\]>|(?!<!\\[CDATA\\[)[^])*?(?=</__>)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-](?:[^;{\s]|\s+(?![\s{]))*(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; !function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",n={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},a={bash:n,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*[email protected]$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:a},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:n}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:a},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:a.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:a.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},n.inside=e.languages.bash;for(var o=["comment","function-name","for-or-select","assign-left","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],s=a.variable[1].inside,i=0;i<o.length;i++)s[o[i]]=e.languages.bash[o[i]];e.languages.shell=e.languages.bash}(Prism); !function(e){function n(e,n){return"___"+e.toUpperCase()+n+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(t,a,r,o){if(t.language===a){var c=t.tokenStack=[];t.code=t.code.replace(r,(function(e){if("function"==typeof o&&!o(e))return e;for(var r,i=c.length;-1!==t.code.indexOf(r=n(a,i));)++i;return c[i]=e,r})),t.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(t,a){if(t.language===a&&t.tokenStack){t.grammar=e.languages[a];var r=0,o=Object.keys(t.tokenStack);!function c(i){for(var u=0;u<i.length&&!(r>=o.length);u++){var g=i[u];if("string"==typeof g||g.content&&"string"==typeof g.content){var l=o[r],s=t.tokenStack[l],f="string"==typeof g?g:g.content,p=n(a,l),k=f.indexOf(p);if(k>-1){++r;var m=f.substring(0,k),d=new e.Token(a,e.tokenize(s,t.grammar),"language-"+a,s),h=f.substring(k+p.length),v=[];m&&v.push.apply(v,c([m])),v.push(d),h&&v.push.apply(v,c([h])),"string"==typeof g?i.splice.apply(i,[u,1].concat(v)):g.content=v}}else g.content&&c(g.content)}return i}(t.tokens)}}}})}(Prism); !function(e){e.languages.django={comment:/^\{#[\s\S]*?#\}$/,tag:{pattern:/(^\{%[+-]?\s*)\w+/,lookbehind:!0,alias:"keyword"},delimiter:{pattern:/^\{[{%][+-]?|[+-]?[}%]\}$/,alias:"punctuation"},string:{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},filter:{pattern:/(\|)\w+/,lookbehind:!0,alias:"function"},test:{pattern:/(\bis\s+(?:not\s+)?)(?!not\b)\w+/,lookbehind:!0,alias:"function"},function:/\b[a-z_]\w+(?=\s*\()/i,keyword:/\b(?:and|as|by|else|for|if|import|in|is|loop|not|or|recursive|with|without)\b/,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,number:/\b\d+(?:\.\d+)?\b/,boolean:/[Ff]alse|[Nn]one|[Tt]rue/,variable:/\b\w+\b/,punctuation:/[{}[\](),.:;]/};var n=/\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}|\{#[\s\S]*?#\}/g,o=e.languages["markup-templating"];e.hooks.add("before-tokenize",(function(e){o.buildPlaceholders(e,"django",n)})),e.hooks.add("after-tokenize",(function(e){o.tokenizePlaceholders(e,"django")})),e.languages.jinja2=e.languages.django,e.hooks.add("before-tokenize",(function(e){o.buildPlaceholders(e,"jinja2",n)})),e.hooks.add("after-tokenize",(function(e){o.tokenizePlaceholders(e,"jinja2")}))}(Prism); Prism.languages.graphql={comment:/#.*/,description:{pattern:/(?:"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*")(?=\s*[a-z_])/i,greedy:!0,alias:"string",inside:{"language-markdown":{pattern:/(^"(?:"")?)(?!\1)[\s\S]+(?=\1$)/,lookbehind:!0,inside:Prism.languages.markdown}}},string:{pattern:/"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*"/,greedy:!0},number:/(?:\B-|\b)\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,boolean:/\b(?:false|true)\b/,variable:/\$[a-z_]\w*/i,directive:{pattern:/@[a-z_]\w*/i,alias:"function"},"attr-name":{pattern:/\b[a-z_]\w*(?=\s*(?:\((?:[^()"]|"(?:\\.|[^\\"\r\n])*")*\))?:)/i,greedy:!0},"atom-input":{pattern:/\b[A-Z]\w*Input\b/,alias:"class-name"},scalar:/\b(?:Boolean|Float|ID|Int|String)\b/,constant:/\b[A-Z][A-Z_\d]*\b/,"class-name":{pattern:/(\b(?:enum|implements|interface|on|scalar|type|union)\s+|&\s*|:\s*|\[)[A-Z_]\w*/,lookbehind:!0},fragment:{pattern:/(\bfragment\s+|\.{3}\s*(?!on\b))[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-mutation":{pattern:/(\bmutation\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-query":{pattern:/(\bquery\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},keyword:/\b(?:directive|enum|extend|fragment|implements|input|interface|mutation|on|query|repeatable|scalar|schema|subscription|type|union)\b/,operator:/[!=|&]|\.{3}/,"property-query":/\w+(?=\s*\()/,object:/\w+(?=\s*\{)/,punctuation:/[!(){}\[\]:=,]/,property:/\w+/},Prism.hooks.add("after-tokenize",(function(n){if("graphql"===n.language)for(var t=n.tokens.filter((function(n){return"string"!=typeof n&&"comment"!==n.type&&"scalar"!==n.type})),e=0;e<t.length;){var a=t[e++];if("keyword"===a.type&&"mutation"===a.content){var r=[];if(c(["definition-mutation","punctuation"])&&"("===l(1).content){e+=2;var i=f(/^\($/,/^\)$/);if(-1===i)continue;for(;e<i;e++){var o=l(0);"variable"===o.type&&(b(o,"variable-input"),r.push(o.content))}e=i+1}if(c(["punctuation","property-query"])&&"{"===l(0).content&&(e++,b(l(0),"property-mutation"),r.length>0)){var s=f(/^\{$/,/^\}$/);if(-1===s)continue;for(var u=e;u<s;u++){var p=t[u];"variable"===p.type&&r.indexOf(p.content)>=0&&b(p,"variable-input")}}}}function l(n){return t[e+n]}function c(n,t){t=t||0;for(var e=0;e<n.length;e++){var a=l(e+t);if(!a||a.type!==n[e])return!1}return!0}function f(n,a){for(var r=1,i=e;i<t.length;i++){var o=t[i],s=o.content;if("punctuation"===o.type&&"string"==typeof s)if(n.test(s))r++;else if(a.test(s)&&0==--r)return i}return-1}function b(n,t){var e=n.alias;e?Array.isArray(e)||(n.alias=e=[e]):n.alias=e=[],e.push(t)}})); Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; !function(e){var n=/[*&][^\s[\]{},]+/,r=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,t="(?:"+r.source+"(?:[ \t]+"+n.source+")?|"+n.source+"(?:[ \t]+"+r.source+")?)",a="(?:[^\\s\\x00-\\x08\\x0e-\\x1f!\"#%&'*,\\-:>[email protected][\\]`{|}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]|[?:-]<PLAIN>)(?:[ \t]*(?:(?![#:])<PLAIN>|:<PLAIN>))*".replace(/<PLAIN>/g,(function(){return"[^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]"})),d="\"(?:[^\"\\\\\r\n]|\\\\.)*\"|'(?:[^'\\\\\r\n]|\\\\.)*'";function o(e,n){n=(n||"").replace(/m/g,"")+"m";var r="([:\\-,[{]\\s*(?:\\s<<prop>>[ \t]+)?)(?:<<value>>)(?=[ \t]*(?:$|,|\\]|\\}|(?:[\r\n]\\s*)?#))".replace(/<<prop>>/g,(function(){return t})).replace(/<<value>>/g,(function(){return e}));return RegExp(r,n)}e.languages.yaml={scalar:{pattern:RegExp("([\\-:]\\s*(?:\\s<<prop>>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\\S[^\r\n]*(?:\\2[^\r\n]+)*)".replace(/<<prop>>/g,(function(){return t}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp("((?:^|[:\\-,[{\r\n?])[ \t]*(?:<<prop>>[ \t]+)?)<<key>>(?=\\s*:\\s)".replace(/<<prop>>/g,(function(){return t})).replace(/<<key>>/g,(function(){return"(?:"+a+"|"+d+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:o("\\d{4}-\\d\\d?-\\d\\d?(?:[tT]|[ \t]+)\\d\\d?:\\d{2}:\\d{2}(?:\\.\\d*)?(?:[ \t]*(?:Z|[-+]\\d\\d?(?::\\d{2})?))?|\\d{4}-\\d{2}-\\d{2}|\\d\\d?:\\d{2}(?::\\d{2}(?:\\.\\d*)?)?"),lookbehind:!0,alias:"number"},boolean:{pattern:o("false|true","i"),lookbehind:!0,alias:"important"},null:{pattern:o("null|~","i"),lookbehind:!0,alias:"important"},string:{pattern:o(d),lookbehind:!0,greedy:!0},number:{pattern:o("[+-]?(?:0x[\\da-f]+|0o[0-7]+|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?|\\.inf|\\.nan)","i"),lookbehind:!0},tag:r,important:n,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(Prism); !function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document&&document.querySelector){var e,t="line-numbers",i="linkable-line-numbers",n=!0;Prism.plugins.lineHighlight={highlightLines:function(r,a,u){var c=(a="string"==typeof a?a:r.getAttribute("data-line")||"").replace(/\s+/g,"").split(",").filter(Boolean),d=+r.getAttribute("data-line-offset")||0,h=(function(){if(void 0===e){var t=document.createElement("div");t.style.fontSize="13px",t.style.lineHeight="1.5",t.style.padding="0",t.style.border="0",t.innerHTML="&nbsp;<br />&nbsp;",document.body.appendChild(t),e=38===t.offsetHeight,document.body.removeChild(t)}return e}()?parseInt:parseFloat)(getComputedStyle(r).lineHeight),f=Prism.util.isActive(r,t),p=r.querySelector("code"),g=f?r:p||r,m=[],v=p&&g!=p?function(e,t){var i=getComputedStyle(e),n=getComputedStyle(t);function r(e){return+e.substr(0,e.length-2)}return t.offsetTop+r(n.borderTopWidth)+r(n.paddingTop)-r(i.paddingTop)}(r,p):0;c.forEach((function(e){var t=e.split("-"),i=+t[0],n=+t[1]||i,o=r.querySelector('.line-highlight[data-range="'+e+'"]')||document.createElement("div");if(m.push((function(){o.setAttribute("aria-hidden","true"),o.setAttribute("data-range",e),o.className=(u||"")+" line-highlight"})),f&&Prism.plugins.lineNumbers){var s=Prism.plugins.lineNumbers.getLine(r,i),l=Prism.plugins.lineNumbers.getLine(r,n);if(s){var a=s.offsetTop+v+"px";m.push((function(){o.style.top=a}))}if(l){var c=l.offsetTop-s.offsetTop+l.offsetHeight+"px";m.push((function(){o.style.height=c}))}}else m.push((function(){o.setAttribute("data-start",String(i)),n>i&&o.setAttribute("data-end",String(n)),o.style.top=(i-d-1)*h+v+"px",o.textContent=new Array(n-i+2).join(" \n")}));m.push((function(){o.style.width=r.scrollWidth+"px"})),m.push((function(){g.appendChild(o)}))}));var y=r.id;if(f&&Prism.util.isActive(r,i)&&y){s(r,i)||m.push((function(){r.classList.add(i)}));var b=parseInt(r.getAttribute("data-start")||"1");o(".line-numbers-rows > span",r).forEach((function(e,t){var i=t+b;e.onclick=function(){var e=y+"."+i;n=!1,location.hash=e,setTimeout((function(){n=!0}),1)}}))}return function(){m.forEach(l)}}};var r=0;Prism.hooks.add("before-sanity-check",(function(e){var t=e.element.parentElement;if(a(t)){var i=0;o(".line-highlight",t).forEach((function(e){i+=e.textContent.length,e.parentNode.removeChild(e)})),i&&/^(?: \n)+$/.test(e.code.slice(-i))&&(e.code=e.code.slice(0,-i))}})),Prism.hooks.add("complete",(function e(i){var n=i.element.parentElement;if(a(n)){clearTimeout(r);var o=Prism.plugins.lineNumbers,l=i.plugins&&i.plugins.lineNumbers;s(n,t)&&o&&!l?Prism.hooks.add("line-numbers",e):(Prism.plugins.lineHighlight.highlightLines(n)(),r=setTimeout(u,1))}})),window.addEventListener("hashchange",u),window.addEventListener("resize",(function(){o("pre").filter(a).map((function(e){return Prism.plugins.lineHighlight.highlightLines(e)})).forEach(l)}))}function o(e,t){return Array.prototype.slice.call((t||document).querySelectorAll(e))}function s(e,t){return e.classList.contains(t)}function l(e){e()}function a(e){return!!(e&&/pre/i.test(e.nodeName)&&(e.hasAttribute("data-line")||e.id&&Prism.util.isActive(e,i)))}function u(){var e=location.hash.slice(1);o(".temporary.line-highlight").forEach((function(e){e.parentNode.removeChild(e)}));var t=(e.match(/\.([\d,-]+)$/)||[,""])[1];if(t&&!document.getElementById(e)){var i=e.slice(0,e.lastIndexOf(".")),r=document.getElementById(i);r&&(r.hasAttribute("data-line")||r.setAttribute("data-line",""),Prism.plugins.lineHighlight.highlightLines(r,t,"temporary ")(),n&&document.querySelector(".temporary.line-highlight").scrollIntoView())}}}(); !function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e="line-numbers",n=/\n(?!$)/g,t=Prism.plugins.lineNumbers={getLine:function(n,t){if("PRE"===n.tagName&&n.classList.contains(e)){var i=n.querySelector(".line-numbers-rows");if(i){var r=parseInt(n.getAttribute("data-start"),10)||1,s=r+(i.children.length-1);t<r&&(t=r),t>s&&(t=s);var l=t-r;return i.children[l]}}},resize:function(e){r([e])},assumeViewportIndependence:!0},i=void 0;window.addEventListener("resize",(function(){t.assumeViewportIndependence&&i===window.innerWidth||(i=window.innerWidth,r(Array.prototype.slice.call(document.querySelectorAll("pre.line-numbers"))))})),Prism.hooks.add("complete",(function(t){if(t.code){var i=t.element,s=i.parentNode;if(s&&/pre/i.test(s.nodeName)&&!i.querySelector(".line-numbers-rows")&&Prism.util.isActive(i,e)){i.classList.remove(e),s.classList.add(e);var l,o=t.code.match(n),a=o?o.length+1:1,u=new Array(a+1).join("<span></span>");(l=document.createElement("span")).setAttribute("aria-hidden","true"),l.className="line-numbers-rows",l.innerHTML=u,s.hasAttribute("data-start")&&(s.style.counterReset="linenumber "+(parseInt(s.getAttribute("data-start"),10)-1)),t.element.appendChild(l),r([s]),Prism.hooks.run("line-numbers",t)}}})),Prism.hooks.add("line-numbers",(function(e){e.plugins=e.plugins||{},e.plugins.lineNumbers=!0}))}function r(e){if(0!=(e=e.filter((function(e){var n,t=(n=e,n?window.getComputedStyle?getComputedStyle(n):n.currentStyle||null:null)["white-space"];return"pre-wrap"===t||"pre-line"===t}))).length){var t=e.map((function(e){var t=e.querySelector("code"),i=e.querySelector(".line-numbers-rows");if(t&&i){var r=e.querySelector(".line-numbers-sizer"),s=t.textContent.split(n);r||((r=document.createElement("span")).className="line-numbers-sizer",t.appendChild(r)),r.innerHTML="0",r.style.display="block";var l=r.getBoundingClientRect().height;return r.innerHTML="",{element:e,lines:s,lineHeights:[],oneLinerHeight:l,sizer:r}}})).filter(Boolean);t.forEach((function(e){var n=e.sizer,t=e.lines,i=e.lineHeights,r=e.oneLinerHeight;i[t.length-1]=void 0,t.forEach((function(e,t){if(e&&e.length>1){var s=n.appendChild(document.createElement("span"));s.style.display="block",s.textContent=e}else i[t]=r}))})),t.forEach((function(e){for(var n=e.sizer,t=e.lineHeights,i=0,r=0;r<t.length;r++)void 0===t[r]&&(t[r]=n.children[i++].getBoundingClientRect().height)})),t.forEach((function(e){var n=e.sizer,t=e.element.querySelector(".line-numbers-rows");n.style.display="none",n.innerHTML="",e.lineHeights.forEach((function(e,n){t.children[n].style.height=e+"px"}))}))}}}(); !function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e=[],t={},n=function(){};Prism.plugins.toolbar={};var a=Prism.plugins.toolbar.registerButton=function(n,a){var r;r="function"==typeof a?a:function(e){var t;return"function"==typeof a.onClick?((t=document.createElement("button")).type="button",t.addEventListener("click",(function(){a.onClick.call(this,e)}))):"string"==typeof a.url?(t=document.createElement("a")).href=a.url:t=document.createElement("span"),a.className&&t.classList.add(a.className),t.textContent=a.text,t},n in t?console.warn('There is a button with the key "'+n+'" registered already.'):e.push(t[n]=r)},r=Prism.plugins.toolbar.hook=function(a){var r=a.element.parentNode;if(r&&/pre/i.test(r.nodeName)&&!r.parentNode.classList.contains("code-toolbar")){var o=document.createElement("div");o.classList.add("code-toolbar"),r.parentNode.insertBefore(o,r),o.appendChild(r);var i=document.createElement("div");i.classList.add("toolbar");var l=e,d=function(e){for(;e;){var t=e.getAttribute("data-toolbar-order");if(null!=t)return(t=t.trim()).length?t.split(/\s*,\s*/g):[];e=e.parentElement}}(a.element);d&&(l=d.map((function(e){return t[e]||n}))),l.forEach((function(e){var t=e(a);if(t){var n=document.createElement("div");n.classList.add("toolbar-item"),n.appendChild(t),i.appendChild(n)}})),o.appendChild(i)}};a("label",(function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-label")){var n,a,r=t.getAttribute("data-label");try{a=document.querySelector("template#"+r)}catch(e){}return a?n=a.content:(t.hasAttribute("data-url")?(n=document.createElement("a")).href=t.getAttribute("data-url"):n=document.createElement("span"),n.textContent=r),n}})),Prism.hooks.add("complete",r)}}(); !function(){function t(t){var e=document.createElement("textarea");e.value=t.getText(),e.style.top="0",e.style.left="0",e.style.position="fixed",document.body.appendChild(e),e.focus(),e.select();try{var o=document.execCommand("copy");setTimeout((function(){o?t.success():t.error()}),1)}catch(e){setTimeout((function(){t.error(e)}),1)}document.body.removeChild(e)}"undefined"!=typeof Prism&&"undefined"!=typeof document&&(Prism.plugins.toolbar?Prism.plugins.toolbar.registerButton("copy-to-clipboard",(function(e){var o=e.element,n=function(t){var e={copy:"Copy","copy-error":"Press Ctrl+C to copy","copy-success":"Copied!","copy-timeout":5e3};for(var o in e){for(var n="data-prismjs-"+o,c=t;c&&!c.hasAttribute(n);)c=c.parentElement;c&&(e[o]=c.getAttribute(n))}return e}(o),c=document.createElement("button");c.className="copy-to-clipboard-button",c.setAttribute("type","button");var r=document.createElement("span");return c.appendChild(r),u("copy"),function(e,o){e.addEventListener("click",(function(){!function(e){navigator.clipboard?navigator.clipboard.writeText(e.getText()).then(e.success,(function(){t(e)})):t(e)}(o)}))}(c,{getText:function(){return o.textContent},success:function(){u("copy-success"),i()},error:function(){u("copy-error"),setTimeout((function(){!function(t){window.getSelection().selectAllChildren(t)}(o)}),1),i()}}),c;function i(){setTimeout((function(){u("copy")}),n["copy-timeout"])}function u(t){r.textContent=n[t],c.setAttribute("data-copy-state",t)}})):console.warn("Copy to Clipboard plugin loaded before Toolbar plugin."))}();

The raw tag ensures that any braces in the JavaScript are not evaluated by the HubL renderer.

Next, we download the prism.css file and place it in our src/css/components directory.

Normally, if we want to load CSS or JS we would use the HubL require_js and require_css functions. In this case, though, our Prism code is a dependency of the code-tab component. Instead of using separate require statements for the component JavaScript, the Prism JavaScript, and Prism CSS, we want to only have one line of code to get the code block going on a page. We could also use webpack to auto-combine the files — and maybe we'll do that in the future to get a tiny performance improvement. But for the sake of keeping this guide approachable for folks of all skill levels, we're going to forgo any build tooling. 

Inside of our code-block.js file, we're going to add two import statements above our code-block and code-tab classes: one import for the Prism stylesheet, and one for the Prism JavaScript.

// load Prism to be used by the web component, used for syntax highlighting. import syntaxHighlighting from "{{ get_asset_url('../css/components/prism.css')}}" assert { type: "css" }; import "{{ get_asset_url('../js/prism.js') }}"; // Register <code-tab> element class CodeTab extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback() { // The Shadow DOM HTML for a <code-tab> element. this.shadowRoot.innerHTML = ` <style> :host{display:block;} </style> <div class="code-snippet"> <pre><code><slot></slot></code></pre> </div> `; } } window.customElements.define("code-tab", CodeTab);

 

In order for our web component to use the stylesheet, we need to attach it to the shadow DOM. To do this, we update our code-tab element's connectedCallback() adopting the stylesheet we just imported.

// load Prism to be used by the web component, used for syntax highlighting. import syntaxHighlighting from "{{ get_asset_url('../css/components/prism.css')}}" assert { type: "css" }; import "{{ get_asset_url('../js/prism.js') }}"; // Register <code-tab> element class CodeTab extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback() { // pass Syntax highlighting stylesheets to be used in the ShadowDOM for <code-tab> elements. this.shadowRoot.adoptedStyleSheets = [syntaxHighlighting]; // The Shadow DOM HTML for a <code-tab> element. this.shadowRoot.innerHTML = ` <style> :host{display:block;} </style> <div class="code-snippet"> <pre><code><slot></slot></code></pre> </div> `; } } window.customElements.define("code-tab", CodeTab);

We're already close to having syntax highlighting working. Now, we need to determine what kind of information we need to make a code block work. For each code tab we need to know:

  • The language of the code in the code tab so the syntax highlighting is correct
  • Whether line numbers are desired along the left side to make it easy to reference

Something I also want to handle is escaping the code. “Escaping the code” means that any HTML code you place between <code-tab> and </code-tab> will be visually displayed and not rendered on the page. It's typically best to do this server side, and I recommend that in all places where that’s an option.

Because of the potential awkwardness that could be caused by HTML accidentally getting rendered rather than the code being visually displayed, we'll default to assuming the code is not already escaped and provide a way for the person using the component to tell the component they already escaped their code.

That makes three variables we're going to need to pass to our code-tab web component. Passing and retrieving this information works almost the exact same way as it does with a normal HTML element.

We'll use three data attributes on the code-tab element. It will look like this:

<code-block> <code-tab data-language="HTML" data-escaped="true" data-line-numbers="true"> <!--The code you want to display --> </code-tab> </code-block>

Technically speaking, the browser would let us write the attributes like this: 

<code-tab language="HTML" escaped="true" line-numbers="true">

But if any of those attribute names ever become web standard, that could cause issues. Data attributes protect us from that, as they're developer defined.

Since we're going to pass information that way, we need to get those values so Prism can use them. We actually use getAttribute() just like we would any other HTML element, the only difference being that we use getAttribute() on this which refers to the code-tab itself.

We're also going to include some logic for whether or not the code is escaped.

// Register <code-tab> element class CodeTab extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback(){ // pass Syntax highlighting stylesheets to be used in the ShadowDOM for <code-tab> elements. this.shadowRoot.adoptedStyleSheets = [syntaxHighlighting]; // Get attributes from <code-tab> element needed for prism and escaping. let language = this.getAttribute("data-language"); let lineNumbers = this.getAttribute("data-line-numbers") == "false" ? "no-line-numbers": "line-numbers"; let isEscaped = this.getAttribute("data-escaped"); // take whatever code is between <code-tab> and </code-tab> and escape it unless the data-escaped attribute is set to true. let code; if(isEscaped == "true"){ code = this.innerHTML.replace(/^\s+/g, ""); // replace strips the leading whitespace } else { code = encode(this.innerHTML.replace(/^\s+/g, "")); // replace strips the leading whitespace } // The Shadow DOM HTML for a <code-tab> element. this.shadowRoot.innerHTML = ` <style> :host{display:block;} :host code[class*="language-"],:host pre[class*="language-"]{margin-top:0;} </style> <div class="code-snippet"> <pre class="${lineNumbers}"><code class="language-${language}">${code}</code></pre> </div> `; // once the web component is initialized, tell Prism to syntax highlight the code blocks. Prism.highlightAllUnder(this.shadowRoot); } } window.customElements.define("code-tab", CodeTab);

 

A few things have changed here. We're getting the following attributes attached to the custom element: language, line-numbers, escaped. We're then using the values in the shadow DOM as classes and the actual code output. We're also telling Prism that once the web component has been initialized, we want it to syntax highlight the code.

You'll notice that where we display our code, we're no longer using a slot directly like <slot></slot> to output its content where that tag goes. We didn't do that here because we actually want to manipulate that output before displaying it. To do this, we're setting a variable of code to the innerHTML of the custom element — then, depending on whether the attribute for escaped is set to true or false, we encode special characters preventing HTML from being rendered instead of displayed as text.

We need to add the logic for encoding the HTML. Above that code tab class, we're going to add the function for encode.

// change string into escaped characters to prevent rendering DOM elements. This is used for the contents of the code blocks if the user doesn't specify that the content is escaped. function encode (str) { var buf = []; for (var i = str.length - 1; i >= 0; i--) { buf.unshift(["&#", str[i].charCodeAt(), ";"].join("")); } return buf.join(""); } // Register <code-tab> element class CodeTab extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback(){ // pass Syntax highlighting stylesheets to be used in the ShadowDOM for <code-tab> elements. this.shadowRoot.adoptedStyleSheets = [syntaxHighlighting]; // Get attributes from <code-tab> element needed for prism and escaping. let language = this.getAttribute("data-language"); let lineNumbers = this.getAttribute("data-line-numbers") == "false" ? "no-line-numbers": "line-numbers"; let isEscaped = this.getAttribute("data-escaped"); // take whatever code is between <code-tab> and </code-tab> and escape it unless the data-escaped attribute is set to true. let code; if(isEscaped == "true"){ code = this.innerHTML.replace(/^\s+/g, ""); // replace strips the leading whitespace } else { code = encode(this.innerHTML.replace(/^\s+/g, "")); // replace strips the leading whitespace } // The Shadow DOM HTML for a <code-tab> element. this.shadowRoot.innerHTML = ` <style> :host{display:block;} :host code[class*="language-"],:host pre[class*="language-"]{margin-top:0;} </style> <div class="code-snippet"> <pre class="${lineNumbers}"><code class="language-${language}">${code}</code></pre> </div> `; // once the web component is initialized, tell Prism to syntax highlight the code blocks. Prism.highlightAllUnder(this.shadowRoot); } } window.customElements.define("code-tab", CodeTab);

Step 3: Add Tabs and Labeling of the code

We're so close to being done, and the hard part is over. We're now going to build out our <code-block> web component, which will handle rendering tabs.

First, we're going to create our element and, in its shadow DOM, include a div which will contain our tabs. Then we'll use <slot> to make all of the code tabs we place inside appear below the tabs div.

// below window.customElements.define("code-tab",CodeTab) // <code-block> custom element - This element is where you place <code-tab> elements into. class CodeBlock extends HTMLElement { constructor() { super(); // attach a ShadowDom to the <code-block> element. The mode makes it so JavaScript outside the root can interact with it's contents. this.attachShadow({ mode: "open" }); } connectedCallback() { // check if code-tab element has data-line-numbers set to false. This value gets used to pass the line numbers option to /* let lineNumbers = this.getAttribute("data-line-numbers") == "false" ? "no-line-numbers": "line-numbers"; */ // let codeTabs = this.querySelectorAll("code-tab"); const codeBlockElement = this; // the element itself const shadowRoot = codeBlockElement.shadowRoot; // The ShadowDOM for the <code-block>. The tablist, is where we will place buttons for the user to tab through. // <slot> determines where <code-tab> elements will be rendered. shadowRoot.innerHTML = ` <style> :host{ --activeColor: rgb(153, 153, 153); --inactiveColor: rgb(45, 45, 45); } .code-block__tablist button{ appearance:none; border-width:0; background:var(--activeColor); padding:15px; color:#fff; border-right:1px solid var(--activeColor); } .code-block__tablist button:last-of-type{ border-right:0; } .code-block__tablist button[aria-selected="true"]{ background:var(--inactiveColor); } </style> <section class="code-block"> <div class="code-block__tablist" role="tablist" aria-label="Code Example"></div> <slot></slot> </section> `; let codeTabs = codeBlockElement.querySelectorAll("code-tab"); } } window.customElements.define("code-block", CodeBlock);

Now, we're going to add logic to generate a button element within the tabs div for every code-tab element. After we generate those buttons, we add the tab logic needed to hide/show the individual tab's content.

// below window.customElements.define("code-tab",CodeTab) // <code-block> custom element - This element is where you place <code-tab> elements into. class CodeBlock extends HTMLElement { constructor() { super(); // attach a ShadowDom to the <code-block> element. The mode makes it so JavaScript outside the root can interact with it's contents. this.attachShadow({ mode: "open" }); } connectedCallback() { // check if code-tab element has data-line-numbers set to false. This value gets used to pass the line numbers option to /* let lineNumbers = this.getAttribute("data-line-numbers") == "false" ? "no-line-numbers": "line-numbers"; */ // let codeTabs = this.querySelectorAll("code-tab"); const codeBlockElement = this; // the element itself const shadowRoot = codeBlockElement.shadowRoot; // The ShadowDOM for the <code-block>. The tablist, is where we will place buttons for the user to tab through. // <slot> determines where <code-tab> elements will be rendered. shadowRoot.innerHTML = ` <style> :host{ --activeColor: rgb(153, 153, 153); --inactiveColor: rgb(45, 45, 45); } .code-block__tablist button{ appearance:none; border-width:0; background:var(--activeColor); padding:15px; color:#fff; border-right:1px solid var(--activeColor); } .code-block__tablist button:last-of-type{ border-right:0; } .code-block__tablist button[aria-selected="true"]{ background:var(--inactiveColor); } </style> <section class="code-block"> <div class="code-block__tablist" role="tablist" aria-label="Code Example"></div> <slot></slot> </section> `; let codeTabs = codeBlockElement.querySelectorAll("code-tab"); // for each <code-tab> element create a corresponding <button> in a tablist div. codeTabs.forEach(function (individualCodeTab, i) { let tabButton; // get the button label from the <code-tab> element let buttonLabel = individualCodeTab.getAttribute("data-label"); // update <code-tab> elements to have corresponding IDs and aria labels to ensure accessibility. individualCodeTab.setAttribute("aria-labelledby", `tab-${i}`); individualCodeTab.setAttribute("id", `tabPanel-${i}`); // if the <button> is for the first <code-tab>, set aria-selected to true because it will be the active tab initially. if (i == 0) { tabButton = `<button role="tab" aria-selected="true" id="tab-${i}" aria-controls="tabPanel-${i}">${buttonLabel}</button>`; } else { tabButton = `<button role="tab" id="tab-${i}" aria-controls="tabPanel-${i}">${buttonLabel}</button>`; // set all of the tabs after the first one to hidden initially. individualCodeTab.hidden = true; } // add button element within the tablist div. shadowRoot.querySelector(".code-block__tablist").innerHTML += tabButton; }); /* handle tabs clicks and panel visibility */ let tabButtons = shadowRoot.querySelectorAll("button[role='tab']"); //let tabPanels = shadowRoot.querySelectorAll("code-tab"); duplicate of codeTabs // for every tabButton click, remove aria-selected for all tabs, add aria-selected to clicked tab tabButtons.forEach(function (tabButton, i) { tabButton.addEventListener("click", function () { let clickedTabButton = this; let activeTabPanelId = clickedTabButton.getAttribute("aria-controls"); let activeCodeTab = codeBlockElement.querySelector( `#${activeTabPanelId}` ); // remove aria-selected from all tabs tabButtons.forEach(function (tabButton) { tabButton.ariaSelected = false; }); // add aria-selected to clicked tab. clickedTabButton.ariaSelected = true; // hide all <code-tab>'s that are not active. codeBlockElement .querySelectorAll(`code-tab:not(#${activeTabPanelId})`) .forEach(function (inactiveTab) { inactiveTab.hidden = true; }); // ensure the active <code-tab> is not hidden. activeCodeTab.hidden = false; }); }); } } window.customElements.define("code-block", CodeBlock);

Step 4: The Fun Part: Using the Code Blocks

You can now create code blocks using HTML in the page.

<code-block> <code-tab data-label="HTML" data-language="html" data-escaped="true"> Your cool escaped html goes here. </code-tab> </code-block>

You can then create modules that use the code block HTML.

{{ require_js(get_asset_url('../../js/code-block.js'),{"type":"module"}) }} {%- set langMap = { "css": "CSS", "html": "HTML", "jinja2": "HubL", "js": "JavaScript", "json": "JSON", "yaml": "YAML", "shell": "Shell script", "php": "PHP", "node": "Node.js", "ruby": "Ruby", "java": "Java", "py": "Python" } -%} <code-block data-line-numbers="{{ module.useLineNumbers }}"> {%- for tab in module.tab -%} {%- set tabDisplayName = langMap[tab.language] -%} <code-tab data-label="{{tabDisplayName}}" data-language="{{ tab.language }}" data-escaped="true" data-highlight="{{ tab.highlightLineNumbers }}"> {{ tab.code_snippet|escape|escape_jinjava }} </code-tab> {%- endfor -%} </code-block> [ { "id" : "tab", "name" : "tab", "display_width" : null, "label" : "Tabs", "help_text" : "Tabs to show, first tab starts as active tab", "required" : false, "locked" : false, "occurrence" : { "min" : 1, "max" : 6, "sorting_label_field" : "tab.language", "default" : 2 }, "children" : [ { "id" : "tab.code_snippet", "name" : "code_snippet", "display_width" : null, "label" : "Code snippet", "required" : false, "locked" : false, "validation_regex" : "", "allow_new_line" : true, "show_emoji_picker" : false, "type" : "text", "default" : "" }, { "id" : "tab.language", "name" : "language", "display_width" : null, "label" : "Language", "required" : false, "locked" : false, "validation_regex" : "", "display" : "select", "choices" : [ [ "css", "CSS" ], [ "html", "HTML" ], [ "jinja2", "HubL" ], [ "js", "JavaScript" ], [ "json", "JSON" ], [ "yaml", "YAML" ], [ "shell", "Shell script" ], [ "php", "PHP" ], [ "node", "Node.js" ], [ "ruby", "Ruby" ], [ "java", "Java" ], [ "py", "Python" ] ], "type" : "choice", "default" : "jinja2" }, { "default": "", "label": "Highlight specific line numbers", "helptext":"You can highlight ranges of numbers, individual lines, and a combination of both. Ex: 1-2, 5, 9-20", "type": "text", "name": "highlightLineNumbers", "id": "highlightLineNumbers" }, ], "tab" : "CONTENT", "expanded" : false, "type" : "group", "default" : [ { "code_snippet" : "", "language" : "jinja2" }, { "code_snippet" : "", "language" : "html" } ] }, { "id" : "useLineNumbers", "name" : "useLineNumbers", "display_width" : null, "label" : "Use line numbers", "required" : false, "locked" : false, "display" : "checkbox", "type" : "boolean", "default" : false } ]

The GitHub Repository contains a more up-to-date example of how you could use it in a module.

You can also use it in a blog post — instructions for that are in the GitHub repository.

You may have already pieced it together, but every code block in this article — and nearly all of the code blocks on the developers.hubspot.com website — are now powered by this web component. Inspect the page and have a look. The only significant difference between the version you see on GitHub and the HubSpot code block is CSS styling.

We hope you enjoyed this tutorial. As noted, all of the code is available in this GitHub repository. Go make some cool and useful websites!